| /* |
| * 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 shouldShowMarkAsExpected(test, expectedSource, expectedVersion) { |
| if (expectedSource === 'release' && (expectedVersion !== test.expectedVersion) |
| || !test.markedAsExpected |
| ) { |
| return false; |
| } |
| if (expectedSource !== 'release' && test.markedAsExpected.length > 0) { |
| return true; |
| } |
| |
| for (let i = 0; i < test.markedAsExpected.length; i++) { |
| if (test.markedAsExpected[i].lastVersion === expectedVersion) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| function processTestsData(tests, oldTestsData, expectedSource, expectedVersion) { |
| 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 (shouldShowMarkAsExpected(test, expectedSource, expectedVersion)) { |
| test.summary = 'markedAsExpected'; |
| } |
| else if (test.percentage < 50) { |
| test.summary = 'exception'; |
| } |
| else { |
| test.summary = 'warning'; |
| } |
| |
| // To simplify the condition in sort |
| test.actualErrors = test.actualErrors || []; |
| |
| // Format date for marks display |
| if (test.markedAsExpected) { |
| test.markedAsExpected.forEach(mark => { |
| if (mark.markTime && !mark.markTimeFormatted) { |
| mark.markTimeFormatted = formatDate(mark.markTime); |
| } |
| }); |
| } |
| |
| // 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; |
| |
| // If old data has markedAsExpected but new data doesn't, keep the old data. |
| if (oldTestsData[idx].markedAsExpected && !test.markedAsExpected) { |
| test.markedAsExpected = oldTestsData[idx].markedAsExpected; |
| } |
| |
| // Ensure markedAsExpected is null when it's empty |
| if (test.markedAsExpected && Array.isArray(test.markedAsExpected) && test.markedAsExpected.length === 0) { |
| test.markedAsExpected = null; |
| } |
| } |
| 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: null, |
| expectedVersion: null, |
| expectedSource: 'release', |
| actualSource: 'local', |
| renderer: 'canvas', |
| useCoarsePointer: 'auto', |
| threads: 4, |
| theme: 'none' |
| }, urlRunConfig), |
| |
| userMeta: null, |
| |
| // Mark as expected form values |
| linkValue: '', |
| commentValue: '', |
| selectedType: 'New Feature' |
| }, |
| |
| async mounted() { |
| socket.on('userMeta', meta => { |
| this.userMeta = meta; |
| }); |
| socket.emit('getUserMeta'); |
| |
| socket.on('updateTestsList', testsList => { |
| this.updateTestsList(testsList); |
| }); |
| |
| // 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.summary === 'markedAsExpected' && b.summary !== 'markedAsExpected') { |
| return -1; |
| } |
| if (a.summary !== 'markedAsExpected' && b.summary === 'markedAsExpected') { |
| return 1; |
| } |
| 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: { |
| validateMarkExpected(lastVersion) { |
| if (this.runConfig.expectedSource !== 'release') { |
| return { |
| valid: true, |
| reason: 'Valid' |
| } |
| } |
| if (lastVersion !== this.runConfig.expectedVersion) { |
| return { |
| valid: false, |
| reason: 'Only valid when the expected version is ' + lastVersion |
| }; |
| } |
| return { |
| valid: true, |
| reason: 'Valid' |
| }; |
| }, |
| |
| getMarkRowClass(data) { |
| if (this.validateMarkExpected(data.row.lastVersion).valid) { |
| return ''; |
| } |
| return 'faded-row'; |
| }, |
| |
| 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); |
| }, |
| markAsExpected() { |
| if (!this.currentTest) { |
| return; |
| } |
| this.showMarkAsExpectedDialog(); |
| }, |
| deleteMark(mark) { |
| this.$confirm('Are you sure you want to delete this mark? This action cannot be undone.', 'Warning', { |
| confirmButtonText: 'Delete', |
| cancelButtonText: 'Cancel', |
| type: 'warning' |
| }).then(() => { |
| socket.emit('deleteMark', { |
| testName: this.currentTest.name, |
| markTime: mark.markTime |
| }, (err) => { |
| if (err) { |
| this.$message.error('Failed to delete: ' + err); |
| return; |
| } |
| |
| if (this.currentTest && this.currentTest.markedAsExpected) { |
| if (Array.isArray(this.currentTest.markedAsExpected)) { |
| this.currentTest.markedAsExpected = this.currentTest.markedAsExpected.filter( |
| item => item.markTime !== mark.markTime |
| ); |
| |
| if (this.currentTest.markedAsExpected.length === 0) { |
| this.currentTest.markedAsExpected = null; |
| } |
| } else if (this.currentTest.markedAsExpected.markTime === mark.markTime) { |
| this.currentTest.markedAsExpected = null; |
| } |
| } |
| |
| this.$message.success('Mark deleted successfully'); |
| }); |
| }).catch(() => { |
| // User canceled |
| }); |
| }, |
| showMarkAsExpectedDialog() { |
| // Only initialize identity fields from user meta |
| let markedBy = this.userMeta?.myGitHubId || ''; |
| let lastVersion = this.userMeta?.lastEChartsVersion || ''; |
| |
| // Create our own custom dialog without using $prompt |
| const h = this.$createElement; |
| |
| this.$msgbox({ |
| title: 'Mark Difference as Expected', |
| message: h('div', { class: 'mark-expected-form' }, [ |
| h('div', { class: 'form-item' }, [ |
| h('div', { class: 'form-label' }, [ |
| 'Your GitHub ID: ', |
| h('span', { style: { color: '#F56C6C' } }, '*') |
| ]), |
| h('input', { |
| class: 'el-input__inner', |
| attrs: { |
| type: 'text', |
| placeholder: '' |
| }, |
| domProps: { |
| value: markedBy |
| }, |
| on: { |
| input: (e) => { |
| markedBy = e.target.value; |
| if (!this.userMeta) { |
| this.userMeta = { |
| myGitHubId: markedBy |
| }; |
| } |
| else { |
| this.userMeta.myGitHubId = markedBy; |
| } |
| } |
| } |
| }) |
| ]), |
| h('div', { class: 'form-item' }, [ |
| h('div', { class: 'form-label' }, [ |
| 'Last Version: ', |
| h('span', { style: { color: '#F56C6C' } }, '*') |
| ]), |
| h('input', { |
| class: 'el-input__inner', |
| attrs: { |
| type: 'text', |
| placeholder: '5.6.0' |
| }, |
| domProps: { |
| value: lastVersion |
| }, |
| on: { |
| input: (e) => { |
| lastVersion = e.target.value; |
| if (!this.userMeta) { |
| this.userMeta = { |
| lastEChartsVersion: lastVersion |
| }; |
| } |
| else { |
| this.userMeta.lastEChartsVersion = lastVersion; |
| } |
| } |
| } |
| }) |
| ]), |
| h('div', { class: 'form-item type-selection' }, [ |
| h('div', { class: 'form-label' }, [ |
| 'Type: ', |
| h('span', { style: { color: '#F56C6C' } }, '*') |
| ]), |
| h('div', { class: 'radio-group' }, [ |
| h('label', { class: 'radio-label' }, [ |
| h('input', { |
| attrs: { |
| type: 'radio', |
| name: 'mark-type', |
| value: 'New Feature', |
| checked: this.selectedType === 'New Feature' |
| }, |
| on: { |
| change: (e) => { |
| this.selectedType = e.target.value; |
| } |
| } |
| }), |
| ' New Feature' |
| ]), |
| h('label', { class: 'radio-label' }, [ |
| h('input', { |
| attrs: { |
| type: 'radio', |
| name: 'mark-type', |
| value: 'Bug Fixing', |
| checked: this.selectedType === 'Bug Fixing' |
| }, |
| on: { |
| change: (e) => { |
| this.selectedType = e.target.value; |
| } |
| } |
| }), |
| ' Bug Fixing' |
| ]), |
| h('label', { class: 'radio-label' }, [ |
| h('input', { |
| attrs: { |
| type: 'radio', |
| name: 'mark-type', |
| value: 'Others', |
| checked: this.selectedType === 'Others' |
| }, |
| on: { |
| change: (e) => { |
| this.selectedType = e.target.value; |
| } |
| } |
| }), |
| ' Others' |
| ]) |
| ]) |
| ]), |
| h('div', { class: 'form-item' }, [ |
| h('div', { class: 'form-label' }, 'PR or commit link (optional but recommended):'), |
| h('input', { |
| class: 'el-input__inner link-input', |
| attrs: { |
| type: 'text', |
| placeholder: 'https://github.com/apache/echarts/pull/xxx' |
| }, |
| domProps: { |
| value: this.linkValue |
| }, |
| on: { |
| input: (e) => { |
| this.linkValue = e.target.value; |
| } |
| } |
| }) |
| ]), |
| h('div', { class: 'form-item' }, [ |
| h('div', { class: 'form-label' }, 'Comment (optional):'), |
| h('textarea', { |
| class: 'el-textarea__inner comment-input', |
| style: { minHeight: '60px' }, |
| domProps: { |
| value: this.commentValue |
| }, |
| on: { |
| input: (e) => { |
| this.commentValue = e.target.value; |
| } |
| } |
| }) |
| ]) |
| ]), |
| showCancelButton: true, |
| confirmButtonText: 'Confirm', |
| cancelButtonText: 'Cancel', |
| customClass: 'mark-as-expected-dialog', |
| beforeClose: (action, instance, done) => { |
| if (action === 'confirm') { |
| if (!this.selectedType) { |
| this.$message({ |
| message: 'Please select a type', |
| type: 'error' |
| }); |
| // Don't close the dialog |
| done(false); |
| return; |
| } |
| |
| if (!markedBy) { |
| this.$message({ |
| message: 'Please enter your GitHub ID', |
| type: 'error' |
| }); |
| done(false); |
| return; |
| } |
| |
| if (!lastVersion) { |
| this.$message({ |
| message: 'Please enter the last version', |
| type: 'error' |
| }); |
| done(false); |
| return; |
| } |
| |
| if (this.linkValue && !(this.linkValue.startsWith('http://') || this.linkValue.startsWith('https://'))) { |
| this.$message({ |
| message: 'Link must start with http:// or https://', |
| type: 'error' |
| }); |
| done(false); |
| return; |
| } |
| |
| // Use a variable to track if the callback has been called |
| let callbackCalled = false; |
| |
| // Create a callback function with proper error handling |
| const handleMarkAsExpectedResponse = function(err) { |
| callbackCalled = true; |
| |
| if (err) { |
| app.$notify({ |
| title: 'Error', |
| message: err, |
| type: 'error', |
| duration: 5000 |
| }); |
| done(false); |
| return; |
| } |
| |
| app.$notify({ |
| title: 'Success', |
| message: 'Marked as expected', |
| type: 'success', |
| duration: 3000 |
| }); |
| done(); |
| }; |
| |
| // Save user meta info for future use |
| socket.emit('saveUserMeta', { |
| myGitHubId: markedBy, |
| lastEChartsVersion: lastVersion |
| }); |
| |
| socket.emit('markAsExpected', { |
| testName: this.currentTest.name, |
| link: this.linkValue, |
| comment: this.commentValue, |
| type: this.selectedType, |
| markedBy: markedBy, |
| lastVersion: lastVersion, |
| markTime: new Date().getTime() |
| }, handleMarkAsExpectedResponse); |
| |
| // Add a timeout to ensure the dialog closes even if callback is not called |
| setTimeout(() => { |
| if (!callbackCalled) { |
| console.error('Socket callback was not called after 5 seconds'); |
| app.$notify({ |
| title: 'Warning', |
| message: 'Operation may not have completed properly', |
| type: 'warning', |
| duration: 5000 |
| }); |
| done(); |
| } |
| }, 5000); |
| } else { |
| done(); |
| } |
| } |
| }); |
| }, |
| 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'); |
| } |
| if (test.theme && test.theme !== 'none' && ecVersion === 'local') { |
| searches.push('__THEME__=' + test.theme); |
| } |
| 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.runConfig.theme = runResult.theme || 'none'; |
| |
| 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(); |
| if (branches.length > 0) { |
| this.branchVersions = branches.map(branch => branch.name); |
| } |
| else { |
| this.branchVersions = []; |
| } |
| } catch (error) { |
| console.error('Failed to fetch branches:', error); |
| this.branchVersions = []; |
| } |
| }, |
| |
| updateTestsList(testsList) { |
| this.fullTests = processTestsData(testsList, this.fullTests, this.runConfig.expectedSource, this.runConfig.expectedVersion); |
| |
| if (!this.currentTestName && this.fullTests.length > 0) { |
| this.currentTestName = this.fullTests[0].name; |
| } |
| |
| if (this.currentTestName) { |
| const currentTest = this.fullTests.find(test => test.name === this.currentTestName); |
| if (!currentTest && this.fullTests.length > 0) { |
| this.currentTestName = this.fullTests[0].name; |
| } |
| } |
| }, |
| } |
| }); |
| |
| 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, |
| theme: app.runConfig.theme, |
| 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, |
| app.runConfig.expectedSource, |
| app.runConfig.expectedVersion |
| ); |
| |
| 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 }); |
| |
| function formatDate(timestamp) { |
| const date = new Date(timestamp); |
| const pad = num => (num < 10 ? '0' + num : num); |
| return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; |
| } |