|  | /* | 
|  | * 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 }); |