You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2021/05/07 07:53:04 UTC

[echarts] branch enhance-visual-regression-test updated: test(visual): fully control the replay timeline.

This is an automated email from the ASF dual-hosted git repository.

shenyi pushed a commit to branch enhance-visual-regression-test
in repository https://gitbox.apache.org/repos/asf/echarts.git


The following commit(s) were added to refs/heads/enhance-visual-regression-test by this push:
     new 05a0c7d  test(visual): fully control the replay timeline.
05a0c7d is described below

commit 05a0c7d17a3b1b483c8b603519e44425124654d9
Author: pissang <bm...@gmail.com>
AuthorDate: Fri May 7 15:51:51 2021 +0800

    test(visual): fully control the replay timeline.
---
 test/lib/simpleRequire.js                          |   9 +-
 test/runTest/cli.js                                | 134 +++++++++--------
 test/runTest/client/client.js                      |   2 +-
 .../{Timeline.js => runtime/ActionPlayback.js}     |  83 +++++------
 test/runTest/runtime/MockDate.js                   |  42 ------
 test/runTest/runtime/main.js                       |  34 ++++-
 test/runTest/runtime/timeline.js                   | 162 +++++++++++++++++++++
 7 files changed, 312 insertions(+), 154 deletions(-)

diff --git a/test/lib/simpleRequire.js b/test/lib/simpleRequire.js
index ecf0b27..fd65fe4 100644
--- a/test/lib/simpleRequire.js
+++ b/test/lib/simpleRequire.js
@@ -26,7 +26,6 @@
 // Limitations:
 //  1. Not support ancient browsers.
 //  2. Only `paths` can be configured
-
 (function (global) {
 
     var requireCfg = { paths: {} }
@@ -220,6 +219,12 @@
                 // Clear before flush. Avoid more require in the callback.
                 pendingRequireCallbacks = [];
                 pendingRequireCallbackParams = [];
+
+                // Start visual regression test before callback
+                if (typeof __VST_START__ !== 'undefined') {
+                    __VST_START__();
+                }
+
                 for (var i = 0; i < requireCallbackToFlush.length; i++) {
                     requireCallbackToFlush[i] && requireCallbackToFlush[i].apply(null, requireCallbackParamsToFlush[i]);
                 }
@@ -234,4 +239,4 @@
     global.require = require;
     global.define = define;
     global.define.amd = {};
-})(window);
\ No newline at end of file
+})(window);
diff --git a/test/runTest/cli.js b/test/runTest/cli.js
index 59b9bf7..9614815 100644
--- a/test/runTest/cli.js
+++ b/test/runTest/cli.js
@@ -24,9 +24,8 @@ const fs = require('fs');
 const path = require('path');
 const program = require('commander');
 const compareScreenshot = require('./compareScreenshot');
-const {testNameFromFile, fileNameFromTest, getVersionDir, buildRuntimeCode, waitTime, getEChartsTestFileName} = require('./util');
+const {testNameFromFile, fileNameFromTest, getVersionDir, buildRuntimeCode, getEChartsTestFileName, waitTime} = require('./util');
 const {origin} = require('./config');
-const Timeline = require('./Timeline');
 const cwebpBin = require('cwebp-bin');
 const { execFile } = require('child_process');
 
