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