提交 05a0c7d1 编写于 作者: P pissang

test(visual): fully control the replay timeline.

上级 210f650d
......@@ -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);
......@@ -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();
......
......@@ -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() {
......
......@@ -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;
}
......
/*
* 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;
......@@ -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.
......
/*
* 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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册