playwright-test
Version:
Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with playwright.
326 lines (294 loc) • 9.04 kB
JavaScript
/* eslint-disable unicorn/prefer-ternary */
/* eslint-disable no-console */
import fs from 'fs'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
import { gracefulExit } from 'exit-hook'
import kleur from 'kleur'
import { lilconfig } from 'lilconfig'
// @ts-ignore
import mergeOptions from 'merge-options'
import sade from 'sade'
import { NodeRunner } from './src/node/runner.js'
import { Runner } from './src/runner.js'
import { benchmark, mocha, none, tape, uvu, zora } from './src/test-runners.js'
import * as DefaultRunners from './src/test-runners.js'
import { detectTestRunner } from './src/utils/auto-detect.js'
import {
defaultOptions,
findTests,
log,
resolveTestRunner,
runnerOptions,
} from './src/utils/index.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const { version } = JSON.parse(
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')
)
const merge = mergeOptions.bind({ ignoreUndefined: true })
// Handle any uncaught errors
process.once(
'uncaughtException',
(/** @type {Error} */ err, /** @type {string} */ origin) => {
if (!origin || origin === 'uncaughtException') {
console.error(err)
gracefulExit(1)
}
}
)
process.once('unhandledRejection', (err) => {
console.error(err)
gracefulExit(1)
})
const extra = `
${kleur.bold('Examples')}
${kleur.dim('$ playwright-test test.js --runner tape')}
${kleur.dim('$ playwright-test test --debug')}
${kleur.dim(
'$ playwright-test "test/**/*.spec.js" --browser webkit --mode worker --incognito --debug'
)}
${kleur.dim('$ playwright-test bench.js --runner benchmark')}
${kleur.gray(
'# Uses benchmark.js to run your benchmark see playwright-test/mocks/benchmark.js for an example.'
)}
${kleur.dim(
'$ playwright-test test --cov && npx nyc report --reporter=html'
)}
${kleur.gray(
'# Enable code coverage in istanbul format which can be used by nyc.'
)}
${kleur.dim(
'$ playwright-test "test/**/*.spec.js" --debug --before ./mocks/before.js'
)}
${kleur.gray(
'# Run a script in a separate tab. Check ./mocks/before.js for an example.\n # Important: You need to call `self.PW_TEST.beforeEnd()` to start the main script.'
)}
${kleur.bold('Runner Options')}
All arguments passed to the cli not listed above will be fowarded to the runner.
${kleur.dim(
"$ playwright-test test.js --runner mocha --bail --grep 'should fail'"
)}
To send a \`false\` flag use --no-bail.
Check https://mochajs.org/api/mocha for \`mocha\` options or \`npx mocha --help\`.
${kleur.bold('Notes')}
DEBUG env var filtering for 'debug' package logging will work as expected.
${kleur.dim('$ DEBUG:app playwright-test test.js')}
Do not let your shell expand globs, always wrap them.
${kleur.dim('$ playwright-test "test/**"')} GOOD
${kleur.dim('$ playwright-test test/**')} BAD
`
const sade2 = new Proxy(sade('playwright-test [files]', true), {
get: (target, prop, receiver) => {
const targetValue = Reflect.get(target, prop, receiver)
if (typeof targetValue === 'function') {
return function (/** @type {any} */ ...args) {
// @ts-ignore
const out = targetValue.apply(this, args)
if (prop === 'help') {
// biome-ignore lint/suspicious/noConsoleLog: <explanation>
console.log(extra)
}
return out
}
}
return targetValue
},
})
const loadEsm = async (/** @type {string} */ filepath) => {
/** @type {any} */
const res = await import(pathToFileURL(filepath).toString())
if (res.default) {
return res.default
}
return res
}
const configLoaders = {
'.js': loadEsm,
'.mjs': loadEsm,
}
sade2
.version(version)
.describe(
'Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with `playwright` and in Node.'
)
.option(
'-r, --runner',
'Test runner. Options: mocha, tape, zora, uvu, none, taps and benchmark. It also accepts a path to a module or a module name that exports a `playwrightTestRunner` object.'
)
.option(
'-b, --browser',
'Browser to run tests. Options: chromium, firefox, webkit.',
defaultOptions.browser
)
.option(
'-m, --mode',
'Run mode. Options: main, worker and node.',
defaultOptions.mode
)
.option(
'-d, --debug',
'Debug mode, keeps browser window open.',
defaultOptions.debug
)
.option('-w, --watch', 'Watch files for changes and re-run tests.')
.option(
'-i, --incognito',
'Use incognito window to run tests.',
defaultOptions.incognito
)
.option(
'-e, --extension',
'Use extension background_page to run tests.',
defaultOptions.extension
)
.option(
'--cov',
"Enable code coverage in istanbul format. Outputs '.nyc_output/coverage-pw.json'.",
defaultOptions.cov
)
.option(
'--report-dir',
'Where to output code coverage in instanbul format.',
defaultOptions.reportDir
)
.option(
'--before',
'Path to a script to be loaded on a separate tab before the main script.'
)
.option('--sw', 'Path to a script to be loaded in a service worker.')
.option(
'--assets',
'Folder with assets to be served by the http server. (default process.cwd())'
)
.option('--cwd', 'Current directory.', defaultOptions.cwd)
.option(
'--extensions',
'File extensions allowed in the bundle.',
defaultOptions.extensions
)
.option('--config', 'Path to the config file')
.action(async (input, opts) => {
let config
try {
if (opts.config) {
config = await lilconfig('playwright-test', {
loaders: configLoaders,
}).load(path.resolve(opts.config))
} else {
config = await lilconfig('playwright-test', {
loaders: configLoaders,
}).search()
if (!config) {
config = await lilconfig('pw-test', {
loaders: configLoaders,
}).search()
}
}
// if the supplied config module was a function, invoke it
if (config && typeof config.config === 'function') {
config.config = await config.config(opts)
}
/**
* Merge cli options with config file options
*
* @type {import('./src/types.js').RunnerOptions}
*/
const options = merge(config ? config.config : {}, {
input: input ? [input, ...opts._] : undefined,
testFiles: [],
cwd: opts.cwd,
assets: opts.assets,
browser: opts.browser,
debug: opts.debug,
mode: opts.mode,
incognito: opts.incognito,
extension: opts.extension,
before: opts.before,
sw: opts.sw,
cov: opts.cov,
reportDir: opts['report-dir'],
extensions: opts.extensions,
beforeTests: opts.beforeTests,
afterTests: opts.afterTests,
})
const testFiles = findTests({
cwd: options.cwd,
extensions: options.extensions.split(','),
filePatterns: options.input ?? [],
})
switch (opts.runner) {
case 'uvu': {
options.testRunner = uvu
break
}
case 'zora': {
options.testRunner = zora
break
}
case 'mocha': {
options.testRunner = mocha
break
}
case 'tape': {
options.testRunner = tape
break
}
case 'benchmark': {
options.testRunner = benchmark
break
}
case 'none': {
options.testRunner = none
break
}
default: {
if (opts.runner) {
options.testRunner = await resolveTestRunner(opts.runner, opts.cwd)
} else {
const testRunner =
options.testRunner ??
(detectTestRunner(testFiles[0], DefaultRunners) ||
DefaultRunners.none)
options.testRunner = testRunner
if (testRunner.moduleId === 'none') {
log.warn('Could not find a test runner. Using "none".')
} else {
log.info(`Autodetected "${testRunner.moduleId}" as the runner.`)
}
}
}
}
let runner
if (opts.mode === 'node') {
runner = new NodeRunner(
merge(options, {
testRunner: {
// merge cli redirected options
options: runnerOptions(opts),
},
}),
testFiles
)
} else {
runner = new Runner(
merge(options, {
testRunner: {
// merge cli redirected options
options: runnerOptions(opts),
},
}),
testFiles
)
}
if (opts.watch) {
runner.watch()
} else {
runner.run()
}
} catch (error) {
console.error(error)
gracefulExit(1)
}
})
.parse(process.argv)