UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

166 lines (140 loc) 5.56 kB
#! /usr/bin/env node 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) }