chore: update prettier defaults and use latest version of finder

This commit is contained in:
Ignacio Anaya
2021-06-10 11:06:10 -03:00
parent 97d94f276b
commit a145aeffb4
31 changed files with 1613 additions and 1596 deletions

View File

@@ -1,15 +1,31 @@
module.exports = {
root: true,
env: {
node: true,
webextensions: true
webextensions: true,
},
extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/prettier"],
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/prettier'],
parserOptions: {
parser: "babel-eslint"
parser: 'babel-eslint',
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
}
};
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
jest: true,
},
},
],
}

6
.prettierrc.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
}

View File

@@ -1,3 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"]
};
presets: ['@vue/cli-plugin-babel/preset'],
}

View File

@@ -1,7 +1,6 @@
{
"name": "headless-recorder",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service build --mode development --watch",
"build": "vue-cli-service build",
@@ -9,7 +8,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@medv/finder": "1.1.2",
"@medv/finder": "2.0.0",
"@tailwindcss/postcss7-compat": "2.0.2",
"@vueuse/core": "4.0.8",
"autoprefixer": "9",
@@ -17,31 +16,31 @@
"postcss": "7",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@2.0.2",
"vue": "3.0.6",
"vue3-highlightjs": "1.0.5"
"vue3-highlightjs": "^1.0.5"
},
"devDependencies": {
"@testing-library/jest-dom": "5.12.0",
"@vue/cli-plugin-babel": "4.5.0",
"@vue/cli-plugin-eslint": "4.5.0",
"@vue/cli-plugin-unit-jest": "4.5.12",
"@vue/cli-service": "4.5.0",
"@vue/compiler-sfc": "3.0.0",
"@vue/eslint-config-prettier": "6.0.0",
"@vue/test-utils": "2.0.0-rc.6",
"babel-eslint": "10.1.0",
"eslint": "6.7.2",
"eslint-plugin-prettier": "3.1.3",
"eslint-plugin-vue": "7.10.0",
"jest": "26.6.3",
"jest-vue-preprocessor": "1.7.1",
"node-sass": "5.0.0",
"playwright": "1.10.0",
"prettier": "1.19.1",
"puppeteer": "9.0.0",
"sass-loader": "10.1.1",
"typescript": "3.9.3",
"vue-cli-plugin-browser-extension": "0.25.1",
"vue-cli-plugin-tailwind": "2.0.6",
"vue-jest": "5.0.0-alpha.9"
"@testing-library/jest-dom": "^5.12.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-unit-jest": "^4.5.12",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^2.0.0-rc.6",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0-0",
"jest": "^26.6.3",
"jest-vue-preprocessor": "^1.7.1",
"node-sass": "^5.0.0",
"playwright": "^1.10.0",
"prettier": "^1.19.1",
"puppeteer": "^9.0.0",
"sass-loader": "^10.1.1",
"typescript": "~3.9.3",
"vue-cli-plugin-browser-extension": "~0.25.1",
"vue-cli-plugin-tailwind": "~2.0.6",
"vue-jest": "^5.0.0-0"
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
autoprefixer: {},
},
}

View File

@@ -1,10 +1,10 @@
import puppeteer from "puppeteer";
import { launchPuppeteerWithExtension } from "./helpers";
import puppeteer from 'puppeteer'
import { launchPuppeteerWithExtension } from './helpers'
describe("install", () => {
test("it installs the extension", async () => {
const browser = await launchPuppeteerWithExtension(puppeteer);
expect(browser).toBeTruthy();
browser.close();
}, 5000);
});
describe('install', () => {
test('it installs the extension', async () => {
const browser = await launchPuppeteerWithExtension(puppeteer)
expect(browser).toBeTruthy()
browser.close()
}, 5000)
})

View File

@@ -1,9 +1,9 @@
import path from "path";
import { scripts } from "../../package.json";
const util = require("util");
const exec = util.promisify(require("child_process").exec);
import path from 'path'
import { scripts } from '../../package.json'
const util = require('util')
const exec = util.promisify(require('child_process').exec)
const extensionPath = path.join(__dirname, "../../dist");
const extensionPath = path.join(__dirname, '../../dist')
export const launchPuppeteerWithExtension = function(puppeteer) {
const options = {
@@ -13,18 +13,18 @@ export const launchPuppeteerWithExtension = function(puppeteer) {
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
"--no-sandbox",
"--disable-setuid-sandbox"
]
};
if (process.env.CI) {
options.executablePath = process.env.PUPPETEER_EXEC_PATH; // Set by docker on github actions
'--no-sandbox',
'--disable-setuid-sandbox',
],
}
return puppeteer.launch(options);
};
if (process.env.CI) {
options.executablePath = process.env.PUPPETEER_EXEC_PATH // Set by docker on github actions
}
return puppeteer.launch(options)
}
export const runBuild = function() {
return exec(scripts.build);
};
return exec(scripts.build)
}

View File

