polendina
Version:
Non-UI browser testing for JavaScript libraries from the command-line
118 lines (104 loc) • 3.7 kB
JavaScript
import puppeteer from 'puppeteer'
// TODO
// const pti = require('puppeteer-to-istanbul')
// const { copyFile } = require('fs').promises
import { log, error, logRaw, logWrite, flushLogs } from './log.js'
// wrap our convoluted _run() function in a pure Promise that can handle
// both standard throws and the callback that ends it. _run() needs to handle
// ends in a few different ways, hence the hack.
export function run (outputDir, port, timeout, mode, runner, coverage) {
return new Promise((resolve, reject) => {
_run(outputDir, port, timeout, mode, runner, coverage, (err, errors) => {
if (err) {
return reject(err)
}
resolve(errors)
})
})
}
// this can throw, or it can end via `callback()` which may or may not contain
// a runtime-error argument and a "number of errors from tests" argument.
// the callback may be triggered by proper test end or failure, or a timeout.
async function _run (outputDir, port, timeout, mode, runner, coverage, callback) {
let executionQueue = Promise.resolve()
const browser = await puppeteer.launch({ args: ['--no-sandbox'], headless: 'new' })
const [page] = await browser.pages()
// this works as a debounce exit method, when process.stdout.write() is used
// instead of console.log, the output goes through the stream handling which
// inserts delays so log output comes later than an end() signal. If we get
// an end(), we delay for a short time and then keep delaying if we get more
// output—it should come in steady increments after an end() as we're only
// waiting for stream delays, not test delays at that point.
let lastCall
function end (errors) {
lastCall = setTimeout(() => {
if (!executionQueue) {
error('end after end')
return
}
executionQueue.then(async () => {
flushLogs(true)
executionQueue = null
/* TODO
if (coverage) {
const jsCoverage = await page.coverage.stopJSCoverage()
pti.write([...jsCoverage])
await copyFile('build/bundle.js.map', '.nyc_output/js/bundle.js.map')
}
*/
await browser.close()
}).catch(callback)
.then(() => { callback(null, errors) })
}, 100)
}
function maybeEnd () {
if (lastCall) {
clearTimeout(lastCall)
end()
}
}
// this should be a rare, or impossible event since we intercept console.log
/*
page.on('console', (msg) => {
if (!executionQueue) {
error(`log after end: ${msg.text()}`)
return
}
const args = []
executionQueue = executionQueue.then(async () => {
logRaw('info', -1, await Promise.all(args))
})
for (const arg of msg.args()) {
args.push(arg.evaluate(n => n))
}
maybeEnd()
})
*/
page.on('error', (error) => console.error(error))
page.on('pageerror', (error) => console.error(error))
await page.exposeFunction('polendinaEnd', (errors) => {
end(errors)
})
await page.exposeFunction('polendinaLog', (args) => {
logRaw(args.shift(), args.shift(), args)
maybeEnd()
})
await page.exposeFunction('polendinaWrite', (args) => {
logWrite(args)
maybeEnd()
})
if (coverage) {
await page.coverage.startJSCoverage()
}
const url = `http://localhost:${port}/?mode=${mode}&runner=${runner}`
log(`Running ${runner} ${mode} tests with Puppeteer via\n ${url}`)
if (runner !== 'mocha') {
log()
}
await page.goto(url)
setTimeout(() => {
const err = new Error(`timeout: tests did not finish cleanly after ${timeout} seconds`)
const cb = () => callback(err)
browser.close().then(cb).catch(cb)
}, timeout * 1000).unref()
}