| /* | |
| * 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', | |
| useCoarsePointer: 'auto', | |
| 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'); | |
| } | |
| 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.isExpectedNightly = runResult.expectedVersion.includes('-dev.'); | |
| this.runConfig.isActualNightly = runResult.actualVersion.includes('-dev.'); | |
| 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); | |
| } | |
| } | |
| }); | |
| 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, | |
| 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) { | |
| socket.emit('syncRunConfig', { | |
| runConfig: app.runConfig, | |
| // Override server config from URL. | |
| forceSet: true | |
| }); | |
| } | |
| }, { deep: true }); |