@@ -1,163 +1,163 @@
import pptrActions from "@/services/pptr-actions";
import ctrl from "@/models/extension-control-messages";
import actions from "@/models/extension-ui-actions";
import pptrActions from '@/services/pptr-actions'
import ctrl from '@/models/extension-control-messages'
import actions from '@/models/extension-ui-actions'
class RecordingController {
constructor() {
this._recording = [];
this._boundedMessageHandler = null;
this._boundedNavigationHandler = null;
this._boundedWaitHandler = null;
this._boundedMenuHandler = null;
this._boundedKeyCommandHandler = null;
this._badgeState = "";
this._isPaused = false;
this._recording = []
this._boundedMessageHandler = null
this._boundedNavigationHandler = null
this._boundedWaitHandler = null
this._boundedMenuHandler = null
this._boundedKeyCommandHandler = null
this._badgeState = ''
this._isPaused = false
// Some events are sent double on page navigations to simplify the event recorder.
// We keep some simple state to disregard events if needed.
this._hasGoto = false;
this._hasViewPort = false;
this._hasGoto = false
this._hasViewPort = false
this._menuId = "PUPPETEER_RECORDER_CONTEXT_MENU";
this._menuId = 'PUPPETEER_RECORDER_CONTEXT_MENU'
this._menuOptions = {
SCREENSHOT: "SCREENSHOT",
SCREENSHOT_CLIPPED: "SCREENSHOT_CLIPPED"
};
SCREENSHOT: 'SCREENSHOT',
SCREENSHOT_CLIPPED: 'SCREENSHOT_CLIPPED',
}
}
boot() {
chrome.extension.onConnect.addListener(port => {
console.debug("listeners connected");
console.debug('listeners connected')
port.onMessage.addListener(msg => {
if (msg.action && msg.action === actions.START) this.start();
if (msg.action && msg.action === actions.STOP) this.stop();
if (msg.action && msg.action === actions.CLEAN_UP) this.cleanUp();
if (msg.action && msg.action === actions.PAUSE) this.pause();
if (msg.action && msg.action === actions.UN_PAUSE) this.unPause();
});
});
if (msg.action && msg.action === actions.START) this.start()
if (msg.action && msg.action === actions.STOP) this.stop()
if (msg.action && msg.action === actions.CLEAN_UP) this.cleanUp()
if (msg.action && msg.action === actions.PAUSE) this.pause()
if (msg.action && msg.action === actions.UN_PAUSE) this.unPause()
})
})
}
start() {
console.debug("start recording");
console.debug('start recording')
this.cleanUp(() => {
this._badgeState = "rec";
this._badgeState = 'rec'
this._hasGoto = false;
this._hasViewPort = false;
this._hasGoto = false
this._hasViewPort = false
this.injectScript();
this.injectScript()
this._boundedMessageHandler = this.handleMessage.bind(this);
this._boundedNavigationHandler = this.handleNavigation.bind(this);
this._boundedWaitHandler = this.handleWait.bind(this);
this._boundedMessageHandler = this.handleMessage.bind(this)
this._boundedNavigationHandler = this.handleNavigation.bind(this)
this._boundedWaitHandler = this.handleWait.bind(this)
chrome.runtime.onMessage.addListener(this._boundedMessageHandler);
chrome.runtime.onMessage.addListener(this._boundedMessageHandler)
chrome.webNavigation.onCompleted.addListener(
this._boundedNavigationHandler
);
)
chrome.webNavigation.onBeforeNavigate.addListener(
this._boundedWaitHandler
);
)
chrome.browserAction.setIcon({ path: "./images/icon-green.png" });
chrome.browserAction.setBadgeText({ text: this._badgeState });
chrome.browserAction.setBadgeBackgroundColor({ color: "#FF0000" });
chrome.browserAction.setIcon({ path: './images/icon-green.png' })
chrome.browserAction.setBadgeText({ text: this._badgeState })
chrome.browserAction.setBadgeBackgroundColor({ color: '#FF0000' })
/**
* Right click menu setup
*/
chrome.contextMenus.removeAll();
chrome.contextMenus.removeAll()
// add the parent and its children
chrome.contextMenus.create({
id: this._menuId,
title: "Headless Recorder",
contexts: ["all"]
});
title: 'Headless Recorder',
contexts: ['all'],
})
chrome.contextMenus.create({
id: this._menuId + this._menuOptions.SCREENSHOT,
title: "Take Screenshot (Ctrl+Shift+A)",
title: 'Take Screenshot (Ctrl+Shift+A)',
parentId: this._menuId,
contexts: ["all"]
});
contexts: ['all'],
})
chrome.contextMenus.create({
id: this._menuId + this._menuOptions.SCREENSHOT_CLIPPED,
title: "Take Screenshot Clipped (Ctrl+Shift+S)",
title: 'Take Screenshot Clipped (Ctrl+Shift+S)',
parentId: this._menuId,
contexts: ["all"]
});
contexts: ['all'],
})
// add the handlers
this._boundedMenuHandler = this.handleMenuInteraction.bind(this);
chrome.contextMenus.onClicked.addListener(this._boundedMenuHandler);
this._boundedMenuHandler = this.handleMenuInteraction.bind(this)
chrome.contextMenus.onClicked.addListener(this._boundedMenuHandler)
this._boundedKeyCommandHandler = this.handleKeyCommands.bind(this);
chrome.commands.onCommand.addListener(this._boundedKeyCommandHandler);
});
this._boundedKeyCommandHandler = this.handleKeyCommands.bind(this)
chrome.commands.onCommand.addListener(this._boundedKeyCommandHandler)
})
}
stop() {
console.debug("stop recording");
this._badgeState = this._recording.length > 0 ? "1" : "";
console.debug('stop recording')
this._badgeState = this._recording.length > 0 ? '1' : ''
chrome.runtime.onMessage.removeListener(this._boundedMessageHandler);
chrome.runtime.onMessage.removeListener(this._boundedMessageHandler)
chrome.webNavigation.onCompleted.removeListener(
this._boundedNavigationHandler
);
)
chrome.webNavigation.onBeforeNavigate.removeListener(
this._boundedWaitHandler
);
chrome.contextMenus.onClicked.removeListener(this._boundedMenuHandler);
)
chrome.contextMenus.onClicked.removeListener(this._boundedMenuHandler)
chrome.browserAction.setIcon({ path: "./images/icon-black.png" });
chrome.browserAction.setBadgeText({ text: this._badgeState });
chrome.browserAction.setBadgeBackgroundColor({ color: "#45C8F1" });
chrome.browserAction.setIcon({ path: './images/icon-black.png' })
chrome.browserAction.setBadgeText({ text: this._badgeState })
chrome.browserAction.setBadgeBackgroundColor({ color: '#45C8F1' })
chrome.storage.local.set({ recording: this._recording }, () => {
console.debug("recording stored");
});
console.debug('recording stored')
})
}
pause() {
console.debug("pause");
this._badgeState = "❚❚";
chrome.browserAction.setBadgeText({ text: this._badgeState });
this._isPaused = true;
console.debug('pause')
this._badgeState = '❚❚'
chrome.browserAction.setBadgeText({ text: this._badgeState })
this._isPaused = true
}
unPause() {
console.debug("unpause");
this._badgeState = "rec";
chrome.browserAction.setBadgeText({ text: this._badgeState });
this._isPaused = false;
console.debug('unpause')
this._badgeState = 'rec'
chrome.browserAction.setBadgeText({ text: this._badgeState })
this._isPaused = false
}
cleanUp(cb) {
console.debug("cleanup");
this._recording = [];
chrome.browserAction.setBadgeText({ text: "" });
chrome.storage.local.remove("recording", () => {
console.debug("stored recording cleared");
if (cb) cb();
});
console.debug('cleanup')
this._recording = []
chrome.browserAction.setBadgeText({ text: '' })
chrome.storage.local.remove('recording', () => {
console.debug('stored recording cleared')
if (cb) cb()
})
}
recordCurrentUrl(href) {
if (!this._hasGoto) {
console.debug("recording goto* for:", href);
console.debug('recording goto* for:', href)
this.handleMessage({
selector: undefined,
value: undefined,
action: pptrActions.GOTO,
href
});
this._hasGoto = true;
href,
})
this._hasGoto = true
}
}
@@ -166,9 +166,9 @@ class RecordingController {
this.handleMessage({
selector: undefined,
value,
action: pptrActions.VIEWPORT
});
this._hasViewPort = true;
action: pptrActions.VIEWPORT,
})
this._hasViewPort = true
}
}
@@ -176,97 +176,97 @@ class RecordingController {
this.handleMessage({
selector: undefined,
value: undefined,
action: pptrActions.NAVIGATION
});
action: pptrActions.NAVIGATION,
})
}
recordScreenshot(value) {
this.handleMessage({
selector: undefined,
value,
action: pptrActions.SCREENSHOT
});
action: pptrActions.SCREENSHOT,
})
}
handleMessage(msg, sender) {
if (msg.control) return this.handleControlMessage(msg, sender);
if (msg.control) return this.handleControlMessage(msg, sender)
if (msg.type === "SIGN_CONNECT") {
return;
if (msg.type === 'SIGN_CONNECT') {
return
}
// to account for clicks etc. we need to record the frameId and url to later target the frame in playback
msg.frameId = sender ? sender.frameId : null;
msg.frameUrl = sender ? sender.url : null;
msg.frameId = sender ? sender.frameId : null
msg.frameUrl = sender ? sender.url : null
if (!this._isPaused) {
this._recording.push(msg);
console.log(msg);
this._recording.push(msg)
console.log(msg)
chrome.storage.local.set({ recording: this._recording }, () => {
console.debug("stored recording updated");
});
console.debug('stored recording updated')
})
}
}
handleControlMessage(msg) {
if (msg.control === ctrl.EVENT_RECORDER_STARTED)
chrome.browserAction.setBadgeText({ text: this._badgeState });
chrome.browserAction.setBadgeText({ text: this._badgeState })
if (msg.control === ctrl.GET_VIEWPORT_SIZE)
this.recordCurrentViewportSize(msg.coordinates);
if (msg.control === ctrl.GET_CURRENT_URL) this.recordCurrentUrl(msg.href);
if (msg.control === ctrl.GET_SCREENSHOT) this.recordScreenshot(msg.value);
this.recordCurrentViewportSize(msg.coordinates)
if (msg.control === ctrl.GET_CURRENT_URL) this.recordCurrentUrl(msg.href)
if (msg.control === ctrl.GET_SCREENSHOT) this.recordScreenshot(msg.value)
}
handleNavigation({ frameId }) {
console.debug("frameId is:", frameId);
this.injectScript();
console.debug('frameId is:', frameId)
this.injectScript()
if (frameId === 0) {
this.recordNavigation();
this.recordNavigation()
}
}
handleMenuInteraction(info) {
console.debug("context menu clicked");
console.debug('context menu clicked')
switch (info.menuItemId) {
case this._menuId + this._menuOptions.SCREENSHOT:
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_MODE);
break;
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_MODE)
break
case this._menuId + this._menuOptions.SCREENSHOT_CLIPPED:
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_CLIPPED_MODE);
break;
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_CLIPPED_MODE)
break
}
}
handleKeyCommands(command) {
switch (command) {
case actions.TOGGLE_SCREENSHOT_MODE:
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_MODE);
break;
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_MODE)
break
case actions.TOGGLE_SCREENSHOT_CLIPPED_MODE:
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_CLIPPED_MODE);
break;
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_CLIPPED_MODE)
break
}
}
toggleScreenShotMode(action) {
console.debug("toggling screenshot mode");
console.debug('toggling screenshot mode')
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
chrome.tabs.sendMessage(tabs[0].id, { action });
});
chrome.tabs.sendMessage(tabs[0].id, { action })
})
}
handleWait() {
chrome.browserAction.setBadgeText({ text: "wait" });
chrome.browserAction.setBadgeText({ text: 'wait' })
}
injectScript() {
chrome.tabs.executeScript({
file: "js/content-script.js",
allFrames: true
});
file: 'js/content-script.js',
allFrames: true,
})
}
}
console.debug("booting recording controller");
window.recordingController = new RecordingController();
window.recordingController.boot();
console.debug('booting recording controller')
window.recordingController = new RecordingController()
window.recordingController.boot()

