klee
Version:
Record browser interactions for testing.
180 lines (154 loc) • 5.1 kB
JavaScript
const puppeteer = require('puppeteer')
const {URL} = require('url')
const uuid = require('uuid')
const {STORE_DIR, BROWSER_CONFIG} = require('./constants')
const {log, logError, pretty} = require('./utils')
const {writeFile, makeDirSync} = require('./files')
module.exports = async function (inputUrl) {
if (!inputUrl) {
return log('A URL is required as a param. Run `npm run record [URL]`.')
}
// force https
const protocol = inputUrl.match(/^(http|https):\/\//) || []
if (protocol.length === 0) {
inputUrl = `https://${inputUrl}`
}
let url = {}
try {
url = new URL(inputUrl)
} catch (error) {
throw new Error(`"${inputUrl}" is not a valid URL`)
}
const {width, height, headless} = BROWSER_CONFIG
log(
'\nRecording interactions. Close the Browser or Page to stop recording.\n'
)
const browser = await puppeteer.launch({
handleSIGINT: true,
args: [
`--window-size=${width},${height}`,
'--disable-infobars',
'--start-maximized',
],
headless,
})
const [page] = await browser.pages()
await page.setViewport({width, height})
// State of the current run.
const state = {requests: [], actions: []}
const siteName = url.hostname.replace(/^www/)
// Urls that we want to track.
// The will be added to an array in order of execution.
const regexDomainChecker = new RegExp(siteName)
log('- Checking for requests that pass this regex:', regexDomainChecker)
// Webpage loaded types to capture:
// Ex: document, image, xhr, script, stylesheet...
const shouldCapture = {
document: true,
}
// register requests.
page.on('request', request => {
const url = request.url()
const resource = request.resourceType()
// Keep in mind possible domain redirections
// How can we identify desired interactions?
if (shouldCapture[resource] && regexDomainChecker.test(url)) {
// const headers = request.headers();
// log(headers);
const {href} = new URL(url)
state.requests.push({url: href})
}
})
// register hash changes (great for SPA)
await page.exposeFunction('onHashChange', url => page.emit('hashchange', url))
page.on('hashchange', url => {
const {href} = new URL(url)
state.requests.push({url: href})
})
// register actions.
await page.exposeFunction('onAction', event => page.emit('action', event))
page.on('action', action => {
state.actions.push(action)
process.stdout.clearLine()
process.stdout.cursorTo(0)
process.stdout.write(`- Actions saved: ${state.actions.length}`)
})
await page.evaluateOnNewDocument(`
function handleEvent(event) {
const { x, y, offsetX, offsetY, timeStamp, target } = event;
const { localName } = target;
// Check for other useful event props
// console.log(event);
if (event.key) {
return onAction({ key: event.key, timeStamp, localName });
}
return onAction({ x, y, offsetX, offsetY, timeStamp, localName });
}
window.addEventListener('hashchange', event => onHashChange(location.href));
document.onmousedown = handleEvent;
document.onkeydown = handleEvent;
`)
log('- Going to:', url.href)
page.goto(url.href)
await page.waitForNavigation({waitUntil: 'load'})
let [actionsSaved, requestsSaved] = [false, false]
function handleDisconnection(resolve, reject) {
// Check page disconnected to stop recordings.
if (actionsSaved || requestsSaved) {
return resolve()
}
log('- Saving recorded STATE...')
const [dirname] = uuid().split('-')
const [domainName] = siteName.split('.')
const interactionDir = `${STORE_DIR}/${domainName}-${dirname}`
const error = makeDirSync(interactionDir)
if (error) {
reject(error)
}
// use cloud solution
let actionPromise = Promise.resolve()
if (state.actions.length > 0) {
const actionsOutput = {
browserConfig: BROWSER_CONFIG,
actions: state.actions,
}
actionPromise = writeFile(
`${interactionDir}/actions.json`,
pretty(actionsOutput)
)
actionsSaved = true
}
// use cloud solution
let requestsPromise = Promise.resolve()
if (state.requests.length > 0) {
requestsPromise = writeFile(
`${interactionDir}/requests.json`,
pretty(state.requests)
)
requestsSaved = true
}
log('- Execution saved:', interactionDir)
log(`- Execution code: [${dirname}]`)
return Promise.all([requestsPromise, actionPromise])
.then(resolve)
.catch(reject)
}
try {
// wait for only the first disconnected/close event.
await Promise.race([
new Promise((resolve, reject) =>
page.on('close', () => handleDisconnection(resolve, reject))
),
new Promise((resolve, reject) =>
browser.on('disconnected', () => handleDisconnection(resolve, reject))
),
])
} catch (error) {
logError('Error while saving recording:', error)
} finally {
if (browser) {
await browser.close()
}
}
return log('- Done.')
}