1// The size of the golden images (DMs) 2const CANVAS_WIDTH = 600; 3const CANVAS_HEIGHT = 600; 4 5const _commonGM = (it, pause, name, callback, assetsToFetchOrPromisesToWaitOn) => { 6 const fetchPromises = []; 7 for (const assetOrPromise of assetsToFetchOrPromisesToWaitOn) { 8 // https://stackoverflow.com/a/9436948 9 if (typeof assetOrPromise === 'string' || assetOrPromise instanceof String) { 10 const newPromise = fetchWithRetries(assetOrPromise) 11 .then((response) => response.arrayBuffer()) 12 .catch((err) => { 13 console.error(err); 14 throw err; 15 }); 16 fetchPromises.push(newPromise); 17 } else if (typeof assetOrPromise.then === 'function') { 18 fetchPromises.push(assetOrPromise); 19 } else { 20 throw 'Neither a string nor a promise ' + assetOrPromise; 21 } 22 } 23 it('draws gm '+name, (done) => { 24 const surface = CanvasKit.MakeCanvasSurface('test'); 25 expect(surface).toBeTruthy('Could not make surface'); 26 if (!surface) { 27 done(); 28 return; 29 } 30 // if fetchPromises is empty, the returned promise will 31 // resolve right away and just call the callback. 32 Promise.all(fetchPromises).then((values) => { 33 try { 34 // If callback returns a promise, the chained .then 35 // will wait for it. 36 return callback(surface.getCanvas(), values, surface); 37 } catch (e) { 38 console.log(`gm ${name} failed with error`, e); 39 expect(e).toBeFalsy(); 40 debugger; 41 done(); 42 } 43 }).then(() => { 44 surface.flush(); 45 if (pause) { 46 reportSurface(surface, name, null); 47 console.error('pausing due to pause_gm being invoked'); 48 } else { 49 reportSurface(surface, name, done); 50 } 51 }).catch((e) => { 52 console.log(`could not load assets for gm ${name}`, e); 53 debugger; 54 done(); 55 }); 56 }) 57}; 58 59const fetchWithRetries = (url) => { 60 const MAX_ATTEMPTS = 3; 61 const DELAY_AFTER_FAILURE = 1000; 62 63 return new Promise((resolve, reject) => { 64 let attempts = 0; 65 const attemptFetch = () => { 66 attempts++; 67 fetch(url).then((resp) => resolve(resp)) 68 .catch((err) => { 69 if (attempts < MAX_ATTEMPTS) { 70 console.warn(`got error in fetching ${url}, retrying`, err); 71 retryAfterDelay(); 72 } else { 73 console.error(`got error in fetching ${url} even after ${attempts} attempts`, err); 74 reject(err); 75 } 76 }); 77 }; 78 const retryAfterDelay = () => { 79 setTimeout(() => { 80 attemptFetch(); 81 }, DELAY_AFTER_FAILURE); 82 } 83 attemptFetch(); 84 }); 85 86} 87 88/** 89 * Takes a name, a callback, and any number of assets or promises. It executes the 90 * callback (presumably, the test) and reports the resulting surface to Gold. 91 * @param name {string} 92 * @param callback {Function}, has two params, the first is a CanvasKit.Canvas 93 * and the second is an array of results from the passed in assets or promises. 94 * If a given assetOrPromise was a string, the result will be an ArrayBuffer. 95 * @param assetsToFetchOrPromisesToWaitOn {string|Promise}. If a string, it will 96 * be treated as a url to fetch and return an ArrayBuffer with the contents as 97 * a result in the callback. Otherwise, the promise will be waited on and its 98 * result will be whatever the promise resolves to. 99 */ 100const gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { 101 _commonGM(it, false, name, callback, assetsToFetchOrPromisesToWaitOn); 102}; 103 104/** 105 * fgm is like gm, except only tests declared with fgm, force_gm, or fit will be 106 * executed. This mimics the behavior of Jasmine.js. 107 */ 108const fgm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { 109 _commonGM(fit, false, name, callback, assetsToFetchOrPromisesToWaitOn); 110}; 111 112/** 113 * force_gm is like gm, except only tests declared with fgm, force_gm, or fit will be 114 * executed. This mimics the behavior of Jasmine.js. 115 */ 116const force_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { 117 fgm(name, callback, assetsToFetchOrPromisesToWaitOn); 118}; 119 120/** 121 * skip_gm does nothing. It is a convenient way to skip a test temporarily. 122 */ 123const skip_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { 124 console.log(`Skipping gm ${name}`); 125 // do nothing, skip the test for now 126}; 127 128/** 129 * pause_gm is like fgm, except the test will not finish right away and clear, 130 * making it ideal for a human to manually inspect the results. 131 */ 132const pause_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { 133 _commonGM(fit, true, name, callback, assetsToFetchOrPromisesToWaitOn); 134}; 135 136const _commonMultipleCanvasGM = (it, pause, name, callback) => { 137 it(`draws gm ${name} on both CanvasKit and using Canvas2D`, (done) => { 138 const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT); 139 skcanvas._config = 'software_canvas'; 140 const realCanvas = document.getElementById('test'); 141 realCanvas._config = 'html_canvas'; 142 realCanvas.width = CANVAS_WIDTH; 143 realCanvas.height = CANVAS_HEIGHT; 144 145 if (pause) { 146 console.log('debugging canvaskit version'); 147 callback(realCanvas); 148 callback(skcanvas); 149 const png = skcanvas.toDataURL(); 150 const img = document.createElement('img'); 151 document.body.appendChild(img); 152 img.src = png; 153 debugger; 154 return; 155 } 156 157 const promises = []; 158 159 for (const canvas of [skcanvas, realCanvas]) { 160 callback(canvas); 161 // canvas has .toDataURL (even though skcanvas is not a real Canvas) 162 // so this will work. 163 promises.push(reportCanvas(canvas, name, canvas._config)); 164 } 165 Promise.all(promises).then(() => { 166 skcanvas.dispose(); 167 done(); 168 }).catch(reportError(done)); 169 }); 170}; 171 172/** 173 * Takes a name and a callback. It executes the callback (presumably, the test) 174 * for both a CanvasKit.Canvas and a native Canvas2D. The result of both will be 175 * uploaded to Gold. 176 * @param name {string} 177 * @param callback {Function}, has one param, either a CanvasKit.Canvas or a native 178 * Canvas2D object. 179 */ 180const multipleCanvasGM = (name, callback) => { 181 _commonMultipleCanvasGM(it, false, name, callback); 182}; 183 184/** 185 * fmultipleCanvasGM is like multipleCanvasGM, except only tests declared with 186 * fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This 187 * mimics the behavior of Jasmine.js. 188 */ 189const fmultipleCanvasGM = (name, callback) => { 190 _commonMultipleCanvasGM(fit, false, name, callback); 191}; 192 193/** 194 * force_multipleCanvasGM is like multipleCanvasGM, except only tests declared 195 * with fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This 196 * mimics the behavior of Jasmine.js. 197 */ 198const force_multipleCanvasGM = (name, callback) => { 199 fmultipleCanvasGM(name, callback); 200}; 201 202/** 203 * pause_multipleCanvasGM is like fmultipleCanvasGM, except the test will not 204 * finish right away and clear, making it ideal for a human to manually inspect the results. 205 */ 206const pause_multipleCanvasGM = (name, callback) => { 207 _commonMultipleCanvasGM(fit, true, name, callback); 208}; 209 210/** 211 * skip_multipleCanvasGM does nothing. It is a convenient way to skip a test temporarily. 212 */ 213const skip_multipleCanvasGM = (name, callback) => { 214 console.log(`Skipping multiple canvas gm ${name}`); 215}; 216 217 218function reportSurface(surface, testname, done) { 219 // In docker, the webgl canvas is blank, but the surface has the pixel 220 // data. So, we copy it out and draw it to a normal canvas to take a picture. 221 // To be consistent across CPU and GPU, we just do it for all configurations 222 // (even though the CPU canvas shows up after flush just fine). 223 let pixels = surface.getCanvas().readPixels(0, 0, { 224 width: CANVAS_WIDTH, 225 height: CANVAS_HEIGHT, 226 colorType: CanvasKit.ColorType.RGBA_8888, 227 alphaType: CanvasKit.AlphaType.Unpremul, 228 colorSpace: CanvasKit.ColorSpace.SRGB, 229 }); 230 if (!pixels) { 231 throw 'Could not get pixels for test '+testname; 232 } 233 pixels = new Uint8ClampedArray(pixels.buffer); 234 const imageData = new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT); 235 236 const reportingCanvas = document.getElementById('report'); 237 if (!reportingCanvas) { 238 throw 'Reporting canvas not found'; 239 } 240 reportingCanvas.getContext('2d').putImageData(imageData, 0, 0); 241 if (!done) { 242 return; 243 } 244 reportCanvas(reportingCanvas, testname).then(() => { 245 surface.delete(); 246 done(); 247 }).catch(reportError(done)); 248} 249 250 251function starPath(CanvasKit, X=128, Y=128, R=116) { 252 const p = new CanvasKit.Path(); 253 p.moveTo(X + R, Y); 254 for (let i = 1; i < 8; i++) { 255 let a = 2.6927937 * i; 256 p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a)); 257 } 258 p.close(); 259 return p; 260} 261