View File

@@ -9,8 +9,8 @@
<script>
export default {
name: "ChecklyBadge"
};
name: 'ChecklyBadge',
}
</script>
<style scoped>

View File

@@ -6,16 +6,16 @@
<script>
export default {
name: "HelloWorld",
name: 'HelloWorld',
mounted() {
browser.runtime.sendMessage({});
browser.runtime.sendMessage({})
},
computed: {
defaultText() {
return browser.i18n.getMessage("extName");
}
}
};
return browser.i18n.getMessage('extName')
},
},
}
</script>
<style scoped>

View File

@@ -62,14 +62,14 @@ node my-script.js</pre
</template>
<script>
export default {
name: "HelpTab"
};
name: 'HelpTab',
}
</script>
<style lang="scss" scoped>
@import "../assets/styles/_variables.scss";
@import "../assets/styles/_mixins.scss";
@import "../assets/styles/_utils.scss";
@import '../assets/styles/_variables.scss';
@import '../assets/styles/_mixins.scss';
@import '../assets/styles/_utils.scss';
.help-tab {
.content {

View File

@@ -40,33 +40,33 @@
</template>
<script>
export default {
name: "RecordingTab",
name: 'RecordingTab',
props: {
isRecording: { type: Boolean, default: false },
liveEvents: {
type: Array,
default: () => {
return [];
}
}
return []
},
},
},
methods: {
parseEventValue(event) {
if (!event) {
return;
return
}
if (event.action === "viewport*")
return `width: ${event.value.width}, height: ${event.value.height}`;
if (event.action === "goto*") return event.href;
if (event.action === "navigation*") return "";
}
}
};
if (event.action === 'viewport*')
return `width: ${event.value.width}, height: ${event.value.height}`
if (event.action === 'goto*') return event.href
if (event.action === 'navigation*') return ''
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/styles/_animations.scss";
@import "../assets/styles/_variables.scss";
@import '../assets/styles/_animations.scss';
@import '../assets/styles/_variables.scss';
.recording-tab {
.content {
@@ -88,7 +88,7 @@ export default {
flex-direction: column-reverse;
.loading:after {
content: ".";
content: '.';
animation: dots 1s steps(5, end) infinite;
animation-delay: 1.5s;
margin-bottom: auto;

View File

@@ -31,31 +31,31 @@
</template>
<script>
export const TYPE = {
PUPPETEER: "puppeteer",
PLAYWRIGHT: "playwright"
};
PUPPETEER: 'puppeteer',
PLAYWRIGHT: 'playwright',
}
export default {
name: "ResultsTab",
name: 'ResultsTab',
props: {
puppeteer: {
type: String,
default: ""
default: '',
},
playwright: {
type: String,
default: ""
default: '',
},
options: {
type: Object,
default: () => ({})
}
default: () => ({}),
},
},
data() {
return {
activeTab: TYPE.PUPPETEER,
tabs: [TYPE.PUPPETEER, TYPE.PLAYWRIGHT]
};
tabs: [TYPE.PUPPETEER, TYPE.PLAYWRIGHT],
}
},
mounted() {
if (
@@ -63,27 +63,27 @@ export default {
this.options.code &&
this.options.code.showPlaywrightFirst
) {
this.activeTab = TYPE.PLAYWRIGHT;
this.tabs = this.tabs.reverse();
this.activeTab = TYPE.PLAYWRIGHT
this.tabs = this.tabs.reverse()
}
this.$emit("update:tab", this.activeTab);
this.$emit('update:tab', this.activeTab)
},
methods: {
code() {
return this.activeTab === TYPE.PUPPETEER
? this.puppeteer
: this.playwright;
: this.playwright
},
changeTab(tab) {
this.activeTab = tab;
this.$emit("update:tab", tab);
}
}
};
this.activeTab = tab
this.$emit('update:tab', tab)
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/styles/_variables.scss";
@import '../assets/styles/_variables.scss';
.results-tab {
.content {

View File

@@ -1,47 +1,46 @@
import eventsToRecord from "@/services/dom-events-to-record";
import UIController from "./UIController";
import actions from "@/models/extension-ui-actions";
import ctrl from "@/models/extension-control-messages";
import finder from "@medv/finder";
import eventsToRecord from '@/services/dom-events-to-record'
import UIController from './UIController'
import actions from '@/models/extension-ui-actions'
import ctrl from '@/models/extension-control-messages'
import finder from '@medv/finder'
const DEFAULT_MOUSE_CURSOR = "default";
const DEFAULT_MOUSE_CURSOR = 'default'
export default class EventRecorder {
constructor() {
this._boundedMessageListener = null;
this._eventLog = [];
this._previousEvent = null;
this._dataAttribute = null;
this._uiController = null;
this._screenShotMode = false;
this._isTopFrame = window.location === window.parent.location;
this._isRecordingClicks = true;
this._boundedMessageListener = null
this._eventLog = []
this._previousEvent = null
this._dataAttribute = null
this._uiController = null
this._screenShotMode = false
this._isTopFrame = window.location === window.parent.location
this._isRecordingClicks = true
}
boot() {
// We need to check the existence of chrome for testing purposes
if (chrome.storage && chrome.storage.local) {
chrome.storage.local.get(["options"], ({ options }) => {
const { dataAttribute } = options ? options.code : {};
chrome.storage.local.get(['options'], ({ options }) => {
const { dataAttribute } = options ? options.code : {}
if (dataAttribute) {
this._dataAttribute = dataAttribute;
this._dataAttribute = dataAttribute
}
this._initializeRecorder();
});
this._initializeRecorder()
})
} else {
this._initializeRecorder();
this._initializeRecorder()
}
}
_initializeRecorder() {
const events = Object.values(eventsToRecord);
const events = Object.values(eventsToRecord)
if (!window.pptRecorderAddedControlListeners) {
this._addAllListeners(events);
this._addAllListeners(events)
this._boundedMessageListener =
this._boundedMessageListener ||
this._handleBackgroundMessage.bind(this);
chrome.runtime.onMessage.addListener(this._boundedMessageListener);
window.pptRecorderAddedControlListeners = true;
this._boundedMessageListener || this._handleBackgroundMessage.bind(this)
chrome.runtime.onMessage.addListener(this._boundedMessageListener)
window.pptRecorderAddedControlListeners = true
}
if (
@@ -49,66 +48,66 @@ export default class EventRecorder {
chrome.runtime &&
chrome.runtime.onMessage
) {
window.document.pptRecorderAddedControlListeners = true;
window.document.pptRecorderAddedControlListeners = true
}
if (this._isTopFrame) {
this._sendMessage({ control: ctrl.EVENT_RECORDER_STARTED });
this._sendMessage({ control: ctrl.EVENT_RECORDER_STARTED })
this._sendMessage({
control: ctrl.GET_CURRENT_URL,
href: window.location.href
});
href: window.location.href,
})
this._sendMessage({
control: ctrl.GET_VIEWPORT_SIZE,
coordinates: { width: window.innerWidth, height: window.innerHeight }
});
console.debug("Puppeteer Recorder in-page EventRecorder started");
coordinates: { width: window.innerWidth, height: window.innerHeight },
})
console.debug('Puppeteer Recorder in-page EventRecorder started')
}
}
_handleBackgroundMessage(msg) {
console.debug("content-script: message from background", msg);
console.debug('content-script: message from background', msg)
if (msg && msg.action) {
switch (msg.action) {
case actions.TOGGLE_SCREENSHOT_MODE:
this._handleScreenshotMode(false);
break;
this._handleScreenshotMode(false)
break
case actions.TOGGLE_SCREENSHOT_CLIPPED_MODE:
this._handleScreenshotMode(true);
break;
this._handleScreenshotMode(true)
break
default:
}
}
}
_addAllListeners(events) {
const boundedRecordEvent = this._recordEvent.bind(this);
const boundedRecordEvent = this._recordEvent.bind(this)
events.forEach(type => {
window.addEventListener(type, boundedRecordEvent, true);
});
window.addEventListener(type, boundedRecordEvent, true)
})
}
_sendMessage(msg) {
// filter messages based on enabled / disabled features
if (msg.action === "click" && !this._isRecordingClicks) return;
if (msg.action === 'click' && !this._isRecordingClicks) return
try {
// poor man's way of detecting whether this script was injected by an actual extension, or is loaded for
// testing purposes
if (chrome.runtime && chrome.runtime.onMessage) {
chrome.runtime.sendMessage(msg);
chrome.runtime.sendMessage(msg)
} else {
this._eventLog.push(msg);
this._eventLog.push(msg)
}
} catch (err) {
console.debug("caught error", err);
console.debug('caught error', err)
}
}
_recordEvent(e) {
if (this._previousEvent && this._previousEvent.timeStamp === e.timeStamp)
return;
this._previousEvent = e;
return
this._previousEvent = e
// we explicitly catch any errors and swallow them, as none node-type events are also ingested.
// for these events we cannot generate selectors, which is OK
@@ -120,67 +119,67 @@ export default class EventRecorder {
action: e.type,
keyCode: e.keyCode ? e.keyCode : null,
href: e.target.href ? e.target.href : null,
coordinates: EventRecorder._getCoordinates(e)
});
coordinates: EventRecorder._getCoordinates(e),
})
} catch (err) {
console.error(err);
console.error(err)
}
}
_getEventLog() {
return this._eventLog;
return this._eventLog
}
_clearEventLog() {
this._eventLog = [];
this._eventLog = []
}
_handleScreenshotMode(isClipped) {
this._disableClickRecording();
this._uiController = new UIController({ showSelector: isClipped });
this._screenShotMode = !this._screenShotMode;
document.body.style.cursor = "crosshair";
this._disableClickRecording()
this._uiController = new UIController({ showSelector: isClipped })
this._screenShotMode = !this._screenShotMode
document.body.style.cursor = 'crosshair'
console.debug("screenshot mode:", this._screenShotMode);
console.debug('screenshot mode:', this._screenShotMode)
if (this._screenShotMode) {
this._uiController.showSelector();
this._uiController.showSelector()
} else {
this._uiController.hideSelector();
this._uiController.hideSelector()
}
this._uiController.on("click", event => {
this._screenShotMode = false;
document.body.style.cursor = DEFAULT_MOUSE_CURSOR;
this._sendMessage({ control: ctrl.GET_SCREENSHOT, value: event.clip });
this._enableClickRecording();
});
this._uiController.on('click', event => {
this._screenShotMode = false
document.body.style.cursor = DEFAULT_MOUSE_CURSOR
this._sendMessage({ control: ctrl.GET_SCREENSHOT, value: event.clip })
this._enableClickRecording()
})
}
_disableClickRecording() {
this._isRecordingClicks = false;
this._isRecordingClicks = false
}
_enableClickRecording() {
this._isRecordingClicks = true;
this._isRecordingClicks = true
}
_getSelector(e) {
if (this._dataAttribute && e.target.getAttribute(this._dataAttribute)) {
return `[${this._dataAttribute}="${e.target.getAttribute(
this._dataAttribute
)}"]`;
)}"]`
}
if (e.target.id) {
return `#${e.target.id}`;
return `#${e.target.id}`
}
return finder(e.target, {
seedMinLength: 5,
optimizedMinLength: e.target.id ? 2 : 10,
attr: name => name === this._dataAttribute
});
attr: name => name === this._dataAttribute,
})
}
static _getCoordinates(evt) {
@@ -188,10 +187,10 @@ export default class EventRecorder {
mouseup: true,
mousedown: true,
mousemove: true,
mouseover: true
};
mouseover: true,
}
return eventsWithCoordinates[evt.type]
? { x: evt.clientX, y: evt.clientY }
: null;
: null
}
}

