UNPKG

klee

Version:

Record browser interactions for testing.

180 lines (154 loc) 5.1 kB
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.') }