playwright-test
Version:
Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with playwright.
259 lines (238 loc) • 6.46 kB
JavaScript
import { mkdirSync } from 'fs'
import { createRequire } from 'module'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { watch } from 'chokidar'
import { execa } from 'execa'
import { asyncExitHook, gracefulExit } from 'exit-hook'
// @ts-ignore
import mergeOptions from 'merge-options'
import { nanoid } from 'nanoid'
// @ts-ignore
import { premove } from 'premove'
import * as DefaultRunners from '../test-runners.js'
import { createPolka, findTests, log } from '../utils/index.js'
import { build } from './utils.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const merge = mergeOptions.bind({ ignoreUndefined: true })
const require = createRequire(import.meta.url)
/**
* @typedef {import('playwright-core').Page} Page
* @typedef {import('playwright-core').BrowserContext} Context
* @typedef {import('playwright-core').Browser} Browser
* @typedef {import('playwright-core').ChromiumBrowserContext} ChromiumBrowserContext
* @typedef {import('../types.js').RunnerOptions} RunnerOptions
* @typedef {import('../types.js').TestRunner} TestRunner
*/
/**
* @type {import('../types.js').RunnerOptions}
*/
const defaultOptions = {
cwd: process.cwd(),
assets: '',
browser: 'chromium',
debug: false,
mode: 'main', // worker
incognito: false,
input: undefined,
extension: false,
testRunner: DefaultRunners.none,
before: undefined,
sw: undefined,
cov: false,
reportDir: '.nyc_output',
extensions: 'js,cjs,mjs,ts,tsx',
buildConfig: {},
buildSWConfig: {},
browserContextOptions: {},
beforeTests: async () => {
// noop
},
afterTests: async () => {
// noop
},
}
export class NodeRunner {
/**
*
* @param {Partial<import('../types.js').RunnerOptions>} options
* @param {string[]} [testFiles]
*/
constructor(options = {}, testFiles) {
/** @type {import('../types.js').RunnerOptions} */
this.options = merge(defaultOptions, options)
/** @type {import('polka').Polka["server"] | undefined} */
this.server = undefined
this.dir = path.join(__dirname, '../.tmp', nanoid())
mkdirSync(this.dir, {
recursive: true,
})
this.stopped = false
this.watching = false
/**
* @type {import('../types.js').RunnerEnv}
*/
this.env = merge(JSON.parse(JSON.stringify(process.env)), {
PW_OPTIONS: JSON.stringify(this.options),
NODE_ENV: 'test',
})
this.tests =
testFiles ??
findTests({
cwd: this.options.cwd,
extensions: this.options.extensions.split(','),
filePatterns: options.input ?? [],
})
if (this.tests.length === 0) {
this.stop(false, 'No test files were found.')
}
}
async #setupServer() {
// setup http server
const { server, url } = await createPolka(
this.dir,
this.options.cwd,
this.options.assets
)
this.env.PW_SERVER = url
this.server = server
}
/**
* Run the tests
*
*/
async runTests() {
const { outName, files } = await build(
this,
this.options.testRunner.buildConfig
? this.options.testRunner.buildConfig(this.options)
: {},
this.options.testRunner.compileRuntime(
this.options,
this.tests.map((t) => t.replaceAll('\\', '/'))
)
)
return { outName, files }
}
async run() {
asyncExitHook(this.#clean.bind(this), {
wait: 1000,
})
await this.#setupServer()
await this.options.beforeTests(this.env)
const sourceMapRegisterPath = require.resolve('source-map-support/register')
try {
const { outName } = await this.runTests()
await (this.options.cov
? execa(
'c8',
[
'--reporter=text',
'--reporter=json',
'--report-dir',
this.options.reportDir,
'--exclude-after-remap',
'--src',
this.dir,
'node',
'-r',
sourceMapRegisterPath,
path.join(this.dir, outName),
],
{
preferLocal: true,
stdio: 'inherit',
}
)
: execa(
'node',
['-r', sourceMapRegisterPath, path.join(this.dir, outName)],
{
preferLocal: true,
stdio: 'inherit',
}
))
} catch {
await this.stop(true, 'Tests failed.')
}
this.stop(false, 'Tests passed.')
}
async watch() {
asyncExitHook(this.#clean.bind(this), {
wait: 1000,
})
await this.#setupServer()
await this.options.beforeTests(this.env)
const sourceMapRegisterPath = require.resolve('source-map-support/register')
const { files, outName } = await this.runTests()
try {
await execa(
'node',
['-r', sourceMapRegisterPath, path.join(this.dir, outName)],
{
stdio: 'inherit',
}
)
} catch {
// noop
}
// Watch for changes
const watcher = watch([...files], {
ignored: /(^|[/\\])\../,
ignoreInitial: true,
awaitWriteFinish: { pollInterval: 100, stabilityThreshold: 500 },
}).on('change', async () => {
try {
log.info('Reloading tests...')
const { files, outName } = await this.runTests()
await execa(
'node',
['-r', sourceMapRegisterPath, path.join(this.dir, outName)],
{
stdio: 'inherit',
}
)
watcher.add([...files])
} catch (/** @type {any} */ error) {
if ('command' in error) {
return
}
console.error(error.stack)
}
})
}
async #clean() {
// Run after tests hook
await this.options.afterTests(this.env)
await premove(this.dir)
const serverClose = new Promise((resolve, reject) => {
if (this.server) {
this.server.close((err) => {
if (err) {
return reject(err)
}
resolve(true)
})
} else {
resolve(true)
}
})
await serverClose
}
/**
* @param {boolean} fail
* @param {string | undefined} [msg]
*/
stop(fail, msg) {
if (this.stopped || this.options.debug) {
return Promise.resolve()
}
this.stopped = true
if (fail && msg) {
log.error(msg)
} else if (msg) {
log.success(msg)
}
gracefulExit(fail ? 1 : 0)
}
}