View File

@@ -1,124 +1,120 @@
import EventEmitter from "events";
import EventEmitter from 'events'
const BORDER_THICKNESS = 3;
const BORDER_THICKNESS = 3
const defaults = {
showSelector: false
};
showSelector: false,
}
class UIController extends EventEmitter {
constructor(options) {
options = Object.assign({}, defaults, options);
options = Object.assign({}, defaults, options)
super();
this._overlay = null;
this._selector = null;
this._element = null;
this._dimensions = {};
this._showSelector = options.showSelector;
super()
this._overlay = null
this._selector = null
this._element = null
this._dimensions = {}
this._showSelector = options.showSelector
this._boundeMouseMove = this._mousemove.bind(this);
this._boundeMouseUp = this._mouseup.bind(this);
this._boundeMouseMove = this._mousemove.bind(this)
this._boundeMouseUp = this._mouseup.bind(this)
}
showSelector() {
console.debug("UIController:show");
console.debug('UIController:show')
if (!this._overlay) {
this._overlay = document.createElement("div");
this._overlay.className = "headlessRecorderOverlay";
this._overlay.style.position = "fixed";
this._overlay.style.top = "0px";
this._overlay.style.left = "0px";
this._overlay.style.width = "100%";
this._overlay.style.height = "100%";
this._overlay.style.pointerEvents = "none";
this._overlay = document.createElement('div')
this._overlay.className = 'headlessRecorderOverlay'
this._overlay.style.position = 'fixed'
this._overlay.style.top = '0px'
this._overlay.style.left = '0px'
this._overlay.style.width = '100%'
this._overlay.style.height = '100%'
this._overlay.style.pointerEvents = 'none'
if (this._showSelector) {
this._selector = document.createElement("div");
this._selector.className = "headlessRecorderOutline";
this._selector.style.position = "fixed";
this._selector = document.createElement('div')
this._selector.className = 'headlessRecorderOutline'
this._selector.style.position = 'fixed'
this._selector.style.border =
BORDER_THICKNESS + "px solid rgba(69,200,241,0.8)";
this._selector.style.borderRadius = "3px";
this._overlay.appendChild(this._selector);
BORDER_THICKNESS + 'px solid rgba(69,200,241,0.8)'
this._selector.style.borderRadius = '3px'
this._overlay.appendChild(this._selector)
}
}
if (!this._overlay.parentNode) {
document.body.appendChild(this._overlay);
document.body.addEventListener("mousemove", this._boundeMouseMove, false);
document.body.addEventListener("click", this._boundeMouseUp, false);
document.body.appendChild(this._overlay)
document.body.addEventListener('mousemove', this._boundeMouseMove, false)
document.body.addEventListener('click', this._boundeMouseUp, false)
}
}
hideSelector() {
console.debug("UIController:hide");
console.debug('UIController:hide')
if (this._overlay) {
document.body.removeChild(this._overlay);
document.body.removeChild(this._overlay)
}
this._overlay = this._selector = this._element = null;
this._dimensions = {};
this._overlay = this._selector = this._element = null
this._dimensions = {}
}
_mousemove(e) {
if (this._element !== e.target) {
this._element = e.target;
this._element = e.target
this._dimensions.top = -window.scrollY;
this._dimensions.left = -window.scrollX;
this._dimensions.top = -window.scrollY
this._dimensions.left = -window.scrollX
let elem = e.target;
let elem = e.target
while (elem && elem !== document.body) {
this._dimensions.top += elem.offsetTop;
this._dimensions.left += elem.offsetLeft;
elem = elem.offsetParent;
this._dimensions.top += elem.offsetTop
this._dimensions.left += elem.offsetLeft
elem = elem.offsetParent
}
this._dimensions.width = this._element.offsetWidth;
this._dimensions.height = this._element.offsetHeight;
this._dimensions.width = this._element.offsetWidth
this._dimensions.height = this._element.offsetHeight
if (this._selector) {
this._selector.style.top =
this._dimensions.top - BORDER_THICKNESS + "px";
this._dimensions.top - BORDER_THICKNESS + 'px'
this._selector.style.left =
this._dimensions.left - BORDER_THICKNESS + "px";
this._selector.style.width = this._dimensions.width + "px";
this._selector.style.height = this._dimensions.height + "px";
this._dimensions.left - BORDER_THICKNESS + 'px'
this._selector.style.width = this._dimensions.width + 'px'
this._selector.style.height = this._dimensions.height + 'px'
console.debug(
`top: ${this._selector.style.top}, left: ${this._selector.style.left}, width: ${this._selector.style.width}, height: ${this._selector.style.height}`
);
)
}
}
}
_mouseup(e) {
this._overlay.style.backgroundColor = "white";
this._overlay.style.backgroundColor = 'white'
setTimeout(() => {
this._overlay.style.backgroundColor = "none";
this._cleanup();
this._overlay.style.backgroundColor = 'none'
this._cleanup()
let clip = null;
let clip = null
if (this._selector) {
clip = {
x: this._selector.style.left,
y: this._selector.style.top,
width: this._selector.style.width,
height: this._selector.style.height
};
height: this._selector.style.height,
}
}
this.emit("click", { clip, raw: e });
}, 100);
this.emit('click', { clip, raw: e })
}, 100)
}
_cleanup() {
document.body.removeEventListener(
"mousemove",
this._boundeMouseMove,
false
);
document.body.removeEventListener("mouseup", this._boundeMouseUp, false);
document.body.removeChild(this._overlay);
document.body.removeEventListener('mousemove', this._boundeMouseMove, false)
document.body.removeEventListener('mouseup', this._boundeMouseUp, false)
document.body.removeChild(this._overlay)
}
}
export default UIController;
export default UIController

