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