662 lines
20 KiB
JavaScript
Generated
662 lines
20 KiB
JavaScript
Generated
/*
|
|
* 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.
|
|
*/
|
|
|
|
const socket = io('/client');
|
|
|
|
// const LOCAL_SAVE_KEY = 'visual-regression-testing-config';
|
|
|
|
let handlingSourceChange = false;
|
|
|
|
function getChangedObject(target, source) {
|
|
let changedObject = {};
|
|
Object.keys(source).forEach(key => {
|
|
if (target[key] !== source[key]) {
|
|
changedObject[key] = source[key];
|
|
}
|
|
});
|
|
return changedObject;
|
|
}
|
|
|
|
function parseParams(str) {
|
|
if (!str) {
|
|
return {};
|
|
}
|
|
const parts = str.split('&');
|
|
const params = {};
|
|
parts.forEach((part) => {
|
|
const kv = part.split('=');
|
|
params[kv[0]] = decodeURIComponent(kv[1]);
|
|
});
|
|
return params;
|
|
}
|
|
|
|
function assembleParams(paramsObj) {
|
|
const paramsArr = [];
|
|
Object.keys(paramsObj).forEach((key) => {
|
|
let val = paramsObj[key];
|
|
paramsArr.push(key + '=' + encodeURIComponent(val));
|
|
});
|
|
return paramsArr.join('&');
|
|
}
|
|
|
|
function processTestsData(tests, oldTestsData) {
|
|
tests.forEach((test, idx) => {
|
|
let passed = 0;
|
|
test.index = idx;
|
|
test.results.forEach(result => {
|
|
// Threshold?
|
|
if (result.diffRatio < 0.0001) {
|
|
passed++;
|
|
}
|
|
let timestamp = test.lastRun || 0;
|
|
result.diff = result.diff + '?' + timestamp;
|
|
result.actual = result.actual + '?' + timestamp;
|
|
result.expected = result.expected + '?' + timestamp;
|
|
});
|
|
test.percentage = passed === 0 ? 0 : Math.round(passed / test.results.length * 100);
|
|
if (test.percentage === 100) {
|
|
test.summary = 'success';
|
|
}
|
|
else if (test.percentage < 50) {
|
|
test.summary = 'exception';
|
|
}
|
|
else {
|
|
test.summary = 'warning';
|
|
}
|
|
|
|
// To simplify the condition in sort
|
|
test.actualErrors = test.actualErrors || [];
|
|
// Keep select status not change.
|
|
if (oldTestsData && oldTestsData[idx]) {
|
|
test.selected = oldTestsData[idx].selected;
|
|
// Keep source information
|
|
test.expectedSource = oldTestsData[idx].expectedSource;
|
|
test.actualSource = oldTestsData[idx].actualSource;
|
|
}
|
|
else {
|
|
test.selected = false;
|
|
// Initialize source information
|
|
test.expectedSource = app.runConfig.expectedSource;
|
|
test.actualSource = app.runConfig.actualSource;
|
|
}
|
|
});
|
|
return tests;
|
|
}
|
|
|
|
const urlRunConfig = {};
|
|
const urlParams = parseParams(window.location.search.substr(1))
|
|
|
|
// Save and restore
|
|
try {
|
|
const runConfig = JSON.parse(urlParams.runConfig);
|
|
Object.assign(urlRunConfig, runConfig);
|
|
}
|
|
catch (e) {}
|
|
|
|
function getVersionFromSource(source, versions, nightlyVersions) {
|
|
if (source === 'PR') {
|
|
// Default PR version can be empty since it needs to be manually selected
|
|
return '#';
|
|
}
|
|
else if (source === 'nightly') {
|
|
return nightlyVersions.length ? nightlyVersions[0] : null;
|
|
}
|
|
else if (source === 'local') {
|
|
return 'local';
|
|
}
|
|
else {
|
|
return versions.length ? versions[0] : null;
|
|
}
|
|
}
|
|
|
|
const app = new Vue({
|
|
el: '#app',
|
|
data: {
|
|
fullTests: [],
|
|
currentTestName: urlParams.test || '',
|
|
searchString: '',
|
|
running: false,
|
|
|
|
allSelected: false,
|
|
lastSelectedIndex: -1,
|
|
|
|
loadingVersion: false,
|
|
|
|
showIframeDialog: false,
|
|
previewIframeSrc: '',
|
|
previewTitle: '',
|
|
|
|
// List of all runs.
|
|
showRunsDialog: false,
|
|
testsRuns: [],
|
|
loadingTestsRuns: false,
|
|
|
|
pageInvisible: false,
|
|
|
|
versions: [],
|
|
nightlyVersions: [],
|
|
prVersions: [],
|
|
branchVersions: [],
|
|
|
|
runConfig: Object.assign({
|
|
sortBy: 'name',
|
|
actualVersion: 'local',
|
|
expectedVersion: null,
|
|
expectedSource: 'release',
|
|
actualSource: 'release',
|
|
renderer: 'canvas',
|
|
useCoarsePointer: 'auto',
|
|
threads: 4
|
|
}, urlRunConfig)
|
|
},
|
|
|
|
async mounted() {
|
|
// Add call to fetch branches
|
|
await this.fetchBranchVersions();
|
|
|
|
// Sync config from server when first time open
|
|
// or switching back
|
|
socket.emit('syncRunConfig', {
|
|
runConfig: this.runConfig,
|
|
// Override server config from URL.
|
|
forceSet: Object.keys(urlRunConfig).length > 0
|
|
});
|
|
socket.on('syncRunConfig_return', res => {
|
|
this.versions = res.versions || [];
|
|
this.nightlyVersions = res.nightlyVersions || [];
|
|
this.prVersions = res.prVersions || [];
|
|
|
|
// Only set versions if they haven't been manually set
|
|
handlingSourceChange = true;
|
|
this.$nextTick(() => {
|
|
if (!this.runConfig.expectedVersion) {
|
|
this.runConfig.expectedVersion = getVersionFromSource(
|
|
this.runConfig.expectedSource,
|
|
this.versions,
|
|
this.nightlyVersions
|
|
);
|
|
}
|
|
|
|
if (!this.runConfig.actualVersion) {
|
|
this.runConfig.actualVersion = getVersionFromSource(
|
|
this.runConfig.actualSource,
|
|
this.versions,
|
|
this.nightlyVersions
|
|
);
|
|
}
|
|
|
|
// Only apply other config changes from server
|
|
const configWithoutVersions = { ...res.runConfig };
|
|
delete configWithoutVersions.expectedVersion;
|
|
delete configWithoutVersions.actualVersion;
|
|
Object.assign(this.runConfig, getChangedObject(this.runConfig, configWithoutVersions));
|
|
|
|
handlingSourceChange = false;
|
|
updateUrl();
|
|
});
|
|
});
|
|
|
|
setTimeout(() => {
|
|
this.scrollToCurrent();
|
|
}, 500);
|
|
|
|
document.addEventListener("visibilitychange", () => {
|
|
if (document.visibilityState === 'visible') {
|
|
this.pageInvisible = false;
|
|
socket.emit('syncRunConfig', {});
|
|
}
|
|
else {
|
|
this.pageInvisible = true;
|
|
}
|
|
});
|
|
|
|
socket.on('run_error', err => {
|
|
app.$notify({
|
|
title: 'Error',
|
|
message: err.message,
|
|
type: 'error',
|
|
duration: 5000
|
|
});
|
|
app.running = false;
|
|
});
|
|
},
|
|
|
|
computed: {
|
|
finishedPercentage() {
|
|
let finishedCount = 0;
|
|
this.fullTests.forEach(test => {
|
|
if (test.status === 'finished') {
|
|
finishedCount++;
|
|
}
|
|
});
|
|
return +(finishedCount / this.fullTests.length * 100).toFixed(1) || 0;
|
|
},
|
|
|
|
tests() {
|
|
let sortFunc = this.runConfig.sortBy === 'name'
|
|
? (a, b) => a.name.localeCompare(b.name)
|
|
: (a, b) => {
|
|
if (a.actualErrors.length === b.actualErrors.length) {
|
|
if (a.percentage === b.percentage) {
|
|
return a.name.localeCompare(b.name);
|
|
}
|
|
else {
|
|
return a.percentage - b.percentage;
|
|
}
|
|
}
|
|
return b.actualErrors.length - a.actualErrors.length;
|
|
};
|
|
|
|
if (!this.searchString) {
|
|
// Not modify the original tests data.
|
|
return this.fullTests.slice().sort(sortFunc);
|
|
}
|
|
|
|
let searchString = this.searchString.toLowerCase();
|
|
return this.fullTests.filter(test => {
|
|
return test.name.toLowerCase().match(searchString);
|
|
}).sort(sortFunc);
|
|
},
|
|
|
|
selectedTests() {
|
|
// Only run visible tests.
|
|
return this.tests.filter(test => {
|
|
return test.selected;
|
|
});
|
|
},
|
|
unfinishedTests() {
|
|
return this.fullTests.filter(test => {
|
|
return test.status !== 'finished';
|
|
});
|
|
},
|
|
failedTests() {
|
|
return this.fullTests.filter(test => {
|
|
return test.status === 'finished' && test.summary !== 'success';
|
|
});
|
|
},
|
|
|
|
currentTest() {
|
|
let currentTest = this.fullTests.find(item => item.name === this.currentTestName);
|
|
if (!currentTest) {
|
|
currentTest = this.fullTests[0];
|
|
}
|
|
return currentTest;
|
|
},
|
|
|
|
currentTestUrl() {
|
|
return window.location.origin + '/test/' + this.currentTestName + '.html';
|
|
},
|
|
|
|
currentTestRecordUrl() {
|
|
return window.location.origin + '/test/runTest/recorder/index.html#' + this.currentTestName;
|
|
},
|
|
|
|
isSelectAllIndeterminate: {
|
|
get() {
|
|
if (!this.tests.length) {
|
|
return true;
|
|
}
|
|
return this.tests.some(test => {
|
|
return test.selected !== this.tests[0].selected;
|
|
});
|
|
},
|
|
set() {}
|
|
},
|
|
|
|
expectedVersionsList() {
|
|
switch (this.runConfig.expectedSource) {
|
|
case 'release':
|
|
return this.versions;
|
|
case 'nightly':
|
|
return this.nightlyVersions;
|
|
case 'PR':
|
|
return this.prVersions;
|
|
case 'local':
|
|
return ['local'];
|
|
default:
|
|
return [];
|
|
}
|
|
},
|
|
|
|
actualVersionsList() {
|
|
switch (this.runConfig.actualSource) {
|
|
case 'release':
|
|
return this.versions;
|
|
case 'nightly':
|
|
return this.nightlyVersions;
|
|
case 'PR':
|
|
return this.prVersions;
|
|
case 'local':
|
|
return ['local'];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
'runConfig.sortBy'() {
|
|
setTimeout(() => {
|
|
this.scrollToCurrent();
|
|
}, 100);
|
|
},
|
|
|
|
'currentTestName'(newVal, oldVal) {
|
|
updateUrl();
|
|
},
|
|
|
|
'runConfig.expectedSource': {
|
|
handler(newVal, oldVal) {
|
|
if (newVal === oldVal) {
|
|
return;
|
|
}
|
|
|
|
handlingSourceChange = true;
|
|
this.$nextTick(() => {
|
|
this.runConfig.expectedVersion = getVersionFromSource(
|
|
newVal,
|
|
this.versions,
|
|
this.nightlyVersions
|
|
);
|
|
handlingSourceChange = false;
|
|
});
|
|
},
|
|
deep: false
|
|
},
|
|
|
|
'runConfig.actualSource': {
|
|
handler(newVal, oldVal) {
|
|
if (newVal === oldVal) {
|
|
return;
|
|
}
|
|
|
|
handlingSourceChange = true;
|
|
this.$nextTick(() => {
|
|
this.runConfig.actualVersion = getVersionFromSource(
|
|
newVal,
|
|
this.versions,
|
|
this.nightlyVersions
|
|
);
|
|
handlingSourceChange = false;
|
|
});
|
|
},
|
|
deep: false
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
scrollToCurrent() {
|
|
const el = document.querySelector(`.test-list>li[title="${this.currentTestName}"]`);
|
|
if (el) {
|
|
el.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
}
|
|
},
|
|
|
|
changeTest(target, testName) {
|
|
if (!target.matches('input[type="checkbox"]') && !target.matches('.el-checkbox__inner')) {
|
|
app.currentTestName = testName;
|
|
}
|
|
},
|
|
toggleSort() {
|
|
this.runConfig.sortBy = this.runConfig.sortBy === 'name' ? 'percentage' : 'name';
|
|
},
|
|
handleSelectAllChange(val) {
|
|
// Only select filtered tests.
|
|
this.tests.forEach(test => {
|
|
test.selected = val;
|
|
});
|
|
this.isSelectAllIndeterminate = false;
|
|
},
|
|
handleSelect(idx) {
|
|
Vue.nextTick(() => {
|
|
this.lastSelectedIndex = idx;
|
|
});
|
|
},
|
|
handleShiftSelect(idx) {
|
|
if (this.lastSelectedIndex < 0) {
|
|
return;
|
|
}
|
|
let start = Math.min(this.lastSelectedIndex, idx);
|
|
let end = Math.max(this.lastSelectedIndex, idx);
|
|
let selected = !this.tests[idx].selected; // Will change
|
|
for (let i = start; i < end; i++) {
|
|
this.tests[i].selected = selected;
|
|
}
|
|
},
|
|
runSingleTest(testName, noHeadless) {
|
|
runTests([testName], noHeadless);
|
|
},
|
|
run(runTarget) {
|
|
let tests;
|
|
if (runTarget === 'selected') {
|
|
tests = this.selectedTests;
|
|
}
|
|
else if (runTarget === 'unfinished') {
|
|
tests = this.unfinishedTests;
|
|
}
|
|
else if (runTarget === 'failed') {
|
|
tests = this.failedTests;
|
|
}
|
|
else {
|
|
tests = this.fullTests;
|
|
}
|
|
runTests(tests.map(test => test.name), false);
|
|
},
|
|
stopTests() {
|
|
this.running = false;
|
|
socket.emit('stop');
|
|
},
|
|
|
|
preview(test, version) {
|
|
let searches = [];
|
|
|
|
let ecVersion = test[version + 'Version'];
|
|
let ecSource = test[version + 'Source'];
|
|
if (ecVersion !== 'local') {
|
|
let distPath = ecSource === 'PR'
|
|
? 'pr-' + ecVersion.replace(/^#/, '')
|
|
: ecVersion;
|
|
searches.push('__ECDIST__=' + distPath);
|
|
}
|
|
if (test.useSVG) {
|
|
searches.push('__RENDERER__=svg');
|
|
}
|
|
if (test.useCoarsePointer) {
|
|
searches.push('__COARSE__POINTER__=true');
|
|
}
|
|
let src = test.fileUrl;
|
|
if (searches.length) {
|
|
src = src + '?' + searches.join('&');
|
|
}
|
|
this.previewIframeSrc = `../../${src}`;
|
|
this.previewTitle = src;
|
|
this.showIframeDialog = true;
|
|
},
|
|
|
|
showAllTestsRuns() {
|
|
this.showRunsDialog = true;
|
|
this.loadingTestsRuns = true;
|
|
socket.emit('getAllTestsRuns');
|
|
},
|
|
|
|
switchTestsRun(runResult) {
|
|
this.runConfig.expectedVersion = runResult.expectedVersion;
|
|
this.runConfig.actualVersion = runResult.actualVersion;
|
|
// TODO
|
|
this.runConfig.renderer = runResult.renderer;
|
|
this.runConfig.useCoarsePointer = runResult.useCoarsePointer;
|
|
|
|
this.showRunsDialog = false;
|
|
},
|
|
|
|
genTestsRunReport(runResult) {
|
|
socket.emit('genTestsRunReport', runResult);
|
|
},
|
|
|
|
delTestsRun(runResult) {
|
|
app.$confirm('Are you sure to delete this run?', 'Warn', {
|
|
confirmButtonText: 'Yes',
|
|
cancelButtonText: 'No',
|
|
center: true
|
|
}).then(value => {
|
|
const idx = this.testsRuns.indexOf(runResult);
|
|
if (idx >= 0) {
|
|
this.testsRuns.splice(idx, 1);
|
|
}
|
|
socket.emit('delTestsRun', {
|
|
id: runResult.id
|
|
});
|
|
}).catch(() => {});
|
|
},
|
|
|
|
open(url, target) {
|
|
window.open(url, target);
|
|
},
|
|
|
|
async fetchBranchVersions() {
|
|
try {
|
|
const response = await fetch('https://api.github.com/repos/apache/echarts/branches?per_page=100');
|
|
const branches = await response.json();
|
|
this.branchVersions = branches.map(branch => branch.name);
|
|
} catch (error) {
|
|
console.error('Failed to fetch branches:', error);
|
|
this.branchVersions = [];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
function runTests(tests, noHeadless) {
|
|
if (!tests.length) {
|
|
app.$notify({
|
|
title: 'No test selected.',
|
|
position: 'top-right'
|
|
});
|
|
return;
|
|
}
|
|
if (!app.runConfig.expectedVersion || !app.runConfig.actualVersion) {
|
|
app.$notify({
|
|
title: 'No echarts version selected.',
|
|
position: 'top-right'
|
|
});
|
|
return;
|
|
}
|
|
app.running = true;
|
|
socket.emit('run', {
|
|
tests,
|
|
expectedSource: app.runConfig.expectedSource,
|
|
expectedVersion: app.runConfig.expectedVersion,
|
|
actualSource: app.runConfig.actualSource,
|
|
actualVersion: app.runConfig.actualVersion,
|
|
threads: app.runConfig.threads,
|
|
renderer: app.runConfig.renderer,
|
|
useCoarsePointer: app.runConfig.useCoarsePointer,
|
|
noHeadless,
|
|
replaySpeed: noHeadless ? 5 : 5
|
|
});
|
|
}
|
|
|
|
|
|
socket.on('connect', () => {
|
|
console.log('Connected');
|
|
});
|
|
|
|
let firstUpdate = true;
|
|
socket.on('update', msg => {
|
|
app.$el.style.display = 'block';
|
|
|
|
// let hasFinishedTest = !!msg.tests.find(test => test.status === 'finished');
|
|
// if (!hasFinishedTest && firstUpdate) {
|
|
// app.$confirm('You haven\'t run any test on these two versions yet!<br />Do you want to start now?', 'Tip', {
|
|
// confirmButtonText: 'Yes',
|
|
// cancelButtonText: 'No',
|
|
// dangerouslyUseHTMLString: true,
|
|
// center: true
|
|
// }).then(value => {
|
|
// runTests(msg.tests.map(test => test.name));
|
|
// }).catch(() => {});
|
|
// }
|
|
|
|
// TODO
|
|
app.running = !!msg.running;
|
|
app.fullTests = processTestsData(msg.tests, app.fullTests);
|
|
|
|
if (!app.currentTestName) {
|
|
app.currentTestName = app.fullTests[0].name;
|
|
}
|
|
|
|
firstUpdate = false;
|
|
});
|
|
socket.on('finish', res => {
|
|
app.$notify({
|
|
type: 'success',
|
|
title: `${res.count} test complete`,
|
|
message: `Cost: ${(res.time / 1000).toFixed(1)} s. Threads: ${res.threads}`,
|
|
position: 'top-right',
|
|
duration: 8000
|
|
});
|
|
console.log(`${res.count} test complete, Cost: ${(res.time / 1000).toFixed(1)} s. Threads: ${res.threads}`);
|
|
app.running = false;
|
|
});
|
|
socket.on('abort', res => {
|
|
app.$notify({
|
|
type: 'info',
|
|
title: `Aborted`,
|
|
duration: 4000
|
|
});
|
|
app.running = false;
|
|
});
|
|
|
|
socket.on('getAllTestsRuns_return', res => {
|
|
app.testsRuns = res.runs;
|
|
app.loadingTestsRuns = false;
|
|
});
|
|
socket.on('genTestsRunReport_return', res => {
|
|
window.open(res.reportUrl, '_blank');
|
|
});
|
|
|
|
function updateUrl() {
|
|
const searchUrl = assembleParams({
|
|
test: app.currentTestName,
|
|
runConfig: JSON.stringify(app.runConfig)
|
|
});
|
|
history.pushState({}, '', location.pathname + '?' + searchUrl);
|
|
}
|
|
|
|
// Only update url when version is changed.
|
|
app.$watch('runConfig', (newVal, oldVal) => {
|
|
if (!app.pageInvisible && !handlingSourceChange) {
|
|
socket.emit('syncRunConfig', {
|
|
runConfig: app.runConfig,
|
|
forceSet: true
|
|
}, err => {
|
|
if (err) {
|
|
app.$notify({
|
|
title: 'Error',
|
|
message: err,
|
|
type: 'error',
|
|
duration: 5000
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}, { deep: true }); |