feat: create basic scaffolding for vue 3 chrome app 🚀
7
.babelrc
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [ "es2015", "stage-0"],
|
|
||||||
"plugins": [
|
|
||||||
"transform-runtime",
|
|
||||||
"transform-object-rest-spread"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
3
.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
33
.eslintrc.js
@@ -1,32 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: 'babel-eslint',
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
|
||||||
node: true,
|
node: true,
|
||||||
"jest/globals": true
|
webextensions: true
|
||||||
|
|
||||||
},
|
},
|
||||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/prettier"],
|
||||||
extends: 'standard',
|
parserOptions: {
|
||||||
// required to lint *.vue files
|
parser: "babel-eslint"
|
||||||
plugins: [
|
|
||||||
'html',
|
|
||||||
'jest'
|
|
||||||
],
|
|
||||||
// add your custom rules here
|
|
||||||
'rules': {
|
|
||||||
// allow paren-less arrow functions
|
|
||||||
'arrow-parens': 0,
|
|
||||||
// allow async-await
|
|
||||||
'generator-star-spacing': 0,
|
|
||||||
// allow debugger during development
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
|
||||||
},
|
},
|
||||||
globals: {
|
rules: {
|
||||||
chrome: false
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
33
.gitignore
vendored
@@ -1,10 +1,29 @@
|
|||||||
node_modules
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
build
|
.vscode
|
||||||
dist
|
*.suo
|
||||||
dist-zip
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Vue Browser Extension Output
|
||||||
*.pem
|
*.pem
|
||||||
*.crx
|
*.pub
|
||||||
test
|
*.zip
|
||||||
.vscode
|
/artifacts
|
||||||
|
|||||||
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: ["@vue/cli-plugin-babel/preset"]
|
||||||
|
};
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
moduleNameMapper: {
|
|
||||||
"^vue$": "vue/dist/vue.common.js"
|
|
||||||
},
|
|
||||||
moduleFileExtensions: [
|
|
||||||
"js",
|
|
||||||
"vue",
|
|
||||||
"json"
|
|
||||||
],
|
|
||||||
transform: {
|
|
||||||
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
|
|
||||||
".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14790
package-lock.json
generated
95
package.json
@@ -1,86 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "headless-recorder",
|
"name": "headless-recorder",
|
||||||
"version": "0.8.2",
|
"version": "1.0.0",
|
||||||
"description": "A Chrome extension for recording browser interaction and generating Puppeteer & Playwright scripts",
|
"private": true,
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development DEBUG=puppeteer-recorder:* webpack --watch",
|
"serve": "vue-cli-service build --mode development --watch",
|
||||||
"build": "NODE_ENV=production webpack",
|
"build": "vue-cli-service build",
|
||||||
"dist": "NODE_ENV=production webpack && node scripts/zip.js",
|
"lint": "vue-cli-service lint"
|
||||||
"test": "npm run unit-test && npm run e2e-test",
|
|
||||||
"test-prod": "NODE_ENV=production npm run unit-test && npm run e2e-test",
|
|
||||||
"unit-test": "jest __tests__/.*.spec.js --silent",
|
|
||||||
"e2e-test": "jest __e2e-tests__ --runInBand --silent",
|
|
||||||
"lint": "eslint --quiet -f codeframe src"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/checkly/headless-recorder"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"puppeteer",
|
|
||||||
"chrome",
|
|
||||||
"extension"
|
|
||||||
],
|
|
||||||
"author": "Tim Nolet",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/checkly/headless-recorder/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/checkly/headless-recorder#readme",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medv/finder": "^1.1.2",
|
"core-js": "3.6.5",
|
||||||
"events": "^3.2.0",
|
"vue": "3.0.0"
|
||||||
"vue": "^2.5.17",
|
|
||||||
"vue-clipboard2": "^0.2.1",
|
|
||||||
"vue-highlightjs": "^1.3.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/test-utils": "^1.0.0-beta.24",
|
"@vue/cli-plugin-babel": "4.5.0",
|
||||||
"babel": "^6.23.0",
|
"@vue/cli-plugin-eslint": "4.5.0",
|
||||||
"babel-core": "^6.26.3",
|
"@vue/cli-service": "4.5.0",
|
||||||
"babel-eslint": "^8.2.6",
|
"@vue/compiler-sfc": "3.0.0",
|
||||||
"babel-jest": "^23.4.2",
|
"@vue/eslint-config-prettier": "6.0.0",
|
||||||
"babel-loader": "^7.1.5",
|
"babel-eslint": "10.1.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"eslint": "6.7.2",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"eslint-plugin-prettier": "3.1.3",
|
||||||
"babel-preset-env": "^1.7.0",
|
"eslint-plugin-vue": "7.0.0-0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"prettier": "1.19.1",
|
||||||
"babel-preset-stage-0": "^6.24.1",
|
"vue-cli-plugin-browser-extension": "0.25.1"
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
|
||||||
"css-loader": "^1.0.0",
|
|
||||||
"eslint": "^5.3.0",
|
|
||||||
"eslint-config-standard": "^12.0.0-alpha.0",
|
|
||||||
"eslint-plugin-html": "^4.0.5",
|
|
||||||
"eslint-plugin-import": "^2.13.0",
|
|
||||||
"eslint-plugin-jest": "^21.21.0",
|
|
||||||
"eslint-plugin-node": "^7.0.1",
|
|
||||||
"eslint-plugin-promise": "^3.8.0",
|
|
||||||
"eslint-plugin-standard": "^3.1.0",
|
|
||||||
"express": "^4.16.3",
|
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
|
||||||
"gren": "0.0.1",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
|
||||||
"jest": "^23.5.0",
|
|
||||||
"jest-vue-preprocessor": "^1.4.0",
|
|
||||||
"lodash": "^4.17.19",
|
|
||||||
"node-sass": "^4.14.1",
|
|
||||||
"pre-commit": "^1.2.2",
|
|
||||||
"puppeteer": "^2.1.1",
|
|
||||||
"sass-loader": "^7.1.0",
|
|
||||||
"style-loader": "^0.22.1",
|
|
||||||
"superagent": "^3.8.3",
|
|
||||||
"vue-loader": "^15.3.0",
|
|
||||||
"vue-style-loader": "^4.1.1",
|
|
||||||
"vue-template-compiler": "^2.5.17",
|
|
||||||
"webpack": "^4.16.5",
|
|
||||||
"webpack-chrome-extension-reloader": "^0.8.3",
|
|
||||||
"webpack-cli": "^3.1.0",
|
|
||||||
"zip-folder": "^1.0.0"
|
|
||||||
},
|
|
||||||
"standard": {
|
|
||||||
"globals": [
|
|
||||||
"chrome"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
public copy/_locales/en/messages.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "headless-recorder-v2",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
12
public copy/browser-extension.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
BIN
public copy/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public copy/icons/128.png
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
public copy/icons/16.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public copy/icons/19.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public copy/icons/38.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public copy/icons/48.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
17
public copy/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
||||||
6
public/_locales/en/messages.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "headless-recorder-v2",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
12
public/browser-extension.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icons/128.png
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
public/icons/16.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/icons/19.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/icons/38.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/icons/48.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
17
public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const zipFolder = require('zip-folder')
|
|
||||||
|
|
||||||
const DEST_DIR = path.join(__dirname, '../dist')
|
|
||||||
const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip')
|
|
||||||
|
|
||||||
const extractExtensionData = () => {
|
|
||||||
const extPackageJson = require('../package.json')
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: extPackageJson.name,
|
|
||||||
version: extPackageJson.version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeDestZipDirIfNotExists = () => {
|
|
||||||
if (!fs.existsSync(DEST_ZIP_DIR)) {
|
|
||||||
fs.mkdirSync(DEST_ZIP_DIR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildZip = (src, dist, zipFilename) => {
|
|
||||||
console.info(`Building ${zipFilename}...`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
zipFolder(src, path.join(dist, zipFilename), (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const main = () => {
|
|
||||||
const {name, version} = extractExtensionData()
|
|
||||||
const zipFilename = `${name}-v${version}.zip`
|
|
||||||
|
|
||||||
makeDestZipDirIfNotExists()
|
|
||||||
|
|
||||||
buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename)
|
|
||||||
.then(() => console.info('OK'))
|
|
||||||
.catch(console.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
7
src/background.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
browser.runtime.onMessage.addListener(function() {
|
||||||
|
console.log("Hello from the background");
|
||||||
|
|
||||||
|
browser.tabs.executeScript({
|
||||||
|
file: "content-script.js"
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import pptrActions from '../code-generator/pptr-actions'
|
|
||||||
import ctrl from '../models/extension-control-messages'
|
|
||||||
import actions from '../models/extension-ui-actions'
|
|
||||||
|
|
||||||
class RecordingController {
|
|
||||||
constructor () {
|
|
||||||
this._recording = []
|
|
||||||
this._boundedMessageHandler = null
|
|
||||||
this._boundedNavigationHandler = null
|
|
||||||
this._boundedWaitHandler = null
|
|
||||||
this._boundedMenuHandler = null
|
|
||||||
this._boundedKeyCommandHandler = null
|
|
||||||
this._badgeState = ''
|
|
||||||
this._isPaused = false
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
chrome.storage.local.set({ recording: this._recording }, () => {
|
|
||||||
console.debug('stored recording updated')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleControlMessage (msg, sender) {
|
|
||||||
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, tab) {
|
|
||||||
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: 'content-script.js', allFrames: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('booting recording controller')
|
|
||||||
window.recordingController = new RecordingController()
|
|
||||||
window.recordingController.boot()
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
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 (events) {
|
|
||||||
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 (width, height) {
|
|
||||||
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') {
|
|
||||||
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, '\\\'')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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}')` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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} })` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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}')`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
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\\\\')")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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
|
|
||||||
// ];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default {
|
|
||||||
GOTO: 'GOTO',
|
|
||||||
VIEWPORT: 'VIEWPORT',
|
|
||||||
WAITFORSELECTOR: 'WAITFORSELECTOR',
|
|
||||||
NAVIGATION: 'NAVIGATION',
|
|
||||||
NAVIGATION_PROMISE: 'NAVIGATION_PROMISE',
|
|
||||||
FRAME_SET: 'FRAME_SET',
|
|
||||||
SCREENSHOT: 'SCREENSHOT'
|
|
||||||
}
|
|
||||||
25
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>{{ defaultText }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "HelloWorld",
|
||||||
|
mounted() {
|
||||||
|
browser.runtime.sendMessage({});
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
defaultText() {
|
||||||
|
return browser.i18n.getMessage("extName");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import eventsToRecord from '../code-generator/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, sender, sendResponse) {
|
|
||||||
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 (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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, _value) => 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = UIController
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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 = process.env.NODE_ENV === 'production' ? '../../../dist' : '../../../build'
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!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/content-script.js" ></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<!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/content-script.js" ></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
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 = process.env.NODE_ENV === 'production' ? '../../../dist' : '../../../build'
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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, reject) => {
|
|
||||||
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
src/content-scripts/content-script.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console.log("Hello from the content-script");
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import EventRecorder from './EventRecorder'
|
|
||||||
window.eventRecorder = new EventRecorder()
|
|
||||||
window.eventRecorder.boot()
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 45 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 130 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 45 KiB |
4
src/main.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
createApp(App).mount("#app");
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Headless Recorder",
|
"name": "Headless Recorder",
|
||||||
"version": "0.8.2",
|
"version": "1.0.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
|
"homepage_url": "http://localhost:8080/",
|
||||||
"description": "A Chrome extension for recording browser interaction and generating Puppeteer & Playwright scripts",
|
"description": "A Chrome extension for recording browser interaction and generating Puppeteer & Playwright scripts",
|
||||||
|
"default_locale": "en",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"webNavigation",
|
"webNavigation",
|
||||||
@@ -11,40 +13,27 @@
|
|||||||
"*://*/"
|
"*://*/"
|
||||||
],
|
],
|
||||||
"icons" : {
|
"icons" : {
|
||||||
"16": "images/app_icon_16.png",
|
"16": "icons/16.png",
|
||||||
"48": "images/app_icon_48.png",
|
"48": "icons/48.png",
|
||||||
"128": "images/app_icon_128.png"
|
"128": "icons/128.png"
|
||||||
},
|
},
|
||||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||||
"browser_action": {
|
|
||||||
"default_icon": "images/icon-black.png",
|
|
||||||
"default_title": "Headless Recorder",
|
|
||||||
"default_popup": "index.html"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"background.js"
|
"js/background.js"
|
||||||
],
|
],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
},
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "__MSG_extName__",
|
||||||
|
"default_icon": {
|
||||||
|
"19": "icons/19.png",
|
||||||
|
"38": "icons/38.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"page": "options.html",
|
"page": "options.html",
|
||||||
"open_in_tab": true
|
"browser_style": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
EVENT_RECORDER_STARTED: 'EVENT_RECORDER_STARTED',
|
|
||||||
GET_VIEWPORT_SIZE: 'GET_VIEWPORT_SIZE',
|
|
||||||
GET_CURRENT_URL: 'GET_CURRENT_URL',
|
|
||||||
GET_SCREENSHOT: 'GET_SCREENSHOT'
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
19
src/options/App.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<hello-world />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HelloWorld from "@/components/HelloWorld.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
components: { HelloWorld }
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
<template>
|
|
||||||
<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="/images/text_racoon_logo.svg" alt="">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { defaults as code } from '../../code-generator/CodeGenerator'
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
code,
|
|
||||||
extension: {
|
|
||||||
telemetry: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
loading: true,
|
|
||||||
saving: false,
|
|
||||||
options: defaults,
|
|
||||||
recordingKeyCodePress: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.load()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
save () {
|
|
||||||
this.saving = true
|
|
||||||
this.$chrome.storage.local.set({ options: this.options }, () => {
|
|
||||||
console.debug('saved options')
|
|
||||||
setTimeout(() => {
|
|
||||||
this.saving = false
|
|
||||||
}, 500)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
load () {
|
|
||||||
this.$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 lang="scss" scoped>
|
|
||||||
@import "~styles/_variables.scss";
|
|
||||||
@import "~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: .75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: $spacer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-warning {
|
|
||||||
display: block;
|
|
||||||
font-size: .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>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { createLocalVue, 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', () => {
|
|
||||||
let localVue
|
|
||||||
beforeEach(() => { localVue = createLocalVue() })
|
|
||||||
|
|
||||||
test('it has the correct pristine / empty state', () => {
|
|
||||||
const mocks = { $chrome: createChromeLocalStorageMock() }
|
|
||||||
const wrapper = mount(App, { mocks, localVue })
|
|
||||||
expect(wrapper.element).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it loads the default options', () => {
|
|
||||||
const mocks = { $chrome: createChromeLocalStorageMock() }
|
|
||||||
const wrapper = mount(App, { mocks, localVue })
|
|
||||||
expect(wrapper.vm.$data.options.code.wrapAsync).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it has the default key code for capturing inputs as 9 (Tab)', () => {
|
|
||||||
const mocks = { $chrome: createChromeLocalStorageMock() }
|
|
||||||
const wrapper = mount(App, { mocks, localVue })
|
|
||||||
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 } }
|
|
||||||
const mocks = { $chrome: createChromeLocalStorageMock(options) }
|
|
||||||
const wrapper = mount(App, { mocks, localVue })
|
|
||||||
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 } }
|
|
||||||
const mocks = { $chrome: createChromeLocalStorageMock(options) }
|
|
||||||
const wrapper = mount(App, { mocks, localVue })
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 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>
|
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="footer"
|
|
||||||
>
|
|
||||||
|
|
||||||
sponsored by
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://checklyhq.com"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="/images/text_racoon_logo.svg"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import App from './components/App.vue'
|
|
||||||
import '../styles/style.scss'
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
Vue.prototype.$chrome = chrome
|
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
|
||||||
new Vue({
|
|
||||||
el: '#root',
|
|
||||||
render: h => h(App)
|
|
||||||
})
|
|
||||||
4
src/options/main.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
createApp(App).mount("#app");
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset='utf-8'>
|
|
||||||
<title>Option for Headless Recorder</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id='root'></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
19
src/popup/App.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<hello-world />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HelloWorld from "@/components/HelloWorld.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
components: { HelloWorld }
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
<template>
|
|
||||||
<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>
|
|
||||||
<a href="#" @click="toggleShowHelp" class="header-button">
|
|
||||||
<img src="/images/help.svg" alt="help" width="18px">
|
|
||||||
</a>
|
|
||||||
<a href="#" @click="openOptions" class="header-button">
|
|
||||||
<img src="/images/settings.svg" alt="settings" width="18px">
|
|
||||||
</a>
|
|
||||||
</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="#" v-clipboard:copy="getCodeForCopy()" @click.prevent="setCopying" v-show="code">{{copyLinkText}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HelpTab v-show="showHelp"></HelpTab>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { version } from '../../../package.json'
|
|
||||||
import PuppeteerCodeGenerator from '../../code-generator/PuppeteerCodeGenerator'
|
|
||||||
import PlaywrightCodeGenerator from '../../code-generator/PlaywrightCodeGenerator'
|
|
||||||
import RecordingTab from './RecordingTab.vue'
|
|
||||||
import ResultsTab from './ResultsTab.vue'
|
|
||||||
import HelpTab from './HelpTab.vue'
|
|
||||||
import ChecklyBadge from './ChecklyBadge.vue'
|
|
||||||
|
|
||||||
import actions from '../../models/extension-ui-actions'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
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')
|
|
||||||
this.$chrome.storage.local.get(['recording', 'options'], ({ recording }) => {
|
|
||||||
console.debug('loaded recording', recording)
|
|
||||||
this.liveEvents = recording
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isRecording && this.code) {
|
|
||||||
this.showResultsTab = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.bus = this.$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) {
|
|
||||||
this.bus.postMessage({ action: actions.UN_PAUSE })
|
|
||||||
this.isPaused = false
|
|
||||||
} else {
|
|
||||||
this.bus.postMessage({ action: actions.PAUSE })
|
|
||||||
this.isPaused = true
|
|
||||||
}
|
|
||||||
this.storeState()
|
|
||||||
},
|
|
||||||
start () {
|
|
||||||
this.trackEvent('Start')
|
|
||||||
this.cleanUp()
|
|
||||||
console.debug('start recorder')
|
|
||||||
this.bus.postMessage({ action: actions.START })
|
|
||||||
},
|
|
||||||
stop () {
|
|
||||||
this.trackEvent('Stop')
|
|
||||||
console.debug('stop recorder')
|
|
||||||
this.bus.postMessage({ action: actions.STOP })
|
|
||||||
|
|
||||||
this.$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 () {
|
|
||||||
console.log('restart')
|
|
||||||
this.cleanUp()
|
|
||||||
this.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 (this.$chrome.runtime.openOptionsPage) {
|
|
||||||
this.$chrome.runtime.openOptionsPage()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadState (cb) {
|
|
||||||
this.$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 () {
|
|
||||||
this.$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 lang="scss" scoped>
|
|
||||||
@import "~styles/_animations.scss";
|
|
||||||
@import "~styles/_variables.scss";
|
|
||||||
@import "~styles/_mixins.scss";
|
|
||||||
.recorder {
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
@include header();
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $gray-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: .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>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<template functional>
|
|
||||||
<div class="checkly-badge text-muted">
|
|
||||||
powered by
|
|
||||||
<a href="https://checklyhq.com" target="_blank">
|
|
||||||
<img src="/images/text_racoon_logo.svg" alt="Checkly logo">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'ChecklyBadge'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" 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>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<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="/images/icon_rec.png"> to
|
|
||||||
<img src="/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="/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="/images/text_racoon_logo.svg" alt="">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HelpTab'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "~styles/_variables.scss";
|
|
||||||
@import "~styles/_mixins.scss";
|
|
||||||
@import "~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>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="tab recording-tab">
|
|
||||||
<div class="content">
|
|
||||||
<div class="empty" v-show="!isRecording">
|
|
||||||
<img src="/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.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 "~styles/_animations.scss";
|
|
||||||
@import "~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>
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<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="/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 "~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;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { createLocalVue, shallowMount } from '@vue/test-utils'
|
|
||||||
import App from '../App'
|
|
||||||
|
|
||||||
const chrome = {
|
|
||||||
storage: {
|
|
||||||
local: {
|
|
||||||
get: jest.fn()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extension: {
|
|
||||||
connect: jest.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mocks = { $chrome: chrome }
|
|
||||||
|
|
||||||
describe('App.vue', () => {
|
|
||||||
let localVue
|
|
||||||
beforeEach(() => {
|
|
||||||
localVue = createLocalVue()
|
|
||||||
localVue.directive('highlightjs', () => {})
|
|
||||||
localVue.directive('clipboard', () => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it has the correct pristine / empty state', () => {
|
|
||||||
const wrapper = shallowMount(App, { mocks, localVue })
|
|
||||||
expect(wrapper.element).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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)
|
|
||||||
wrapper.setProps({ isRecording: true })
|
|
||||||
expect(wrapper.element).toMatchSnapshot()
|
|
||||||
expect(wrapper.find('.event-list').isEmpty()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it has the correct recording Puppeteer custom events state', () => {
|
|
||||||
const wrapper = mount(RecordingTab)
|
|
||||||
wrapper.setProps({
|
|
||||||
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').isEmpty()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it has the correct recording DOM events state', () => {
|
|
||||||
const wrapper = mount(RecordingTab)
|
|
||||||
wrapper.setProps({ isRecording: true, liveEvents: [{ action: 'click', selector: '.main > a.link', href: 'http://example.com' }] })
|
|
||||||
expect(wrapper.element).toMatchSnapshot()
|
|
||||||
expect(wrapper.find('.event-list').isEmpty()).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { createLocalVue, mount } from '@vue/test-utils'
|
|
||||||
import ResultsTab from '../ResultsTab'
|
|
||||||
|
|
||||||
describe('RecordingTab.vue', () => {
|
|
||||||
let localVue
|
|
||||||
beforeEach(() => {
|
|
||||||
localVue = createLocalVue()
|
|
||||||
localVue.directive('highlightjs', () => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it has the correct pristine / empty state', () => {
|
|
||||||
const wrapper = mount(ResultsTab, { localVue })
|
|
||||||
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, { localVue })
|
|
||||||
wrapper.setProps({ 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, { localVue })
|
|
||||||
expect(wrapper.findAll('.tabs__action').length).toEqual(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it render playwright first when option is present', async () => {
|
|
||||||
const wrapper = await mount(ResultsTab, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
options: {
|
|
||||||
code: {
|
|
||||||
showPlaywrightFirst: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(wrapper.find('.tabs__action').text()).toEqual('🎭 playwright')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
// 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>
|
|
||||||
0.8.2
|
|
||||||
</small>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="recording-badge"
|
|
||||||
style="display: none;"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="red-dot"
|
|
||||||
/>
|
|
||||||
|
|
||||||
recording
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="header-button"
|
|
||||||
href="#"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="help"
|
|
||||||
src="/images/help.svg"
|
|
||||||
width="18px"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="header-button"
|
|
||||||
href="#"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="settings"
|
|
||||||
src="/images/settings.svg"
|
|
||||||
width="18px"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="main"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="tabs"
|
|
||||||
>
|
|
||||||
<recordingtab-stub
|
|
||||||
code=""
|
|
||||||
liveevents=""
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<checklybadge-stub />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<helptab-stub
|
|
||||||
style="display: none;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
// 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="/images/Desert.svg"
|
|
||||||
width="78px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</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="/images/Desert.svg"
|
|
||||||
width="78px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
style=""
|
|
||||||
>
|
|
||||||
<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="/images/Desert.svg"
|
|
||||||
width="78px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
style=""
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</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="/images/Desert.svg"
|
|
||||||
width="78px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
style=""
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="text-muted text-center loading"
|
|
||||||
>
|
|
||||||
Waiting for events
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
class="event-list"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// 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"
|
|
||||||
>
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
<img
|
|
||||||
src="/images/puppeteer.png"
|
|
||||||
width="16"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="tabs__action--text"
|
|
||||||
>
|
|
||||||
puppeteer
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tabs__action"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
🎭
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
<img
|
|
||||||
src="/images/puppeteer.png"
|
|
||||||
width="16"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="tabs__action--text"
|
|
||||||
>
|
|
||||||
puppeteer
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tabs__action"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
🎭
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="tabs__action--text"
|
|
||||||
>
|
|
||||||
playwright
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<pre>
|
|
||||||
|
|
||||||
<code
|
|
||||||
class="javascript"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
export default (function(){var E;var g=window,n=document,p=function(a){var b=g._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===g["ga-disable-"+a])return!0;try{var c=g.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(f){}a=[];b=n.cookie.split(";");c=/^\s*AMP_TOKEN=\s*(.*?)\s*$/;for(var d=0;d<b.length;d++){var e=b[d].match(c);e&&a.push(e[1])}for(b=0;b<a.length;b++)if("$OPT_OUT"==decodeURIComponent(a[b]))return!0;return!1};var q=function(a){return encodeURIComponent?encodeURIComponent(a).replace(/\(/g,"%28").replace(/\)/g,"%29"):a},r=/^(www\.)?google(\.com?)?(\.[a-z]{2})?$/,u=/(^|\.)doubleclick\.net$/i;function Aa(a,b){switch(b){case 0:return""+a;case 1:return 1*a;case 2:return!!a;case 3:return 1E3*a}return a}function Ba(a){return"function"==typeof a}function Ca(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")}function F(a,b){return void 0==a||"-"==a&&!b||""==a}function Da(a){if(!a||""==a)return"";for(;a&&-1<" \n\r\t".indexOf(a.charAt(0));)a=a.substring(1);for(;a&&-1<" \n\r\t".indexOf(a.charAt(a.length-1));)a=a.substring(0,a.length-1);return a}
|
|
||||||
function Ea(){return Math.round(2147483647*Math.random())}function Fa(){}function G(a,b){if(encodeURIComponent instanceof Function)return b?encodeURI(a):encodeURIComponent(a);H(68);return escape(a)}function I(a){a=a.split("+").join(" ");if(decodeURIComponent instanceof Function)try{return decodeURIComponent(a)}catch(b){H(17)}else H(68);return unescape(a)}var Ga=function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)};
|
|
||||||
function Ia(a,b){if(a){var c=J.createElement("script");c.type="text/javascript";c.async=!0;c.src=a;c.id=b;a=J.getElementsByTagName("script")[0];a.parentNode.insertBefore(c,a);return c}}function K(a){return a&&0<a.length?a[0]:""}function L(a){var b=a?a.length:0;return 0<b?a[b-1]:""}var nf=function(){this.prefix="ga.";this.values={}};nf.prototype.set=function(a,b){this.values[this.prefix+a]=b};nf.prototype.get=function(a){return this.values[this.prefix+a]};
|
|
||||||
nf.prototype.contains=function(a){return void 0!==this.get(a)};function Ka(a){0==a.indexOf("www.")&&(a=a.substring(4));return a.toLowerCase()}
|
|
||||||
function La(a,b){var c={url:a,protocol:"http",host:"",path:"",R:new nf,anchor:""};if(!a)return c;var d=a.indexOf("://");0<=d&&(c.protocol=a.substring(0,d),a=a.substring(d+3));d=a.search("/|\\?|#");if(0<=d)c.host=a.substring(0,d).toLowerCase(),a=a.substring(d);else return c.host=a.toLowerCase(),c;d=a.indexOf("#");0<=d&&(c.anchor=a.substring(d+1),a=a.substring(0,d));d=a.indexOf("?");0<=d&&(Na(c.R,a.substring(d+1)),a=a.substring(0,d));c.anchor&&b&&Na(c.R,c.anchor);a&&"/"==a.charAt(0)&&(a=a.substring(1));
|
|
||||||
c.path=a;return c}
|
|
||||||
function Oa(a,b){function c(a){var b=(a.hostname||"").split(":")[0].toLowerCase(),c=(a.protocol||"").toLowerCase();c=1*a.port||("http:"==c?80:"https:"==c?443:"");a=a.pathname||"";0==a.indexOf("/")||(a="/"+a);return[b,""+c,a]}b=b||J.createElement("a");b.href=J.location.href;var d=(b.protocol||"").toLowerCase(),e=c(b),f=b.search||"",Be=d+"//"+e[0]+(e[1]?":"+e[1]:"");0==a.indexOf("//")?a=d+a:0==a.indexOf("/")?a=Be+a:a&&0!=a.indexOf("?")?0>a.split("/")[0].indexOf(":")&&(a=Be+e[2].substring(0,e[2].lastIndexOf("/"))+
|
|
||||||
"/"+a):a=Be+e[2]+(a||f);b.href=a;d=c(b);return{protocol:(b.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:b.search||"",url:a||""}}function Na(a,b){function c(b,c){a.contains(b)||a.set(b,[]);a.get(b).push(c)}b=Da(b).split("&");for(var d=0;d<b.length;d++)if(b[d]){var e=b[d].indexOf("=");0>e?c(b[d],"1"):c(b[d].substring(0,e),b[d].substring(e+1))}}
|
|
||||||
function Pa(a,b){return F(a)||"["==a.charAt(0)&&"]"==a.charAt(a.length-1)?"-":a.indexOf(J.domain+(b&&"/"!=b?b:""))==(0==a.indexOf("http://")?7:0==a.indexOf("https://")?8:0)?"0":a};var Qa=0;function Ra(a,b,c){1<=Qa||1<=100*Math.random()||ld()||(a=["utmt=error","utmerr="+a,"utmwv=5.7.2","utmn="+Ea(),"utmsp=1"],b&&a.push("api="+b),c&&a.push("msg="+G(c.substring(0,100))),M.w&&a.push("aip=1"),Sa(a.join("&")),Qa++)};var Ta=0,Ua={};function N(a){return Va("x"+Ta++,a)}function Va(a,b){Ua[a]=!!b;return a}
|
|
||||||
var Wa=N(),Xa=Va("anonymizeIp"),Ya=N(),$a=N(),ab=N(),bb=N(),O=N(),P=N(),cb=N(),db=N(),eb=N(),fb=N(),gb=N(),hb=N(),ib=N(),jb=N(),kb=N(),lb=N(),nb=N(),ob=N(),pb=N(),qb=N(),rb=N(),sb=N(),tb=N(),ub=N(),vb=N(),wb=N(),xb=N(),yb=N(),zb=N(),Ab=N(),Bb=N(),Cb=N(),Db=N(),Eb=N(),Fb=N(!0),Gb=Va("currencyCode"),v=Va("storeGac"),Hb=Va("page"),Ib=Va("title"),Jb=N(),Kb=N(),Lb=N(),Mb=N(),Nb=N(),Ob=N(),Pb=N(),Qb=N(),Rb=N(),Q=N(!0),Sb=N(!0),Tb=N(!0),Ub=N(!0),Vb=N(!0),Wb=N(!0),Zb=N(!0),$b=N(!0),ac=N(!0),bc=N(!0),cc=N(!0),
|
|
||||||
R=N(!0),dc=N(!0),ec=N(!0),fc=N(!0),gc=N(!0),hc=N(!0),ic=N(!0),jc=N(!0),S=N(!0),kc=N(!0),lc=N(!0),mc=N(!0),nc=N(!0),oc=N(!0),pc=N(!0),qc=N(!0),rc=Va("campaignParams"),sc=N(),tc=Va("hitCallback"),uc=N();N();var vc=N(),wc=N(),xc=N(),yc=N(),zc=N(),Ac=N(),Bc=N(),Cc=N(),Dc=N(),Ec=N(),Fc=N(),Gc=N(),Hc=N(),Ic=N();N();
|
|
||||||
var Mc=N(),Nc=N(),Yb=N(),Jc=N(),Kc=N(),Lc=Va("utmtCookieName"),Cd=Va("displayFeatures"),Oc=N(),of=Va("gtmid"),Oe=Va("uaName"),Pe=Va("uaDomain"),Qe=Va("uaPath"),pf=Va("linkid"),w=N(),x=N(),y=N(),z=N();var Re=function(){function a(a,c,d){T(qf.prototype,a,c,d)}a("_createTracker",qf.prototype.hb,55);a("_getTracker",qf.prototype.oa,0);a("_getTrackerByName",qf.prototype.u,51);a("_getTrackers",qf.prototype.pa,130);a("_anonymizeIp",qf.prototype.aa,16);a("_forceSSL",qf.prototype.la,125);a("_getPlugin",Pc,120)},Se=function(){function a(a,c,d){T(U.prototype,a,c,d)}Qc("_getName",$a,58);Qc("_getAccount",Wa,64);Qc("_visitCode",Q,54);Qc("_getClientInfo",ib,53,1);Qc("_getDetectTitle",lb,56,1);Qc("_getDetectFlash",
|
|
||||||
jb,65,1);Qc("_getLocalGifPath",wb,57);Qc("_getServiceMode",xb,59);V("_setClientInfo",ib,66,2);V("_setAccount",Wa,3);V("_setNamespace",Ya,48);V("_setAllowLinker",fb,11,2);V("_setDetectFlash",jb,61,2);V("_setDetectTitle",lb,62,2);V("_setLocalGifPath",wb,46,0);V("_setLocalServerMode",xb,92,void 0,0);V("_setRemoteServerMode",xb,63,void 0,1);V("_setLocalRemoteServerMode",xb,47,void 0,2);V("_setSampleRate",vb,45,1);V("_setCampaignTrack",kb,36,2);V("_setAllowAnchor",gb,7,2);V("_setCampNameKey",ob,41);V("_setCampContentKey",
|
|
||||||
tb,38);V("_setCampIdKey",nb,39);V("_setCampMediumKey",rb,40);V("_setCampNOKey",ub,42);V("_setCampSourceKey",qb,43);V("_setCampTermKey",sb,44);V("_setCampCIdKey",pb,37);V("_setCookiePath",P,9,0);V("_setMaxCustomVariables",yb,0,1);V("_setVisitorCookieTimeout",cb,28,1);V("_setSessionCookieTimeout",db,26,1);V("_setCampaignCookieTimeout",eb,29,1);V("_setReferrerOverride",Jb,49);V("_setSiteSpeedSampleRate",Dc,132);V("_storeGac",v,143);a("_trackPageview",U.prototype.Fa,1);a("_trackEvent",U.prototype.F,4);
|
|
||||||
a("_trackPageLoadTime",U.prototype.Ea,100);a("_trackSocial",U.prototype.Ga,104);a("_trackTrans",U.prototype.Ia,18);a("_sendXEvent",U.prototype.ib,78);a("_createEventTracker",U.prototype.ia,74);a("_getVersion",U.prototype.qa,60);a("_setDomainName",U.prototype.B,6);a("_setAllowHash",U.prototype.va,8);a("_getLinkerUrl",U.prototype.na,52);a("_link",U.prototype.link,101);a("_linkByPost",U.prototype.ua,102);a("_setTrans",U.prototype.za,20);a("_addTrans",U.prototype.$,21);a("_addItem",U.prototype.Y,19);
|
|
||||||
a("_clearTrans",U.prototype.ea,105);a("_setTransactionDelim",U.prototype.Aa,82);a("_setCustomVar",U.prototype.wa,10);a("_deleteCustomVar",U.prototype.ka,35);a("_getVisitorCustomVar",U.prototype.ra,50);a("_setXKey",U.prototype.Ca,83);a("_setXValue",U.prototype.Da,84);a("_getXKey",U.prototype.sa,76);a("_getXValue",U.prototype.ta,77);a("_clearXKey",U.prototype.fa,72);a("_clearXValue",U.prototype.ga,73);a("_createXObj",U.prototype.ja,75);a("_addIgnoredOrganic",U.prototype.W,15);a("_clearIgnoredOrganic",
|
|
||||||
U.prototype.ba,97);a("_addIgnoredRef",U.prototype.X,31);a("_clearIgnoredRef",U.prototype.ca,32);a("_addOrganic",U.prototype.Z,14);a("_clearOrganic",U.prototype.da,70);a("_cookiePathCopy",U.prototype.ha,30);a("_get",U.prototype.ma,106);a("_set",U.prototype.xa,107);a("_addEventListener",U.prototype.addEventListener,108);a("_removeEventListener",U.prototype.removeEventListener,109);a("_addDevId",U.prototype.V);a("_getPlugin",Pc,122);a("_setPageGroup",U.prototype.ya,126);a("_trackTiming",U.prototype.Ha,
|
|
||||||
124);a("_initData",U.prototype.initData,2);a("_setVar",U.prototype.Ba,22);V("_setSessionTimeout",db,27,3);V("_setCookieTimeout",eb,25,3);V("_setCookiePersistence",cb,24,1);a("_setAutoTrackOutbound",Fa,79);a("_setTrackOutboundSubdomains",Fa,81);a("_setHrefExamineLimit",Fa,80)};function Pc(a){var b=this.plugins_;if(b)return b.get(a)}
|
|
||||||
var T=function(a,b,c,d){a[b]=function(){try{return void 0!=d&&H(d),c.apply(this,arguments)}catch(e){throw Ra("exc",b,e&&e.name),e;}}},Qc=function(a,b,c,d){U.prototype[a]=function(){try{return H(c),Aa(this.a.get(b),d)}catch(e){throw Ra("exc",a,e&&e.name),e;}}},V=function(a,b,c,d,e){U.prototype[a]=function(f){try{H(c),void 0==e?this.a.set(b,Aa(f,d)):this.a.set(b,e)}catch(Be){throw Ra("exc",a,Be&&Be.name),Be;}}},Te=function(a,b){return{type:b,target:a,stopPropagation:function(){throw"aborted";}}};var Rc=new RegExp(/(^|\.)doubleclick\.net$/i),Sc=function(a,b){return Rc.test(J.location.hostname)?!0:"/"!==b?!1:0!=a.indexOf("www.google.")&&0!=a.indexOf(".google.")&&0!=a.indexOf("google.")||-1<a.indexOf("google.org")?!1:!0},Tc=function(a){var b=a.get(bb),c=a.c(P,"/");Sc(b,c)&&a.stopPropagation()};var Zc=function(){var a={},b={},c=new Uc;this.g=function(a,b){c.add(a,b)};var d=new Uc;this.v=function(a,b){d.add(a,b)};var e=!1,f=!1,Be=!0;this.T=function(){e=!0};this.j=function(a){this.load();this.set(sc,a,!0);a=new Vc(this);e=!1;d.cb(this);e=!0;b={};this.store();a.Ja()};this.load=function(){e&&(e=!1,this.Ka(),Wc(this),f||(f=!0,c.cb(this),Xc(this),Wc(this)),e=!0)};this.store=function(){e&&(f?(e=!1,Xc(this),e=!0):this.load())};this.get=function(c){Ua[c]&&this.load();return void 0!==b[c]?b[c]:a[c]};
|
|
||||||
this.set=function(c,d,e){Ua[c]&&this.load();e?b[c]=d:a[c]=d;Ua[c]&&this.store()};this.Za=function(b){a[b]=this.b(b,0)+1};this.b=function(a,b){a=this.get(a);return void 0==a||""===a?b:1*a};this.c=function(a,b){a=this.get(a);return void 0==a?b:a+""};this.Ka=function(){if(Be){var b=this.c(bb,""),c=this.c(P,"/");Sc(b,c)||(a[O]=a[hb]&&""!=b?Yc(b):1,Be=!1)}}};Zc.prototype.stopPropagation=function(){throw"aborted";};
|
|
||||||
var Vc=function(a){var b=this;this.fb=0;var c=a.get(tc);this.Ua=function(){0<b.fb&&c&&(b.fb--,b.fb||c())};this.Ja=function(){!b.fb&&c&&setTimeout(c,10)};a.set(uc,b,!0)};function $c(a,b){b=b||[];for(var c=0;c<b.length;c++){var d=b[c];if(""+a==d||0==d.indexOf(a+"."))return d}return"-"}
|
|
||||||
var bd=function(a,b,c){c=c?"":a.c(O,"1");b=b.split(".");if(6!==b.length||ad(b[0],c))return!1;c=1*b[1];var d=1*b[2],e=1*b[3],f=1*b[4];b=1*b[5];if(!(0<=c&&0<d&&0<e&&0<f&&0<=b))return!1;a.set(Q,c);a.set(Vb,d);a.set(Wb,e);a.set(Zb,f);a.set($b,b);return!0},cd=function(a){var b=a.get(Q),c=a.get(Vb),d=a.get(Wb),e=a.get(Zb),f=a.b($b,1);return[a.b(O,1),void 0!=b?b:"-",c||"-",d||"-",e||"-",f].join(".")},dd=function(a){return[a.b(O,1),a.b(cc,0),a.b(R,1),a.b(dc,0)].join(".")},ed=function(a,b,c){c=c?"":a.c(O,
|
|
||||||
"1");var d=b.split(".");if(4!==d.length||ad(d[0],c))d=null;a.set(cc,d?1*d[1]:0);a.set(R,d?1*d[2]:10);a.set(dc,d?1*d[3]:a.get(ab));return null!=d||!ad(b,c)},fd=function(a,b){var c=G(a.c(Tb,"")),d=[],e=a.get(Fb);if(!b&&e){for(b=0;b<e.length;b++){var f=e[b];f&&1==f.scope&&d.push(b+"="+G(f.name)+"="+G(f.value)+"=1")}0<d.length&&(c+="|"+d.join("^"))}return c?a.b(O,1)+"."+c:null},gd=function(a,b,c){c=c?"":a.c(O,"1");b=b.split(".");if(2>b.length||ad(b[0],c))return!1;b=b.slice(1).join(".").split("|");0<b.length&&
|
|
||||||
a.set(Tb,I(b[0]));if(1>=b.length)return!0;b=b[1].split(-1==b[1].indexOf(",")?"^":",");for(c=0;c<b.length;c++){var d=b[c].split("=");if(4==d.length){var e={};e.name=I(d[1]);e.value=I(d[2]);e.scope=1;a.get(Fb)[d[0]]=e}}return!0},hd=function(a,b){return(b=Ue(a,b))?[a.b(O,1),a.b(ec,0),a.b(fc,1),a.b(gc,1),b].join("."):""},Ue=function(a){function b(b,e){F(a.get(b))||(b=a.c(b,""),b=b.split(" ").join("%20"),b=b.split("+").join("%20"),c.push(e+"="+b))}var c=[];b(ic,"utmcid");b(nc,"utmcsr");b(S,"utmgclid");
|
|
||||||
b(kc,"utmgclsrc");b(lc,"utmdclid");b(mc,"utmdsid");b(jc,"utmccn");b(oc,"utmcmd");b(pc,"utmctr");b(qc,"utmcct");return c.join("|")},id=function(a,b,c){c=c?"":a.c(O,"1");b=b.split(".");if(5>b.length||ad(b[0],c))return a.set(ec,void 0),a.set(fc,void 0),a.set(gc,void 0),a.set(ic,void 0),a.set(jc,void 0),a.set(nc,void 0),a.set(oc,void 0),a.set(pc,void 0),a.set(qc,void 0),a.set(S,void 0),a.set(kc,void 0),a.set(lc,void 0),a.set(mc,void 0),!1;a.set(ec,1*b[1]);a.set(fc,1*b[2]);a.set(gc,1*b[3]);Ve(a,b.slice(4).join("."));
|
|
||||||
return!0},Ve=function(a,b){function c(a){return(a=b.match(a+"=(.*?)(?:\\|utm|$)"))&&2==a.length?a[1]:void 0}function d(b,c){c?(c=e?I(c):c.split("%20").join(" "),a.set(b,c)):a.set(b,void 0)}-1==b.indexOf("=")&&(b=I(b));var e="2"==c("utmcvr");d(ic,c("utmcid"));d(jc,c("utmccn"));d(nc,c("utmcsr"));d(oc,c("utmcmd"));d(pc,c("utmctr"));d(qc,c("utmcct"));d(S,c("utmgclid"));d(kc,c("utmgclsrc"));d(lc,c("utmdclid"));d(mc,c("utmdsid"))},ad=function(a,b){return b?a!=b:!/^\d+$/.test(a)};var Uc=function(){this.filters=[]};Uc.prototype.add=function(a,b){this.filters.push({name:a,s:b})};Uc.prototype.cb=function(a){try{for(var b=0;b<this.filters.length;b++)this.filters[b].s.call(W,a)}catch(c){}};function jd(a){100!=a.get(vb)&&a.get(Q)%1E4>=100*a.get(vb)&&a.stopPropagation()}function kd(a){ld(a.get(Wa))&&a.stopPropagation()}function md(a){"file:"==J.location.protocol&&a.stopPropagation()}function Ge(a){He()&&a.stopPropagation()}
|
|
||||||
function nd(a){a.get(Ib)||a.set(Ib,J.title,!0);a.get(Hb)||a.set(Hb,J.location.pathname+J.location.search,!0)}function lf(a){a.get(Wa)&&"UA-XXXXX-X"!=a.get(Wa)||a.stopPropagation()};var od=new function(){var a=[];this.set=function(b){a[b]=!0};this.encode=function(){for(var b=[],c=0;c<a.length;c++)a[c]&&(b[Math.floor(c/6)]^=1<<c%6);for(c=0;c<b.length;c++)b[c]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(b[c]||0);return b.join("")+"~"}};function H(a){od.set(a)};var W=window,J=document,ld=function(a){var b=W._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===W["ga-disable-"+a])return!0;try{var c=W.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(d){}return!1},He=function(){return W.navigator&&"preview"==W.navigator.loadPurpose},We=function(a,b){setTimeout(a,b)},pd=function(a){var b=[],c=J.cookie.split(";");a=new RegExp("^\\s*"+a+"=\\s*(.*?)\\s*$");for(var d=0;d<c.length;d++){var e=c[d].match(a);e&&b.push(e[1])}return b},X=function(a,b,c,d,e,f){e=
|
|
||||||
ld(e)?!1:Sc(d,c)?!1:He()?!1:!0;e&&((b=mf(b))&&2E3<b.length&&(b=b.substring(0,2E3),H(69)),a=a+"="+b+"; path="+c+"; ",f&&(a+="expires="+(new Date((new Date).getTime()+f)).toGMTString()+"; "),d&&(a+="domain="+d+";"),J.cookie=a)},mf=function(a){if(!a)return a;var b=a.indexOf(";");-1!=b&&(a=a.substring(0,b),H(141));if(!(0<=W.navigator.userAgent.indexOf("Firefox")))return a;a=a.replace(/\n|\r/g," ");b=0;for(var c=a.length;b<c;++b){var d=a.charCodeAt(b)&255;if(10==d||13==d)a=a.substring(0,b)+"?"+a.substring(b+
|
|
||||||
1)}return a};var A,B=/^.*Version\/?(\d+)[^\d].*$/i;var qd,rd,sd=function(){if(!qd){var a={},b=W.navigator,c=W.screen;a.jb=c?c.width+"x"+c.height:"-";a.P=c?c.colorDepth+"-bit":"-";a.language=(b&&(b.language||b.browserLanguage)||"-").toLowerCase();a.javaEnabled=b&&b.javaEnabled()?1:0;a.characterSet=J.characterSet||J.charset||"-";try{var d=J.documentElement,e=J.body,f=e&&e.clientWidth&&e.clientHeight;b=[];d&&d.clientWidth&&d.clientHeight&&("CSS1Compat"===J.compatMode||!f)?b=[d.clientWidth,d.clientHeight]:f&&(b=[e.clientWidth,e.clientHeight]);var Be=
|
|
||||||
0>=b[0]||0>=b[1]?"":b.join("x");a.Wa=Be}catch(k){H(135)}qd=a}},td=function(){sd();var a=qd,b=W.navigator;a=b.appName+b.version+a.language+b.platform+b.userAgent+a.javaEnabled+a.jb+a.P+(J.cookie?J.cookie:"")+(J.referrer?J.referrer:"");b=a.length;for(var c=W.history.length;0<c;)a+=c--^b++;return Yc(a)},ud=function(a){sd();var b=qd;a.set(Lb,b.jb);a.set(Mb,b.P);a.set(Pb,b.language);a.set(Qb,b.characterSet);a.set(Nb,b.javaEnabled);a.set(Rb,b.Wa);if(a.get(ib)&&a.get(jb)){if(!(b=rd)){var c,d;var e="ShockwaveFlash";
|
|
||||||
if((b=(b=W.navigator)?b.plugins:void 0)&&0<b.length)for(c=0;c<b.length&&!d;c++)e=b[c],-1<e.name.indexOf("Shockwave Flash")&&(d=e.description.split("Shockwave Flash ")[1]);else{e=e+"."+e;try{c=new ActiveXObject(e+".7"),d=c.GetVariable("$version")}catch(f){}if(!d)try{c=new ActiveXObject(e+".6"),d="WIN 6,0,21,0",c.AllowScriptAccess="always",d=c.GetVariable("$version")}catch(f){}if(!d)try{c=new ActiveXObject(e),d=c.GetVariable("$version")}catch(f){}d&&(d=d.split(" ")[1].split(","),d=d[0]+"."+d[1]+" r"+
|
|
||||||
d[2])}b=d?d:"-"}rd=b;a.set(Ob,rd)}else a.set(Ob,"-")};var vd=function(a){if(Ba(a))this.s=a;else{var b=a[0],c=b.lastIndexOf(":"),d=b.lastIndexOf(".");this.h=this.i=this.l="";-1==c&&-1==d?this.h=b:-1==c&&-1!=d?(this.i=b.substring(0,d),this.h=b.substring(d+1)):-1!=c&&-1==d?(this.l=b.substring(0,c),this.h=b.substring(c+1)):c>d?(this.i=b.substring(0,d),this.l=b.substring(d+1,c),this.h=b.substring(c+1)):(this.i=b.substring(0,d),this.h=b.substring(d+1));this.Xa=a.slice(1);this.Ma=!this.l&&"_require"==this.h;this.J=!this.i&&!this.l&&"_provide"==this.h}},Y=function(){T(Y.prototype,
|
|
||||||
"push",Y.prototype.push,5);T(Y.prototype,"_getPlugin",Pc,121);T(Y.prototype,"_createAsyncTracker",Y.prototype.Sa,33);T(Y.prototype,"_getAsyncTracker",Y.prototype.Ta,34);this.I=new nf;this.eb=[]};E=Y.prototype;E.Na=function(a,b,c){var d=this.I.get(a);if(!Ba(d))return!1;b.plugins_=b.plugins_||new nf;b.plugins_.set(a,new d(b,c||{}));return!0};E.push=function(a){var b=Z.Va.apply(this,arguments);b=Z.eb.concat(b);for(Z.eb=[];0<b.length&&!Z.O(b[0])&&!(b.shift(),0<Z.eb.length););Z.eb=Z.eb.concat(b);return 0};
|
|
||||||
E.Va=function(a){for(var b=[],c=0;c<arguments.length;c++)try{var d=new vd(arguments[c]);d.J?this.O(d):b.push(d)}catch(e){}return b};
|
|
||||||
E.O=function(a){try{if(a.s)a.s.apply(W);else if(a.J)this.I.set(a.Xa[0],a.Xa[1]);else{var b="_gat"==a.i?M:"_gaq"==a.i?Z:M.u(a.i);if(a.Ma){if(!this.Na(a.Xa[0],b,a.Xa[2])){if(!a.Pa){var c=Oa(""+a.Xa[1]);var d=c.protocol,e=J.location.protocol;var f;if(f="https:"==d||d==e?!0:"http:"!=d?!1:"http:"==e)a:{var Be=Oa(J.location.href);if(!(c.query||0<=c.url.indexOf("?")||0<=c.path.indexOf("://")||c.host==Be.host&&c.port==Be.port)){var k="http:"==c.protocol?80:443,Ja=M.S;for(b=0;b<Ja.length;b++)if(c.host==Ja[b][0]&&
|
|
||||||
(c.port||k)==(Ja[b][1]||k)&&0==c.path.indexOf(Ja[b][2])){f=!0;break a}}f=!1}f&&!ld()&&(a.Pa=Ia(c.url))}return!0}}else a.l&&(b=b.plugins_.get(a.l)),b[a.h].apply(b,a.Xa)}}catch(t){}};E.Sa=function(a,b){return M.hb(a,b||"")};E.Ta=function(a){return M.u(a)};var yd=function(){function a(a,b,c,d){void 0==f[a]&&(f[a]={});void 0==f[a][b]&&(f[a][b]=[]);f[a][b][c]=d}function b(a,b,c){if(void 0!=f[a]&&void 0!=f[a][b])return f[a][b][c]}function c(a,b){if(void 0!=f[a]&&void 0!=f[a][b]){f[a][b]=void 0;b=!0;var c;for(c=0;c<Be.length;c++)if(void 0!=f[a][Be[c]]){b=!1;break}b&&(f[a]=void 0)}}function d(a){var b="",c=!1,d;for(d=0;d<Be.length;d++){var e=a[Be[d]];if(void 0!=e){c&&(b+=Be[d]);var f=e,Ja=[];for(e=0;e<f.length;e++)if(void 0!=f[e]){c="";1!=e&&void 0==f[e-
|
|
||||||
1]&&(c+=e.toString()+"!");var fa,Ke=f[e],Le="";for(fa=0;fa<Ke.length;fa++){var Me=Ke.charAt(fa);var m=k[Me];Le+=void 0!=m?m:Me}c+=Le;Ja.push(c)}b+="("+Ja.join("*")+")";c=!1}else c=!0}return b}var e=this,f=[],Be=["k","v"],k={"'":"'0",")":"'1","*":"'2","!":"'3"};e.Ra=function(a){return void 0!=f[a]};e.A=function(){for(var a="",b=0;b<f.length;b++)void 0!=f[b]&&(a+=b.toString()+d(f[b]));return a};e.Qa=function(a){if(void 0==a)return e.A();for(var b=a.A(),c=0;c<f.length;c++)void 0==f[c]||a.Ra(c)||(b+=
|
|
||||||
c.toString()+d(f[c]));return b};e.f=function(b,c,d){if(!wd(d))return!1;a(b,"k",c,d);return!0};e.o=function(b,c,d){if(!xd(d))return!1;a(b,"v",c,d.toString());return!0};e.getKey=function(a,c){return b(a,"k",c)};e.N=function(a,c){return b(a,"v",c)};e.L=function(a){c(a,"k")};e.M=function(a){c(a,"v")};T(e,"_setKey",e.f,89);T(e,"_setValue",e.o,90);T(e,"_getKey",e.getKey,87);T(e,"_getValue",e.N,88);T(e,"_clearKey",e.L,85);T(e,"_clearValue",e.M,86)};function wd(a){return"string"==typeof a}
|
|
||||||
function xd(a){return!("number"==typeof a||void 0!=Number&&a instanceof Number)||Math.round(a)!=a||isNaN(a)||Infinity==a?!1:!0};var zd=function(a){var b=W.gaGlobal;a&&!b&&(W.gaGlobal=b={});return b},Ad=function(){var a=zd(!0).hid;null==a&&(a=Ea(),zd(!0).hid=a);return a},Dd=function(a){a.set(Kb,Ad());var b=zd();if(b&&b.dh==a.get(O)){var c=b.sid;c&&(a.get(ac)?H(112):H(132),a.set(Zb,c),a.get(Sb)&&a.set(Wb,c));b=b.vid;a.get(Sb)&&b&&(b=b.split("."),a.set(Q,1*b[0]),a.set(Vb,1*b[1]))}};var Ed,Fd=function(a,b,c,d){var e=a.c(bb,""),f=a.c(P,"/");d=void 0!=d?d:a.b(cb,0);a=a.c(Wa,"");X(b,c,f,e,a,d)},Xc=function(a){var b=a.c(bb,""),c=a.c(P,"/"),d=a.c(Wa,"");X("__utma",cd(a),c,b,d,a.get(cb));X("__utmb",dd(a),c,b,d,a.get(db));X("__utmc",""+a.b(O,1),c,b,d);var e=hd(a,!0);e?X("__utmz",e,c,b,d,a.get(eb)):X("__utmz","",c,b,"",-1);(e=fd(a,!1))?X("__utmv",e,c,b,d,a.get(cb)):X("__utmv","",c,b,"",-1);if(1==a.get(v)&&(e=a.get(w))){var f=a.get(x);b=a.c(bb,"");c=a.c(P,"/");d=a.c(Wa,"");var Be=a.b(y,
|
|
||||||
0);a=Math.min(a.b(cb,7776E6),a.b(eb,7776E6),7776E6);a=Math.min(a,1E3*Be+a-(new Date).getTime());if(!f||"aw.ds"==f)if(f=["1",Be+"",q(e)].join("."),0<a&&(e="_gac_"+q(d),!(p(d)||u.test(J.location.hostname)||"/"==c&&r.test(b))&&((d=f)&&1200<d.length&&(d=d.substring(0,1200)),c=e+"="+d+"; path="+c+"; ",a&&(c+="expires="+(new Date((new Date).getTime()+a)).toGMTString()+"; "),b&&"none"!==b&&(c+="domain="+b+";"),b=J.cookie,J.cookie=c,b==J.cookie)))for(b=[],c=J.cookie.split(";"),a=new RegExp("^\\s*"+e+"=\\s*(.*?)\\s*$"),
|
|
||||||
d=0;d<c.length;d++)(e=c[d].match(a))&&b.push(e[1])}},Wc=function(a){var b=a.b(O,1);if(!bd(a,$c(b,pd("__utma"))))return a.set(Ub,!0),!1;var c=!ed(a,$c(b,pd("__utmb")));a.set(bc,c);id(a,$c(b,pd("__utmz")));gd(a,$c(b,pd("__utmv")));if(1==a.get(v)){b=a.get(w);var d=a.get(x);if(!b||d&&"aw.ds"!=d){if(J){b=[];d=J.cookie.split(";");for(var e=/^\s*_gac_(UA-\d+-\d+)=\s*(.+?)\s*$/,f=0;f<d.length;f++){var Be=d[f].match(e);Be&&b.push({Oa:Be[1],value:Be[2]})}d={};if(b&&b.length)for(e=0;e<b.length;e++)f=b[e].value.split("."),
|
|
||||||
"1"==f[0]&&3==f.length&&f[1]&&(d[b[e].Oa]||(d[b[e].Oa]=[]),d[b[e].Oa].push({timestamp:f[1],kb:f[2]}));b=d}else b={};(b=b[a.get(Wa)])&&0<b.length&&(b=b[0],a.set(y,b.timestamp),a.set(w,b.kb),a.set(x,void 0))}}Ed=!c;return!0},Gd=function(a){Ed||0<pd("__utmb").length||(X("__utmd","1",a.c(P,"/"),a.c(bb,""),a.c(Wa,""),1E4),0==pd("__utmd").length&&a.stopPropagation())};var h=0,Jd=function(a){void 0==a.get(Q)?Hd(a):a.get(Ub)&&!a.get(Mc)?Hd(a):a.get(bc)&&Id(a)},Kd=function(a){a.get(hc)&&!a.get(ac)&&(Id(a),a.set(fc,a.get($b)))},Hd=function(a){h++;1<h&&H(137);var b=a.get(ab);a.set(Sb,!0);a.set(Q,Ea()^td(a)&2147483647);a.set(Tb,"");a.set(Vb,b);a.set(Wb,b);a.set(Zb,b);a.set($b,1);a.set(ac,!0);a.set(cc,0);a.set(R,10);a.set(dc,b);a.set(Fb,[]);a.set(Ub,!1);a.set(bc,!1)},Id=function(a){h++;1<h&&H(137);a.set(Wb,a.get(Zb));a.set(Zb,a.get(ab));a.Za($b);a.set(ac,!0);a.set(cc,
|
|
||||||
0);a.set(R,10);a.set(dc,a.get(ab));a.set(bc,!1)};var Ld="daum:q eniro:search_word naver:query pchome:q images.google:q google:q yahoo:p yahoo:q msn:q bing:q aol:query aol:q lycos:q lycos:query ask:q cnn:query virgilio:qs baidu:wd baidu:word alice:qs yandex:text najdi:q seznam:q rakuten:qt biglobe:q goo.ne:MT search.smt.docomo:MT onet:qt onet:q kvasir:q terra:query rambler:query conduit:q babylon:q search-results:q avg:q comcast:q incredimail:q startsiden:q go.mail.ru:q centrum.cz:q 360.cn:q sogou:query tut.by:query globo:q ukr:q so.com:q haosou.com:q auone:q".split(" "),
|
|
||||||
Sd=function(a){if(a.get(kb)&&!a.get(Mc)){var b=!F(a.get(ic))||!F(a.get(nc))||!F(a.get(S))||!F(a.get(lc));for(var c={},d=0;d<Md.length;d++){var e=Md[d];c[e]=a.get(e)}(d=a.get(rc))?(H(149),e=new nf,Na(e,d),d=e):d=La(J.location.href,a.get(gb)).R;if("1"!=L(d.get(a.get(ub)))||!b)if(d=Xe(a,d)||Qd(a),d||b||!a.get(ac)||(Pd(a,void 0,"(direct)",void 0,void 0,void 0,"(direct)","(none)",void 0,void 0),d=!0),d&&(a.set(hc,Rd(a,c)),b="(direct)"==a.get(nc)&&"(direct)"==a.get(jc)&&"(none)"==a.get(oc),a.get(hc)||a.get(ac)&&
|
|
||||||
!b))a.set(ec,a.get(ab)),a.set(fc,a.get($b)),a.Za(gc)}},Xe=function(a,b){function c(c,d){d=d||"-";return(c=L(b.get(a.get(c))))&&"-"!=c?I(c):d}var d=L(b.get(a.get(nb)))||"-",e=L(b.get(a.get(qb)))||"-",f=L(b.get(a.get(pb)))||"-",Be=L(b.get("gclsrc"))||"-",k=L(b.get("dclid"))||"-";"-"!=f&&a.set(w,f);"-"!=Be&&a.set(x,Be);var Ja=c(ob,"(not set)"),t=c(rb,"(not set)"),Za=c(sb),Ma=c(tb);if(F(d)&&F(f)&&F(k)&&F(e))return!1;var mb=!F(f)&&!F(Be);mb=F(e)&&(!F(k)||mb);var Xb=F(Za);if(mb||Xb){var Bd=Nd(a);Bd=La(Bd,
|
|
||||||
!0);(Bd=Od(a,Bd))&&!F(Bd[1]&&!Bd[2])&&(mb&&(e=Bd[0]),Xb&&(Za=Bd[1]))}Pd(a,d,e,f,Be,k,Ja,t,Za,Ma);return!0},Qd=function(a){var b=Nd(a),c=La(b,!0);(b=!(void 0!=b&&null!=b&&""!=b&&"0"!=b&&"-"!=b&&0<=b.indexOf("://")))||(b=c&&-1<c.host.indexOf("google")&&c.R.contains("q")&&"cse"==c.path);if(b)return!1;if((b=Od(a,c))&&!b[2])return Pd(a,void 0,b[0],void 0,void 0,void 0,"(organic)","organic",b[1],void 0),!0;if(b||!a.get(ac))return!1;a:{b=a.get(Bb);for(var d=Ka(c.host),e=0;e<b.length;++e)if(-1<d.indexOf(b[e])){a=
|
|
||||||
!1;break a}Pd(a,void 0,d,void 0,void 0,void 0,"(referral)","referral",void 0,"/"+c.path);a=!0}return a},Od=function(a,b){for(var c=a.get(zb),d=0;d<c.length;++d){var e=c[d].split(":");if(-1<b.host.indexOf(e[0].toLowerCase())){var f=b.R.get(e[1]);if(f&&(f=K(f),!f&&-1<b.host.indexOf("google.")&&(f="(not provided)"),!e[3]||-1<b.url.indexOf(e[3]))){f||H(151);a:{b=f;a=a.get(Ab);b=I(b).toLowerCase();for(c=0;c<a.length;++c)if(b==a[c]){a=!0;break a}a=!1}return[e[2]||e[0],f,a]}}}return null},Pd=function(a,
|
|
||||||
b,c,d,e,f,Be,k,Ja,t){a.set(ic,b);a.set(nc,c);a.set(S,d);a.set(kc,e);a.set(lc,f);a.set(jc,Be);a.set(oc,k);a.set(pc,Ja);a.set(qc,t)},Md=[jc,ic,S,lc,nc,oc,pc,qc],Rd=function(a,b){function c(a){a=(""+a).split("+").join("%20");return a=a.split(" ").join("%20")}function d(c){var d=""+(a.get(c)||"");c=""+(b[c]||"");return 0<d.length&&d==c}if(d(S)||d(lc))return H(131),!1;for(var e=0;e<Md.length;e++){var f=Md[e],Be=b[f]||"-";f=a.get(f)||"-";if(c(Be)!=c(f))return!0}return!1},Td=new RegExp(/^https?:\/\/(www\.)?google(\.com?)?(\.[a-z]{2}t?)?\/?$/i),
|
|
||||||
jf=/^https?:\/\/(r\.)?search\.yahoo\.com?(\.jp)?\/?[^?]*$/i,rf=/^https?:\/\/(www\.)?bing\.com\/?$/i,Nd=function(a){a=Pa(a.get(Jb),a.get(P));try{if(Td.test(a))return H(136),a+"?q=";if(jf.test(a))return H(150),a+"?p=(not provided)";if(rf.test(a))return a+"?q=(not provided)"}catch(b){H(145)}return a};var Ud,Vd,Wd=function(a){Ud=a.c(S,"");Vd=a.c(kc,"")},Xd=function(a){var b=a.c(S,""),c=a.c(kc,"");b!=Ud&&(-1<c.indexOf("ds")?a.set(mc,void 0):!F(Ud)&&-1<Vd.indexOf("ds")&&a.set(mc,Ud))};var Zd=function(a){Yd(a,J.location.href)?(a.set(Mc,!0),H(12)):a.set(Mc,!1)},Yd=function(a,b){if(!a.get(fb))return!1;var c=La(b,a.get(gb));b=K(c.R.get("__utma"));var d=K(c.R.get("__utmb")),e=K(c.R.get("__utmc")),f=K(c.R.get("__utmx")),Be=K(c.R.get("__utmz")),k=K(c.R.get("__utmv"));c=K(c.R.get("__utmk"));if(Yc(""+b+d+e+f+Be+k)!=c){b=I(b);d=I(d);e=I(e);f=I(f);e=$d(b+d+e+f,Be,k,c);if(!e)return!1;Be=e[0];k=e[1]}if(!bd(a,b,!0))return!1;ed(a,d,!0);id(a,Be,!0);gd(a,k,!0);ae(a,f,!0);return!0},ce=function(a,
|
|
||||||
b,c){var d=cd(a)||"-";var e=dd(a)||"-",f=""+a.b(O,1)||"-",Be=be(a)||"-",k=hd(a,!1)||"-";a=fd(a,!1)||"-";var Ja=Yc(""+d+e+f+Be+k+a),t=[];t.push("__utma="+d);t.push("__utmb="+e);t.push("__utmc="+f);t.push("__utmx="+Be);t.push("__utmz="+k);t.push("__utmv="+a);t.push("__utmk="+Ja);d=t.join("&");if(!d)return b;e=b.indexOf("#");if(c)return 0>e?b+"#"+d:b+"&"+d;c="";0<e&&(c=b.substring(e),b=b.substring(0,e));return 0>b.indexOf("?")?b+"?"+d+c:b+"&"+d+c},$d=function(a,b,c,d){for(var e=0;3>e;e++){for(var f=
|
|
||||||
0;3>f;f++){if(d==Yc(a+b+c))return H(127),[b,c];var Be=b.replace(/ /g,"%20"),k=c.replace(/ /g,"%20");if(d==Yc(a+Be+k))return H(128),[Be,k];Be=Be.replace(/\+/g,"%20");k=k.replace(/\+/g,"%20");if(d==Yc(a+Be+k))return H(129),[Be,k];try{var Ja=b.match("utmctr=(.*?)(?:\\|utm|$)");if(Ja&&2==Ja.length&&(Be=b.replace(Ja[1],G(I(Ja[1]))),d==Yc(a+Be+c)))return H(139),[Be,c]}catch(t){}b=I(b)}c=I(c)}};var de="|",fe=function(a,b,c,d,e,f,Be,k,Ja){var t=ee(a,b);t||(t={},a.get(Cb).push(t));t.id_=b;t.affiliation_=c;t.total_=d;t.tax_=e;t.shipping_=f;t.city_=Be;t.state_=k;t.country_=Ja;t.items_=t.items_||[];return t},ge=function(a,b,c,d,e,f,Be){a=ee(a,b)||fe(a,b,"",0,0,0,"","","");a:{if(a&&a.items_){var k=a.items_;for(var Ja=0;Ja<k.length;Ja++)if(k[Ja].sku_==c){k=k[Ja];break a}}k=null}Ja=k||{};Ja.transId_=b;Ja.sku_=c;Ja.name_=d;Ja.category_=e;Ja.price_=f;Ja.quantity_=Be;k||a.items_.push(Ja);return Ja},
|
|
||||||
ee=function(a,b){a=a.get(Cb);for(var c=0;c<a.length;c++)if(a[c].id_==b)return a[c];return null};var he,ie=function(a){if(!he){var b=J.location.hash;var c=W.name,d=/^#?gaso=([^&]*)/;if(c=(b=(b=b&&b.match(d)||c&&c.match(d))?b[1]:K(pd("GASO")))&&b.match(/^(?:!([-0-9a-z.]{1,40})!)?([-.\w]{10,1200})$/i))Fd(a,"GASO",""+b,0),M._gasoDomain=a.get(bb),M._gasoCPath=a.get(P),a=c[1],Ia("https://www.google.com/analytics/web/inpage/pub/inpage.js?"+(a?"prefix="+a+"&":"")+Ea(),"_gasojs");he=!0}};var ae=function(a,b,c){c&&(b=I(b));c=a.b(O,1);b=b.split(".");2>b.length||!/^\d+$/.test(b[0])||(b[0]=""+c,Fd(a,"__utmx",b.join("."),void 0))},be=function(a,b){a=$c(a.get(O),pd("__utmx"));"-"==a&&(a="");return b?G(a):a},Ye=function(a){try{var b=La(J.location.href,!1),c=decodeURIComponent(L(b.R.get("utm_referrer")))||"";c&&a.set(Jb,c);var d=decodeURIComponent(K(b.R.get("utm_expid")))||"";d&&(d=d.split(".")[0],a.set(Oc,""+d))}catch(e){H(146)}},l=function(a){var b=W.gaData&&W.gaData.expId;b&&a.set(Oc,
|
|
||||||
""+b)};var ke=function(a,b){var c=Math.min(a.b(Dc,0),100);if(a.b(Q,0)%100>=c)return!1;c=Ze()||$e();if(void 0==c)return!1;var d=c[0];if(void 0==d||Infinity==d||isNaN(d))return!1;0<d?af(c)?b(je(c)):b(je(c.slice(0,1))):Ga(W,"load",function(){ke(a,b)},!1);return!0},me=function(a,b,c,d){var e=new yd;e.f(14,90,b.substring(0,500));e.f(14,91,a.substring(0,150));e.f(14,92,""+le(c));void 0!=d&&e.f(14,93,d.substring(0,500));e.o(14,90,c);return e},af=function(a){for(var b=1;b<a.length;b++)if(isNaN(a[b])||Infinity==
|
|
||||||
a[b]||0>a[b])return!1;return!0},le=function(a){return isNaN(a)||0>a?0:5E3>a?10*Math.floor(a/10):5E4>a?100*Math.floor(a/100):41E5>a?1E3*Math.floor(a/1E3):41E5},je=function(a){for(var b=new yd,c=0;c<a.length;c++)b.f(14,c+1,""+le(a[c])),b.o(14,c+1,a[c]);return b},Ze=function(){var a=W.performance||W.webkitPerformance;if(a=a&&a.timing){var b=a.navigationStart;if(0==b)H(133);else return[a.loadEventStart-b,a.domainLookupEnd-a.domainLookupStart,a.connectEnd-a.connectStart,a.responseStart-a.requestStart,
|
|
||||||
a.responseEnd-a.responseStart,a.fetchStart-b,a.domInteractive-b,a.domContentLoadedEventStart-b]}},$e=function(){if(W.top==W){var a=W.external,b=a&&a.onloadT;a&&!a.isValidLoadTime&&(b=void 0);2147483648<b&&(b=void 0);0<b&&a.setPageReadyTime();if(void 0!=b)return[b]}};var cf=function(a){if(a.get(Sb))try{a:{var b=pd(a.get(Oe)||"_ga");if(b&&!(1>b.length)){for(var c=[],d=0;d<b.length;d++){var e=b[d].split("."),f=e.shift();if(("GA1"==f||"1"==f)&&1<e.length){var Be=e.shift().split("-");1==Be.length&&(Be[1]="1");Be[0]*=1;Be[1]*=1;var k={Ya:Be,$a:e.join(".")}}else k=void 0;k&&c.push(k)}if(1==c.length){var Ja=c[0].$a;break a}if(0!=c.length){var t=a.get(Pe)||a.get(bb);c=bf(c,(0==t.indexOf(".")?t.substr(1):t).split(".").length,0);if(1==c.length){Ja=c[0].$a;break a}var Za=
|
|
||||||
a.get(Qe)||a.get(P);(b=Za)?(1<b.length&&"/"==b.charAt(b.length-1)&&(b=b.substr(0,b.length-1)),0!=b.indexOf("/")&&(b="/"+b),Za=b):Za="/";c=bf(c,"/"==Za?1:Za.split("/").length,1);Ja=c[0].$a;break a}}Ja=void 0}if(Ja){var Ma=(""+Ja).split(".");2==Ma.length&&/[0-9.]/.test(Ma)&&(H(114),a.set(Q,Ma[0]),a.set(Vb,Ma[1]),a.set(Sb,!1))}}catch(mb){H(115)}},bf=function(a,b,c){for(var d=[],e=[],f=128,Be=0;Be<a.length;Be++){var k=a[Be];k.Ya[c]==b?d.push(k):k.Ya[c]==f?e.push(k):k.Ya[c]<f&&(e=[k],f=k.Ya[c])}return 0<
|
|
||||||
d.length?d:e};var kf=/^gtm\d+$/,hf=function(a){var b=!!a.b(Cd,1);if(b)if(H(140),"page"!=a.get(sc))a.set(Kc,"",!0);else if(b=a.c(Lc,""),b||(b=(b=a.c($a,""))&&"~0"!=b?kf.test(b)?"__utmt_"+G(a.c(Wa,"")):"__utmt_"+G(b):"__utmt"),0<pd(b).length)a.set(Kc,"",!0);else if(X(b,"1",a.c(P,"/"),a.c(bb,""),a.c(Wa,""),6E5),0<pd(b).length){a.set(Kc,Ea(),!0);a.set(Yb,1,!0);if(void 0!==W.__ga4__)b=W.__ga4__;else{if(void 0===A){var c=W.navigator.userAgent;if(c){b=c;try{b=decodeURIComponent(c)}catch(d){}if(c=!(0<=b.indexOf("Chrome"))&&
|
|
||||||
!(0<=b.indexOf("CriOS"))&&(0<=b.indexOf("Safari/")||0<=b.indexOf("Safari,")))b=B.exec(b),c=11<=(b?Number(b[1]):-1);A=c}else A=!1}b=A}b?(a.set(z,C(a),!0),a.set(Jc,"https://ssl.google-analytics.com/j/__utm.gif",!0)):a.set(Jc,Ne()+"/r/__utm.gif?",!0)}},C=function(a){a=aa(a);return{gb:"t=dc&_r=3&"+a,google:"t=sr&slf_rd=1&_r=4&"+a,count:0}},aa=function(a){function b(a,b){c.push(a+"="+G(b))}var c=[];b("v","1");b("_v","5.7.2");b("tid",a.get(Wa));b("cid",a.get(Q)+"."+a.get(Vb));b("jid",a.get(Kc));b("aip",
|
|
||||||
"1");return c.join("&")+"&z="+Ea()};var U=function(a,b,c){function d(a){return function(b){if((b=b.get(Nc)[a])&&b.length)for(var c=Te(e,a),d=0;d<b.length;d++)b[d].call(e,c)}}var e=this;this.a=new Zc;this.get=function(a){return this.a.get(a)};this.set=function(a,b,c){this.a.set(a,b,c)};this.set(Wa,b||"UA-XXXXX-X");this.set($a,a||"");this.set(Ya,c||"");this.set(ab,Math.round((new Date).getTime()/1E3));this.set(P,"/");this.set(cb,63072E6);this.set(eb,15768E6);this.set(db,18E5);this.set(fb,!1);this.set(yb,50);this.set(gb,!1);this.set(hb,
|
|
||||||
!0);this.set(ib,!0);this.set(jb,!0);this.set(kb,!0);this.set(lb,!0);this.set(ob,"utm_campaign");this.set(nb,"utm_id");this.set(pb,"gclid");this.set(qb,"utm_source");this.set(rb,"utm_medium");this.set(sb,"utm_term");this.set(tb,"utm_content");this.set(ub,"utm_nooverride");this.set(vb,100);this.set(Dc,1);this.set(Ec,!1);this.set(wb,"/__utm.gif");this.set(xb,1);this.set(Cb,[]);this.set(Fb,[]);this.set(zb,Ld.slice(0));this.set(Ab,[]);this.set(Bb,[]);this.B("auto");this.set(Jb,J.referrer);this.set(v,!0);
|
|
||||||
this.set(y,Math.round((new Date).getTime()/1E3));Ye(this.a);this.set(Nc,{hit:[],load:[]});this.a.g("0",Zd);this.a.g("1",Wd);this.a.g("2",Jd);this.a.g("3",cf);this.a.g("4",Sd);this.a.g("5",Xd);this.a.g("6",Kd);this.a.g("7",d("load"));this.a.g("8",ie);this.a.v("A",kd);this.a.v("B",md);this.a.v("C",Ge);this.a.v("D",Jd);this.a.v("E",jd);this.a.v("F",Tc);this.a.v("G",ne);this.a.v("H",lf);this.a.v("I",Gd);this.a.v("J",nd);this.a.v("K",ud);this.a.v("L",Dd);this.a.v("M",l);this.a.v("N",hf);this.a.v("O",d("hit"));
|
|
||||||
this.a.v("P",oe);this.a.v("Q",pe);0===this.get(ab)&&H(111);this.a.T();this.H=void 0};E=U.prototype;E.m=function(){var a=this.get(Db);a||(a=new yd,this.set(Db,a));return a};E.La=function(a){for(var b in a){var c=a[b];a.hasOwnProperty(b)&&this.set(b,c,!0)}};E.K=function(a){if(this.get(Ec))return!1;var b=this,c=ke(this.a,function(c){b.set(Hb,a,!0);b.ib(c)});this.set(Ec,c);return c};
|
|
||||||
E.Fa=function(a){a&&Ca(a)?(H(13),this.set(Hb,a,!0)):"object"===typeof a&&null!==a&&this.La(a);this.H=a=this.get(Hb);this.a.j("page");this.K(a)};E.F=function(a,b,c,d,e){if(""==a||!wd(a)||""==b||!wd(b)||void 0!=c&&!wd(c)||void 0!=d&&!xd(d))return!1;this.set(wc,a,!0);this.set(xc,b,!0);this.set(yc,c,!0);this.set(zc,d,!0);this.set(vc,!!e,!0);this.a.j("event");return!0};
|
|
||||||
E.Ha=function(a,b,c,d,e){var f=this.a.b(Dc,0);1*e===e&&(f=e);if(this.a.b(Q,0)%100>=f)return!1;c=1*(""+c);if(""==a||!wd(a)||""==b||!wd(b)||!xd(c)||isNaN(c)||0>c||0>f||100<f||void 0!=d&&(""==d||!wd(d)))return!1;this.ib(me(a,b,c,d));return!0};E.Ga=function(a,b,c,d){if(!a||!b)return!1;this.set(Ac,a,!0);this.set(Bc,b,!0);this.set(Cc,c||J.location.href,!0);d&&this.set(Hb,d,!0);this.a.j("social");return!0};E.Ea=function(){this.set(Dc,10);this.K(this.H)};E.Ia=function(){this.a.j("trans")};
|
|
||||||
E.ib=function(a){this.set(Eb,a,!0);this.a.j("event")};E.ia=function(a){this.initData();var b=this;return{_trackEvent:function(c,d,e){H(91);b.F(a,c,d,e)}}};E.ma=function(a){return this.get(a)};E.xa=function(a,b){if(a)if(Ca(a))this.set(a,b);else if("object"==typeof a)for(var c in a)a.hasOwnProperty(c)&&this.set(c,a[c])};E.addEventListener=function(a,b){(a=this.get(Nc)[a])&&a.push(b)};E.removeEventListener=function(a,b){a=this.get(Nc)[a];for(var c=0;a&&c<a.length;c++)if(a[c]==b){a.splice(c,1);break}};
|
|
||||||
E.qa=function(){return"5.7.2"};E.B=function(a){this.get(hb);a="auto"==a?Ka(J.domain):a&&"-"!=a&&"none"!=a?a.toLowerCase():"";this.set(bb,a)};E.va=function(a){this.set(hb,!!a)};E.na=function(a,b){return ce(this.a,a,b)};E.link=function(a,b){this.a.get(fb)&&a&&(J.location.href=ce(this.a,a,b))};E.ua=function(a,b){this.a.get(fb)&&a&&a.action&&(a.action=ce(this.a,a.action,b))};
|
|
||||||
E.za=function(){this.initData();var a=this.a,b=J.getElementById?J.getElementById("utmtrans"):J.utmform&&J.utmform.utmtrans?J.utmform.utmtrans:null;if(b&&b.value){a.set(Cb,[]);b=b.value.split("UTM:");for(var c=0;c<b.length;c++){b[c]=Da(b[c]);for(var d=b[c].split(de),e=0;e<d.length;e++)d[e]=Da(d[e]);"T"==d[0]?fe(a,d[1],d[2],d[3],d[4],d[5],d[6],d[7],d[8]):"I"==d[0]&&ge(a,d[1],d[2],d[3],d[4],d[5],d[6])}}};E.$=function(a,b,c,d,e,f,Be,k){return fe(this.a,a,b,c,d,e,f,Be,k)};
|
|
||||||
E.Y=function(a,b,c,d,e,f){return ge(this.a,a,b,c,d,e,f)};E.Aa=function(a){de=a||"|"};E.ea=function(){this.set(Cb,[])};E.wa=function(a,b,c,d){var e=this.a;if(0>=a||a>e.get(yb))a=!1;else if(!b||!c||128<b.length+c.length)a=!1;else{1!=d&&2!=d&&(d=3);var f={};f.name=b;f.value=c;f.scope=d;e.get(Fb)[a]=f;a=!0}a&&this.a.store();return a};E.ka=function(a){this.a.get(Fb)[a]=void 0;this.a.store()};E.ra=function(a){return(a=this.a.get(Fb)[a])&&1==a.scope?a.value:void 0};
|
|
||||||
E.Ca=function(a,b,c){12==a&&1==b?this.set(pf,c):this.m().f(a,b,c)};E.Da=function(a,b,c){this.m().o(a,b,c)};E.sa=function(a,b){return this.m().getKey(a,b)};E.ta=function(a,b){return this.m().N(a,b)};E.fa=function(a){this.m().L(a)};E.ga=function(a){this.m().M(a)};E.ja=function(){return new yd};E.W=function(a){a&&this.get(Ab).push(a.toLowerCase())};E.ba=function(){this.set(Ab,[])};E.X=function(a){a&&this.get(Bb).push(a.toLowerCase())};E.ca=function(){this.set(Bb,[])};
|
|
||||||
E.Z=function(a,b,c,d,e){if(a&&b){a=[a,b.toLowerCase()].join(":");if(d||e)a=[a,d,e].join(":");d=this.get(zb);d.splice(c?0:d.length,0,a)}};E.da=function(){this.set(zb,[])};E.ha=function(a){this.a.load();var b=this.get(P),c=be(this.a);this.set(P,a);this.a.store();ae(this.a,c);this.set(P,b)};E.ya=function(a,b){if(0<a&&5>=a&&Ca(b)&&""!=b){var c=this.get(Fc)||[];c[a]=b;this.set(Fc,c)}};E.V=function(a){a=""+a;if(a.match(/^[A-Za-z0-9]{1,5}$/)){var b=this.get(Ic)||[];b.push(a);this.set(Ic,b)}};
|
|
||||||
E.initData=function(){this.a.load()};E.Ba=function(a){a&&""!=a&&(this.set(Tb,a),this.a.j("var"))};var ne=function(a){"trans"!==a.get(sc)&&500<=a.b(cc,0)&&a.stopPropagation();if("event"===a.get(sc)){var b=(new Date).getTime(),c=a.b(dc,0),d=a.b(Zb,0);c=Math.floor((b-(c!=d?c:1E3*c))/1E3);0<c&&(a.set(dc,b),a.set(R,Math.min(10,a.b(R,0)+c)));0>=a.b(R,0)&&a.stopPropagation()}},pe=function(a){"event"===a.get(sc)&&a.set(R,Math.max(0,a.b(R,10)-1))};var qe=function(){var a=[];this.add=function(b,c,d){d&&(c=G(""+c));a.push(b+"="+c)};this.toString=function(){return a.join("&")}},re=function(a,b){(b||2!=a.get(xb))&&a.Za(cc)},se=function(a,b){b.add("utmwv","5.7.2");b.add("utms",a.get(cc));b.add("utmn",Ea());var c=J.location.hostname;F(c)||b.add("utmhn",c,!0);a=a.get(vb);100!=a&&b.add("utmsp",a,!0)},te=function(a,b){b.add("utmht",(new Date).getTime());b.add("utmac",Da(a.get(Wa)));a.get(Oc)&&b.add("utmxkey",a.get(Oc),!0);a.get(vc)&&b.add("utmni",1);
|
|
||||||
a.get(of)&&b.add("utmgtm",a.get(of),!0);var c=a.get(Ic);c&&0<c.length&&b.add("utmdid",c.join("."));ff(a,b);!1!==a.get(Xa)&&(a.get(Xa)||M.w)&&b.add("aip",1);void 0!==a.get(Kc)&&b.add("utmjid",a.c(Kc,""),!0);a.b(Yb,0)&&b.add("utmredir",a.b(Yb,0),!0);M.bb||(M.bb=a.get(Wa));(1<M.ab()||M.bb!=a.get(Wa))&&b.add("utmmt",1);b.add("utmu",od.encode())},ue=function(a,b){a=a.get(Fc)||[];for(var c=[],d=1;d<a.length;d++)a[d]&&c.push(d+":"+G(a[d].replace(/%/g,"%25").replace(/:/g,"%3A").replace(/,/g,"%2C")));c.length&&
|
|
||||||
b.add("utmpg",c.join(","))},ff=function(a,b){function c(a,b){b&&d.push(a+"="+b+";")}var d=[];c("__utma",cd(a));c("__utmz",hd(a,!1));c("__utmv",fd(a,!0));c("__utmx",be(a));b.add("utmcc",d.join("+"),!0)},ve=function(a,b){a.get(ib)&&(b.add("utmcs",a.get(Qb),!0),b.add("utmsr",a.get(Lb)),a.get(Rb)&&b.add("utmvp",a.get(Rb)),b.add("utmsc",a.get(Mb)),b.add("utmul",a.get(Pb)),b.add("utmje",a.get(Nb)),b.add("utmfl",a.get(Ob),!0))},we=function(a,b){a.get(lb)&&a.get(Ib)&&b.add("utmdt",a.get(Ib),!0);b.add("utmhid",
|
|
||||||
a.get(Kb));b.add("utmr",Pa(a.get(Jb),a.get(P)),!0);b.add("utmp",G(a.get(Hb),!0),!0)},xe=function(a,b){for(var c=a.get(Db),d=a.get(Eb),e=a.get(Fb)||[],f=0;f<e.length;f++){var Be=e[f];Be&&(c||(c=new yd),c.f(8,f,Be.name),c.f(9,f,Be.value),3!=Be.scope&&c.f(11,f,""+Be.scope))}F(a.get(wc))||F(a.get(xc),!0)||(c||(c=new yd),c.f(5,1,a.get(wc)),c.f(5,2,a.get(xc)),e=a.get(yc),void 0!=e&&c.f(5,3,e),e=a.get(zc),void 0!=e&&c.o(5,1,e));F(a.get(pf))||(c||(c=new yd),c.f(12,1,a.get(pf)));c?b.add("utme",c.Qa(d),!0):
|
|
||||||
d&&b.add("utme",d.A(),!0)},ye=function(a,b,c){var d=new qe;re(a,c);se(a,d);d.add("utmt","tran");d.add("utmtid",b.id_,!0);d.add("utmtst",b.affiliation_,!0);d.add("utmtto",b.total_,!0);d.add("utmttx",b.tax_,!0);d.add("utmtsp",b.shipping_,!0);d.add("utmtci",b.city_,!0);d.add("utmtrg",b.state_,!0);d.add("utmtco",b.country_,!0);xe(a,d);ve(a,d);we(a,d);(b=a.get(Gb))&&d.add("utmcu",b,!0);c||(ue(a,d),te(a,d));return d.toString()},ze=function(a,b,c){var d=new qe;re(a,c);se(a,d);d.add("utmt","item");d.add("utmtid",
|
|
||||||
b.transId_,!0);d.add("utmipc",b.sku_,!0);d.add("utmipn",b.name_,!0);d.add("utmiva",b.category_,!0);d.add("utmipr",b.price_,!0);d.add("utmiqt",b.quantity_,!0);xe(a,d);ve(a,d);we(a,d);(b=a.get(Gb))&&d.add("utmcu",b,!0);c||(ue(a,d),te(a,d));return d.toString()},Ae=function(a,b){var c=a.get(sc);if("page"==c)c=new qe,re(a,b),se(a,c),xe(a,c),ve(a,c),we(a,c),b||(ue(a,c),te(a,c)),a=[c.toString()];else if("event"==c)c=new qe,re(a,b),se(a,c),c.add("utmt","event"),xe(a,c),ve(a,c),we(a,c),b||(ue(a,c),te(a,c)),
|
|
||||||
a=[c.toString()];else if("var"==c)c=new qe,re(a,b),se(a,c),c.add("utmt","var"),!b&&te(a,c),a=[c.toString()];else if("trans"==c){c=[];for(var d=a.get(Cb),e=0;e<d.length;++e){c.push(ye(a,d[e],b));for(var f=d[e].items_,Be=0;Be<f.length;++Be)c.push(ze(a,f[Be],b))}a=c}else"social"==c?b?a=[]:(c=new qe,re(a,b),se(a,c),c.add("utmt","social"),c.add("utmsn",a.get(Ac),!0),c.add("utmsa",a.get(Bc),!0),c.add("utmsid",a.get(Cc),!0),xe(a,c),ve(a,c),we(a,c),ue(a,c),te(a,c),a=[c.toString()]):"feedback"==c?b?a=[]:(c=
|
|
||||||
new qe,re(a,b),se(a,c),c.add("utmt","feedback"),c.add("utmfbid",a.get(Gc),!0),c.add("utmfbpr",a.get(Hc),!0),xe(a,c),ve(a,c),we(a,c),ue(a,c),te(a,c),a=[c.toString()]):a=[];return a},oe=function(a){var b=a.get(xb),c=a.get(uc),d=c&&c.Ua,e=0,f=a.get(z);if(0==b||2==b){var Be=a.get(wb)+"?";var k=Ae(a,!0);for(var Ja=0,t=k.length;Ja<t;Ja++)Sa(k[Ja],d,Be,!0),e++}if(1==b||2==b)for(k=Ae(a),a=a.c(Jc,""),Ja=0,t=k.length;Ja<t;Ja++)try{if(f){var Za=k[Ja];b=(b=d)||Fa;df("",b,a+"?"+Za,f)}else Sa(k[Ja],d,a);e++}catch(Ma){Ma&&
|
|
||||||
Ra(Ma.name,void 0,Ma.message)}c&&(c.fb=e)};var Ne=function(){return"https:"==J.location.protocol||M.G?"https://ssl.google-analytics.com":"http://www.google-analytics.com"},Ce=function(a){this.name="len";this.message=a+"-8192"},De=function(a){this.name="ff2post";this.message=a+"-2036"},Sa=function(a,b,c,d){b=b||Fa;if(d||2036>=a.length)gf(a,b,c);else if(8192>=a.length){if(0<=W.navigator.userAgent.indexOf("Firefox")&&![].reduce)throw new De(a.length);df(a,b)||ef(a,b)||Ee(a,b)||b()}else throw new Ce(a.length);},gf=function(a,b,c){c=c||Ne()+"/__utm.gif?";
|
|
||||||
var d=new Image(1,1);d.src=c+a;d.onload=function(){d.onload=null;d.onerror=null;b()};d.onerror=function(){d.onload=null;d.onerror=null;b()}},ef=function(a,b){if(0!=Ne().indexOf(J.location.protocol))return!1;var c=W.XDomainRequest;if(!c)return!1;c=new c;c.open("POST",Ne()+"/p/__utm.gif");c.onerror=function(){b()};c.onload=b;c.send(a);return!0},df=function(a,b,c,d){var e=W.XMLHttpRequest;if(!e)return!1;var f=new e;if(!("withCredentials"in f))return!1;f.open("POST",c||Ne()+"/p/__utm.gif",!0);f.withCredentials=
|
|
||||||
!0;f.setRequestHeader("Content-Type","text/plain");f.onreadystatechange=function(){if(4==f.readyState){if(d)try{var a=f.responseText;if(1>a.length||"1"!=a.charAt(0))Ra("xhr","ver",a),b();else if(3<d.count++)Ra("xhr","tmr",""+d.count),b();else if(1==a.length)b();else{var c=a.charAt(1);if("d"==c){var e=d.gb;a=(a=b)||Fa;df("",a,"https://stats.g.doubleclick.net/j/collect?"+e,d)}else if("g"==c){var t="https://www.google.%/ads/ga-audiences?".replace("%","com");gf(d.google,b,t);var Za=a.substring(2);if(Za)if(/^[a-z.]{1,6}$/.test(Za)){var Ma=
|
|
||||||
"https://www.google.%/ads/ga-audiences?".replace("%",Za);gf(d.google,Fa,Ma)}else Ra("tld","bcc",Za)}else Ra("xhr","brc",c),b()}}catch(mb){b()}else b();f=null}};f.send(a);return!0},Ee=function(a,b){if(!J.body)return We(function(){Ee(a,b)},100),!0;a=encodeURIComponent(a);try{var c=J.createElement('<iframe name="'+a+'"></iframe>')}catch(e){c=J.createElement("iframe"),c.name=a}c.height="0";c.width="0";c.style.display="none";c.style.visibility="hidden";var d=Ne()+"/u/post_iframe.html";Ga(W,"beforeunload",
|
|
||||||
function(){c.src="";c.parentNode&&c.parentNode.removeChild(c)});setTimeout(b,1E3);J.body.appendChild(c);c.src=d;return!0};var qf=function(){this.G=this.w=!1;0==Ea()%1E4&&(H(142),this.G=!0);this.C={};this.D=[];this.U=0;this.S=[["www.google-analytics.com","","/plugins/"]];this._gasoCPath=this._gasoDomain=this.bb=void 0;Re();Se()};E=qf.prototype;E.oa=function(a,b){return this.hb(a,void 0,b)};E.hb=function(a,b,c){b&&H(23);c&&H(67);void 0==b&&(b="~"+M.U++);a=new U(b,a,c);M.C[b]=a;M.D.push(a);return a};E.u=function(a){a=a||"";return M.C[a]||M.hb(void 0,a)};E.pa=function(){return M.D.slice(0)};E.ab=function(){return M.D.length};
|
|
||||||
E.aa=function(){this.w=!0};E.la=function(){this.G=!0};var Fe=function(a){if("prerender"==J.visibilityState)return!1;a();return!0};var M=new qf;var D=W._gat;D&&Ba(D._getTracker)?M=D:W._gat=M;var Z=new Y;(function(a){if(!Fe(a)){H(123);var b=!1,c=function(){if(!b&&Fe(a)){b=!0;var d=J,e=c;d.removeEventListener?d.removeEventListener("visibilitychange",e,!1):d.detachEvent&&d.detachEvent("onvisibilitychange",e)}};Ga(J,"visibilitychange",c)}})(function(){var a=W._gaq,b=!1;if(a&&Ba(a.push)&&(b="[object Array]"==Object.prototype.toString.call(Object(a)),!b)){Z=a;return}W._gaq=Z;b&&Z.push.apply(Z,a)});function Yc(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};}).call(this);
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import VueHighlightJS from 'vue-highlightjs'
|
|
||||||
import VueClipboard from 'vue-clipboard2'
|
|
||||||
import App from './components/App.vue'
|
|
||||||
import * as gaSource from './ga'
|
|
||||||
import '../styles/style.scss'
|
|
||||||
|
|
||||||
// GA settings
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unexpected-multiline
|
|
||||||
(function () {
|
|
||||||
const ga = document.createElement('script')
|
|
||||||
ga.type = 'text/javascript'; ga.async = true
|
|
||||||
ga.src = gaSource
|
|
||||||
const s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s)
|
|
||||||
})()
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
window._gaq = window._gaq || []
|
|
||||||
// eslint-disable-next-line func-call-spacing
|
|
||||||
window._gaq.push(['_setAccount', 'UA-110523681-4'])
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
Vue.use(VueHighlightJS)
|
|
||||||
Vue.use(VueClipboard)
|
|
||||||
|
|
||||||
Vue.prototype.$chrome = chrome
|
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
|
||||||
new Vue({
|
|
||||||
el: '#root',
|
|
||||||
render: h => h(App)
|
|
||||||
})
|
|
||||||
4
src/popup/main.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
createApp(App).mount("#app");
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset='utf-8'>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id='root'></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.text-muted {
|
|
||||||
color: $gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.w-100 {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||