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