// The size of the golden images (DMs) const CANVAS_WIDTH = 600; const CANVAS_HEIGHT = 600; const _commonGM = (it, pause, name, callback, assetsToFetchOrPromisesToWaitOn) => { const fetchPromises = []; for (const assetOrPromise of assetsToFetchOrPromisesToWaitOn) { // https://stackoverflow.com/a/9436948 if (typeof assetOrPromise === 'string' || assetOrPromise instanceof String) { const newPromise = fetch(assetOrPromise) .then((response) => response.arrayBuffer()); fetchPromises.push(newPromise); } else if (typeof assetOrPromise.then === 'function') { fetchPromises.push(assetOrPromise); } else { throw 'Neither a string nor a promise ' + assetOrPromise; } } it('draws gm '+name, (done) => { const surface = CanvasKit.MakeCanvasSurface('test'); expect(surface).toBeTruthy('Could not make surface'); if (!surface) { done(); return; } // if fetchPromises is empty, the returned promise will // resolve right away and just call the callback. Promise.all(fetchPromises).then((values) => { try { // If callback returns a promise, the chained .then // will wait for it. return callback(surface.getCanvas(), values); } catch (e) { console.log(`gm ${name} failed with error`, e); expect(e).toBeFalsy(); debugger; done(); } }).then(() => { surface.flush(); if (pause) { reportSurface(surface, name, null); } else { reportSurface(surface, name, done); } }).catch((e) => { console.log(`could not load assets for gm ${name}`, e); debugger; done(); }); }) }; /** * Takes a name, a callback, and any number of assets or promises. It executes the * callback (presumably, the test) and reports the resulting surface to Gold. * @param name {string} * @param callback {Function}, has two params, the first is a CanvasKit.Canvas * and the second is an array of results from the passed in assets or promises. * If a given assetOrPromise was a string, the result will be an ArrayBuffer. * @param assetsToFetchOrPromisesToWaitOn {string|Promise}. If a string, it will * be treated as a url to fetch and return an ArrayBuffer with the contents as * a result in the callback. Otherwise, the promise will be waited on and its * result will be whatever the promise resolves to. */ const gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { _commonGM(it, false, name, callback, assetsToFetchOrPromisesToWaitOn); }; /** * fgm is like gm, except only tests declared with fgm, force_gm, or fit will be * executed. This mimics the behavior of Jasmine.js. */ const fgm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { _commonGM(fit, false, name, callback, assetsToFetchOrPromisesToWaitOn); }; /** * force_gm is like gm, except only tests declared with fgm, force_gm, or fit will be * executed. This mimics the behavior of Jasmine.js. */ const force_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { fgm(name, callback, assetsToFetchOrPromisesToWaitOn); }; /** * skip_gm does nothing. It is a convenient way to skip a test temporarily. */ const skip_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { console.log(`Skipping gm ${name}`); // do nothing, skip the test for now }; /** * pause_gm is like fgm, except the test will not finish right away and clear, * making it ideal for a human to manually inspect the results. */ const pause_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => { _commonGM(fit, true, name, callback, assetsToFetchOrPromisesToWaitOn); }; const _commonMultipleCanvasGM = (it, pause, name, callback) => { it(`draws gm ${name} on both CanvasKit and using Canvas2D`, (done) => { const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT); skcanvas._config = 'software_canvas'; const realCanvas = document.getElementById('test'); realCanvas._config = 'html_canvas'; realCanvas.width = CANVAS_WIDTH; realCanvas.height = CANVAS_HEIGHT; if (pause) { console.log('debugging canvaskit version'); callback(realCanvas); callback(skcanvas); const png = skcanvas.toDataURL(); const img = document.createElement('img'); document.body.appendChild(img); img.src = png; debugger; return; } const promises = []; for (const canvas of [skcanvas, realCanvas]) { callback(canvas); // canvas has .toDataURL (even though skcanvas is not a real Canvas) // so this will work. promises.push(reportCanvas(canvas, name, canvas._config)); } Promise.all(promises).then(() => { skcanvas.dispose(); done(); }).catch(reportError(done)); }); }; /** * Takes a name and a callback. It executes the callback (presumably, the test) * for both a CanvasKit.Canvas and a native Canvas2D. The result of both will be * uploaded to Gold. * @param name {string} * @param callback {Function}, has one param, either a CanvasKit.Canvas or a native * Canvas2D object. */ const multipleCanvasGM = (name, callback) => { _commonMultipleCanvasGM(it, false, name, callback); }; /** * fmultipleCanvasGM is like multipleCanvasGM, except only tests declared with * fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This * mimics the behavior of Jasmine.js. */ const fmultipleCanvasGM = (name, callback) => { _commonMultipleCanvasGM(fit, false, name, callback); }; /** * force_multipleCanvasGM is like multipleCanvasGM, except only tests declared * with fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This * mimics the behavior of Jasmine.js. */ const force_multipleCanvasGM = (name, callback) => { fmultipleCanvasGM(name, callback); }; /** * pause_multipleCanvasGM is like fmultipleCanvasGM, except the test will not * finish right away and clear, making it ideal for a human to manually inspect the results. */ const pause_multipleCanvasGM = (name, callback) => { _commonMultipleCanvasGM(fit, true, name, callback); }; /** * skip_multipleCanvasGM does nothing. It is a convenient way to skip a test temporarily. */ const skip_multipleCanvasGM = (name, callback) => { console.log(`Skipping multiple canvas gm ${name}`); }; function reportSurface(surface, testname, done) { // In docker, the webgl canvas is blank, but the surface has the pixel // data. So, we copy it out and draw it to a normal canvas to take a picture. // To be consistent across CPU and GPU, we just do it for all configurations // (even though the CPU canvas shows up after flush just fine). let pixels = surface.getCanvas().readPixels(0, 0, { width: CANVAS_WIDTH, height: CANVAS_HEIGHT, colorType: CanvasKit.ColorType.RGBA_8888, alphaType: CanvasKit.AlphaType.Unpremul, colorSpace: CanvasKit.ColorSpace.SRGB, }); if (!pixels) { throw 'Could not get pixels for test '+testname; } pixels = new Uint8ClampedArray(pixels.buffer); const imageData = new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT); const reportingCanvas = document.getElementById('report'); reportingCanvas.getContext('2d').putImageData(imageData, 0, 0); if (!done) { return; } reportCanvas(reportingCanvas, testname).then(() => { // TODO(kjlubick): should we call surface.delete() here? done(); }).catch(reportError(done)); } function starPath(CanvasKit, X=128, Y=128, R=116) { const p = new CanvasKit.Path(); p.moveTo(X + R, Y); for (let i = 1; i < 8; i++) { let a = 2.6927937 * i; p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a)); } p.close(); return p; }