| // The size of the golden images (DMs) | |
| const CANVAS_WIDTH = 600; | |
| const CANVAS_HEIGHT = 600; | |
| const SHOULD_SKIP = 'should_skip'; | |
| const _commonGM = (it, pause, name, callback, assetsToFetchOrPromisesToWaitOn) => { | |
| if (name.includes(' ')) { | |
| throw name + " cannot contain spaces"; | |
| } | |
| const fetchPromises = []; | |
| for (const assetOrPromise of assetsToFetchOrPromisesToWaitOn) { | |
| // https://stackoverflow.com/a/9436948 | |
| if (typeof assetOrPromise === 'string' || assetOrPromise instanceof String) { | |
| const newPromise = fetchWithRetries(assetOrPromise) | |
| .then((response) => response.arrayBuffer()) | |
| .catch((err) => { | |
| console.error(err); | |
| throw err; | |
| }); | |
| 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. Otherwise, we'll pass the return value on, | |
| // which could indicate to skip this test and not report it to Gold. | |
| surface.getCanvas().clear(CanvasKit.WHITE); | |
| return callback(surface.getCanvas(), values, surface); | |
| } catch (e) { | |
| console.log(`gm ${name} failed with error`, e); | |
| expect(e).toBeFalsy(); | |
| debugger; | |
| done(); | |
| } | |
| }).then((shouldSkip) => { | |
| surface.flush(); | |
| if (shouldSkip === SHOULD_SKIP) { | |
| surface.delete(); | |
| done(); | |
| console.log(`skipped gm ${name}`); | |
| return; | |
| } | |
| if (pause) { | |
| reportSurface(surface, name, null); | |
| console.error('pausing due to pause_gm being invoked'); | |
| } else { | |
| reportSurface(surface, name, done); | |
| } | |
| }).catch((e) => { | |
| console.log(`could not load assets for gm ${name}`, e); | |
| debugger; | |
| done(); | |
| }); | |
| }) | |
| }; | |
| const fetchWithRetries = (url) => { | |
| const MAX_ATTEMPTS = 3; | |
| const DELAY_AFTER_FAILURE = 1000; | |
| return new Promise((resolve, reject) => { | |
| let attempts = 0; | |
| const attemptFetch = () => { | |
| attempts++; | |
| fetch(url).then((resp) => resolve(resp)) | |
| .catch((err) => { | |
| if (attempts < MAX_ATTEMPTS) { | |
| console.warn(`got error in fetching ${url}, retrying`, err); | |
| retryAfterDelay(); | |
| } else { | |
| console.error(`got error in fetching ${url} even after ${attempts} attempts`, err); | |
| reject(err); | |
| } | |
| }); | |
| }; | |
| const retryAfterDelay = () => { | |
| setTimeout(() => { | |
| attemptFetch(); | |
| }, DELAY_AFTER_FAILURE); | |
| } | |
| attemptFetch(); | |
| }); | |
| } | |
| /** | |
| * 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) => { | |
| if (name.includes(' ')) { | |
| throw name + " cannot contain spaces"; | |
| } | |
| 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) { | |
| // Sometimes, 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'); | |
| if (!reportingCanvas) { | |
| throw 'Reporting canvas not found'; | |
| } | |
| reportingCanvas.getContext('2d').putImageData(imageData, 0, 0); | |
| if (!done) { | |
| return; | |
| } | |
| reportCanvas(reportingCanvas, testname).then(() => { | |
| surface.delete(); | |
| done(); | |
| }).catch(reportError(done)); | |
| } | |
| function starPath(CanvasKit, X=128, Y=128, R=116) { | |
| const p = new CanvasKit.PathBuilder(); | |
| 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.detachAndDelete(); | |
| } |