| /* |
| * 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('/recorder'); |
| |
| function getNthChild(el) { |
| let i = 1; |
| let elTagName = el.tagName; |
| let elClassName = el.className; |
| let nthSameClass = 1; |
| while (el.previousSibling) { |
| el = el.previousSibling; |
| if (el.tagName === elTagName && el.className === elClassName) { |
| nthSameClass++; |
| } |
| i++; |
| } |
| return [i, nthSameClass]; |
| } |
| function getUniqueSelector(el) { |
| if (el.tagName.toLowerCase() === 'body') { |
| return ''; |
| } |
| let selector = ''; |
| if (el.id) { |
| // id has highest priority. |
| return el.id; |
| } |
| else { |
| selector = el.tagName.toLowerCase(); |
| for (let className of el.classList) { |
| selector += '.' + className; |
| } |
| let [idx, nthSameClass] = getNthChild(el); |
| if (nthSameClass > 1) { |
| selector += `:nth-child(${idx})`; |
| } |
| } |
| let parentSelector = el.parentNode && getUniqueSelector(el.parentNode); |
| if (parentSelector) { |
| selector = parentSelector + '>' + selector; |
| } |
| return selector; |
| } |
| |
| const app = new Vue({ |
| el: '#app', |
| data: { |
| tests: [], |
| |
| currentTestName: '', |
| actions: [], |
| currentAction: null, |
| recordingAction: null, |
| |
| recordingTimeElapsed: 0, |
| |
| config: { |
| screenshotAfterMouseUp: true, |
| screenshotDelay: 400 |
| }, |
| |
| drawerVisible: true |
| }, |
| computed: { |
| url() { |
| if (!this.currentTestName) { |
| return ''; |
| } |
| return window.location.origin + '/test/' + this.currentTestName + '.html'; |
| } |
| }, |
| methods: { |
| refreshPage() { |
| const $iframe = getIframe(); |
| if ($iframe.contentWindow) { |
| $iframe.contentWindow.location.reload(); |
| } |
| }, |
| newAction() { |
| this.currentAction = { |
| name: 'Action ' + (this.actions.length + 1), |
| ops: [] |
| }; |
| this.actions.push(this.currentAction); |
| }, |
| select(actionName) { |
| this.currentAction = this.actions.find(action => { |
| return action.name === actionName; |
| }); |
| if (this.currentAction) { |
| const $iframe = getIframe(); |
| if ($iframe.contentWindow) { |
| $iframe.contentWindow.scrollTo({ |
| left: this.currentAction.scrollX, |
| top: this.currentAction.scrollY, |
| behavior: 'smooth' |
| }); |
| } |
| } |
| }, |
| |
| doDelete(actionName) { |
| app.$confirm('Aure you sure?', 'Delete this action', { |
| confirmButtonText: 'Yes', |
| cancelButtonText: 'No', |
| type: 'warning' |
| }).then(() => { |
| this.deletePopoverVisible = false; |
| let idx = _.findIndex(this.actions, action => action.name === actionName); |
| if (idx >= 0) { |
| if (this.currentAction === this.actions[idx]) { |
| this.currentAction = this.actions[idx + 1] || this.actions[idx - 1]; |
| } |
| this.actions.splice(idx, 1); |
| saveData(); |
| } |
| }).catch(e => {}); |
| }, |
| |
| clearOps(actionName) { |
| app.$confirm('Aure you sure?', 'Clear this action', { |
| confirmButtonText: 'Yes', |
| cancelButtonText: 'No', |
| type: 'warning' |
| }).then(() => { |
| this.deletePopoverVisible = false; |
| let action = this.actions.find(action => action.name === actionName); |
| if (action) { |
| action.ops = []; |
| } |
| saveData(); |
| }).catch(e => {}); |
| }, |
| |
| run() { |
| socket.emit('runSingle', { |
| testName: app.currentTestName |
| }); |
| } |
| } |
| }); |
| |
| let time = Date.now(); |
| function updateTime() { |
| let dTime = Date.now() - time; |
| time += dTime; |
| if (app.recordingAction) { |
| app.recordingTimeElapsed += dTime; |
| } |
| requestAnimationFrame(updateTime); |
| } |
| requestAnimationFrame(updateTime); |
| |
| function getIframe() { |
| return document.body.querySelector('#test-view'); |
| } |
| |
| function saveData() { |
| // Save |
| if (app.currentTestName) { |
| socket.emit('saveActions', { |
| testName: app.currentTestName, |
| actions: app.actions |
| }); |
| |
| let test = app.tests.find(testOpt => testOpt.name === app.currentTestName); |
| test.actions = app.actions.length; |
| } |
| } |
| |
| function getEventTime() { |
| return Date.now() - app.recordingAction.timestamp; |
| } |
| function notify(title, message) { |
| app.$notify.info({ |
| title, |
| message, |
| position: 'top-left', |
| customClass: 'op-notice' |
| }); |
| } |
| |
| function keyboardRecordingHandler(e) { |
| if (e.keyCode === 82 && e.altKey) { |
| let $iframe = getIframe(); |
| if (!app.recordingAction) { |
| // Create a new action if currentAction has ops. |
| if (!app.currentAction || app.currentAction.ops.length > 0) { |
| app.newAction(); |
| } |
| |
| app.recordingAction = app.currentAction; |
| if (app.recordingAction) { |
| app.recordingAction.scrollY = $iframe.contentWindow.scrollY; |
| app.recordingAction.scrollX = $iframe.contentWindow.scrollX; |
| app.recordingAction.timestamp = Date.now(); |
| |
| app.recordingTimeElapsed = 0; |
| } |
| } |
| else { |
| if (app.recordingAction && |
| (app.recordingAction.scrollY !== $iframe.contentWindow.scrollY |
| || app.recordingAction.scrollX !== $iframe.contentWindow.scrollX)) { |
| app.recordingAction.ops = []; |
| app.$alert('You can\'t scroll the page during the action recording. Please create another action after scrolled to the next demo.', 'Recording Fail', { |
| confirmButtonText: 'Get!' |
| }); |
| |
| } |
| else { |
| saveData(); |
| } |
| app.recordingAction = null; |
| } |
| // Get scroll |
| } |
| else if (e.keyCode === 83 && e.altKey) { |
| if (app.recordingAction) { |
| app.recordingAction.ops.push({ |
| type: 'screenshot', |
| time: getEventTime() |
| }); |
| notify('screenshot', ''); |
| } |
| } |
| } |
| |
| function sign(value) { |
| return value > 0 ? 1 : -1; |
| } |
| |
| function recordIframeEvents(iframe, app) { |
| let innerDocument = iframe.contentWindow.document; |
| |
| |
| function addMouseOp(type, e) { |
| if (app.recordingAction) { |
| let time = getEventTime(); |
| let op = { |
| type, |
| time: time, |
| x: e.clientX, |
| y: e.clientY |
| }; |
| app.recordingAction.ops.push(op); |
| if (type === 'mousewheel') { |
| // TODO Sreenshot after mousewheel? |
| op.deltaY = e.deltaY; |
| |
| // In a reversed direction. |
| // When creating WheelEvent, the sign of wheelData and deltaY are same |
| if (sign(e.wheelDelta) !== sign(e.deltaY)) { |
| op.deltaY = -op.deltaY; |
| } |
| } |
| if (type === 'mouseup' && app.config.screenshotAfterMouseUp) { |
| // Add a auto screenshot after mouseup |
| app.recordingAction.ops.push({ |
| time: time + 1, |
| delay: app.config.screenshotDelay, |
| type: 'screenshot-auto' |
| }); |
| } |
| notify(type, `(x: ${e.clientX}, y: ${e.clientY})`); |
| } |
| } |
| |
| innerDocument.addEventListener('keyup', keyboardRecordingHandler); |
| |
| let preventRecordingFollowingMouseEvents = false; |
| innerDocument.body.addEventListener('mousemove', _.throttle(e => { |
| if (!preventRecordingFollowingMouseEvents) { |
| addMouseOp('mousemove', e); |
| } |
| }, 200), true); |
| innerDocument.body.addEventListener('mousedown', e => { |
| // Can't recording mouse event on select. |
| // So just prevent it and add a specific 'select' change event. |
| if (e.target.tagName.toLowerCase() === 'select') { |
| preventRecordingFollowingMouseEvents = true; |
| return; |
| } |
| addMouseOp('mousedown', e); |
| }, true); |
| innerDocument.body.addEventListener('mouseup', e => { |
| if (!preventRecordingFollowingMouseEvents) { |
| addMouseOp('mouseup', e); |
| } |
| preventRecordingFollowingMouseEvents = false; |
| }, true); |
| iframe.contentWindow.addEventListener('mousewheel', e => { |
| addMouseOp('mousewheel', e); |
| }, true); |
| |
| |
| innerDocument.body.addEventListener('change', e => { |
| if (app.recordingAction) { |
| let selector = getUniqueSelector(e.target); |
| let time = getEventTime(); |
| let commonData = { |
| type: 'valuechange', |
| selector, |
| value: e.target.value, |
| time: time |
| }; |
| if (e.target.tagName.toLowerCase() === 'select') { |
| commonData.target = 'select'; |
| notify('valuechange', `select(${commonData.value})`); |
| } |
| if (commonData.target) { |
| app.recordingAction.ops.push(commonData); |
| |
| if (app.config.screenshotAfterMouseUp) { |
| // Add a auto screenshot after mouseup |
| app.recordingAction.ops.push({ |
| time: time + 1, |
| delay: app.config.screenshotDelay, |
| type: 'screenshot-auto' |
| }); |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| function init() { |
| app.$el.style.display = 'block'; |
| |
| document.addEventListener('keyup', keyboardRecordingHandler); |
| |
| socket.on('updateActions', data => { |
| if (data.testName === app.currentTestName) { |
| app.actions = data.actions; |
| if (!app.currentAction) { |
| app.currentAction = app.actions[0]; |
| } |
| } |
| }); |
| socket.on('getTests', ({tests}) => { |
| app.tests = tests; |
| }); |
| |
| let $iframe = getIframe(); |
| $iframe.onload = () => { |
| recordIframeEvents($iframe, app); |
| }; |
| |
| function updateTestHash() { |
| app.currentTestName = window.location.hash.slice(1); |
| // Reset |
| app.actions = []; |
| app.currentAction = null; |
| app.recordingAction = null; |
| |
| socket.emit('changeTest', {testName: app.currentTestName}); |
| |
| } |
| updateTestHash(); |
| window.addEventListener('hashchange', updateTestHash); |
| } |
| |
| socket.on('connect', () => { |
| console.log('Connected'); |
| init(); |
| }); |