@applicvision/js-toolbox
Version:
A collection of tools for modern JavaScript development
166 lines (140 loc) • 5.56 kB
JavaScript
import path from 'node:path'
import fs from 'node:fs/promises'
import { exec } from 'node:child_process'
import { hrtime } from 'node:process'
import { parseArguments } from '@applicvision/js-toolbox/args'
import { findFiles } from '@applicvision/js-toolbox/find-files'
import { getRootSuite } from '@applicvision/js-toolbox/test'
import style from '@applicvision/js-toolbox/style'
import { NodeLogger, SimpleLogger } from '../av-test/test-logger.js'
import { createServer } from '../av-test-browser/server.js'
import { pathToFileURL } from 'node:url'
const logger = {
animated: NodeLogger,
simple: SimpleLogger
}
const defaultLogger = 'animated'
const configEndsWith = process.env.npm_package_config_testa_endsWith
const defaultTestEndsWith = '.test.js'
/**
* @template {boolean} [T=false]
* @param {string[]} args
* @param {{useFileUrls?: T}} options
* @returns {Promise<T extends true ? URL[] : string[]>}
*/
async function resolveFilesToRun(args, { useFileUrls } = {}) {
const ignoreSet = new Set(['node_modules'])
const fileMatcher = filename =>
filename.endsWith(configEndsWith ?? defaultTestEndsWith)
if (args?.length > 0) {
const results = await Promise.all(args.map(async arg => {
try {
const file = await fs.stat(arg)
if (file.isDirectory()) {
return findFiles(arg, { ignoreSet, fileMatcher, useFileUrls })
} else {
return useFileUrls ? pathToFileURL(arg) : arg
}
} catch (er) {
if (er.code === 'ENOENT') {
console.warn(
style.bold.red('Warning:'),
'file does not exist', arg)
return []
}
throw er
}
}))
// @ts-ignore
return results.flat()
}
return findFiles('.', { ignoreSet, fileMatcher, useFileUrls })
}
async function run(pathSpec, options) {
if (options.mode === 'browser' || process.env.JS_TOOLBOX_TEST_MODE == 'browser') {
const server = createServer(await resolveFilesToRun(pathSpec, { useFileUrls: true }), options.watch)
const { port = 3000, mapimport = [] } = options
const url = await server.start(port, [].concat(mapimport))
console.log(`Test runner server is running. Please visit ${url} in your browser to run tests.`)
return
}
const startTime = hrtime.bigint()
if (options.filter) {
options.filter = [].concat(options.filter).map(filter => new RegExp(filter, 'i'))
}
options.timeFunction = hrtime.bigint
let LoggerClass = logger[options.logger ?? defaultLogger]
if (!LoggerClass) {
console.warn(`unknown logger: '${options.logger}'.`, 'Using default instead.')
LoggerClass = logger.animated
}
const testLogger = new LoggerClass()
const testFiles = await resolveFilesToRun(pathSpec)
const rootSuite = getRootSuite(testLogger)
for (const file of testFiles) {
await rootSuite.addTestFile(file, () => import(path.resolve(file)))
}
await rootSuite.run(options)
process.exitCode = rootSuite.failed ? 1 : 0
printTestSummary(rootSuite.status, Number(hrtime.bigint() - startTime) / 1e9)
if (options.notify) {
showNotification(rootSuite.status)
}
}
const formatter = Intl.NumberFormat('en-US')
function printTestSummary(status, totalTime) {
const { passed, skipped, failed } = status
console.group()
console.log(
style.custom('green', passed ? 'bold' : 'dim')(`${passed} ✓`.padStart(4)),
style.dim(' Tests passed'))
console.log(style.custom('red', failed ? 'bold' : 'dim')(`${failed} ✗`.padStart(4)),
style.dim(' Tests failed')
)
console.log(style.custom('cyan', skipped ? 'bold' : 'dim')(`${skipped} -`.padStart(4)),
style.dim(' Tests skipped')
)
console.log()
console.log('Total time:', `${formatter.format(totalTime)} s`)
console.groupEnd()
}
function showNotification({ passed, skipped, failed }) {
const message = [
failed && `${failed} Failed`,
`${passed} Passed`,
skipped && `${skipped} Skipped`
].filter(row => row).join('\n')
const title = failed ? '❌ Tests failed' : '✅ All tests passed'
exec(`osascript -e 'display notification "${message}" with title "${title}"'`, (error) => {
if (error) {
console.log('Notification could not be shown. Error:', error)
}
})
}
let parsedArguments
try {
parsedArguments = parseArguments()
.option('filter', { description: 'Specify a regex to only run matching tests and describes.' })
.option('logger', {
description: `Specify a logger for the test. Available: ${Object.keys(logger).join(', ')}. Default: ${defaultLogger}`
})
.option('mode', { description: 'Specify mode, node or browser. Default node.' })
.option('watch', { description: 'In browser mode, reload on file changes in specified directory.' })
.option('port', { description: 'In browser mode, select the port to run the server on. Default 3000.' })
.option('mapimport', { short: false, description: 'In browser mode, map an import to support bare specifiers. Map to /node_modules/[module] to resolve local dependencies.' })
.option('timeout', { short: false, description: 'Set general time limit for tests. Default 4s.' })
.flag('bail', { short: false, description: 'Exit immediately on first test failure.' })
.flag('only', { short: false, description: 'Run only tests marked with only.' })
.flag('notify', { short: false, description: 'Show MacOS notification (osascript)' })
.help('Welcome to the fast test runner.\nUsage: [<options>] [--] <pathspec>')
.parse()
} catch (error) {
console.error(error.message)
process.exitCode = 1
}
if (parsedArguments?.help) {
console.log(parsedArguments.help)
} else if (parsedArguments) {
run(parsedArguments.args, parsedArguments.options)
}