blob: f110fbd3f11187b51b1eb519c3efb3d108dc3ec5 [file] [log] [blame]
/*
* 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';
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;
}
else {
test.selected = false;
}
});
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) {}
const app = new Vue({
el: '#app',
data: {
fullTests: [],
currentTestName: urlParams.test || '',
searchString: '',
running: false,
allSelected: false,
lastSelectedIndex: -1,
expectedVersionsList: [],
actualVersionsList: [],
loadingVersion: false,
showIframeDialog: false,
previewIframeSrc: '',
previewTitle: '',
// List of all runs.
showRunsDialog: false,
testsRuns: [],
loadingTestsRuns: false,
pageInvisible: false,
runConfig: Object.assign({
sortBy: 'name',
isActualNightly: false,
isExpectedNightly: false,
actualVersion: 'local',
expectedVersion: null,
renderer: 'canvas',
threads: 4
}, urlRunConfig)
},
mounted() {
// 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.expectedVersionsList = res.expectedVersionsList;
this.actualVersionsList = res.actualVersionsList;
// Only assign on changed object to avoid unnecessary vue change.
Object.assign(this.runConfig, getChangedObject(this.runConfig, res.runConfig));
updateUrl();
});
setTimeout(() => {
this.scrollToCurrent();
}, 500);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'visible') {
this.pageInvisible = false;
socket.emit('syncRunConfig', {});
}
else {
this.pageInvisible = true;
}
});
},
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() {}
}
},
watch: {
'runConfig.sortBy'() {
setTimeout(() => {
this.scrollToCurrent();
}, 100);
},
'currentTestName'(newVal, oldVal) {
updateUrl();
}
},
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'];
if (ecVersion !== 'local') {
searches.push('__ECDIST__=' + ecVersion);
}
if (test.useSVG) {
searches.push('__RENDERER__=svg');
}
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.isExpectedNightly = runResult.expectedVersion.includes('-dev.');
this.runConfig.isActualNightly = runResult.actualVersion.includes('-dev.');
this.runConfig.renderer = runResult.renderer;
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);
}
}
});
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,
expectedVersion: app.runConfig.expectedVersion,
actualVersion: app.runConfig.actualVersion,
threads: app.runConfig.threads,
renderer: app.runConfig.renderer,
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) {
socket.emit('syncRunConfig', {
runConfig: app.runConfig,
// Override server config from URL.
forceSet: true
});
}
}, { deep: true });