feat: create basic scaffolding for vue 3 chrome app 🚀

This commit is contained in:
Ignacio Anaya
2021-06-10 10:42:03 -03:00
parent bc31cdbea2
commit 5215e22237
103 changed files with 8180 additions and 11236 deletions

View File

@@ -1,7 +0,0 @@
{
"presets": [ "es2015", "stage-0"],
"plugins": [
"transform-runtime",
"transform-object-rest-spread"
]
}

3
.browserslistrc Normal file
View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@@ -1,32 +1,15 @@
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
node: true,
"jest/globals": true
webextensions: true
},
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
extends: 'standard',
// required to lint *.vue files
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
extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/prettier"],
parserOptions: {
parser: "babel-eslint"
},
globals: {
chrome: false
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
}
}
};

33
.gitignore vendored
View File

@@ -1,10 +1,29 @@
node_modules
.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
build
dist
dist-zip
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vue Browser Extension Output
*.pem
*.crx
test
.vscode
*.pub
*.zip
/artifacts

3
babel.config.js Normal file
View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,86 +1,27 @@
{
"name": "headless-recorder",
"version": "0.8.2",
"description": "A Chrome extension for recording browser interaction and generating Puppeteer & Playwright scripts",
"main": "index.js",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "NODE_ENV=development DEBUG=puppeteer-recorder:* webpack --watch",
"build": "NODE_ENV=production webpack",
"dist": "NODE_ENV=production webpack && node scripts/zip.js",
"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"
"serve": "vue-cli-service build --mode development --watch",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"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": {
"@medv/finder": "^1.1.2",
"events": "^3.2.0",
"vue": "^2.5.17",
"vue-clipboard2": "^0.2.1",
"vue-highlightjs": "^1.3.3"
"core-js": "3.6.5",
"vue": "3.0.0"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.24",
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.6",
"babel-jest": "^23.4.2",
"babel-loader": "^7.1.5",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-0": "^6.24.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"
]
"@vue/cli-plugin-babel": "4.5.0",
"@vue/cli-plugin-eslint": "4.5.0",
"@vue/cli-service": "4.5.0",
"@vue/compiler-sfc": "3.0.0",
"@vue/eslint-config-prettier": "6.0.0",
"babel-eslint": "10.1.0",
"eslint": "6.7.2",
"eslint-plugin-prettier": "3.1.3",
"eslint-plugin-vue": "7.0.0-0",
"prettier": "1.19.1",
"vue-cli-plugin-browser-extension": "0.25.1"
}
}

View File

@@ -0,0 +1,6 @@
{
"extName": {
"message": "headless-recorder-v2",
"description": ""
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public copy/icons/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

BIN
public copy/icons/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public copy/icons/19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public copy/icons/38.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public copy/icons/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

17
public copy/index.html Normal file
View 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>

View File

@@ -0,0 +1,6 @@
{
"extName": {
"message": "headless-recorder-v2",
"description": ""
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icons/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

BIN
public/icons/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/icons/19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/icons/38.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public/icons/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

17
public/index.html Normal file
View 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>

View File

@@ -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()

View File

@@ -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)
})

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

7
src/background.js Normal file
View File

@@ -0,0 +1,7 @@
browser.runtime.onMessage.addListener(function() {
console.log("Hello from the background");
browser.tabs.executeScript({
file: "content-script.js"
});
});

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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, '\\\'')
}
}

View File

@@ -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}')` })
}
}

View File

@@ -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} })` })
}
}

View File

@@ -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}')`)
})
})

View File

@@ -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\\\\')")
})
})

View File

@@ -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
// ];

View File

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

View 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>

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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()
})

View File

@@ -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')
})
})

View File

@@ -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>

View File

@@ -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>

View File

@@ -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')
})
})

View File

@@ -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()
})
}

View File

@@ -0,0 +1 @@
console.log("Hello from the content-script");

View File

@@ -1,3 +0,0 @@
import EventRecorder from './EventRecorder'
window.eventRecorder = new EventRecorder()
window.eventRecorder.boot()

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 45 KiB

4
src/main.js Normal file
View File

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

View File

@@ -1,8 +1,10 @@
{
"name": "Headless Recorder",
"version": "0.8.2",
"version": "1.0.0",
"manifest_version": 2,
"homepage_url": "http://localhost:8080/",
"description": "A Chrome extension for recording browser interaction and generating Puppeteer & Playwright scripts",
"default_locale": "en",
"permissions": [
"storage",
"webNavigation",
@@ -11,40 +13,27 @@
"*://*/"
],
"icons" : {
"16": "images/app_icon_16.png",
"48": "images/app_icon_48.png",
"128": "images/app_icon_128.png"
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"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": {
"scripts": [
"background.js"
"js/background.js"
],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_title": "__MSG_extName__",
"default_icon": {
"19": "icons/19.png",
"38": "icons/38.png"
}
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
"browser_style": true
}
}

View File

@@ -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'
}

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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()
})
})
})

View File

@@ -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>
`;

View File

@@ -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
View File

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

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()
})
})

View File

@@ -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)
})
})

View File

@@ -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')
})
})

View File

@@ -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>
`;

View File

@@ -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 &gt; 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>
`;

View File

@@ -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>
`;

View File

@@ -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);

View File

@@ -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
View File

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

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
.text-muted {
color: $gray;
}
.text-center {
text-align: center;
}

View File

@@ -1,3 +0,0 @@
.w-100 {
width: 100%;
}

View File

@@ -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;

View File

@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More