blob: 66f18dee73a5c91d98899fc988350d0743f13755 [file] [log] [blame] [edit]
/*
* 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();
});