@@ -124,7 +123,6 @@ async function takeScreenshot(page, fullPage, fileUrl, desc, isExpected, minor)
 }
 
 async function runActions(page, testOpt, isExpected, screenshots) {
-    let timeline = new Timeline(page);
     let actions;
     try {
         let actContent = fs.readFileSync(path.join(__dirname, 'actions', testOpt.name + '.json'));
@@ -135,43 +133,9 @@ async function runActions(page, testOpt, isExpected, screenshots) {
         return;
     }
 
-    let playbackSpeed = +program.speed;
-
-    for (let action of actions) {
-        await page.evaluate((x, y) => {
-            window.scrollTo(x, y);
-        }, action.scrollX, action.scrollY);
-
-        let count = 0;
-        async function _innerTakeScreenshot() {
-            if (!program.save) {
-                return;
-            }
-            const desc = action.desc || action.name;
-            const {
-                screenshotName,
-                screenshotPath,
-                rawScreenshotPath
-            } = await takeScreenshot(page, false, testOpt.fileUrl, desc, isExpected, count++);
-            screenshots.push({
-                screenshotName,
-                desc,
-                screenshotPath,
-                rawScreenshotPath
-            });
-        }
-        await timeline.runAction(action, _innerTakeScreenshot, playbackSpeed);
-
-        if (count === 0) {
-            await waitTime(200);
-            await _innerTakeScreenshot();
-        }
-
-        // const desc = action.desc || action.name;
-        // const {screenshotName, screenshotPath} = await takeScreenshot(page, false, testOpt.fileUrl, desc, version);
-        // screenshots.push({screenshotName, desc, screenshotPath});
-    }
-    timeline.stop();
+    await page.evaluate(async (actions) => {
+        await __VST_RUN_ACTIONS__(actions);
+    }, actions);
 }
 
 async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
@@ -184,6 +148,65 @@ async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
     page.setRequestInterception(true);
     page.on('request', request => replaceEChartsVersion(request, version));
 
+
+    async function pageScreenshot() {
+        if (!program.save) {
+            return;
+        }
+        // Final shot.
+        await page.mouse.move(0, 0);
+        const desc = 'Full Shot';
+        const {
+            screenshotName,
+            screenshotPath,
+            rawScreenshotPath
+        } = await takeScreenshot(page, true, fileUrl, desc, isExpected);
+        screenshots.push({
+            screenshotName,
+            desc,
+            screenshotPath,
+            rawScreenshotPath
+        });
+    }
+
+    await page.exposeFunction('__VST_MOUSE_MOVE__', async (x, y) =>  {
+        await page.mouse.move(x, y);
+    });
+    await page.exposeFunction('__VST_MOUSE_DOWN__', async () =>  {
+        await page.mouse.down();
+    });
+    await page.exposeFunction('__VST_MOUSE_UP__', async () =>  {
+        await page.mouse.up();
+    });
+
+    let waitClientScreenshot = new Promise((resolve) => {
+        // TODO wait for this function exposed?
+        page.exposeFunction('__VST_FULL_SCREENSHOT__', () =>  {
+            pageScreenshot().then(resolve);
+        });
+    });
+
+    let actionScreenshotCount = {};
+
+    await page.exposeFunction('__VST_ACTION_SCREENSHOT__', async (action) =>  {
+        if (!program.save) {
+            return;
+        }
+        const desc = action.desc || action.name;
+        actionScreenshotCount[action.name] = actionScreenshotCount[action.name] || 0;
+        const {
+            screenshotName,
+            screenshotPath,
+            rawScreenshotPath
+        } = await takeScreenshot(page, false, testOpt.fileUrl, desc, isExpected, actionScreenshotCount[action.name]++);
+        screenshots.push({
+            screenshotName,
+            desc,
+            screenshotPath,
+            rawScreenshotPath
+        });
+    });
+
     await page.evaluateOnNewDocument(runtimeCode);
 
     page.on('console', msg => {
@@ -203,23 +226,18 @@ async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
             timeout: 10000
         });
 
-        await waitTime(500);  // Wait for animation or something else. Pending
-        // Final shot.
-        await page.mouse.move(0, 0);
-        if (program.save) {
-            let desc = 'Full Shot';
-            const {
-                screenshotName,
-                screenshotPath,
-                rawScreenshotPath
-            } = await takeScreenshot(page, true, fileUrl, desc, isExpected);
-            screenshots.push({
-                screenshotName,
-                desc,
-                screenshotPath,
-                rawScreenshotPath
-            });
-        }
+        let autoscreenshotTimeout;
+
+        await Promise.race([
+            waitClientScreenshot,
+            new Promise(resolve => {
+                autoscreenshotTimeout = setTimeout(() => {
+                    console.log(`Automatically screenshot in ${testNameFromFile(fileUrl)}`);
+                    pageScreenshot().then(resolve)
+                }, 1000)
+            })
+        ]);
+        clearTimeout(autoscreenshotTimeout);
 
         await runActions(page, testOpt, isExpected, screenshots);
     }
