refactor: update code to Vue 3 and refresh test cases
7
jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: '@vue/cli-plugin-unit-jest',
|
||||
transform: {
|
||||
'^.+\\.vue$': 'vue-jest',
|
||||
},
|
||||
setupFilesAfterEnv: ['./jest.setup.js'],
|
||||
}
|
||||
1
jest.setup.js
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom'
|
||||
12637
package-lock.json
generated
26
package.json
@@ -5,23 +5,43 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service build --mode development --watch",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit __tests__/.*.spec.js",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medv/finder": "1.1.2",
|
||||
"@tailwindcss/postcss7-compat": "2.0.2",
|
||||
"@vueuse/core": "4.0.8",
|
||||
"autoprefixer": "9",
|
||||
"core-js": "3.6.5",
|
||||
"vue": "3.0.0"
|
||||
"postcss": "7",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@2.0.2",
|
||||
"vue": "3.0.6",
|
||||
"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.0.0-0",
|
||||
"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",
|
||||
"vue-cli-plugin-browser-extension": "0.25.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"
|
||||
}
|
||||
}
|
||||
|
||||
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extName": {
|
||||
"message": "headless-recorder-v2",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 513 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 513 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/icon-black.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/icon-green.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/icon_rec.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/icon_wait.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
10
src/__e2e-tests__/build.spec.js
Normal file
@@ -0,0 +1,10 @@
|
||||
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);
|
||||
});
|
||||
30
src/__e2e-tests__/helpers.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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");
|
||||
|
||||
export const launchPuppeteerWithExtension = function(puppeteer) {
|
||||
const options = {
|
||||
headless: false,
|
||||
ignoreHTTPSErrors: true,
|
||||
devtools: true,
|
||||
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
|
||||
}
|
||||
|
||||
return puppeteer.launch(options);
|
||||
};
|
||||
|
||||
export const runBuild = function() {
|
||||
return exec(scripts.build);
|
||||
};
|
||||
252
src/assets/images/Desert.svg
Normal file
@@ -0,0 +1,252 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="110px"
|
||||
height="110px" viewBox="0 0 110 110" style="enable-background:new 0 0 110 110;" xml:space="preserve">
|
||||
<g id="Artboard" style="display:none;">
|
||||
<rect x="-963" y="-178" style="display:inline;fill:#808080;" width="1376" height="359"/>
|
||||
</g>
|
||||
<g id="R-Multicolor" style="display:none;">
|
||||
<circle style="display:inline;fill:#32BEA6;" cx="55" cy="55" r="55"/>
|
||||
<g style="display:inline;">
|
||||
<path style="fill:#EDBC7C;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
<path style="fill:#F8E1C2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.796,0,1.559,0.316,2.121,0.879
|
||||
L87,83H23z"/>
|
||||
<circle style="fill:#FACB1B;" cx="31" cy="31" r="8"/>
|
||||
<path style="fill:#107665;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
<path style="fill:#0D9681;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Multicolor">
|
||||
<g>
|
||||
<path style="fill:#EDBC7C;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
<path style="fill:#F8E1C2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.796,0,1.559,0.316,2.121,0.879
|
||||
L87,83H23z"/>
|
||||
<circle style="fill:#FACB1B;" cx="31" cy="31" r="8"/>
|
||||
<path style="fill:#107665;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
<path style="fill:#0D9681;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Blue" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#53BAD4;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#BBE7F2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#0081A1;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#009FC7;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="R-Blue" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<circle style="fill:#81D2EB;" cx="55" cy="55" r="55"/>
|
||||
</g>
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#53BAD4;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#BBE7F2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#0081A1;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#009FC7;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Green" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#5DCFC3;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#AAF0E9;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#009687;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#00B8A5;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="R-Green" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<circle style="fill:#87E0C8;" cx="55" cy="55" r="55"/>
|
||||
</g>
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#5DCFC3;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#AAF0E9;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#009687;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#00B8A5;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Red" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#E8A099;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFD7D4;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#C23023;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#E54B44;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="R-Red" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<circle style="fill:#FABBAF;" cx="55" cy="55" r="55"/>
|
||||
</g>
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#E8A099;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFD7D4;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#C23023;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#E54B44;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Yellow" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#F5C43D;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFE9A1;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#E07000;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FA9200;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="R-Yellow" style="display:none;">
|
||||
<g style="display:inline;">
|
||||
<circle style="fill:#FFD75E;" cx="55" cy="55" r="55"/>
|
||||
</g>
|
||||
<g style="display:inline;">
|
||||
<g>
|
||||
<path style="fill:#F5C43D;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
|
||||
v4h64v-4L83.879,79.879z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFE9A1;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
|
||||
L87,83H23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#E07000;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
|
||||
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
|
||||
C82,37.66,79.28,35,76,35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FA9200;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
|
||||
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/context_menu.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
12
src/assets/images/help.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>_ionicons_svg_ios-help-circle</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="_ionicons_svg_ios-help-circle" fill="#8492A6" fill-rule="nonzero">
|
||||
<path d="M16,0 C7.16153846,0 0,7.16153846 0,16 C0,24.8384615 7.16153846,32 16,32 C24.8384615,32 32,24.8384615 32,16 C32,7.16153846 24.8384615,0 16,0 Z M15.6692308,23.3846154 C14.7615385,23.3846154 14.0230769,22.6923077 14.0230769,21.8 C14.0230769,20.9153846 14.7615385,20.2153846 15.6692308,20.2153846 C16.5846154,20.2153846 17.3230769,20.9076923 17.3230769,21.8 C17.3230769,22.6923077 16.5923077,23.3846154 15.6692308,23.3846154 Z M18.7615385,15.9307692 C17.4230769,16.7076923 16.9692308,17.2769231 16.9692308,18.2615385 L16.9692308,18.8692308 L14.3,18.8692308 L14.2769231,18.2076923 C14.1461538,16.6230769 14.7,15.6384615 16.0923077,14.8230769 C17.3923077,14.0461538 17.9384615,13.5538462 17.9384615,12.6 C17.9384615,11.6461538 17.0153846,10.9461538 15.8692308,10.9461538 C14.7076923,10.9461538 13.8692308,11.7 13.8076923,12.8384615 L11.0769231,12.8384615 C11.1307692,10.3615385 12.9615385,8.60769231 16.0538462,8.60769231 C18.9384615,8.60769231 20.9230769,10.2076923 20.9230769,12.5076923 C20.9230769,14.0384615 20.1846154,15.0923077 18.7615385,15.9307692 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/icon-black.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/images/icon-green.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/images/icon_rec.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/icon_wait.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/puppeteer.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src/assets/images/recorder.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
12
src/assets/images/settings.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>_ionicons_svg_ios-settings</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="_ionicons_svg_ios-settings" fill="#8492A6" fill-rule="nonzero">
|
||||
<path d="M29.3583333,16 C29.3583333,14.25 30.45,12.7583333 32,12.1583333 C31.5916667,10.45 30.9166667,8.85 30.025,7.4 C29.4916667,7.63333333 28.925,7.75833333 28.35,7.75833333 C27.3,7.75833333 26.25,7.35833333 25.4416667,6.55833333 C24.2,5.31666667 23.925,3.49166667 24.5916667,1.975 C23.15,1.08333333 21.5416667,0.408333333 19.8416667,1.18423789e-15 C19.25,1.54166667 17.75,2.64166667 16,2.64166667 C14.25,2.64166667 12.75,1.54166667 12.1583333,0 C10.45,0.408333333 8.85,1.08333333 7.4,1.975 C8.075,3.48333333 7.79166667,5.31666667 6.55,6.55833333 C5.75,7.35833333 4.69166667,7.75833333 3.64166667,7.75833333 C3.06666667,7.75833333 2.5,7.64166667 1.96666667,7.4 C1.08333333,8.85833333 0.408333333,10.4583333 0,12.1666667 C1.54166667,12.7583333 2.64166667,14.25 2.64166667,16.0083333 C2.64166667,17.7583333 1.55,19.25 0.00833333333,19.85 C0.416666667,21.5583333 1.09166667,23.1583333 1.98333333,24.6083333 C2.51666667,24.375 3.08333333,24.2583333 3.65,24.2583333 C4.7,24.2583333 5.75,24.6583333 6.55833333,25.4583333 C7.79166667,26.6916667 8.075,28.525 7.40833333,30.0333333 C8.85833333,30.925 10.4666667,31.6 12.1666667,32.0083333 C12.7583333,30.4666667 14.25,29.375 16,29.375 C17.75,29.375 19.2416667,30.4666667 19.8333333,32.0083333 C21.5416667,31.6 23.1416667,30.925 24.5916667,30.0333333 C23.925,28.525 24.2083333,26.7 25.4416667,25.4583333 C26.2416667,24.6583333 27.2916667,24.2583333 28.35,24.2583333 C28.9166667,24.2583333 29.4916667,24.375 30.0166667,24.6083333 C30.9083333,23.1583333 31.5833333,21.55 31.9916667,19.85 C30.4583333,19.25 29.3583333,17.7583333 29.3583333,16 Z M16.075,22.6583333 C12.3833333,22.6583333 9.40833333,19.6666667 9.40833333,15.9916667 C9.40833333,12.3166667 12.3833333,9.325 16.075,9.325 C19.7666667,9.325 22.7416667,12.3166667 22.7416667,15.9916667 C22.7416667,19.6666667 19.7666667,22.6583333 16.075,22.6583333 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
43
src/assets/images/text_racoon_logo.svg
Normal file
|
After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
163
src/assets/styles/_animations.scss
Normal file
@@ -0,0 +1,163 @@
|
||||
.shake {
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
.flash-once {
|
||||
animation: flash-once 3.5s ease 1;
|
||||
}
|
||||
|
||||
@keyframes fade-up {
|
||||
0% {
|
||||
transform: translate3d(0, 10px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fade-in .3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation-name: spin;
|
||||
animation-duration: 2000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {transform:rotate(0deg);}
|
||||
to {transform:rotate(360deg);}
|
||||
}
|
||||
|
||||
|
||||
.bounceIn {
|
||||
animation-name: bounceIn;
|
||||
transform-origin: center bottom;
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0%, 20%, 40%, 60%, 80%, 100% {
|
||||
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
}
|
||||
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(.8, .8, .8);
|
||||
transform: scale3d(.8, .8, .8);
|
||||
}
|
||||
|
||||
20% {
|
||||
-webkit-transform: scale3d(1.1, 1.1, 1.1);
|
||||
transform: scale3d(1.1, 1.1, 1.1);
|
||||
}
|
||||
|
||||
40% {
|
||||
-webkit-transform: scale3d(.9, .9, .9);
|
||||
transform: scale3d(.9, .9, .9);
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1.03, 1.03, 1.03);
|
||||
transform: scale3d(1.03, 1.03, 1.03);
|
||||
}
|
||||
|
||||
80% {
|
||||
-webkit-transform: scale3d(.97, .97, .97);
|
||||
transform: scale3d(.97, .97, .97);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% {
|
||||
color: rgba(0,0,0,0);
|
||||
text-shadow:
|
||||
.25em 0 0 rgba(0,0,0,0),
|
||||
.5em 0 0 rgba(0,0,0,0);}
|
||||
40% {
|
||||
color: #8492A6;
|
||||
text-shadow:
|
||||
.25em 0 0 rgba(0,0,0,0),
|
||||
.5em 0 0 rgba(0,0,0,0);}
|
||||
60% {
|
||||
text-shadow:
|
||||
.25em 0 0 #8492A6,
|
||||
.5em 0 0 rgba(0,0,0,0);}
|
||||
80%, 100% {
|
||||
text-shadow:
|
||||
.25em 0 0 #8492A6,
|
||||
.5em 0 0 #8492A6;}}
|
||||
|
||||
@keyframes recording {
|
||||
0% {
|
||||
box-shadow: 0px 0px 5px 0px rgba(173,0,0,.3);
|
||||
}
|
||||
65% {
|
||||
box-shadow: 0px 0px 5px 5px rgba(173,0,0,.3);
|
||||
}
|
||||
90% {
|
||||
box-shadow: 0px 0px 5px 5px rgba(173,0,0,0);
|
||||
}
|
||||
}
|
||||
39
src/assets/styles/_buttons.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
button {
|
||||
&.btn {
|
||||
display: inline-block;
|
||||
font-weight: 300;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
letter-spacing: 1px;
|
||||
transition: all .15s ease;
|
||||
|
||||
&.btn-sm {
|
||||
padding: .4rem .8rem;
|
||||
font-size: .8rem;
|
||||
border-radius: .2rem
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
color: #fff;
|
||||
background-color: $brand-primary;
|
||||
border-color: $brand-primary;
|
||||
}
|
||||
|
||||
&.btn-outline-primary {
|
||||
color: $brand-primary;
|
||||
background-color: transparent;
|
||||
border-color: $brand-primary
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
color: #fff;
|
||||
background-color: $brand-danger;
|
||||
border-color: $brand-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/assets/styles/_mixins.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@mixin header () {
|
||||
background: $gray-lightest;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 0 $spacer;
|
||||
font-weight: 500;
|
||||
}
|
||||
@mixin footer() {
|
||||
background: $gray-lightest;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 $spacer;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid $gray-light;
|
||||
}
|
||||
21
src/assets/styles/_transitions.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
// drop down
|
||||
.drop-down-enter, .drop-down-leave-to {
|
||||
transform: translateX(0) translateY(-20px);
|
||||
transition-timing-function: cubic-bezier(.74,.04,.26,1.05);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.drop-down-enter-active, .drop-down-leave-active {
|
||||
transition: all .15s
|
||||
}
|
||||
|
||||
// move left
|
||||
.move-left-enter, .move-left-leave-to {
|
||||
transform: translateY(0) translateX(-80px);
|
||||
transition-timing-function: cubic-bezier(.74,.04,.26,1.05);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.move-left-enter-active, .move-left-leave-active {
|
||||
transition: all .15s
|
||||
}
|
||||
8
src/assets/styles/_typography.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.text-muted {
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
3
src/assets/styles/_utils.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
38
src/assets/styles/_variables.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
// spacing
|
||||
$spacer: 12px;
|
||||
|
||||
// colors
|
||||
$white: #fff;
|
||||
$black: #1f2d3d;
|
||||
$red: #FF4949;
|
||||
$orange: #F19B45;
|
||||
$yellow: #FFC82C;
|
||||
$green: #13CE66;
|
||||
|
||||
$blue: #45C8F1;
|
||||
$blue-light: #EBFAFF;
|
||||
$blue-lightest: #F0F8FF;
|
||||
$teal: #205E71;
|
||||
$pink: #FF659D;
|
||||
|
||||
$gray-dark: #3C4858;
|
||||
$gray: #8492A6;
|
||||
$gray-light: #E0E6ED;
|
||||
$gray-lighter: #EFF2F7;
|
||||
$gray-lightest: #F9FAFC;
|
||||
|
||||
$brand-primary: $blue;
|
||||
$brand-success: $green;
|
||||
$brand-info: $teal;
|
||||
$brand-warning: $orange;
|
||||
$brand-danger: $red;
|
||||
$brand-inverse: $gray-dark;
|
||||
$brand-accent: $pink;
|
||||
|
||||
// typography
|
||||
|
||||
$text-muted: $gray;
|
||||
|
||||
// layout
|
||||
|
||||
$max-content-height: 400px;
|
||||
71
src/assets/styles/github-gist.css
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* GitHub Gist Theme
|
||||
* Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
30
src/assets/styles/main.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@import "variables";
|
||||
@import "buttons";
|
||||
@import "typography";
|
||||
@import "transitions";
|
||||
@import "animations";
|
||||
@import "github-gist.css";
|
||||
@import "mixins";
|
||||
|
||||
//reset
|
||||
|
||||
html {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
color: $gray-dark;
|
||||
width: 350px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
5
src/assets/tailwind.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
@@ -1,7 +1,272 @@
|
||||
browser.runtime.onMessage.addListener(function() {
|
||||
console.log("Hello from the background");
|
||||
import pptrActions from "@/services/pptr-actions";
|
||||
import ctrl from "@/models/extension-control-messages";
|
||||
import actions from "@/models/extension-ui-actions";
|
||||
|
||||
browser.tabs.executeScript({
|
||||
file: "content-script.js"
|
||||
});
|
||||
});
|
||||
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;
|
||||
|
||||
// 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._menuId = "PUPPETEER_RECORDER_CONTEXT_MENU";
|
||||
this._menuOptions = {
|
||||
SCREENSHOT: "SCREENSHOT",
|
||||
SCREENSHOT_CLIPPED: "SCREENSHOT_CLIPPED"
|
||||
};
|
||||
}
|
||||
|
||||
boot() {
|
||||
chrome.extension.onConnect.addListener(port => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
console.debug("start recording");
|
||||
this.cleanUp(() => {
|
||||
this._badgeState = "rec";
|
||||
|
||||
this._hasGoto = false;
|
||||
this._hasViewPort = false;
|
||||
|
||||
this.injectScript();
|
||||
|
||||
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.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" });
|
||||
|
||||
/**
|
||||
* Right click menu setup
|
||||
*/
|
||||
|
||||
chrome.contextMenus.removeAll();
|
||||
|
||||
// add the parent and its children
|
||||
|
||||
chrome.contextMenus.create({
|
||||
id: this._menuId,
|
||||
title: "Headless Recorder",
|
||||
contexts: ["all"]
|
||||
});
|
||||
|
||||
chrome.contextMenus.create({
|
||||
id: this._menuId + this._menuOptions.SCREENSHOT,
|
||||
title: "Take Screenshot (Ctrl+Shift+A)",
|
||||
parentId: this._menuId,
|
||||
contexts: ["all"]
|
||||
});
|
||||
|
||||
chrome.contextMenus.create({
|
||||
id: this._menuId + this._menuOptions.SCREENSHOT_CLIPPED,
|
||||
title: "Take Screenshot Clipped (Ctrl+Shift+S)",
|
||||
parentId: this._menuId,
|
||||
contexts: ["all"]
|
||||
});
|
||||
|
||||
// add the handlers
|
||||
|
||||
this._boundedMenuHandler = this.handleMenuInteraction.bind(this);
|
||||
chrome.contextMenus.onClicked.addListener(this._boundedMenuHandler);
|
||||
|
||||
this._boundedKeyCommandHandler = this.handleKeyCommands.bind(this);
|
||||
chrome.commands.onCommand.addListener(this._boundedKeyCommandHandler);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
console.debug("stop recording");
|
||||
this._badgeState = this._recording.length > 0 ? "1" : "";
|
||||
|
||||
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.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");
|
||||
});
|
||||
}
|
||||
|
||||
pause() {
|
||||
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;
|
||||
}
|
||||
|
||||
cleanUp(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);
|
||||
this.handleMessage({
|
||||
selector: undefined,
|
||||
value: undefined,
|
||||
action: pptrActions.GOTO,
|
||||
href
|
||||
});
|
||||
this._hasGoto = true;
|
||||
}
|
||||
}
|
||||
|
||||
recordCurrentViewportSize(value) {
|
||||
if (!this._hasViewPort) {
|
||||
this.handleMessage({
|
||||
selector: undefined,
|
||||
value,
|
||||
action: pptrActions.VIEWPORT
|
||||
});
|
||||
this._hasViewPort = true;
|
||||
}
|
||||
}
|
||||
|
||||
recordNavigation() {
|
||||
this.handleMessage({
|
||||
selector: undefined,
|
||||
value: undefined,
|
||||
action: pptrActions.NAVIGATION
|
||||
});
|
||||
}
|
||||
|
||||
recordScreenshot(value) {
|
||||
this.handleMessage({
|
||||
selector: undefined,
|
||||
value,
|
||||
action: pptrActions.SCREENSHOT
|
||||
});
|
||||
}
|
||||
|
||||
handleMessage(msg, sender) {
|
||||
if (msg.control) return this.handleControlMessage(msg, sender);
|
||||
|
||||
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;
|
||||
|
||||
if (!this._isPaused) {
|
||||
this._recording.push(msg);
|
||||
console.log(msg);
|
||||
chrome.storage.local.set({ recording: this._recording }, () => {
|
||||
console.debug("stored recording updated");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleControlMessage(msg) {
|
||||
if (msg.control === ctrl.EVENT_RECORDER_STARTED)
|
||||
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);
|
||||
}
|
||||
|
||||
handleNavigation({ frameId }) {
|
||||
console.debug("frameId is:", frameId);
|
||||
this.injectScript();
|
||||
if (frameId === 0) {
|
||||
this.recordNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuInteraction(info) {
|
||||
console.debug("context menu clicked");
|
||||
switch (info.menuItemId) {
|
||||
case this._menuId + this._menuOptions.SCREENSHOT:
|
||||
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_MODE);
|
||||
break;
|
||||
case this._menuId + this._menuOptions.SCREENSHOT_CLIPPED:
|
||||
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_CLIPPED_MODE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyCommands(command) {
|
||||
switch (command) {
|
||||
case actions.TOGGLE_SCREENSHOT_MODE:
|
||||
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_MODE);
|
||||
break;
|
||||
case actions.TOGGLE_SCREENSHOT_CLIPPED_MODE:
|
||||
this.toggleScreenShotMode(actions.TOGGLE_SCREENSHOT_CLIPPED_MODE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toggleScreenShotMode(action) {
|
||||
console.debug("toggling screenshot mode");
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, { action });
|
||||
});
|
||||
}
|
||||
|
||||
handleWait() {
|
||||
chrome.browserAction.setBadgeText({ text: "wait" });
|
||||
}
|
||||
|
||||
injectScript() {
|
||||
chrome.tabs.executeScript({
|
||||
file: "js/content-script.js",
|
||||
allFrames: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("booting recording controller");
|
||||
window.recordingController = new RecordingController();
|
||||
window.recordingController.boot();
|
||||
|
||||
29
src/components/ChecklyBadge.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="checkly-badge text-muted">
|
||||
powered by
|
||||
<a href="https://checklyhq.com" target="_blank">
|
||||
<img src="@/assets/images/text_racoon_logo.svg" alt="Checkly logo" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ChecklyBadge"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkly-badge {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-left: 8px;
|
||||
width: 80px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
103
src/components/HelpTab.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="tab help-tab">
|
||||
<div class="content">
|
||||
<h4>Recording</h4>
|
||||
<ul>
|
||||
<li>To start recording hit <strong>Record</strong></li>
|
||||
<li>
|
||||
Hit <kbd>tab</kbd> after you finish typing in an
|
||||
<kbd>input</kbd> element.
|
||||
</li>
|
||||
<li>Click links, inputs and other elements.</li>
|
||||
<li>
|
||||
Wait for full page load on each navigation. The icon will switch from
|
||||
<img src="@/assets/images/icon_rec.png" /> to
|
||||
<img src="@/assets/images/icon_wait.png" />.
|
||||
</li>
|
||||
<li>
|
||||
Click <strong>Pause</strong> when you want to navigate without
|
||||
recording anything. Hit <strong>Resume</strong> to continue recording.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>Controls</h4>
|
||||
<p>
|
||||
While recording, right click to show extra controls that trigger various
|
||||
functions like recording screenshots.
|
||||
<img
|
||||
src="@/assets/images/context_menu.png"
|
||||
alt="context menu"
|
||||
class="w-100"
|
||||
/>
|
||||
</p>
|
||||
<h4>Keyboard shortcuts</h4>
|
||||
<ul>
|
||||
<li>Take screenshot: Ctrl+Shift+A</li>
|
||||
<li>Take clipped screenshot: Ctrl+Shift+M</li>
|
||||
</ul>
|
||||
<p>
|
||||
For more help and examples,
|
||||
<a href="https://checklyhq.com/headless-recorder" target="_blank"
|
||||
>go to the help docs</a
|
||||
>
|
||||
</p>
|
||||
<h4>Replaying</h4>
|
||||
<p>
|
||||
Install
|
||||
<a href="https://github.com/GoogleChrome/puppeteer">Puppeteer</a> or
|
||||
<a href="https://playwright.dev/">Playwright</a> on your machine. Copy
|
||||
and paste the code into a file and run as a standard node program
|
||||
</p>
|
||||
<pre>
|
||||
npm install puppeteer playwright
|
||||
node my-script.js</pre
|
||||
>
|
||||
</div>
|
||||
<div class="help-footer text-muted">
|
||||
powered by
|
||||
<a href="https://checklyhq.com" target="_blank">
|
||||
<img src="@/assets/images/text_racoon_logo.svg" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "HelpTab"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/styles/_variables.scss";
|
||||
@import "../assets/styles/_mixins.scss";
|
||||
@import "../assets/styles/_utils.scss";
|
||||
|
||||
.help-tab {
|
||||
.content {
|
||||
padding: $spacer;
|
||||
text-align: left;
|
||||
|
||||
ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #272822;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.help-footer {
|
||||
@include footer();
|
||||
font-weight: normal;
|
||||
justify-content: flex-end;
|
||||
img {
|
||||
margin-left: 8px;
|
||||
width: 80px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
src/components/RecordingTab.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="tab recording-tab">
|
||||
<div class="content">
|
||||
<div class="empty" v-show="!isRecording">
|
||||
<img src="@/assets/images/Desert.svg" alt="desert" width="78px" />
|
||||
<h3>No recorded events yet</h3>
|
||||
<p class="text-muted">Click record to begin</p>
|
||||
<div class="nag-cta" v-show="!isRecording">
|
||||
<a href="https://checklyhq.com/headless-recorder" target="_blank"
|
||||
>Puppeteer Recorder is now <strong>Headless Recorder</strong> and
|
||||
supports Playwright →</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="events" v-show="isRecording">
|
||||
<p
|
||||
class="text-muted text-center loading"
|
||||
v-show="liveEvents.length === 0"
|
||||
>
|
||||
Waiting for events
|
||||
</p>
|
||||
<ul class="event-list">
|
||||
<li
|
||||
v-for="(event, index) in liveEvents"
|
||||
:key="index"
|
||||
class="event-list-item"
|
||||
>
|
||||
<div class="event-label">{{ index + 1 }}.</div>
|
||||
<div class="event-description">
|
||||
<div class="event-action">{{ event.action }}</div>
|
||||
<div class="event-props text-muted">
|
||||
{{ event.selector || parseEventValue(event) }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "RecordingTab",
|
||||
props: {
|
||||
isRecording: { type: Boolean, default: false },
|
||||
liveEvents: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseEventValue(event) {
|
||||
if (!event) {
|
||||
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";
|
||||
|
||||
.recording-tab {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
.empty {
|
||||
padding: $spacer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.events {
|
||||
max-height: $max-content-height;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.loading:after {
|
||||
content: ".";
|
||||
animation: dots 1s steps(5, end) infinite;
|
||||
animation-delay: 1.5s;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.event-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.event-list-item {
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid $gray-light;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 32px;
|
||||
|
||||
.event-label {
|
||||
vertical-align: top;
|
||||
margin-right: $spacer;
|
||||
}
|
||||
|
||||
.event-description {
|
||||
margin-right: auto;
|
||||
display: inline-block;
|
||||
|
||||
.event-action {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.event-props {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.nag-cta {
|
||||
margin-bottom: $spacer;
|
||||
a {
|
||||
color: $pink;
|
||||
font-size: 80%;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
287
src/components/ResultsTab.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="tab results-tab">
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab"
|
||||
class="tabs__action"
|
||||
v-bind:class="{ selected: activeTab === tab }"
|
||||
@click.prevent="changeTab(tab)"
|
||||
>
|
||||
<span v-if="tab === 'playwright'">🎭</span>
|
||||
<img
|
||||
v-if="tab === 'puppeteer'"
|
||||
src="@/assets/images/puppeteer.png"
|
||||
width="16"
|
||||
/>
|
||||
<span class="tabs__action--text">{{ tab }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<pre v-if="code()" v-highlightjs="code()">
|
||||
<code class="javascript">
|
||||
</code>
|
||||
</pre>
|
||||
<pre v-else>
|
||||
<code>No code yet...</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export const TYPE = {
|
||||
PUPPETEER: "puppeteer",
|
||||
PLAYWRIGHT: "playwright"
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ResultsTab",
|
||||
props: {
|
||||
puppeteer: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
playwright: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: TYPE.PUPPETEER,
|
||||
tabs: [TYPE.PUPPETEER, TYPE.PLAYWRIGHT]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (
|
||||
this.options &&
|
||||
this.options.code &&
|
||||
this.options.code.showPlaywrightFirst
|
||||
) {
|
||||
this.activeTab = TYPE.PLAYWRIGHT;
|
||||
this.tabs = this.tabs.reverse();
|
||||
}
|
||||
this.$emit("update:tab", this.activeTab);
|
||||
},
|
||||
methods: {
|
||||
code() {
|
||||
return this.activeTab === TYPE.PUPPETEER
|
||||
? this.puppeteer
|
||||
: this.playwright;
|
||||
},
|
||||
changeTab(tab) {
|
||||
this.activeTab = tab;
|
||||
this.$emit("update:tab", tab);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/styles/_variables.scss";
|
||||
|
||||
.results-tab {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.generated-code {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
max-height: $max-content-height;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 0 $spacer;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
code {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
padding: $spacer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid $gray-lighter;
|
||||
&__action {
|
||||
padding: 12px 20px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: $gray-dark;
|
||||
outline: none;
|
||||
border-bottom: 4px solid transparent;
|
||||
text-transform: capitalize;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&--text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
&.selected {
|
||||
border-bottom-color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: #011627;
|
||||
color: #d6deeb;
|
||||
}
|
||||
|
||||
/* General Purpose */
|
||||
.hljs-keyword {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-built_in {
|
||||
color: #addb67;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-type {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #ff5874;
|
||||
}
|
||||
.hljs-number {
|
||||
color: #f78c6c;
|
||||
}
|
||||
.hljs-regexp {
|
||||
color: #5ca7e4;
|
||||
}
|
||||
.hljs-string {
|
||||
color: #ecc48d;
|
||||
}
|
||||
.hljs-subst {
|
||||
color: #d3423e;
|
||||
}
|
||||
.hljs-symbol {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-class {
|
||||
color: #ffcb8b;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-title {
|
||||
color: #dcdcaa;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-params {
|
||||
color: #7fdbca;
|
||||
}
|
||||
|
||||
/* Meta */
|
||||
.hljs-comment {
|
||||
color: #637777;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-meta {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-meta-keyword {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-meta-string {
|
||||
color: #ecc48d;
|
||||
}
|
||||
|
||||
/* Tags, attributes, config */
|
||||
.hljs-section {
|
||||
color: #82b1ff;
|
||||
}
|
||||
.hljs-tag,
|
||||
.hljs-name {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-attr {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-attribute {
|
||||
color: #80cbc4;
|
||||
}
|
||||
.hljs-variable {
|
||||
color: #addb67;
|
||||
}
|
||||
|
||||
/* Markup */
|
||||
.hljs-bullet {
|
||||
color: #d9f5dd;
|
||||
}
|
||||
.hljs-code {
|
||||
color: #80cbc4;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
color: #addb67;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-formula {
|
||||
color: #c792ea;
|
||||
}
|
||||
.hljs-link {
|
||||
color: #ff869a;
|
||||
}
|
||||
.hljs-quote {
|
||||
color: #697098;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* CSS */
|
||||
.hljs-selector-tag {
|
||||
color: #ff6363;
|
||||
}
|
||||
|
||||
.hljs-selector-id {
|
||||
color: #fad430;
|
||||
}
|
||||
|
||||
.hljs-selector-class {
|
||||
color: #addb67;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Templates */
|
||||
.hljs-template-tag {
|
||||
color: #c792ea;
|
||||
}
|
||||
.hljs-template-variable {
|
||||
color: #addb67;
|
||||
}
|
||||
|
||||
/* diff */
|
||||
.hljs-addition {
|
||||
color: #addb67ff;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #ef535090;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
57
src/components/__tests__/RecordingTab.spec.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import RecordingTab from '../RecordingTab'
|
||||
|
||||
describe('RecordingTab.vue', () => {
|
||||
test('it has the correct pristine / empty state', () => {
|
||||
const wrapper = mount(RecordingTab)
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('it has the correct waiting for events state', () => {
|
||||
const wrapper = mount(RecordingTab, { props: { isRecording: true } })
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
expect(wrapper.find('.event-list').element).toBeEmpty()
|
||||
})
|
||||
|
||||
test('it has the correct recording Puppeteer custom events state', () => {
|
||||
const wrapper = mount(RecordingTab, {
|
||||
props: {
|
||||
isRecording: true,
|
||||
liveEvents: [
|
||||
{
|
||||
action: 'goto*',
|
||||
href: 'http://example.com',
|
||||
},
|
||||
{
|
||||
action: 'viewport*',
|
||||
selector: undefined,
|
||||
value: { width: 1280, height: 800 },
|
||||
},
|
||||
{
|
||||
action: 'navigation*',
|
||||
selector: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
expect(wrapper.find('.event-list').element).not.toBeEmpty()
|
||||
})
|
||||
|
||||
test('it has the correct recording DOM events state', () => {
|
||||
const wrapper = mount(RecordingTab, {
|
||||
props: {
|
||||
isRecording: true,
|
||||
liveEvents: [
|
||||
{
|
||||
action: 'click',
|
||||
selector: '.main > a.link',
|
||||
href: 'http://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
expect(wrapper.find('.event-list').element).not.toBeEmpty()
|
||||
})
|
||||
})
|
||||
41
src/components/__tests__/ResultsTab.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import VueHighlightJS from 'vue3-highlightjs'
|
||||
|
||||
import ResultsTab from '../ResultsTab'
|
||||
|
||||
describe('RecordingTab.vue', () => {
|
||||
test('it has the correct pristine / empty state', () => {
|
||||
const wrapper = mount(ResultsTab)
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
expect(wrapper.find('code.javascript').exists()).toBe(false)
|
||||
})
|
||||
|
||||
test('it show a code box when there is code', () => {
|
||||
const wrapper = mount(ResultsTab, {
|
||||
global: {
|
||||
plugins: [VueHighlightJS],
|
||||
},
|
||||
props: { puppeteer: `await page.click('.class')` },
|
||||
})
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
expect(wrapper.find('code.javascript').exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('it render tabs for puppeteer & playwright', () => {
|
||||
const wrapper = mount(ResultsTab)
|
||||
expect(wrapper.findAll('.tabs__action').length).toEqual(2)
|
||||
})
|
||||
|
||||
test('it render playwright first when option is present', async () => {
|
||||
const wrapper = await mount(ResultsTab, {
|
||||
props: {
|
||||
options: {
|
||||
code: {
|
||||
showPlaywrightFirst: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.tabs__action').text()).toEqual('🎭playwright')
|
||||
})
|
||||
})
|
||||
328
src/components/__tests__/__snapshots__/RecordingTab.spec.js.snap
Normal file
@@ -0,0 +1,328 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecordingTab.vue it has the correct pristine / empty state 1`] = `
|
||||
<div
|
||||
class="tab recording-tab"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="empty"
|
||||
>
|
||||
<img
|
||||
alt="desert"
|
||||
src=""
|
||||
width="0"
|
||||
/>
|
||||
<h3>
|
||||
No recorded events yet
|
||||
</h3>
|
||||
<p
|
||||
class="text-muted"
|
||||
>
|
||||
Click record to begin
|
||||
</p>
|
||||
<div
|
||||
class="nag-cta"
|
||||
>
|
||||
<a
|
||||
href="https://checklyhq.com/headless-recorder"
|
||||
target="_blank"
|
||||
>
|
||||
Puppeteer Recorder is now
|
||||
<strong>
|
||||
Headless Recorder
|
||||
</strong>
|
||||
and supports Playwright →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="events"
|
||||
style="display: none;"
|
||||
>
|
||||
<p
|
||||
class="text-muted text-center loading"
|
||||
>
|
||||
Waiting for events
|
||||
</p>
|
||||
<ul
|
||||
class="event-list"
|
||||
>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RecordingTab.vue it has the correct recording DOM events state 1`] = `
|
||||
<div
|
||||
class="tab recording-tab"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="empty"
|
||||
style="display: none;"
|
||||
>
|
||||
<img
|
||||
alt="desert"
|
||||
src=""
|
||||
width="0"
|
||||
/>
|
||||
<h3>
|
||||
No recorded events yet
|
||||
</h3>
|
||||
<p
|
||||
class="text-muted"
|
||||
>
|
||||
Click record to begin
|
||||
</p>
|
||||
<div
|
||||
class="nag-cta"
|
||||
style="display: none;"
|
||||
>
|
||||
<a
|
||||
href="https://checklyhq.com/headless-recorder"
|
||||
target="_blank"
|
||||
>
|
||||
Puppeteer Recorder is now
|
||||
<strong>
|
||||
Headless Recorder
|
||||
</strong>
|
||||
and supports Playwright →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="events"
|
||||
>
|
||||
<p
|
||||
class="text-muted text-center loading"
|
||||
style="display: none;"
|
||||
>
|
||||
Waiting for events
|
||||
</p>
|
||||
<ul
|
||||
class="event-list"
|
||||
>
|
||||
|
||||
<li
|
||||
class="event-list-item"
|
||||
>
|
||||
<div
|
||||
class="event-label"
|
||||
>
|
||||
1.
|
||||
</div>
|
||||
<div
|
||||
class="event-description"
|
||||
>
|
||||
<div
|
||||
class="event-action"
|
||||
>
|
||||
click
|
||||
</div>
|
||||
<div
|
||||
class="event-props text-muted"
|
||||
>
|
||||
.main > a.link
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RecordingTab.vue it has the correct recording Puppeteer custom events state 1`] = `
|
||||
<div
|
||||
class="tab recording-tab"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="empty"
|
||||
style="display: none;"
|
||||
>
|
||||
<img
|
||||
alt="desert"
|
||||
src=""
|
||||
width="0"
|
||||
/>
|
||||
<h3>
|
||||
No recorded events yet
|
||||
</h3>
|
||||
<p
|
||||
class="text-muted"
|
||||
>
|
||||
Click record to begin
|
||||
</p>
|
||||
<div
|
||||
class="nag-cta"
|
||||
style="display: none;"
|
||||
>
|
||||
<a
|
||||
href="https://checklyhq.com/headless-recorder"
|
||||
target="_blank"
|
||||
>
|
||||
Puppeteer Recorder is now
|
||||
<strong>
|
||||
Headless Recorder
|
||||
</strong>
|
||||
and supports Playwright →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="events"
|
||||
>
|
||||
<p
|
||||
class="text-muted text-center loading"
|
||||
style="display: none;"
|
||||
>
|
||||
Waiting for events
|
||||
</p>
|
||||
<ul
|
||||
class="event-list"
|
||||
>
|
||||
|
||||
<li
|
||||
class="event-list-item"
|
||||
>
|
||||
<div
|
||||
class="event-label"
|
||||
>
|
||||
1.
|
||||
</div>
|
||||
<div
|
||||
class="event-description"
|
||||
>
|
||||
<div
|
||||
class="event-action"
|
||||
>
|
||||
goto*
|
||||
</div>
|
||||
<div
|
||||
class="event-props text-muted"
|
||||
>
|
||||
http://example.com
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="event-list-item"
|
||||
>
|
||||
<div
|
||||
class="event-label"
|
||||
>
|
||||
2.
|
||||
</div>
|
||||
<div
|
||||
class="event-description"
|
||||
>
|
||||
<div
|
||||
class="event-action"
|
||||
>
|
||||
viewport*
|
||||
</div>
|
||||
<div
|
||||
class="event-props text-muted"
|
||||
>
|
||||
width: 1280, height: 800
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="event-list-item"
|
||||
>
|
||||
<div
|
||||
class="event-label"
|
||||
>
|
||||
3.
|
||||
</div>
|
||||
<div
|
||||
class="event-description"
|
||||
>
|
||||
<div
|
||||
class="event-action"
|
||||
>
|
||||
navigation*
|
||||
</div>
|
||||
<div
|
||||
class="event-props text-muted"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RecordingTab.vue it has the correct waiting for events state 1`] = `
|
||||
<div
|
||||
class="tab recording-tab"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="empty"
|
||||
style="display: none;"
|
||||
>
|
||||
<img
|
||||
alt="desert"
|
||||
src=""
|
||||
width="0"
|
||||
/>
|
||||
<h3>
|
||||
No recorded events yet
|
||||
</h3>
|
||||
<p
|
||||
class="text-muted"
|
||||
>
|
||||
Click record to begin
|
||||
</p>
|
||||
<div
|
||||
class="nag-cta"
|
||||
style="display: none;"
|
||||
>
|
||||
<a
|
||||
href="https://checklyhq.com/headless-recorder"
|
||||
target="_blank"
|
||||
>
|
||||
Puppeteer Recorder is now
|
||||
<strong>
|
||||
Headless Recorder
|
||||
</strong>
|
||||
and supports Playwright →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="events"
|
||||
>
|
||||
<p
|
||||
class="text-muted text-center loading"
|
||||
>
|
||||
Waiting for events
|
||||
</p>
|
||||
<ul
|
||||
class="event-list"
|
||||
>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
118
src/components/__tests__/__snapshots__/ResultsTab.spec.js.snap
Normal file
@@ -0,0 +1,118 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecordingTab.vue it has the correct pristine / empty state 1`] = `
|
||||
<div
|
||||
class="tab results-tab"
|
||||
>
|
||||
<div
|
||||
class="tabs"
|
||||
>
|
||||
|
||||
<button
|
||||
class="tabs__action selected"
|
||||
>
|
||||
<!--v-if-->
|
||||
<img
|
||||
src=""
|
||||
width="16"
|
||||
/>
|
||||
<span
|
||||
class="tabs__action--text"
|
||||
>
|
||||
puppeteer
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="tabs__action"
|
||||
>
|
||||
<span>
|
||||
🎭
|
||||
</span>
|
||||
<!--v-if-->
|
||||
<span
|
||||
class="tabs__action--text"
|
||||
>
|
||||
playwright
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<pre>
|
||||
|
||||
<code>
|
||||
No code yet...
|
||||
</code>
|
||||
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RecordingTab.vue it show a code box when there is code 1`] = `
|
||||
<div
|
||||
class="tab results-tab"
|
||||
>
|
||||
<div
|
||||
class="tabs"
|
||||
>
|
||||
|
||||
<button
|
||||
class="tabs__action selected"
|
||||
>
|
||||
<!--v-if-->
|
||||
<img
|
||||
src=""
|
||||
width="16"
|
||||
/>
|
||||
<span
|
||||
class="tabs__action--text"
|
||||
>
|
||||
puppeteer
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="tabs__action"
|
||||
>
|
||||
<span>
|
||||
🎭
|
||||
</span>
|
||||
<!--v-if-->
|
||||
<span
|
||||
class="tabs__action--text"
|
||||
>
|
||||
playwright
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<pre>
|
||||
|
||||
<code
|
||||
class="javascript hljs"
|
||||
>
|
||||
<span
|
||||
class="hljs-keyword"
|
||||
>
|
||||
await
|
||||
</span>
|
||||
page.click(
|
||||
<span
|
||||
class="hljs-string"
|
||||
>
|
||||
'.class'
|
||||
</span>
|
||||
)
|
||||
</code>
|
||||
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
197
src/content-scripts/EventRecorder.js
Normal file
@@ -0,0 +1,197 @@
|
||||
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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 : {};
|
||||
if (dataAttribute) {
|
||||
this._dataAttribute = dataAttribute;
|
||||
}
|
||||
this._initializeRecorder();
|
||||
});
|
||||
} else {
|
||||
this._initializeRecorder();
|
||||
}
|
||||
}
|
||||
|
||||
_initializeRecorder() {
|
||||
const events = Object.values(eventsToRecord);
|
||||
if (!window.pptRecorderAddedControlListeners) {
|
||||
this._addAllListeners(events);
|
||||
this._boundedMessageListener =
|
||||
this._boundedMessageListener ||
|
||||
this._handleBackgroundMessage.bind(this);
|
||||
chrome.runtime.onMessage.addListener(this._boundedMessageListener);
|
||||
window.pptRecorderAddedControlListeners = true;
|
||||
}
|
||||
|
||||
if (
|
||||
!window.document.pptRecorderAddedControlListeners &&
|
||||
chrome.runtime &&
|
||||
chrome.runtime.onMessage
|
||||
) {
|
||||
window.document.pptRecorderAddedControlListeners = true;
|
||||
}
|
||||
|
||||
if (this._isTopFrame) {
|
||||
this._sendMessage({ control: ctrl.EVENT_RECORDER_STARTED });
|
||||
this._sendMessage({
|
||||
control: ctrl.GET_CURRENT_URL,
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
_handleBackgroundMessage(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;
|
||||
case actions.TOGGLE_SCREENSHOT_CLIPPED_MODE:
|
||||
this._handleScreenshotMode(true);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addAllListeners(events) {
|
||||
const boundedRecordEvent = this._recordEvent.bind(this);
|
||||
events.forEach(type => {
|
||||
window.addEventListener(type, boundedRecordEvent, true);
|
||||
});
|
||||
}
|
||||
|
||||
_sendMessage(msg) {
|
||||
// filter messages based on enabled / disabled features
|
||||
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);
|
||||
} else {
|
||||
this._eventLog.push(msg);
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug("caught error", err);
|
||||
}
|
||||
}
|
||||
|
||||
_recordEvent(e) {
|
||||
if (this._previousEvent && this._previousEvent.timeStamp === e.timeStamp)
|
||||
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
|
||||
try {
|
||||
this._sendMessage({
|
||||
selector: this._getSelector(e),
|
||||
value: e.target.value,
|
||||
tagName: e.target.tagName,
|
||||
action: e.type,
|
||||
keyCode: e.keyCode ? e.keyCode : null,
|
||||
href: e.target.href ? e.target.href : null,
|
||||
coordinates: EventRecorder._getCoordinates(e)
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
_getEventLog() {
|
||||
return this._eventLog;
|
||||
}
|
||||
|
||||
_clearEventLog() {
|
||||
this._eventLog = [];
|
||||
}
|
||||
|
||||
_handleScreenshotMode(isClipped) {
|
||||
this._disableClickRecording();
|
||||
this._uiController = new UIController({ showSelector: isClipped });
|
||||
this._screenShotMode = !this._screenShotMode;
|
||||
document.body.style.cursor = "crosshair";
|
||||
|
||||
console.debug("screenshot mode:", this._screenShotMode);
|
||||
|
||||
if (this._screenShotMode) {
|
||||
this._uiController.showSelector();
|
||||
} else {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
_disableClickRecording() {
|
||||
this._isRecordingClicks = false;
|
||||
}
|
||||
|
||||
_enableClickRecording() {
|
||||
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 finder(e.target, {
|
||||
seedMinLength: 5,
|
||||
optimizedMinLength: e.target.id ? 2 : 10,
|
||||
attr: name => name === this._dataAttribute
|
||||
});
|
||||
}
|
||||
|
||||
static _getCoordinates(evt) {
|
||||
const eventsWithCoordinates = {
|
||||
mouseup: true,
|
||||
mousedown: true,
|
||||
mousemove: true,
|
||||
mouseover: true
|
||||
};
|
||||
return eventsWithCoordinates[evt.type]
|
||||
? { x: evt.clientX, y: evt.clientY }
|
||||
: null;
|
||||
}
|
||||
}
|
||||
124
src/content-scripts/UIController.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
const BORDER_THICKNESS = 3;
|
||||
|
||||
const defaults = {
|
||||
showSelector: false
|
||||
};
|
||||
|
||||
class UIController extends EventEmitter {
|
||||
constructor(options) {
|
||||
options = Object.assign({}, defaults, options);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
showSelector() {
|
||||
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";
|
||||
|
||||
if (this._showSelector) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (!this._overlay.parentNode) {
|
||||
document.body.appendChild(this._overlay);
|
||||
document.body.addEventListener("mousemove", this._boundeMouseMove, false);
|
||||
document.body.addEventListener("click", this._boundeMouseUp, false);
|
||||
}
|
||||
}
|
||||
|
||||
hideSelector() {
|
||||
console.debug("UIController:hide");
|
||||
if (this._overlay) {
|
||||
document.body.removeChild(this._overlay);
|
||||
}
|
||||
this._overlay = this._selector = this._element = null;
|
||||
this._dimensions = {};
|
||||
}
|
||||
|
||||
_mousemove(e) {
|
||||
if (this._element !== e.target) {
|
||||
this._element = e.target;
|
||||
|
||||
this._dimensions.top = -window.scrollY;
|
||||
this._dimensions.left = -window.scrollX;
|
||||
|
||||
let elem = e.target;
|
||||
|
||||
while (elem && elem !== document.body) {
|
||||
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;
|
||||
|
||||
if (this._selector) {
|
||||
this._selector.style.top =
|
||||
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";
|
||||
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";
|
||||
setTimeout(() => {
|
||||
this._overlay.style.backgroundColor = "none";
|
||||
this._cleanup();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default UIController;
|
||||
45
src/content-scripts/__tests__/UIController.spec.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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();
|
||||
|
||||
document.body.innerHTML =
|
||||
"<div>" +
|
||||
' <div id="username">UserName</div>' +
|
||||
' <button id="button"></button>' +
|
||||
"</div>";
|
||||
|
||||
const uic = new UIController();
|
||||
uic.showSelector();
|
||||
|
||||
const handleClick = jest.fn();
|
||||
uic.on("click", handleClick);
|
||||
|
||||
const el = document.querySelector("#username");
|
||||
el.click();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(handleClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Shows and hides the selector", () => {
|
||||
const uic = new UIController();
|
||||
|
||||
uic.showSelector();
|
||||
let overlay = document.querySelector(".headlessRecorderOverlay");
|
||||
let outline = document.querySelector(".headlessRecorderOutline");
|
||||
|
||||
expect(overlay).toBeDefined();
|
||||
expect(outline).toBeDefined();
|
||||
|
||||
uic.hideSelector();
|
||||
overlay = document.querySelector(".headlessRecorderOverlay");
|
||||
outline = document.querySelector(".headlessRecorderOutline");
|
||||
|
||||
expect(overlay).toBeNull();
|
||||
expect(outline).toBeNull();
|
||||
});
|
||||
69
src/content-scripts/__tests__/attributes.spec.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import puppeteer from 'puppeteer'
|
||||
import { launchPuppeteerWithExtension } from '@/__e2e-tests__/helpers'
|
||||
import { waitForAndGetEvents, cleanEventLog, startServer } from './helpers'
|
||||
|
||||
let server
|
||||
let port
|
||||
let browser
|
||||
let page
|
||||
|
||||
describe('attributes', () => {
|
||||
beforeAll(async done => {
|
||||
const buildDir = '../../../dist'
|
||||
const fixture = './fixtures/attributes.html'
|
||||
{
|
||||
const { server: _s, port: _p } = await startServer(buildDir, fixture)
|
||||
server = _s
|
||||
port = _p
|
||||
}
|
||||
return done()
|
||||
}, 20000)
|
||||
|
||||
afterAll(done => {
|
||||
server.close(() => {
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await launchPuppeteerWithExtension(puppeteer)
|
||||
page = await browser.newPage()
|
||||
await page.goto(`http://localhost:${port}/`)
|
||||
await cleanEventLog(page)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
browser.close()
|
||||
})
|
||||
|
||||
test('it should load the content', async () => {
|
||||
const content = await page.$('#content-root')
|
||||
expect(content).toBeTruthy()
|
||||
})
|
||||
|
||||
test('it should use data attributes throughout selector', async () => {
|
||||
await page.evaluate('window.eventRecorder._dataAttribute = "data-qa"')
|
||||
await page.click('span')
|
||||
|
||||
const event = (await waitForAndGetEvents(page, 1))[0]
|
||||
expect(event.selector).toEqual(
|
||||
'body > #content-root > [data-qa="article-wrapper"] > [data-qa="article-body"] > span'
|
||||
)
|
||||
})
|
||||
|
||||
test('it should use data attributes throughout selector even when id is set', async () => {
|
||||
await page.evaluate('window.eventRecorder._dataAttribute = "data-qa"')
|
||||
await page.click('#link')
|
||||
|
||||
const event = (await waitForAndGetEvents(page, 1))[0]
|
||||
expect(event.selector).toEqual('[data-qa="link"]')
|
||||
})
|
||||
|
||||
test('it should use id throughout selector when data attributes is not set', async () => {
|
||||
await page.evaluate('window.eventRecorder._dataAttribute = null')
|
||||
await page.click('#link')
|
||||
|
||||
const event = (await waitForAndGetEvents(page, 1))[0]
|
||||
expect(event.selector).toEqual('#link')
|
||||
})
|
||||
})
|
||||
19
src/content-scripts/__tests__/fixtures/attributes.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>forms</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content-root">
|
||||
<div data-qa="article-wrapper" class="wrapper">
|
||||
<h1 data-qa="article-title" class="title">Lorem</h1>
|
||||
<div data-qa="article-body" class="body">
|
||||
<span>Read More...</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" id="link" data-qa="link">Click here</a>
|
||||
</div>
|
||||
<script src="./build/js/content-script.js" ></script>
|
||||
</body>
|
||||
</html>
|
||||
64
src/content-scripts/__tests__/fixtures/forms.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>forms</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/handler" method="post">
|
||||
<fieldset>
|
||||
<legend>Inputs</legend>
|
||||
<div>
|
||||
<label>text input</label>
|
||||
<input type="text">
|
||||
</div>
|
||||
<div>
|
||||
<label for="msg">text area</label>
|
||||
<textarea id="msg"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>radio</label>
|
||||
<input type="radio" id="radioChoice1"
|
||||
name="contact" value="radioChoice1">
|
||||
<label>radioChoice1</label>
|
||||
|
||||
<input type="radio" id="radioChoice2"
|
||||
name="contact" value="radioChoice2">
|
||||
<label>radioChoice2</label>
|
||||
|
||||
<input type="radio" id="radioChoice3"
|
||||
name="contact" value="radioChoice3">
|
||||
<label>radioChoice3</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Select</legend>
|
||||
<select>
|
||||
<option value="">--Please choose an option--</option>
|
||||
<option value="dog">Dog</option>
|
||||
<option value="cat">Cat</option>
|
||||
<option value="hamster">Hamster</option>
|
||||
<option value="parrot">Parrot</option>
|
||||
<option value="spider">Spider</option>
|
||||
<option value="goldfish">Goldfish</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Checkboxes</legend>
|
||||
<div>
|
||||
<input id="checkbox1" type="checkbox" name="interest" value="checkbox1">
|
||||
<label>Coding</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="checkbox2" type="checkbox" name="interest" value="checkbox2">
|
||||
<label>Music</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div>
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
<script src="./build/js/content-script.js" ></script>
|
||||
</body>
|
||||
</html>
|
||||
99
src/content-scripts/__tests__/forms.spec.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import puppeteer from 'puppeteer'
|
||||
import _ from 'lodash'
|
||||
import { launchPuppeteerWithExtension } from '@/__e2e-tests__/helpers'
|
||||
import { waitForAndGetEvents, cleanEventLog, startServer } from './helpers'
|
||||
|
||||
let server
|
||||
let port
|
||||
let browser
|
||||
let page
|
||||
|
||||
describe('forms', () => {
|
||||
beforeAll(async done => {
|
||||
const buildDir = '../../../dist'
|
||||
const fixture = './fixtures/forms.html'
|
||||
{
|
||||
const { server: _s, port: _p } = await startServer(buildDir, fixture)
|
||||
server = _s
|
||||
port = _p
|
||||
}
|
||||
return done()
|
||||
}, 20000)
|
||||
|
||||
afterAll(done => {
|
||||
server.close(() => {
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await launchPuppeteerWithExtension(puppeteer)
|
||||
page = await browser.newPage()
|
||||
await page.goto(`http://localhost:${port}/`)
|
||||
await cleanEventLog(page)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
browser.close()
|
||||
})
|
||||
|
||||
const tab = 1
|
||||
const change = 1
|
||||
test('it should load the form', async () => {
|
||||
const form = await page.$('form')
|
||||
expect(form).toBeTruthy()
|
||||
})
|
||||
|
||||
test('it should record text input elements', async () => {
|
||||
const string = 'I like turtles'
|
||||
await page.type('input[type="text"]', string)
|
||||
await page.keyboard.press('Tab')
|
||||
|
||||
const eventLog = await waitForAndGetEvents(
|
||||
page,
|
||||
string.length + tab + change
|
||||
)
|
||||
const event = _.find(eventLog, e => {
|
||||
return e.action === 'keydown' && e.keyCode === 9
|
||||
})
|
||||
expect(event.value).toEqual(string)
|
||||
})
|
||||
|
||||
test('it should record textarea elements', async () => {
|
||||
const string = 'I like turtles\n but also cats'
|
||||
await page.type('textarea', string)
|
||||
await page.keyboard.press('Tab')
|
||||
|
||||
const eventLog = await waitForAndGetEvents(
|
||||
page,
|
||||
string.length + tab + change
|
||||
)
|
||||
const event = _.find(eventLog, e => {
|
||||
return e.action === 'keydown' && e.keyCode === 9
|
||||
})
|
||||
expect(event.value).toEqual(string)
|
||||
})
|
||||
|
||||
test('it should record radio input elements', async () => {
|
||||
await page.click('#radioChoice1')
|
||||
await page.click('#radioChoice3')
|
||||
const eventLog = await waitForAndGetEvents(page, 2 + 2 * change)
|
||||
expect(eventLog[0].value).toEqual('radioChoice1')
|
||||
expect(eventLog[2].value).toEqual('radioChoice3')
|
||||
})
|
||||
|
||||
test('it should record select and option elements', async () => {
|
||||
await page.select('select', 'hamster')
|
||||
const eventLog = await waitForAndGetEvents(page, 1)
|
||||
expect(eventLog[0].value).toEqual('hamster')
|
||||
expect(eventLog[0].tagName).toEqual('SELECT')
|
||||
})
|
||||
|
||||
test('it should record checkbox input elements', async () => {
|
||||
await page.click('#checkbox1')
|
||||
await page.click('#checkbox2')
|
||||
const eventLog = await waitForAndGetEvents(page, 2 + 2 * change)
|
||||
expect(eventLog[0].value).toEqual('checkbox1')
|
||||
expect(eventLog[2].value).toEqual('checkbox2')
|
||||
})
|
||||
})
|
||||
51
src/content-scripts/__tests__/helpers.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
|
||||
export const waitForAndGetEvents = async function(page, amount) {
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
export const cleanEventLog = function(page) {
|
||||
return page.evaluate(() => {
|
||||
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 retry = e => {
|
||||
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();
|
||||
});
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
console.log("Hello from the content-script");
|
||||
3
src/content-scripts/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import EventRecorder from './EventRecorder'
|
||||
window.eventRecorder = new EventRecorder()
|
||||
window.eventRecorder.boot()
|
||||
@@ -1,4 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "Headless Recorder",
|
||||
"version": "1.0.0",
|
||||
"manifest_version": 2,
|
||||
"homepage_url": "http://localhost:8080/",
|
||||
"description": "A Chrome extension for recording browser interaction and generating Puppeteer & Playwright scripts",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
@@ -10,6 +9,7 @@
|
||||
"webNavigation",
|
||||
"activeTab",
|
||||
"contextMenus",
|
||||
"management",
|
||||
"*://*/"
|
||||
],
|
||||
"icons" : {
|
||||
@@ -34,6 +34,23 @@
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"browser_style": true
|
||||
"browser_style": true,
|
||||
"open_in_tab": true
|
||||
},
|
||||
"commands": {
|
||||
"TOGGLE_SCREENSHOT_MODE": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+A",
|
||||
"mac": "Command+Shift+A"
|
||||
},
|
||||
"description": "Take screenshot"
|
||||
},
|
||||
"TOGGLE_SCREENSHOT_CLIPPED_MODE": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+S",
|
||||
"mac": "Command+Shift+S"
|
||||
},
|
||||
"description": "Take screenshot clipped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
src/models/extension-control-messages.js
Normal file
@@ -0,0 +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"
|
||||
};
|
||||
9
src/models/extension-ui-actions.js
Normal file
@@ -0,0 +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"
|
||||
};
|
||||
@@ -1,19 +1,328 @@
|
||||
<template>
|
||||
<hello-world />
|
||||
<div class="options">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
Headless Recorder Options
|
||||
<small class="saving-badge text-muted" v-show="saving">
|
||||
Saving...
|
||||
</small>
|
||||
</div>
|
||||
<div class="content" v-if="!loading">
|
||||
<div class="settings-block">
|
||||
<h4 class="settings-block-title">
|
||||
Code Recorder settings
|
||||
</h4>
|
||||
<div class="settings-block-main">
|
||||
<div class="settings-group">
|
||||
<label class="settings-label">custom data attribute</label>
|
||||
<input
|
||||
id="options-code-dataAttribute"
|
||||
type="text"
|
||||
v-model.trim="options.code.dataAttribute"
|
||||
@change="save"
|
||||
placeholder="your custom data-* attribute"
|
||||
/>
|
||||
<small
|
||||
>Define an attribute that we'll attempt to use when selecting
|
||||
the elements, i.e "data-custom". This is handy when React or Vue
|
||||
based apps generate random class names.</small
|
||||
>
|
||||
<small class="settings-warning"
|
||||
>⚠️ When data attribute is set, it will take precedence from
|
||||
over other any selector (even ID)</small
|
||||
>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<label class="settings-label">set key code</label>
|
||||
<div class="settings-block">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
@click="listenForKeyCodePress"
|
||||
>
|
||||
{{
|
||||
recordingKeyCodePress
|
||||
? "Capturing"
|
||||
: "Click to capture key code"
|
||||
}}
|
||||
</button>
|
||||
<input
|
||||
id="options-code-keyCode"
|
||||
readonly
|
||||
disabled
|
||||
type="number"
|
||||
v-model.number="options.code.keyCode"
|
||||
placeholder="Key Code for input fields (ex. 9 = Tab)"
|
||||
/>
|
||||
</div>
|
||||
<small
|
||||
>What key will be used for capturing input changes. The value
|
||||
here is the key code. This will not handle multiple keys.</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-block">
|
||||
<h4 class="settings-block-title">
|
||||
Code Generator settings
|
||||
</h4>
|
||||
<div class="settings-block-main">
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-code-wrapAsync"
|
||||
type="checkbox"
|
||||
v-model="options.code.wrapAsync"
|
||||
@change="save"
|
||||
/>
|
||||
Wrap code in async function
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-code-headless"
|
||||
type="checkbox"
|
||||
v-model="options.code.headless"
|
||||
@change="save"
|
||||
/>
|
||||
Set <code>headless</code> in puppeteer launch options
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-code-waitForNavigation"
|
||||
type="checkbox"
|
||||
v-model="options.code.waitForNavigation"
|
||||
@change="save"
|
||||
/>
|
||||
Add <code>waitForNavigation</code> lines on navigation
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-code-waitForSelectorOnClick"
|
||||
type="checkbox"
|
||||
v-model="options.code.waitForSelectorOnClick"
|
||||
@change="save"
|
||||
/>
|
||||
Add <code>waitForSelector</code> lines before every
|
||||
<code>page.click()</code>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-code-blankLinesBetweenBlocks"
|
||||
type="checkbox"
|
||||
v-model="options.code.blankLinesBetweenBlocks"
|
||||
@change="save"
|
||||
/>
|
||||
Add blank lines between code blocks
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-code-showPlaywrightFirst"
|
||||
type="checkbox"
|
||||
v-model="options.code.showPlaywrightFirst"
|
||||
@change="save"
|
||||
/>
|
||||
Show Playwright tab first
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-block">
|
||||
<h4 class="settings-block-title">
|
||||
Extension settings
|
||||
</h4>
|
||||
<div class="settings-block-main">
|
||||
<div class="settings-group">
|
||||
<label>
|
||||
<input
|
||||
id="options-telemetry"
|
||||
type="checkbox"
|
||||
v-model="options.extension.telemetry"
|
||||
@change="save"
|
||||
/>
|
||||
Allow recording of usage telemetry
|
||||
</label>
|
||||
<br />
|
||||
<small
|
||||
>We only record clicks for basic product development, no website
|
||||
content or input data. Data is never, ever shared with 3rd
|
||||
parties.</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
sponsored by
|
||||
<a href="https://checklyhq.com" target="_blank">
|
||||
<img src="@/assets/images/text_racoon_logo.svg" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from "@/components/HelloWorld.vue";
|
||||
import { defaults as code } from "@/services/CodeGenerator";
|
||||
|
||||
const defaults = {
|
||||
code,
|
||||
extension: {
|
||||
telemetry: true
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: { HelloWorld }
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
saving: false,
|
||||
options: defaults,
|
||||
recordingKeyCodePress: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.saving = true;
|
||||
chrome.storage.local.set({ options: this.options }, () => {
|
||||
console.debug("saved options");
|
||||
setTimeout(() => {
|
||||
this.saving = false;
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
load() {
|
||||
chrome.storage.local.get("options", ({ options }) => {
|
||||
if (options) {
|
||||
console.debug("loaded options", JSON.stringify(options));
|
||||
this.options = options;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
listenForKeyCodePress() {
|
||||
this.recordingKeyCodePress = true;
|
||||
const keyDownFunction = e => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/styles/_variables.scss";
|
||||
@import "../assets/styles/_mixins.scss";
|
||||
|
||||
.options {
|
||||
height: 100%;
|
||||
min-height: 580px;
|
||||
background: $gray-lighter;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
.container {
|
||||
padding: 0 2 * $spacer;
|
||||
width: 550px;
|
||||
margin: 0 auto;
|
||||
|
||||
.content {
|
||||
background: white;
|
||||
padding: 2 * $spacer;
|
||||
border-radius: 4px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@include footer();
|
||||
background: $gray-lighter;
|
||||
font-weight: normal;
|
||||
justify-content: center;
|
||||
img {
|
||||
margin-left: 8px;
|
||||
width: 80px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
@include header();
|
||||
background: $gray-lighter;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings-block {
|
||||
.settings-label {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
|
||||
.settings-warning {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: $pink;
|
||||
margin: $spacer 0;
|
||||
}
|
||||
|
||||
.settings-block-title {
|
||||
margin: 0;
|
||||
padding-bottom: $spacer;
|
||||
border-bottom: 1px solid $gray-light;
|
||||
}
|
||||
|
||||
.settings-block-main {
|
||||
padding: $spacer 0;
|
||||
margin-bottom: $spacer;
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: $spacer;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
border: 1px solid $gray-light;
|
||||
padding-left: 15px;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
-webkit-box-sizing: border-box;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
86
src/options/__tests__/App.spec.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from '../App'
|
||||
|
||||
function createChromeLocalStorageMock(options) {
|
||||
let ops = options || {}
|
||||
return {
|
||||
options,
|
||||
storage: {
|
||||
local: {
|
||||
get: (key, cb) => {
|
||||
return cb(ops)
|
||||
},
|
||||
set: (options, cb) => {
|
||||
ops = options
|
||||
cb()
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('App.vue', () => {
|
||||
beforeEach(() => {
|
||||
window.chrome = null
|
||||
})
|
||||
|
||||
test('it has the correct pristine / empty state', () => {
|
||||
window.chrome = createChromeLocalStorageMock()
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('it loads the default options', () => {
|
||||
window.chrome = createChromeLocalStorageMock()
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.vm.$data.options.code.wrapAsync).toBeTruthy()
|
||||
})
|
||||
|
||||
test('it has the default key code for capturing inputs as 9 (Tab)', () => {
|
||||
window.chrome = createChromeLocalStorageMock()
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.vm.$data.options.code.keyCode).toBe(9)
|
||||
})
|
||||
|
||||
test('clicking the button will listen for the next keydown and update the key code option', () => {
|
||||
const options = { code: { keyCode: 9 } }
|
||||
window.chrome = createChromeLocalStorageMock(options)
|
||||
const wrapper = mount(App)
|
||||
|
||||
return wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
wrapper.find('button').element.click()
|
||||
const event = new KeyboardEvent('keydown', { keyCode: 16 })
|
||||
window.dispatchEvent(event)
|
||||
return wrapper.vm.$nextTick()
|
||||
})
|
||||
.then(() => {
|
||||
expect(wrapper.vm.$data.options.code.keyCode).toBe(16)
|
||||
})
|
||||
})
|
||||
|
||||
test("it stores and loads the user's edited options", () => {
|
||||
const options = { code: { wrapAsync: true } }
|
||||
window.chrome = createChromeLocalStorageMock(options)
|
||||
const wrapper = mount(App)
|
||||
|
||||
return wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
const checkBox = wrapper.find('#options-code-wrapAsync')
|
||||
checkBox.trigger('click')
|
||||
expect(wrapper.find('.saving-badge').text()).toEqual('Saving...')
|
||||
return wrapper.vm.$nextTick()
|
||||
})
|
||||
.then(() => {
|
||||
// we need to simulate a page reload
|
||||
wrapper.vm.load()
|
||||
return wrapper.vm.$nextTick()
|
||||
})
|
||||
.then(() => {
|
||||
const checkBox = wrapper.find('#options-code-wrapAsync')
|
||||
return expect(checkBox.element.checked).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
38
src/options/__tests__/__snapshots__/App.spec.js.snap
Normal file
@@ -0,0 +1,38 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App.vue it has the correct pristine / empty state 1`] = `
|
||||
<div
|
||||
class="options"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
Headless Recorder Options
|
||||
<small
|
||||
class="saving-badge text-muted"
|
||||
style="display: none;"
|
||||
>
|
||||
Saving...
|
||||
</small>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="footer"
|
||||
>
|
||||
sponsored by
|
||||
<a
|
||||
href="https://checklyhq.com"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src=""
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,19 +1,340 @@
|
||||
<template>
|
||||
<hello-world />
|
||||
<div id="headless-recorder" class="recorder">
|
||||
<div class="header">
|
||||
<a href="#" @click="goHome">
|
||||
Headless recorder
|
||||
<span class="text-muted"
|
||||
><small>{{ version }}</small></span
|
||||
>
|
||||
</a>
|
||||
<div class="left">
|
||||
<div class="recording-badge" v-show="isRecording">
|
||||
<span class="red-dot"></span>
|
||||
{{ recordingBadgeText }}
|
||||
</div>
|
||||
<button @click="toggleShowHelp" class="header-button">
|
||||
<img src="@/assets/images/help.svg" alt="help"/>
|
||||
</button>
|
||||
<button @click="openOptions" class="header-button">
|
||||
<img src="@/assets/images/settings.svg" alt="settings"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="tabs" v-show="!showHelp">
|
||||
<RecordingTab
|
||||
:code="code"
|
||||
:is-recording="isRecording"
|
||||
:live-events="liveEvents"
|
||||
v-show="!showResultsTab"
|
||||
/>
|
||||
<div class="recording-footer" v-show="!showResultsTab">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@click="toggleRecord"
|
||||
:class="isRecording ? 'btn-danger' : 'btn-primary'"
|
||||
>
|
||||
{{ recordButtonText }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary btn-outline-primary"
|
||||
@click="togglePause"
|
||||
v-show="isRecording"
|
||||
>
|
||||
{{ pauseButtonText }}
|
||||
</button>
|
||||
<a href="#" @click="showResultsTab = true" v-show="code">view code</a>
|
||||
<checkly-badge v-if="!isRecording"></checkly-badge>
|
||||
</div>
|
||||
<ResultsTab
|
||||
:puppeteer="code"
|
||||
:playwright="codeForPlaywright"
|
||||
:options="options"
|
||||
v-if="showResultsTab"
|
||||
v-on:update:tab="currentResultTab = $event"
|
||||
/>
|
||||
<div class="results-footer" v-show="showResultsTab">
|
||||
<button class="btn btn-sm btn-primary" @click="restart" v-show="code">
|
||||
Restart
|
||||
</button>
|
||||
<a href="#" @click.prevent="setCopying" v-show="code">{{
|
||||
copyLinkText
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<HelpTab v-show="showHelp"></HelpTab>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from "@/components/HelloWorld.vue";
|
||||
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 actions from "@/models/extension-ui-actions";
|
||||
|
||||
let bus;
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: { HelloWorld }
|
||||
components: { ResultsTab, RecordingTab, HelpTab, ChecklyBadge },
|
||||
data() {
|
||||
return {
|
||||
code: "",
|
||||
codeForPlaywright: "",
|
||||
options: {},
|
||||
showResultsTab: false,
|
||||
showHelp: false,
|
||||
liveEvents: [],
|
||||
recording: [],
|
||||
isRecording: false,
|
||||
isPaused: false,
|
||||
isCopying: false,
|
||||
bus: null,
|
||||
version,
|
||||
currentResultTab: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadState(() => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isRecording && this.code) {
|
||||
this.showResultsTab = true;
|
||||
}
|
||||
});
|
||||
|
||||
bus = chrome.extension.connect({ name: "recordControls" });
|
||||
},
|
||||
methods: {
|
||||
toggleRecord() {
|
||||
if (this.isRecording) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.start();
|
||||
}
|
||||
this.isRecording = !this.isRecording;
|
||||
this.storeState();
|
||||
},
|
||||
togglePause() {
|
||||
if (this.isPaused) {
|
||||
bus.postMessage({ action: actions.UN_PAUSE });
|
||||
this.isPaused = false;
|
||||
} else {
|
||||
bus.postMessage({ action: actions.PAUSE });
|
||||
this.isPaused = true;
|
||||
}
|
||||
this.storeState();
|
||||
},
|
||||
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 });
|
||||
|
||||
chrome.storage.local.get(
|
||||
["recording", "options"],
|
||||
({ recording, options }) => {
|
||||
console.debug("loaded recording", recording);
|
||||
console.debug("loaded options", options);
|
||||
|
||||
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();
|
||||
}
|
||||
);
|
||||
},
|
||||
restart() {
|
||||
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();
|
||||
},
|
||||
openOptions() {
|
||||
this.trackEvent("Options");
|
||||
if (chrome.runtime.openOptionsPage) {
|
||||
chrome.runtime.openOptionsPage();
|
||||
}
|
||||
},
|
||||
loadState(cb) {
|
||||
chrome.storage.local.get(
|
||||
["controls", "code", "options", "codeForPlaywright"],
|
||||
({ controls, code, options, codeForPlaywright }) => {
|
||||
if (controls) {
|
||||
this.isRecording = controls.isRecording;
|
||||
this.isPaused = controls.isPaused;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
if (codeForPlaywright) {
|
||||
this.codeForPlaywright = codeForPlaywright;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
this.options = options;
|
||||
}
|
||||
cb();
|
||||
}
|
||||
);
|
||||
},
|
||||
storeState() {
|
||||
chrome.storage.local.set({
|
||||
code: this.code,
|
||||
codeForPlaywright: this.codeForPlaywright,
|
||||
controls: {
|
||||
isRecording: this.isRecording,
|
||||
isPaused: this.isPaused
|
||||
}
|
||||
});
|
||||
},
|
||||
setCopying() {
|
||||
this.trackEvent("Copy");
|
||||
this.isCopying = true;
|
||||
setTimeout(() => {
|
||||
this.isCopying = false;
|
||||
}, 1500);
|
||||
},
|
||||
goHome() {
|
||||
this.showResultsTab = false;
|
||||
this.showHelp = false;
|
||||
},
|
||||
toggleShowHelp() {
|
||||
this.trackEvent("Help");
|
||||
this.showHelp = !this.showHelp;
|
||||
},
|
||||
trackEvent(event) {
|
||||
if (
|
||||
this.options &&
|
||||
this.options.extension &&
|
||||
this.options.extension.telemetry
|
||||
) {
|
||||
if (window._gaq) window._gaq.push(["_trackEvent", event, "clicked"]);
|
||||
}
|
||||
},
|
||||
trackPageView() {
|
||||
if (
|
||||
this.options &&
|
||||
this.options.extension &&
|
||||
this.options.extension.telemetry
|
||||
) {
|
||||
if (window._gaq) window._gaq.push(["_trackPageview"]);
|
||||
}
|
||||
},
|
||||
getCodeForCopy() {
|
||||
return this.currentResultTab === "puppeteer"
|
||||
? this.code
|
||||
: this.codeForPlaywright;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
recordingBadgeText() {
|
||||
return this.isPaused ? "paused" : "recording";
|
||||
},
|
||||
recordButtonText() {
|
||||
return this.isRecording ? "Stop" : "Record";
|
||||
},
|
||||
pauseButtonText() {
|
||||
return this.isPaused ? "Resume" : "Pause";
|
||||
},
|
||||
copyLinkText() {
|
||||
return this.isCopying ? "copied!" : "copy to clipboard";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/styles/_animations.scss";
|
||||
@import "../assets/styles/_variables.scss";
|
||||
@import "../assets/styles/_mixins.scss";
|
||||
.recorder {
|
||||
font-size: 14px;
|
||||
|
||||
.header {
|
||||
@include header();
|
||||
|
||||
&-button {
|
||||
color: $gray-dark;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.recording-badge {
|
||||
color: $brand-danger;
|
||||
.red-dot {
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
background-color: $brand-danger;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.4rem;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.header-button {
|
||||
margin-left: $spacer;
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recording-footer {
|
||||
@include footer();
|
||||
img {
|
||||
margin-left: 8px;
|
||||
width: 80px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.results-footer {
|
||||
@include footer();
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
21
src/popup/__tests__/App.spec.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import App from '../App'
|
||||
|
||||
const chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
extension: {
|
||||
connect: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
describe('App.vue', () => {
|
||||
test('it has the correct pristine / empty state', () => {
|
||||
window.chrome = chrome
|
||||
const wrapper = shallowMount(App)
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
110
src/popup/__tests__/__snapshots__/App.spec.js.snap
Normal file
@@ -0,0 +1,110 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App.vue it has the correct pristine / empty state 1`] = `
|
||||
<div
|
||||
class="recorder"
|
||||
id="headless-recorder"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
>
|
||||
Headless recorder
|
||||
<span
|
||||
class="text-muted"
|
||||
>
|
||||
<small>
|
||||
1.0.0
|
||||
</small>
|
||||
</span>
|
||||
</a>
|
||||
<div
|
||||
class="left"
|
||||
>
|
||||
<div
|
||||
class="recording-badge"
|
||||
style="display: none;"
|
||||
>
|
||||
<span
|
||||
class="red-dot"
|
||||
/>
|
||||
recording
|
||||
</div>
|
||||
<button
|
||||
class="header-button"
|
||||
>
|
||||
<img
|
||||
alt="help"
|
||||
src=""
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="header-button"
|
||||
>
|
||||
<img
|
||||
alt="settings"
|
||||
src=""
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="main"
|
||||
>
|
||||
<div
|
||||
class="tabs"
|
||||
>
|
||||
<recording-tab-stub
|
||||
code=""
|
||||
is-recording="false"
|
||||
live-events=""
|
||||
/>
|
||||
<div
|
||||
class="recording-footer"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary btn-outline-primary"
|
||||
style="display: none;"
|
||||
>
|
||||
Pause
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
style="display: none;"
|
||||
>
|
||||
view code
|
||||
</a>
|
||||
<checkly-badge-stub />
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="results-footer"
|
||||
style="display: none;"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
style="display: none;"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
style="display: none;"
|
||||
>
|
||||
copy to clipboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<help-tab-stub
|
||||
style="display: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,4 +1,10 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import VueHighlightJS from "vue3-highlightjs";
|
||||
import "highlight.js/styles/a11y-dark.css";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
import "@/assets/styles/main.scss";
|
||||
|
||||
createApp(App)
|
||||
.use(VueHighlightJS)
|
||||
.mount("#app");
|
||||
|
||||
25
src/services/Block.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default class Block {
|
||||
constructor(frameId, line) {
|
||||
this._lines = [];
|
||||
this._frameId = frameId;
|
||||
|
||||
if (line) {
|
||||
line.frameId = this._frameId;
|
||||
this._lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
addLineToTop(line) {
|
||||
line.frameId = this._frameId;
|
||||
this._lines.unshift(line);
|
||||
}
|
||||
|
||||
addLine(line) {
|
||||
line.frameId = this._frameId;
|
||||
this._lines.push(line);
|
||||
}
|
||||
|
||||
getLines() {
|
||||
return this._lines;
|
||||
}
|
||||
}
|
||||
268
src/services/CodeGenerator.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import domEvents from "./dom-events-to-record";
|
||||
import Block from "./Block";
|
||||
import pptrActions from "./pptr-actions";
|
||||
|
||||
export const defaults = {
|
||||
wrapAsync: true,
|
||||
headless: true,
|
||||
waitForNavigation: true,
|
||||
waitForSelectorOnClick: true,
|
||||
blankLinesBetweenBlocks: true,
|
||||
dataAttribute: "",
|
||||
showPlaywrightFirst: false,
|
||||
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._hasNavigation = false;
|
||||
}
|
||||
|
||||
generate() {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
_getHeader() {
|
||||
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;
|
||||
}
|
||||
|
||||
_getFooter() {
|
||||
return this._options.wrapAsync ? this._wrappedFooter : this._footer;
|
||||
}
|
||||
|
||||
_parseEvents(events) {
|
||||
console.debug(`generating code for ${events ? events.length : 0} events`);
|
||||
let result = "";
|
||||
|
||||
if (!events) return result;
|
||||
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const {
|
||||
action,
|
||||
selector,
|
||||
value,
|
||||
href,
|
||||
keyCode,
|
||||
tagName,
|
||||
frameId,
|
||||
frameUrl
|
||||
} = events[i];
|
||||
const escapedSelector = selector
|
||||
? selector.replace(/\\/g, "\\\\")
|
||||
: selector;
|
||||
|
||||
// we need to keep a handle on what frames events originate from
|
||||
this._setFrames(frameId, frameUrl);
|
||||
|
||||
switch (action) {
|
||||
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 pptrActions.GOTO:
|
||||
this._blocks.push(this._handleGoto(href, frameId));
|
||||
break;
|
||||
case pptrActions.VIEWPORT:
|
||||
this._blocks.push(this._handleViewport(value.width, value.height));
|
||||
break;
|
||||
case pptrActions.NAVIGATION:
|
||||
this._blocks.push(this._handleWaitForNavigation());
|
||||
this._hasNavigation = true;
|
||||
break;
|
||||
case pptrActions.SCREENSHOT:
|
||||
this._blocks.push(this._handleScreenshot(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._hasNavigation && this._options.waitForNavigation) {
|
||||
console.debug("Adding navigationPromise declaration");
|
||||
const block = new Block(this._frameId, {
|
||||
type: pptrActions.NAVIGATION_PROMISE,
|
||||
value: "const navigationPromise = page.waitForNavigation()"
|
||||
});
|
||||
this._blocks.unshift(block);
|
||||
}
|
||||
|
||||
console.debug("post processing blocks:", this._blocks);
|
||||
this._postProcess();
|
||||
|
||||
const indent = this._options.wrapAsync ? " " : "";
|
||||
const newLine = `\n`;
|
||||
|
||||
for (let block of this._blocks) {
|
||||
const lines = block.getLines();
|
||||
for (let line of lines) {
|
||||
result += indent + line.value + newLine;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_setFrames(frameId, frameUrl) {
|
||||
if (frameId && frameId !== 0) {
|
||||
this._frameId = frameId;
|
||||
this._frame = `frame_${frameId}`;
|
||||
this._allFrames[frameId] = frameUrl;
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
if (this._options.blankLinesBetweenBlocks && this._blocks.length > 0) {
|
||||
this._postProcessAddBlankLines();
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyDown(selector, value) {
|
||||
const block = new Block(this._frameId);
|
||||
block.addLine({
|
||||
type: domEvents.KEYDOWN,
|
||||
value: `await ${this._frame}.type('${selector}', '${this._escapeUserInput(
|
||||
value
|
||||
)}')`
|
||||
});
|
||||
return block;
|
||||
}
|
||||
|
||||
_handleClick(selector) {
|
||||
const block = new Block(this._frameId);
|
||||
if (this._options.waitForSelectorOnClick) {
|
||||
block.addLine({
|
||||
type: domEvents.CLICK,
|
||||
value: `await ${this._frame}.waitForSelector('${selector}')`
|
||||
});
|
||||
}
|
||||
block.addLine({
|
||||
type: domEvents.CLICK,
|
||||
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}')`
|
||||
});
|
||||
}
|
||||
|
||||
_handleGoto(href) {
|
||||
return new Block(this._frameId, {
|
||||
type: pptrActions.GOTO,
|
||||
value: `await ${this._frame}.goto('${href}')`
|
||||
});
|
||||
}
|
||||
|
||||
_handleViewport() {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
_handleScreenshot(options) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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} } })`
|
||||
});
|
||||
} else {
|
||||
block = new Block(this._frameId, {
|
||||
type: pptrActions.SCREENSHOT,
|
||||
value: `await ${this._frame}.screenshot({ path: 'screenshot_${this._screenshotCounter}.png' })`
|
||||
});
|
||||
}
|
||||
|
||||
this._screenshotCounter++;
|
||||
return block;
|
||||
}
|
||||
|
||||
_handleWaitForNavigation() {
|
||||
const block = new Block(this._frameId);
|
||||
if (this._options.waitForNavigation) {
|
||||
block.addLine({
|
||||
type: pptrActions.NAVIGATION,
|
||||
value: `await navigationPromise`
|
||||
});
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
_postProcessSetFrames() {
|
||||
for (let [i, block] of this._blocks.entries()) {
|
||||
const lines = block.getLines();
|
||||
for (let line of lines) {
|
||||
if (
|
||||
line.frameId &&
|
||||
Object.keys(this._allFrames).includes(line.frameId.toString())
|
||||
) {
|
||||
const declaration = `const frame_${
|
||||
line.frameId
|
||||
} = frames.find(f => f.url() === '${this._allFrames[line.frameId]}')`;
|
||||
this._blocks[i].addLineToTop({
|
||||
type: pptrActions.FRAME_SET,
|
||||
value: declaration
|
||||
});
|
||||
this._blocks[i].addLineToTop({
|
||||
type: pptrActions.FRAME_SET,
|
||||
value: "let frames = await page.frames()"
|
||||
});
|
||||
delete this._allFrames[line.frameId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_postProcessAddBlankLines() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
_escapeUserInput(value) {
|
||||
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||
}
|
||||
}
|
||||
50
src/services/PlaywrightCodeGenerator.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import pptrActions from "./pptr-actions";
|
||||
import Block from "./Block";
|
||||
import CodeGenerator from "./CodeGenerator";
|
||||
|
||||
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 footer = `await browser.close()`;
|
||||
|
||||
const wrappedHeader = `(async () => {
|
||||
${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;
|
||||
}
|
||||
|
||||
generate(events) {
|
||||
return (
|
||||
importPlaywright +
|
||||
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} })`
|
||||
});
|
||||
}
|
||||
|
||||
_handleChange(selector, value) {
|
||||
return new Block(this._frameId, {
|
||||
type: pptrActions.CHANGE,
|
||||
value: `await ${this._frame}.selectOption('${selector}', '${value}')`
|
||||
});
|
||||
}
|
||||
}
|
||||
43
src/services/PuppeteerCodeGenerator.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import pptrActions from "./pptr-actions";
|
||||
import Block from "./Block";
|
||||
import CodeGenerator from "./CodeGenerator";
|
||||
|
||||
const importPuppeteer = `const puppeteer = require('puppeteer');\n`;
|
||||
|
||||
const header = `const browser = await puppeteer.launch()
|
||||
const page = await browser.newPage()`;
|
||||
|
||||
const footer = `await browser.close()`;
|
||||
|
||||
const wrappedHeader = `(async () => {
|
||||
const browser = await puppeteer.launch()
|
||||
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;
|
||||
}
|
||||
|
||||
generate(events) {
|
||||
return (
|
||||
importPuppeteer +
|
||||
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} })`
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/services/__tests__/PlaywrightCodeGenerator.spec.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import PlaywrightCodeGenerator from '../PlaywrightCodeGenerator'
|
||||
|
||||
describe('PlaywrightCodeGenerator', () => {
|
||||
test('it should generate nothing when there are no events', () => {
|
||||
const events = []
|
||||
const codeGenerator = new PlaywrightCodeGenerator()
|
||||
expect(codeGenerator._parseEvents(events)).toBeFalsy()
|
||||
})
|
||||
|
||||
test('it generates a page.selectOption() only for select dropdowns', () => {
|
||||
const events = [
|
||||
{
|
||||
action: 'change',
|
||||
selector: 'select#animals',
|
||||
tagName: 'SELECT',
|
||||
value: 'hamster',
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PlaywrightCodeGenerator()
|
||||
expect(codeGenerator._parseEvents(events)).toContain(
|
||||
`await page.selectOption('${events[0].selector}', '${events[0].value}')`
|
||||
)
|
||||
})
|
||||
})
|
||||
171
src/services/__tests__/PuppeteerCodeGenerator.spec.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import PuppeteerCodeGenerator from '../PuppeteerCodeGenerator'
|
||||
import pptrActions from '../pptr-actions'
|
||||
|
||||
describe('PuppeteerCodeGenerator', () => {
|
||||
test('it should generate nothing when there are no events', () => {
|
||||
const events = []
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
expect(codeGenerator._parseEvents(events)).toBeFalsy()
|
||||
})
|
||||
|
||||
test('it generates a page.select() only for select dropdowns', () => {
|
||||
const events = [
|
||||
{
|
||||
action: 'change',
|
||||
selector: 'select#animals',
|
||||
tagName: 'SELECT',
|
||||
value: 'hamster',
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
expect(codeGenerator._parseEvents(events)).toContain(
|
||||
"await page.select('select#animals', 'hamster')"
|
||||
)
|
||||
})
|
||||
|
||||
test('it generates the correct waitForNavigation code', () => {
|
||||
const events = [
|
||||
{ action: 'click', selector: 'a.link' },
|
||||
{ action: pptrActions.NAVIGATION },
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const code = codeGenerator._parseEvents(events)
|
||||
const lines = code.split('\n')
|
||||
expect(lines[1].trim()).toEqual(
|
||||
'const navigationPromise = page.waitForNavigation()'
|
||||
)
|
||||
expect(lines[4].trim()).toEqual("await page.click('a.link')")
|
||||
expect(lines[6].trim()).toEqual('await navigationPromise')
|
||||
})
|
||||
|
||||
test('it does not generate waitForNavigation code when turned off', () => {
|
||||
const events = [
|
||||
{ action: 'navigation*' },
|
||||
{ action: 'click', selector: 'a.link' },
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator({
|
||||
waitForNavigation: false,
|
||||
})
|
||||
expect(codeGenerator._parseEvents(events)).not.toContain(
|
||||
'const navigationPromise = page.waitForNavigation()\n'
|
||||
)
|
||||
expect(codeGenerator._parseEvents(events)).not.toContain(
|
||||
'await navigationPromise\n'
|
||||
)
|
||||
})
|
||||
|
||||
test('it generates the correct waitForSelector code before clicks', () => {
|
||||
const events = [{ action: 'click', selector: 'a.link' }]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
|
||||
expect(result).toContain("await page.waitForSelector('a.link')")
|
||||
expect(result).toContain("await page.click('a.link')")
|
||||
})
|
||||
|
||||
test('it does not generate the waitForSelector code before clicks when turned off', () => {
|
||||
const events = [{ action: 'click', selector: 'a.link' }]
|
||||
const codeGenerator = new PuppeteerCodeGenerator({
|
||||
waitForSelectorOnClick: false,
|
||||
})
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
|
||||
expect(result).not.toContain("await page.waitForSelector('a.link')")
|
||||
expect(result).toContain("await page.click('a.link')")
|
||||
})
|
||||
|
||||
test('it uses the default page frame when events originate from frame 0', () => {
|
||||
const events = [
|
||||
{
|
||||
action: 'click',
|
||||
selector: 'a.link',
|
||||
frameId: 0,
|
||||
frameUrl: 'https://some.site.com',
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
expect(result).toContain("await page.click('a.link')")
|
||||
})
|
||||
|
||||
test('it uses a different frame when events originate from an iframe', () => {
|
||||
const events = [
|
||||
{
|
||||
action: 'click',
|
||||
selector: 'a.link',
|
||||
frameId: 123,
|
||||
frameUrl: 'https://some.iframe.com',
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
expect(result).toContain("await frame_123.click('a.link')")
|
||||
})
|
||||
|
||||
test('it adds a frame selection preamble when events originate from an iframe', () => {
|
||||
const events = [
|
||||
{
|
||||
action: 'click',
|
||||
selector: 'a.link',
|
||||
frameId: 123,
|
||||
frameUrl: 'https://some.iframe.com',
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
expect(result).toContain('let frames = await page.frames()')
|
||||
expect(result).toContain(
|
||||
"const frame_123 = frames.find(f => f.url() === 'https://some.iframe.com'"
|
||||
)
|
||||
})
|
||||
|
||||
test('it generates the correct current page screenshot code', () => {
|
||||
const events = [{ action: pptrActions.SCREENSHOT }]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
|
||||
expect(result).toContain(
|
||||
"await page.screenshot({ path: 'screenshot_1.png' })"
|
||||
)
|
||||
})
|
||||
|
||||
test('it generates the correct clipped page screenshot code', () => {
|
||||
const events = [
|
||||
{
|
||||
action: pptrActions.SCREENSHOT,
|
||||
value: { x: '10px', y: '300px', width: '800px', height: '600px' },
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
|
||||
expect(result).toContain(
|
||||
"await page.screenshot({ path: 'screenshot_1.png', clip: { x: 10, y: 300, width: 800, height: 600 } })"
|
||||
)
|
||||
})
|
||||
|
||||
test('it generates the correct escaped value', () => {
|
||||
const events = [
|
||||
{
|
||||
action: 'keydown',
|
||||
keyCode: 9,
|
||||
selector: 'input.value',
|
||||
value: "hello');console.log('world",
|
||||
},
|
||||
]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
|
||||
expect(result).toContain(
|
||||
"await page.type('input.value', 'hello\\');console.log(\\'world')"
|
||||
)
|
||||
})
|
||||
|
||||
test('it generates the correct escaped value with backslash', () => {
|
||||
const events = [{ action: 'click', selector: 'button.\\hello\\' }]
|
||||
const codeGenerator = new PuppeteerCodeGenerator()
|
||||
const result = codeGenerator._parseEvents(events)
|
||||
|
||||
expect(result).toContain("await page.click('button.\\\\hello\\\\')")
|
||||
})
|
||||
})
|
||||
92
src/services/dom-events-to-record.js
Normal file
@@ -0,0 +1,92 @@
|
||||
export default {
|
||||
CLICK: "click",
|
||||
DBLCLICK: "dblclick",
|
||||
CHANGE: "change",
|
||||
KEYDOWN: "keydown",
|
||||
SELECT: "select",
|
||||
SUBMIT: "submit",
|
||||
LOAD: "load",
|
||||
UNLOAD: "unload"
|
||||
};
|
||||
|
||||
// const events = [
|
||||
// abort,
|
||||
// afterprint,
|
||||
// beforeprint,
|
||||
// beforeunload,
|
||||
// blur,
|
||||
// canplay,
|
||||
// canplaythrough,
|
||||
// change,
|
||||
// click,
|
||||
// contextmenu,
|
||||
// copy,
|
||||
// cuechange,
|
||||
// cut,
|
||||
// dblclick,
|
||||
// DOMContentLoaded,
|
||||
// drag,
|
||||
// dragend,
|
||||
// dragenter,
|
||||
// dragleave,
|
||||
// dragover,
|
||||
// dragstart,
|
||||
// drop,
|
||||
// durationchange,
|
||||
// emptied,
|
||||
// ended,
|
||||
// error,
|
||||
// focus,
|
||||
// focusin,
|
||||
// focusout,
|
||||
// formchange,
|
||||
// forminput,
|
||||
// hashchange,
|
||||
// input,
|
||||
// invalid,
|
||||
// keydown,
|
||||
// keypress,
|
||||
// keyup,
|
||||
// load,
|
||||
// loadeddata,
|
||||
// loadedmetadata,
|
||||
// loadstart,
|
||||
// message,
|
||||
// mousedown,
|
||||
// mouseenter,
|
||||
// mouseleave,
|
||||
// mousemove,
|
||||
// mouseout,
|
||||
// mouseover,
|
||||
// mouseup,
|
||||
// mousewheel,
|
||||
// offline,
|
||||
// online,
|
||||
// pagehide,
|
||||
// pageshow,
|
||||
// paste,
|
||||
// pause,
|
||||
// play,
|
||||
// playing,
|
||||
// popstate,
|
||||
// progress,
|
||||
// ratechange,
|
||||
// readystatechange,
|
||||
// redo,
|
||||
// reset,
|
||||
// resize,
|
||||
// scroll,
|
||||
// seeked,
|
||||
// seeking,
|
||||
// select,
|
||||
// show,
|
||||
// stalled,
|
||||
// storage,
|
||||
// submit,
|
||||
// suspend,
|
||||
// timeupdate,
|
||||
// undo,
|
||||
// unload,
|
||||
// volumechange,
|
||||
// waiting
|
||||
// ];
|
||||
9
src/services/pptr-actions.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
GOTO: "GOTO",
|
||||
VIEWPORT: "VIEWPORT",
|
||||
WAITFORSELECTOR: "WAITFORSELECTOR",
|
||||
NAVIGATION: "NAVIGATION",
|
||||
NAVIGATION_PROMISE: "NAVIGATION_PROMISE",
|
||||
FRAME_SET: "FRAME_SET",
|
||||
SCREENSHOT: "SCREENSHOT"
|
||||
};
|
||||
1028
tailwind.config.js
Normal file
@@ -19,7 +19,7 @@ module.exports = {
|
||||
},
|
||||
contentScripts: {
|
||||
entries: {
|
||||
"content-script": ["src/content-scripts/content-script.js"]
|
||||
"content-script": ["src/content-scripts/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||