View File

@@ -1,45 +1,45 @@
import UIController from "../UIController";
import UIController from '../UIController'
// this test NEEDS to come first because of shitty JSDOM.
// See https://github.com/facebook/jest/issues/1224
it("Registers mouse events", () => {
jest.useFakeTimers();
it('Registers mouse events', () => {
jest.useFakeTimers()
document.body.innerHTML =
"<div>" +
'<div>' +
' <div id="username">UserName</div>' +
' <button id="button"></button>' +
"</div>";
'</div>'
const uic = new UIController();
uic.showSelector();
const uic = new UIController()
uic.showSelector()
const handleClick = jest.fn();
uic.on("click", handleClick);
const handleClick = jest.fn()
uic.on('click', handleClick)
const el = document.querySelector("#username");
el.click();
const el = document.querySelector('#username')
el.click()
jest.runAllTimers();
jest.runAllTimers()
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(handleClick).toHaveBeenCalled();
});
expect(setTimeout).toHaveBeenCalledTimes(1)
expect(handleClick).toHaveBeenCalled()
})
it("Shows and hides the selector", () => {
const uic = new UIController();
it('Shows and hides the selector', () => {
const uic = new UIController()
uic.showSelector();
let overlay = document.querySelector(".headlessRecorderOverlay");
let outline = document.querySelector(".headlessRecorderOutline");
uic.showSelector()
let overlay = document.querySelector('.headlessRecorderOverlay')
let outline = document.querySelector('.headlessRecorderOutline')
expect(overlay).toBeDefined();
expect(outline).toBeDefined();
expect(overlay).toBeDefined()
expect(outline).toBeDefined()
uic.hideSelector();
overlay = document.querySelector(".headlessRecorderOverlay");
outline = document.querySelector(".headlessRecorderOutline");
uic.hideSelector()
overlay = document.querySelector('.headlessRecorderOverlay')
outline = document.querySelector('.headlessRecorderOutline')
expect(overlay).toBeNull();
expect(outline).toBeNull();
});
expect(overlay).toBeNull()
expect(outline).toBeNull()
})

View File

@@ -1,51 +1,51 @@
import express from "express";
import path from "path";
import express from 'express'
import path from 'path'
export const waitForAndGetEvents = async function(page, amount) {
await waitForRecorderEvents(page, amount);
return getEventLog(page);
};
await waitForRecorderEvents(page, amount)
return getEventLog(page)
}
export const waitForRecorderEvents = function(page, amount) {
return page.waitForFunction(
`window.eventRecorder._getEventLog().length >= ${amount || 1}`
);
};
)
}
export const getEventLog = function(page) {
return page.evaluate(() => {
return window.eventRecorder._getEventLog();
});
};
return window.eventRecorder._getEventLog()
})
}
export const cleanEventLog = function(page) {
return page.evaluate(() => {
return window.eventRecorder._clearEventLog();
});
};
return window.eventRecorder._clearEventLog()
})
}
export const startServer = function(buildDir, file) {
return new Promise(resolve => {
const app = express();
app.use("/build", express.static(path.join(__dirname, buildDir)));
app.get("/", (req, res) => {
res.status(200).sendFile(file, { root: __dirname });
});
let server;
let port;
const app = express()
app.use('/build', express.static(path.join(__dirname, buildDir)))
app.get('/', (req, res) => {
res.status(200).sendFile(file, { root: __dirname })
})
let server
let port
const retry = e => {
if (e.code === "EADDRINUSE") {
setTimeout(() => connect, 1000);
if (e.code === 'EADDRINUSE') {
setTimeout(() => connect, 1000)
}
};
}
const connect = () => {
port = 0 | (Math.random() * 1000 + 3000);
server = app.listen(port);
server.once("error", retry);
server.once("listening", () => {
return resolve({ server, port });
});
};
connect();
});
};
port = 0 | (Math.random() * 1000 + 3000)
server = app.listen(port)
server.once('error', retry)
server.once('listening', () => {
return resolve({ server, port })
})
}
connect()
})
}

View File

@@ -1,6 +1,6 @@
export default {
EVENT_RECORDER_STARTED: "EVENT_RECORDER_STARTED",
GET_VIEWPORT_SIZE: "GET_VIEWPORT_SIZE",
GET_CURRENT_URL: "GET_CURRENT_URL",
GET_SCREENSHOT: "GET_SCREENSHOT"
};
EVENT_RECORDER_STARTED: 'EVENT_RECORDER_STARTED',
GET_VIEWPORT_SIZE: 'GET_VIEWPORT_SIZE',
GET_CURRENT_URL: 'GET_CURRENT_URL',
GET_SCREENSHOT: 'GET_SCREENSHOT',
}

View File

@@ -1,9 +1,9 @@
export default {
TOGGLE_SCREENSHOT_MODE: "TOGGLE_SCREENSHOT_MODE",
TOGGLE_SCREENSHOT_CLIPPED_MODE: "TOGGLE_SCREENSHOT_CLIPPED_MODE",
START: "START",
STOP: "STOP",
CLEAN_UP: "CLEAN_UP",
PAUSE: "PAUSE",
UN_PAUSE: "UN_PAUSE"
};
TOGGLE_SCREENSHOT_MODE: 'TOGGLE_SCREENSHOT_MODE',
TOGGLE_SCREENSHOT_CLIPPED_MODE: 'TOGGLE_SCREENSHOT_CLIPPED_MODE',
START: 'START',
STOP: 'STOP',
CLEAN_UP: 'CLEAN_UP',
PAUSE: 'PAUSE',
UN_PAUSE: 'UN_PAUSE',
}

View File