@@ -325,7 +343,7 @@ async function runTests(pendingTests) {
     // TODO Not hardcoded.
     // let runtimeCode = fs.readFileSync(path.join(__dirname, 'tmp/testRuntime.js'), 'utf-8');
     let runtimeCode = await buildRuntimeCode();
-    runtimeCode = `window.__TEST_PLAYBACK_SPEED__ = ${program.speed || 1};\n${runtimeCode}`;
+    runtimeCode = `window.__VST_PLAYBACK_SPEED__ = ${program.speed || 1};\n${runtimeCode}`;
 
     process.on('exit', () => {
         browser.close();
diff --git a/test/runTest/client/client.js b/test/runTest/client/client.js
index 484088d..26214e7 100644
--- a/test/runTest/client/client.js
+++ b/test/runTest/client/client.js
@@ -182,7 +182,7 @@ const app = new Vue({
                     finishedCount++;
                 }
             });
-            return +(finishedCount / this.fullTests.length * 100).toFixed(0) || 0;
+            return +(finishedCount / this.fullTests.length * 100).toFixed(1) || 0;
         },
 
         tests() {
diff --git a/test/runTest/Timeline.js b/test/runTest/runtime/ActionPlayback.js
similarity index 65%
rename from test/runTest/Timeline.js
rename to test/runTest/runtime/ActionPlayback.js
index 787a93b..5732a93 100644
--- a/test/runTest/Timeline.js
+++ b/test/runTest/runtime/ActionPlayback.js
@@ -17,21 +17,25 @@
 * under the License.
 */
 
-const {waitTime} = require('./util');
+import * as timeline from './timeline';
 
-module.exports = class Timeline {
+function waitTime(time) {
+    return new Promise(resolve => {
+        setTimeout(() => {
+            resolve();
+        }, time);
+    });
+};
 
-    constructor(page) {
-        this._page = page;
+export class ActionPlayback {
 
+    constructor() {
         this._timer = 0;
         this._current = 0;
 
         this._ops = [];
         this._currentOpIndex = 0;
 
-        this._client;
-
         this._isLastOpMousewheel = false;
     }
 
@@ -43,11 +47,7 @@ module.exports = class Timeline {
     }
 
 
-    async runAction(action, takeScreenshot, playbackSpeed) {
-        if (!this._client) {
-            this._client = await this._page.target().createCDPSession();
-        }
-
+    async runAction(action, playbackSpeed) {
         this.stop();
 
         playbackSpeed = playbackSpeed || 1;
@@ -75,13 +75,21 @@ module.exports = class Timeline {
                 self._elapsedTime += dTime * playbackSpeed;
                 self._current = current;
 
-                await self._update(takeScreenshot, playbackSpeed);
+                await self._update(
+                    async () => {
+                        // Pause timeline when doing screenshot to avoid screenshot needs taking a while.
+                        timeline.pause();
+                        await __VST_ACTION_SCREENSHOT__(action);
+                        timeline.resume();
+                    },
+                    playbackSpeed
+                );
                 if (self._currentOpIndex >= self._ops.length) {
                     // Finished
                     resolve();
                 }
                 else {
-                    self._timer = setTimeout(tick, 16);
+                    self._timer = setTimeout(tick, 0);
                 }
             }
             tick();
@@ -96,7 +104,7 @@ module.exports = class Timeline {
         }
     }
 
-    async _update(takeScreenshot, playbackSpeed) {
+    async _update(playbackSpeed) {
         let op = this._ops[this._currentOpIndex];
 
         if (op.time > this._elapsedTime) {
@@ -108,50 +116,37 @@ module.exports = class Timeline {
         let takenScreenshot = false;
         switch (op.type) {
             case 'mousedown':
-                await page.mouse.move(op.x, op.y);
-                await page.mouse.down();
+                await __VST_MOUSE_MOVE__(op.x, op.y);
+                await __VST_MOUSE_DOWN__();
                 break;
             case 'mouseup':
-                await page.mouse.move(op.x, op.y);
+                await __VST_MOUSE_MOVE__(op.x, op.y);
                 await page.mouse.up();
                 break;
             case 'mousemove':
-                await page.mouse.move(op.x, op.y);
+                await __VST_MOUSE_MOVE__(op.x, op.y);
                 break;
             case 'mousewheel':
-                await page.evaluate((x, y, deltaX, deltaY) => {
-                    let element = document.elementFromPoint(x, y);
-                    // Here dispatch mousewheel event because echarts used it.
-                    // TODO Consider upgrade?
-                    let event = new WheelEvent('mousewheel', {
-                        // PENDING
-                        // Needs inverse delta?
-                        deltaY,
-                        clientX: x, clientY: y,
-                        // Needs bubble to parent container
-                        bubbles: true
-                    });
-
-                    element.dispatchEvent(event);
-                }, op.x, op.y, op.deltaX || 0, op.deltaY);
+                let element = document.elementFromPoint(op.x, op.y);
+                // Here dispatch mousewheel event because echarts used it.
+                // TODO Consider upgrade?
+                let event = new WheelEvent('mousewheel', {
+                    // PENDING
+                    // Needs inverse delta?
+                    deltaY,
+                    clientX: x, clientY: y,
+                    // Needs bubble to parent container
+                    bubbles: true
+                });
+                element.dispatchEvent(event);
                 this._isLastOpMousewheel = true;
-                // console.log('mousewheel', op.x, op.y, op.deltaX, op.deltaY);
-                // await this._client.send('Input.dispatchMouseEvent', {
-                //     type: 'mouseWheel',
-                //     x: op.x,
-                //     y: op.y,
-                //     deltaX: op.deltaX,
-                //     deltaY: op.deltaY
-                // });
                 break;
             case 'screenshot':
                 await takeScreenshot();
                 takenScreenshot = true;
                 break;
             case 'valuechange':
-                if (op.target === 'select') {
-                    await page.select(op.selector, op.value);
-                }
+                document.querySelector(op.selector).value = op.value;
                 break;
         }
 
diff --git a/test/runTest/runtime/MockDate.js b/test/runTest/runtime/MockDate.js
deleted file mode 100644
index 6ebccb2..0000000
--- a/test/runTest/runtime/MockDate.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership.  The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License.  You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied.  See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-
-// Mock date.
-const NativeDate = window.Date;
-
-const fixedTimestamp = 1566458693300;
-const actualTimestamp = NativeDate.now();
-const mockNow = function () {
-    // speed up
-    return fixedTimestamp + (NativeDate.now() - actualTimestamp) * window.__TEST_PLAYBACK_SPEED__;
-};
-function MockDate(...args) {
-    if (!args.length) {
-        return new NativeDate(mockNow());
-    }
-    else {
-        return new NativeDate(...args);
-    }
-}
-MockDate.prototype = Object.create(NativeDate.prototype);
-Object.setPrototypeOf(MockDate, NativeDate);
-MockDate.now = mockNow;
-
-
-export default MockDate;
diff --git a/test/runTest/runtime/main.js b/test/runTest/runtime/main.js
index 9173e0f..5dc592b 100644
--- a/test/runTest/runtime/main.js
+++ b/test/runTest/runtime/main.js
@@ -18,13 +18,8 @@
 */
 
 import seedrandom from 'seedrandom';
-import MockDate from './MockDate';
-
-window.Date = MockDate;
-
-if (typeof __TEST_PLAYBACK_SPEED__ === 'undefined') {
-    window.__TEST_PLAYBACK_SPEED__ = 1;
-}
+import { ActionPlayback } from './ActionPlayback';
+import * as timeline from './timeline';
 
 let myRandom = new seedrandom('echarts-random');
 // Random for echarts code.
@@ -42,6 +37,31 @@ window.__random__inner__ = function () {
     return val;
 };
 
+let vstStarted = false;
+
+window.__VST_START__ = function () {
+    if (vstStarted) {
+        return;
+    }
+    vstStarted = true;
+    timeline.start();
+    // Screenshot after 500ms
+    setTimeout(function () {
+        // Pause timeline until run actions.
+        timeline.pause();
+        __VST_FULL_SCREENSHOT__();
+    }, 500);
+}
+
+window.__VST_RUN_ACTIONS__ = async function (actions) {
+    timeline.resume();
+    const actionPlayback = new ActionPlayback();
+    for (let action of actions) {
+        await actionPlayback.runAction(action);
+    }
+    actionPlayback.stop();
+}
+
 window.addEventListener('DOMContentLoaded', () => {
     let style = document.createElement('style');
     // Disable all css animation since it will cause screenshot inconsistent.
diff --git a/test/runTest/runtime/timeline.js b/test/runTest/runtime/timeline.js
new file mode 100644
index 0000000..aee6117
--- /dev/null
+++ b/test/runTest/runtime/timeline.js
@@ -0,0 +1,162 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+if (typeof __VST_PLAYBACK_SPEED__ === 'undefined') {
+    window.__VST_PLAYBACK_SPEED__ = 1;
+}
+const nativeRaf = window.requestAnimationFrame;
+const FIXED_FRAME_TIME = 16;
+const MAX_FRAME_TIME = 80;
+const TIMELINE_START = 1566458693300;
+
+window.__VST_TIMELINE_PAUSED__ = true;
+
+let realFrameStartTime = 0;
+
+/** Control timeline loop */
+let rafCbs = [];
+let frameIdx = 0;
+let timelineTime = 0;
+function timelineLoop() {
+    if (!__VST_TIMELINE_PAUSED__) {
+        realFrameStartTime = NativeDate.now();
+        frameIdx++;
+        timelineTime += FIXED_FRAME_TIME;
+        const currentRafCbs = rafCbs;
+        // Clear before calling the callbacks. raf may be registered in the callback
+        rafCbs = [];
+        currentRafCbs.forEach((cb) => {
+            cb();
+        });
+        flushTimeoutHandlers();
+        flushIntervalHandlers();
+    }
+    nativeRaf(timelineLoop);
+}
+nativeRaf(timelineLoop);
+
+window.requestAnimationFrame = function (cb) {
+    rafCbs.push(cb);
+};
+
+/** Mock setTimeout, setInterval */
+let timeoutHandlers = [];
+let intervalHandlers = [];
+
+let timeoutId = 1;
+let intervalId = 1;
+window.setTimeout = function (cb, timeout) {
+    const elapsedFrame = Math.ceil(Math.max(timeout || 0, 1) / FIXED_FRAME_TIME);
+    timeoutHandlers.push({
+        callback: cb,
+        id: timeoutId,
+        frame: frameIdx + elapsedFrame
+    });
+
+    return timeoutId++;
+}
+
+window.clearTimeout = function (id) {
+    const idx = timeoutHandlers.findIndex(handler => {
+        handler.id === id
+    });
+    if (idx >= 0) {
+        timeoutHandlers.splice(idx, 1);
+    }
+}
+
+function flushTimeoutHandlers() {
+    let newTimeoutHandlers = [];
+    for (let i = 0; i < timeoutHandlers.length; i++) {
+        const handler = timeoutHandlers[i];
+        if (handler.frame === frameIdx) {
+            handler.callback();
+        }
+        else {
+            newTimeoutHandlers.push(handler);
+        }
+    }
+    timeoutHandlers = newTimeoutHandlers;
+}
+
+window.setInterval = function (cb, interval) {
+    const intervalFrame = Math.ceil(Math.max(interval || 0, 1) / FIXED_FRAME_TIME);
+    intervalHandlers.push({
+        callback: cb,
+        id: intervalId,
+        intervalFrame,
+        frame: frameIdx + intervalFrame
+    })
+
+    return intervalId++;
+}
+
+window.clearInterval = function () {
+    const idx = intervalHandlers.findIndex(handler => {
+        handler.id === id
+    });
+    if (idx >= 0) {
+        intervalHandlers.splice(idx, 1);
+    }
+}
+
+function flushIntervalHandlers() {
+    for (let i = 0; i < intervalHandlers.length; i++) {
+        const handler = intervalHandlers[i];
+        if (handler.frame === frameIdx) {
+            handler.callback();
+            handler.frame += handler.intervalFrame;
+        }
+    }
+}
+
+/** Mock Date */
+
+const NativeDate = window.Date;
+
+const mockNow = function () {
+    // speed up
+    return TIMELINE_START + timelineTime * window.__VST_PLAYBACK_SPEED__;
+};
+function MockDate(...args) {
+    if (!args.length) {
+        return new NativeDate(mockNow());
+    }
+    else {
+        return new NativeDate(...args);
+    }
+}
+MockDate.prototype = Object.create(NativeDate.prototype);
+Object.setPrototypeOf(MockDate, NativeDate);
+MockDate.now = mockNow;
+
+window.Date = MockDate;
+
+
+export function start() {
+    window.__VST_TIMELINE_PAUSED__ = false;
+}
+
+export function pause() {
+    window.__VST_TIMELINE_PAUSED__ = true;
+}
+
+export function resume() {
+    window.__VST_TIMELINE_PAUSED__ = false;
+}
\ No newline at end of file

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org