• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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