@@ -41,8 +41,8 @@
>
{{
recordingKeyCodePress
? "Capturing"
: "Click to capture key code"
? 'Capturing'
: 'Click to capture key code'
}}
</button>
<input
@@ -171,68 +171,68 @@
</template>
<script>
import { defaults as code } from "@/services/CodeGenerator";
import { defaults as code } from '@/services/CodeGenerator'
const defaults = {
code,
extension: {
telemetry: true
}
};
telemetry: true,
},
}
export default {
name: "App",
name: 'App',
data() {
return {
loading: true,
saving: false,
options: defaults,
recordingKeyCodePress: false
};
recordingKeyCodePress: false,
}
},
mounted() {
this.load();
this.load()
},
methods: {
save() {
this.saving = true;
this.saving = true
chrome.storage.local.set({ options: this.options }, () => {
console.debug("saved options");
console.debug('saved options')
setTimeout(() => {
this.saving = false;
}, 500);
});
this.saving = false
}, 500)
})
},
load() {
chrome.storage.local.get("options", ({ options }) => {
chrome.storage.local.get('options', ({ options }) => {
if (options) {
console.debug("loaded options", JSON.stringify(options));
this.options = options;
console.debug('loaded options', JSON.stringify(options))
this.options = options
}
this.loading = false;
});
this.loading = false
})
},
listenForKeyCodePress() {
this.recordingKeyCodePress = true;
this.recordingKeyCodePress = true
const keyDownFunction = e => {
this.recordingKeyCodePress = false;
this.updateKeyCodeWithNumber(e);
window.removeEventListener("keydown", keyDownFunction, false);
e.preventDefault();
};
window.addEventListener("keydown", keyDownFunction, false);
this.recordingKeyCodePress = false
this.updateKeyCodeWithNumber(e)
window.removeEventListener('keydown', keyDownFunction, false)
e.preventDefault()
}
window.addEventListener('keydown', keyDownFunction, false)
},
updateKeyCodeWithNumber(evt) {
this.options.code.keyCode = parseInt(evt.keyCode, 10);
this.save();
}
}
};
this.options.code.keyCode = parseInt(evt.keyCode, 10)
this.save()
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/styles/_variables.scss";
@import "../assets/styles/_mixins.scss";
@import '../assets/styles/_variables.scss';
@import '../assets/styles/_mixins.scss';
.options {
height: 100%;
@@ -308,8 +308,8 @@ export default {
display: block;
}
}
input[type="text"],
input[type="number"] {
input[type='text'],
input[type='number'] {
margin-bottom: 10px;
width: 100%;
border: 1px solid $gray-light;
@@ -319,7 +319,7 @@ export default {
border-radius: 10px;
-webkit-box-sizing: border-box;
}
input[type="number"] {
input[type='number'] {
width: 50px;
}
}

View File

@@ -1,4 +1,4 @@
import { createApp } from "vue";
import App from "./App.vue";
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount("#app");
createApp(App).mount('#app')

View File

@@ -13,10 +13,10 @@
{{ recordingBadgeText }}
</div>
<button @click="toggleShowHelp" class="header-button">
<img src="@/assets/images/help.svg" alt="help"/>
<img src="@/assets/images/help.svg" alt="help" />
</button>
<button @click="openOptions" class="header-button">
<img src="@/assets/images/settings.svg" alt="settings"/>
<img src="@/assets/images/settings.svg" alt="settings" />
</button>
</div>
</div>
@@ -68,26 +68,26 @@
</template>
<script>
import { version } from "../../package.json";
import { version } from '../../package.json'
import PuppeteerCodeGenerator from "@/services/PuppeteerCodeGenerator";
import PlaywrightCodeGenerator from "@/services/PlaywrightCodeGenerator";
import RecordingTab from "@/components/RecordingTab.vue";
import ResultsTab from "@/components/ResultsTab.vue";
import HelpTab from "@/components/HelpTab.vue";
import ChecklyBadge from "@/components/ChecklyBadge.vue";
import PuppeteerCodeGenerator from '@/services/PuppeteerCodeGenerator'
import PlaywrightCodeGenerator from '@/services/PlaywrightCodeGenerator'
import RecordingTab from '@/components/RecordingTab.vue'
import ResultsTab from '@/components/ResultsTab.vue'
import HelpTab from '@/components/HelpTab.vue'
import ChecklyBadge from '@/components/ChecklyBadge.vue'
import actions from "@/models/extension-ui-actions";
import actions from '@/models/extension-ui-actions'
let bus;
let bus
export default {
name: "App",
name: 'App',
components: { ResultsTab, RecordingTab, HelpTab, ChecklyBadge },
data() {
return {
code: "",
codeForPlaywright: "",
code: '',
codeForPlaywright: '',
options: {},
showResultsTab: false,
showHelp: false,
@@ -98,116 +98,116 @@ export default {
isCopying: false,
bus: null,
version,
currentResultTab: null
};
currentResultTab: null,
}
},
mounted() {
this.loadState(() => {
this.trackPageView();
this.trackPageView()
if (this.isRecording) {
console.debug("opened in recording state, fetching recording events");
chrome.storage.local.get(["recording", "options"], ({ recording }) => {
console.debug("loaded recording", recording);
this.liveEvents = recording;
});
console.debug('opened in recording state, fetching recording events')
chrome.storage.local.get(['recording', 'options'], ({ recording }) => {
console.debug('loaded recording', recording)
this.liveEvents = recording
})
}
if (!this.isRecording && this.code) {
this.showResultsTab = true;
this.showResultsTab = true
}
});
})
bus = chrome.extension.connect({ name: "recordControls" });
bus = chrome.extension.connect({ name: 'recordControls' })
},
methods: {
toggleRecord() {
if (this.isRecording) {
this.stop();
this.stop()
} else {
this.start();
this.start()
}
this.isRecording = !this.isRecording;
this.storeState();
this.isRecording = !this.isRecording
this.storeState()
},
togglePause() {
if (this.isPaused) {
bus.postMessage({ action: actions.UN_PAUSE });
this.isPaused = false;
bus.postMessage({ action: actions.UN_PAUSE })
this.isPaused = false
} else {
bus.postMessage({ action: actions.PAUSE });
this.isPaused = true;
bus.postMessage({ action: actions.PAUSE })
this.isPaused = true
}
this.storeState();
this.storeState()
},
start() {
this.trackEvent("Start");
this.cleanUp();
console.debug("start recorder");
bus.postMessage({ action: actions.START });
this.trackEvent('Start')
this.cleanUp()
console.debug('start recorder')
bus.postMessage({ action: actions.START })
},
stop() {
this.trackEvent("Stop");
console.debug("stop recorder");
bus.postMessage({ action: actions.STOP });
this.trackEvent('Stop')
console.debug('stop recorder')
bus.postMessage({ action: actions.STOP })
chrome.storage.local.get(
["recording", "options"],
['recording', 'options'],
({ recording, options }) => {
console.debug("loaded recording", recording);
console.debug("loaded options", options);
console.debug('loaded recording', recording)
console.debug('loaded options', options)
this.recording = recording;
const codeOptions = options ? options.code : {};
this.recording = recording
const codeOptions = options ? options.code : {}
const codeGen = new PuppeteerCodeGenerator(codeOptions);
const codeGenPlaywright = new PlaywrightCodeGenerator(codeOptions);
this.code = codeGen.generate(this.recording);
this.codeForPlaywright = codeGenPlaywright.generate(this.recording);
this.showResultsTab = true;
this.storeState();
const codeGen = new PuppeteerCodeGenerator(codeOptions)
const codeGenPlaywright = new PlaywrightCodeGenerator(codeOptions)
this.code = codeGen.generate(this.recording)
this.codeForPlaywright = codeGenPlaywright.generate(this.recording)
this.showResultsTab = true
this.storeState()
}
);
)
},
restart() {
this.cleanUp();
bus.postMessage({ action: actions.CLEAN_UP });
this.cleanUp()
bus.postMessage({ action: actions.CLEAN_UP })
},
cleanUp() {
this.recording = this.liveEvents = [];
this.code = "";
this.codeForPlaywright = "";
this.showResultsTab = this.isRecording = this.isPaused = false;
this.storeState();
this.recording = this.liveEvents = []
this.code = ''
this.codeForPlaywright = ''
this.showResultsTab = this.isRecording = this.isPaused = false
this.storeState()
},
openOptions() {
this.trackEvent("Options");
this.trackEvent('Options')
if (chrome.runtime.openOptionsPage) {
chrome.runtime.openOptionsPage();
chrome.runtime.openOptionsPage()
}
},
loadState(cb) {
chrome.storage.local.get(
["controls", "code", "options", "codeForPlaywright"],
['controls', 'code', 'options', 'codeForPlaywright'],
({ controls, code, options, codeForPlaywright }) => {
if (controls) {
this.isRecording = controls.isRecording;
this.isPaused = controls.isPaused;
this.isRecording = controls.isRecording
this.isPaused = controls.isPaused
}
if (code) {
this.code = code;
this.code = code
}
if (codeForPlaywright) {
this.codeForPlaywright = codeForPlaywright;
this.codeForPlaywright = codeForPlaywright
}
if (options) {
this.options = options;
this.options = options
}
cb();
cb()
}
);
)
},
storeState() {
chrome.storage.local.set({
@@ -215,24 +215,24 @@ export default {
codeForPlaywright: this.codeForPlaywright,
controls: {
isRecording: this.isRecording,
isPaused: this.isPaused
}
});
isPaused: this.isPaused,
},
})
},
setCopying() {
this.trackEvent("Copy");
this.isCopying = true;
this.trackEvent('Copy')
this.isCopying = true
setTimeout(() => {
this.isCopying = false;
}, 1500);
this.isCopying = false
}, 1500)
},
goHome() {
this.showResultsTab = false;
this.showHelp = false;
this.showResultsTab = false
this.showHelp = false
},
toggleShowHelp() {
this.trackEvent("Help");
this.showHelp = !this.showHelp;
this.trackEvent('Help')
this.showHelp = !this.showHelp
},
trackEvent(event) {
if (
@@ -240,7 +240,7 @@ export default {
this.options.extension &&
this.options.extension.telemetry
) {
if (window._gaq) window._gaq.push(["_trackEvent", event, "clicked"]);
if (window._gaq) window._gaq.push(['_trackEvent', event, 'clicked'])
}
},
trackPageView() {
@@ -249,36 +249,36 @@ export default {
this.options.extension &&
this.options.extension.telemetry
) {
if (window._gaq) window._gaq.push(["_trackPageview"]);
if (window._gaq) window._gaq.push(['_trackPageview'])
}
},
getCodeForCopy() {
return this.currentResultTab === "puppeteer"
return this.currentResultTab === 'puppeteer'
? this.code
: this.codeForPlaywright;
}
: this.codeForPlaywright
},
},
computed: {
recordingBadgeText() {
return this.isPaused ? "paused" : "recording";
return this.isPaused ? 'paused' : 'recording'
},
recordButtonText() {
return this.isRecording ? "Stop" : "Record";
return this.isRecording ? 'Stop' : 'Record'
},
pauseButtonText() {
return this.isPaused ? "Resume" : "Pause";
return this.isPaused ? 'Resume' : 'Pause'
},
copyLinkText() {
return this.isCopying ? "copied!" : "copy to clipboard";
}
}
};
return this.isCopying ? 'copied!' : 'copy to clipboard'
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/styles/_animations.scss";
@import "../assets/styles/_variables.scss";
@import "../assets/styles/_mixins.scss";
@import '../assets/styles/_animations.scss';
@import '../assets/styles/_variables.scss';
@import '../assets/styles/_mixins.scss';
.recorder {
font-size: 14px;

View File

@@ -1,10 +1,10 @@
import { createApp } from "vue";
import App from "./App.vue";
import VueHighlightJS from "vue3-highlightjs";
import "highlight.js/styles/a11y-dark.css";
import { createApp } from 'vue'
import App from './App.vue'
import VueHighlightJS from 'vue3-highlightjs'
import 'highlight.js/styles/a11y-dark.css'
import "@/assets/styles/main.scss";
import '@/assets/styles/main.scss'
createApp(App)
.use(VueHighlightJS)
.mount("#app");
.mount('#app')

View File

@@ -1,25 +1,25 @@
export default class Block {
constructor(frameId, line) {
this._lines = [];
this._frameId = frameId;
this._lines = []
this._frameId = frameId
if (line) {
line.frameId = this._frameId;
this._lines.push(line);
line.frameId = this._frameId
this._lines.push(line)
}
}
addLineToTop(line) {
line.frameId = this._frameId;
this._lines.unshift(line);
line.frameId = this._frameId
this._lines.unshift(line)
}
addLine(line) {
line.frameId = this._frameId;
this._lines.push(line);
line.frameId = this._frameId
this._lines.push(line)
}
getLines() {
return this._lines;
return this._lines
}
}

View File

@@ -1,6 +1,6 @@
import domEvents from "./dom-events-to-record";
import Block from "./Block";
import pptrActions from "./pptr-actions";
import domEvents from './dom-events-to-record'
import Block from './Block'
import pptrActions from './pptr-actions'
export const defaults = {
wrapAsync: true,
@@ -8,45 +8,45 @@ export const defaults = {
waitForNavigation: true,
waitForSelectorOnClick: true,
blankLinesBetweenBlocks: true,
dataAttribute: "",
dataAttribute: '',
showPlaywrightFirst: false,
keyCode: 9
};
keyCode: 9,
}
export default class CodeGenerator {
constructor(options) {
this._options = Object.assign(defaults, options);
this._blocks = [];
this._frame = "page";
this._frameId = 0;
this._allFrames = {};
this._screenshotCounter = 1;
this._options = Object.assign(defaults, options)
this._blocks = []
this._frame = 'page'
this._frameId = 0
this._allFrames = {}
this._screenshotCounter = 1
this._hasNavigation = false;
this._hasNavigation = false
}
generate() {
throw new Error("Not implemented.");
throw new Error('Not implemented.')
}
_getHeader() {
console.debug(this._options);
let hdr = this._options.wrapAsync ? this._wrappedHeader : this._header;
console.debug(this._options)
let hdr = this._options.wrapAsync ? this._wrappedHeader : this._header
hdr = this._options.headless
? hdr
: hdr.replace("launch()", "launch({ headless: false })");
return hdr;
: hdr.replace('launch()', 'launch({ headless: false })')
return hdr
}
_getFooter() {
return this._options.wrapAsync ? this._wrappedFooter : this._footer;
return this._options.wrapAsync ? this._wrappedFooter : this._footer
}
_parseEvents(events) {
console.debug(`generating code for ${events ? events.length : 0} events`);
let result = "";
console.debug(`generating code for ${events ? events.length : 0} events`)
let result = ''
if (!events) return result;
if (!events) return result
for (let i = 0; i < events.length; i++) {
const {
@@ -57,178 +57,178 @@ export default class CodeGenerator {
keyCode,
tagName,
frameId,
frameUrl
} = events[i];
frameUrl,
} = events[i]
const escapedSelector = selector
? selector.replace(/\\/g, "\\\\")
: selector;
? selector.replace(/\\/g, '\\\\')
: selector
// we need to keep a handle on what frames events originate from
this._setFrames(frameId, frameUrl);
this._setFrames(frameId, frameUrl)
switch (action) {
case "keydown":
case 'keydown':
if (keyCode === this._options.keyCode) {
this._blocks.push(
this._handleKeyDown(escapedSelector, value, keyCode)
);
)
}
break;
case "click":
this._blocks.push(this._handleClick(escapedSelector, events));
break;
case "change":
if (tagName === "SELECT") {
this._blocks.push(this._handleChange(escapedSelector, value));
break
case 'click':
this._blocks.push(this._handleClick(escapedSelector, events))
break
case 'change':
if (tagName === 'SELECT') {
this._blocks.push(this._handleChange(escapedSelector, value))
}
break;
break
case pptrActions.GOTO:
this._blocks.push(this._handleGoto(href, frameId));
break;
this._blocks.push(this._handleGoto(href, frameId))
break
case pptrActions.VIEWPORT:
this._blocks.push(this._handleViewport(value.width, value.height));
break;
this._blocks.push(this._handleViewport(value.width, value.height))
break
case pptrActions.NAVIGATION:
this._blocks.push(this._handleWaitForNavigation());
this._hasNavigation = true;
break;
this._blocks.push(this._handleWaitForNavigation())
this._hasNavigation = true
break
case pptrActions.SCREENSHOT:
this._blocks.push(this._handleScreenshot(value));
break;
this._blocks.push(this._handleScreenshot(value))
break
}
}
if (this._hasNavigation && this._options.waitForNavigation) {
console.debug("Adding navigationPromise declaration");
console.debug('Adding navigationPromise declaration')
const block = new Block(this._frameId, {
type: pptrActions.NAVIGATION_PROMISE,
value: "const navigationPromise = page.waitForNavigation()"
});
this._blocks.unshift(block);
value: 'const navigationPromise = page.waitForNavigation()',
})
this._blocks.unshift(block)
}
console.debug("post processing blocks:", this._blocks);
this._postProcess();
console.debug('post processing blocks:', this._blocks)
this._postProcess()
const indent = this._options.wrapAsync ? " " : "";
const newLine = `\n`;
const indent = this._options.wrapAsync ? ' ' : ''
const newLine = `\n`
for (let block of this._blocks) {
const lines = block.getLines();
const lines = block.getLines()
for (let line of lines) {
result += indent + line.value + newLine;
result += indent + line.value + newLine
}
}
return result;
return result
}
_setFrames(frameId, frameUrl) {
if (frameId && frameId !== 0) {
this._frameId = frameId;
this._frame = `frame_${frameId}`;
this._allFrames[frameId] = frameUrl;
this._frameId = frameId
this._frame = `frame_${frameId}`
this._allFrames[frameId] = frameUrl
} else {
this._frameId = 0;
this._frame = "page";
this._frameId = 0
this._frame = 'page'
}
}
_postProcess() {
// when events are recorded from different frames, we want to add a frame setter near the code that uses that frame
if (Object.keys(this._allFrames).length > 0) {
this._postProcessSetFrames();
this._postProcessSetFrames()
}
if (this._options.blankLinesBetweenBlocks && this._blocks.length > 0) {
this._postProcessAddBlankLines();
this._postProcessAddBlankLines()
}
}
_handleKeyDown(selector, value) {
const block = new Block(this._frameId);
const block = new Block(this._frameId)
block.addLine({
type: domEvents.KEYDOWN,
value: `await ${this._frame}.type('${selector}', '${this._escapeUserInput(
value
)}')`
});
return block;
)}')`,
})
return block
}
_handleClick(selector) {
const block = new Block(this._frameId);
const block = new Block(this._frameId)
if (this._options.waitForSelectorOnClick) {
block.addLine({
type: domEvents.CLICK,
value: `await ${this._frame}.waitForSelector('${selector}')`
});
value: `await ${this._frame}.waitForSelector('${selector}')`,
})
}
block.addLine({
type: domEvents.CLICK,
value: `await ${this._frame}.click('${selector}')`
});
return block;
value: `await ${this._frame}.click('${selector}')`,
})
return block
}
_handleChange(selector, value) {
return new Block(this._frameId, {
type: domEvents.CHANGE,
value: `await ${this._frame}.select('${selector}', '${value}')`
});
value: `await ${this._frame}.select('${selector}', '${value}')`,
})
}
_handleGoto(href) {
return new Block(this._frameId, {
type: pptrActions.GOTO,
value: `await ${this._frame}.goto('${href}')`
});
value: `await ${this._frame}.goto('${href}')`,
})
}
_handleViewport() {
throw new Error("Not implemented.");
throw new Error('Not implemented.')
}
_handleScreenshot(options) {
let block;
let block
if (options && options.x && options.y && options.width && options.height) {
// remove the tailing 'px'
for (let prop in options) {
if (options.hasOwnProperty(prop) && options[prop].slice(-2) === "px") { // eslint-disable-line
options[prop] = options[prop].substring(0, options[prop].length - 2);
options[prop] = options[prop].substring(0, options[prop].length - 2)
}
}
block = new Block(this._frameId, {
type: pptrActions.SCREENSHOT,
value: `await ${this._frame}.screenshot({ path: 'screenshot_${this._screenshotCounter}.png', clip: { x: ${options.x}, y: ${options.y}, width: ${options.width}, height: ${options.height} } })`
});
value: `await ${this._frame}.screenshot({ path: 'screenshot_${this._screenshotCounter}.png', clip: { x: ${options.x}, y: ${options.y}, width: ${options.width}, height: ${options.height} } })`,
})
} else {
block = new Block(this._frameId, {
type: pptrActions.SCREENSHOT,
value: `await ${this._frame}.screenshot({ path: 'screenshot_${this._screenshotCounter}.png' })`
});
value: `await ${this._frame}.screenshot({ path: 'screenshot_${this._screenshotCounter}.png' })`,
})
}
this._screenshotCounter++;
return block;
this._screenshotCounter++
return block
}
_handleWaitForNavigation() {
const block = new Block(this._frameId);
const block = new Block(this._frameId)
if (this._options.waitForNavigation) {
block.addLine({
type: pptrActions.NAVIGATION,
value: `await navigationPromise`
});
value: `await navigationPromise`,
})
}
return block;
return block
}
_postProcessSetFrames() {
for (let [i, block] of this._blocks.entries()) {
const lines = block.getLines();
const lines = block.getLines()
for (let line of lines) {
if (
line.frameId &&
@@ -236,33 +236,33 @@ export default class CodeGenerator {
) {
const declaration = `const frame_${
line.frameId
} = frames.find(f => f.url() === '${this._allFrames[line.frameId]}')`;
} = frames.find(f => f.url() === '${this._allFrames[line.frameId]}')`
this._blocks[i].addLineToTop({
type: pptrActions.FRAME_SET,
value: declaration
});
value: declaration,
})
this._blocks[i].addLineToTop({
type: pptrActions.FRAME_SET,
value: "let frames = await page.frames()"
});
delete this._allFrames[line.frameId];
break;
value: 'let frames = await page.frames()',
})
delete this._allFrames[line.frameId]
break
}
}
}
}
_postProcessAddBlankLines() {
let i = 0;
let i = 0
while (i <= this._blocks.length) {
const blankLine = new Block();
blankLine.addLine({ type: null, value: "" });
this._blocks.splice(i, 0, blankLine);
i += 2;
const blankLine = new Block()
blankLine.addLine({ type: null, value: '' })
this._blocks.splice(i, 0, blankLine)
i += 2
}
}
_escapeUserInput(value) {
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
}
}

View File

@@ -1,28 +1,28 @@
import pptrActions from "./pptr-actions";
import Block from "./Block";
import CodeGenerator from "./CodeGenerator";
import pptrActions from './pptr-actions'
import Block from './Block'
import CodeGenerator from './CodeGenerator'
const importPlaywright = `const { chromium } = require('playwright');\n`;
const importPlaywright = `const { chromium } = require('playwright');\n`
const header = `const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()`;
const page = await context.newPage()`
const footer = `await browser.close()`;
const footer = `await browser.close()`
const wrappedHeader = `(async () => {
${header}\n`;
${header}\n`
const wrappedFooter = ` ${footer}
})()`;
})()`
export default class PlaywrightCodeGenerator extends CodeGenerator {
constructor(options) {
super(options);
this._header = header;
this._wrappedHeader = wrappedHeader;
this._footer = footer;
this._wrappedFooter = wrappedFooter;
super(options)
this._header = header
this._wrappedHeader = wrappedHeader
this._footer = footer
this._wrappedFooter = wrappedFooter
}
generate(events) {
@@ -31,20 +31,20 @@ export default class PlaywrightCodeGenerator extends CodeGenerator {
this._getHeader() +
this._parseEvents(events) +
this._getFooter()
);
)
}
_handleViewport(width, height) {
return new Block(this._frameId, {
type: pptrActions.VIEWPORT,
value: `await ${this._frame}.setViewportSize({ width: ${width}, height: ${height} })`
});
value: `await ${this._frame}.setViewportSize({ width: ${width}, height: ${height} })`,
})
}
_handleChange(selector, value) {
return new Block(this._frameId, {
type: pptrActions.CHANGE,
value: `await ${this._frame}.selectOption('${selector}', '${value}')`
});
value: `await ${this._frame}.selectOption('${selector}', '${value}')`,
})
}
}

View File

@@ -1,28 +1,28 @@
import pptrActions from "./pptr-actions";
import Block from "./Block";
import CodeGenerator from "./CodeGenerator";
import pptrActions from './pptr-actions'
import Block from './Block'
import CodeGenerator from './CodeGenerator'
const importPuppeteer = `const puppeteer = require('puppeteer');\n`;
const importPuppeteer = `const puppeteer = require('puppeteer');\n`
const header = `const browser = await puppeteer.launch()
const page = await browser.newPage()`;
const page = await browser.newPage()`
const footer = `await browser.close()`;
const footer = `await browser.close()`
const wrappedHeader = `(async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()\n`;
const page = await browser.newPage()\n`
const wrappedFooter = ` await browser.close()
})()`;
})()`
export default class PuppeteerCodeGenerator extends CodeGenerator {
constructor(options) {
super(options);
this._header = header;
this._wrappedHeader = wrappedHeader;
this._footer = footer;
this._wrappedFooter = wrappedFooter;
super(options)
this._header = header
this._wrappedHeader = wrappedHeader
this._footer = footer
this._wrappedFooter = wrappedFooter
}
generate(events) {
@@ -31,13 +31,13 @@ export default class PuppeteerCodeGenerator extends CodeGenerator {
this._getHeader() +
this._parseEvents(events) +
this._getFooter()
);
)
}
_handleViewport(width, height) {
return new Block(this._frameId, {
type: pptrActions.VIEWPORT,
value: `await ${this._frame}.setViewport({ width: ${width}, height: ${height} })`
});
value: `await ${this._frame}.setViewport({ width: ${width}, height: ${height} })`,
})
}
}

View File

@@ -1,13 +1,13 @@
export default {
CLICK: "click",
DBLCLICK: "dblclick",
CHANGE: "change",
KEYDOWN: "keydown",
SELECT: "select",
SUBMIT: "submit",
LOAD: "load",
UNLOAD: "unload"
};
CLICK: 'click',
DBLCLICK: 'dblclick',
CHANGE: 'change',
KEYDOWN: 'keydown',
SELECT: 'select',
SUBMIT: 'submit',
LOAD: 'load',
UNLOAD: 'unload',
}
// const events = [
// abort,

View File

@@ -1,9 +1,9 @@
export default {
GOTO: "GOTO",
VIEWPORT: "VIEWPORT",
WAITFORSELECTOR: "WAITFORSELECTOR",
NAVIGATION: "NAVIGATION",
NAVIGATION_PROMISE: "NAVIGATION_PROMISE",
FRAME_SET: "FRAME_SET",
SCREENSHOT: "SCREENSHOT"
};
GOTO: 'GOTO',
VIEWPORT: 'VIEWPORT',
WAITFORSELECTOR: 'WAITFORSELECTOR',
NAVIGATION: 'NAVIGATION',
NAVIGATION_PROMISE: 'NAVIGATION_PROMISE',
FRAME_SET: 'FRAME_SET',
SCREENSHOT: 'SCREENSHOT',
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,28 @@
module.exports = {
pages: {
popup: {
template: "public/browser-extension.html",
entry: "./src/popup/main.js",
title: "Popup"
template: 'public/browser-extension.html',
entry: './src/popup/main.js',
title: 'Popup',
},
options: {
template: "public/browser-extension.html",
entry: "./src/options/main.js",
title: "Options"
}
template: 'public/browser-extension.html',
entry: './src/options/main.js',
title: 'Options',
},
},
pluginOptions: {
browserExtension: {
componentOptions: {
background: {
entry: "src/background.js"
entry: 'src/background.js',
},
contentScripts: {
entries: {
"content-script": ["src/content-scripts/index.js"]
}
}
}
}
}
};
'content-script': ['src/content-scripts/index.js'],
},
},
},
},
},
}