UNPKG

@web/test-runner

Version:
283 lines (245 loc) 9.17 kB
import { CoverageConfig, TestRunnerCoreConfig, TestRunnerGroupConfig } from '@web/test-runner-core'; import { chromeLauncher } from '@web/test-runner-chrome'; import { emulateMediaPlugin, selectOptionPlugin, setUserAgentPlugin, setViewportPlugin, sendKeysPlugin, filePlugin, snapshotPlugin, sendMousePlugin, } from '@web/test-runner-commands/plugins'; import { getPortPromise } from 'portfinder'; import path from 'path'; import { cpus } from 'os'; import { TestRunnerCliArgs } from './readCliArgs.js'; import { mergeConfigs } from './mergeConfigs.js'; import { TestRunnerConfig } from './TestRunnerConfig.js'; import { esbuildPlugin, nodeResolvePlugin } from '@web/dev-server'; import { TestRunnerStartError } from '../TestRunnerStartError.js'; import { collectGroupConfigs } from './collectGroupConfigs.js'; import { playwrightLauncher, puppeteerLauncher } from './loadLauncher.js'; import { defaultReporter } from '../reporter/defaultReporter.js'; import { TestRunnerLogger } from '../logger/TestRunnerLogger.js'; const secondMs = 1000; const minuteMs = secondMs * 60; const defaultConfig: Partial<TestRunnerConfig> = { rootDir: process.cwd(), protocol: 'http:', hostname: 'localhost', middleware: [], plugins: [], watch: false, concurrentBrowsers: 2, concurrency: Math.max(1, cpus().length / 2), browserStartTimeout: minuteMs / 2, testsStartTimeout: secondMs * 20, testsFinishTimeout: minuteMs * 2, browserLogs: true, }; const defaultCoverageConfig: CoverageConfig = { exclude: ['**/node_modules/**/*', '**/web_modules/**/*'], threshold: { statements: 0, functions: 0, branches: 0, lines: 0 }, report: true, reportDir: 'coverage', reporters: ['lcov'], }; function validate(config: Record<string, unknown>, key: string, type: string) { if (config[key] == null) { return; } if (typeof config[key] !== type) { throw new TestRunnerStartError(`Configuration error: The ${key} setting should be a ${type}.`); } } const stringSettings = ['rootDir', 'hostname']; const numberSettings = [ 'port', 'concurrentBrowsers', 'concurrency', 'browserStartTimeout', 'testsStartTimeout', 'testsFinishTimeout', ]; const booleanSettings = [ 'watch', 'preserveSymlinks', 'browserLogs', 'coverage', 'staticLogging', 'manual', 'open', 'debug', ]; export function validateConfig(config: Partial<TestRunnerConfig>) { stringSettings.forEach(key => validate(config, key, 'string')); numberSettings.forEach(key => validate(config, key, 'number')); booleanSettings.forEach(key => validate(config, key, 'boolean')); if ( config.esbuildTarget != null && !(typeof config.esbuildTarget === 'string' || Array.isArray(config.esbuildTarget)) ) { throw new TestRunnerStartError( `Configuration error: The esbuildTarget setting should be a string or array.`, ); } if (config.files != null && !(typeof config.files === 'string' || Array.isArray(config.files))) { throw new TestRunnerStartError( `Configuration error: The files setting should be a string or an array.`, ); } return config as TestRunnerConfig; } async function parseConfigGroups(config: TestRunnerConfig, cliArgs: TestRunnerCliArgs) { const groupConfigs: TestRunnerGroupConfig[] = []; const configPatterns: string[] = []; if (cliArgs.groups) { // groups are provided from CLI args configPatterns.push(cliArgs.groups); } else if (config.groups) { // groups are provided from config for (const entry of config.groups) { if (typeof entry === 'object') { groupConfigs.push(entry); } else { configPatterns.push(entry); } } } // group entries which are strings are globs which point to group conigs groupConfigs.push(...(await collectGroupConfigs(configPatterns))); return groupConfigs; } export async function parseConfig( config: Partial<TestRunnerConfig>, cliArgs: TestRunnerCliArgs = {}, ): Promise<{ config: TestRunnerCoreConfig; groupConfigs: TestRunnerGroupConfig[] }> { const cliArgsConfig: Partial<TestRunnerConfig> = { ...(cliArgs as Omit<TestRunnerCliArgs, 'groups' | 'browsers'>), }; // CLI has properties with the same name as the config, but with different values // delete them so they don't overwrite when merging, the CLI values are read separately delete cliArgsConfig.groups; delete cliArgsConfig.browsers; const mergedConfigs = mergeConfigs(defaultConfig, config, cliArgsConfig); // backwards compatibility for configs written for es-dev-server, where middleware was // spelled incorrectly as middlewares if (Array.isArray((mergedConfigs as any).middlewares)) { mergedConfigs.middleware!.push(...(mergedConfigs as any).middlewares); } const finalConfig = validateConfig(mergedConfigs); // filter out non-objects from plugin list finalConfig.plugins = (finalConfig.plugins ?? []).filter(pl => typeof pl === 'object'); // ensure rootDir is always resolved if (typeof finalConfig.rootDir === 'string') { finalConfig.rootDir = path.resolve(finalConfig.rootDir); } else { throw new TestRunnerStartError('No rootDir specified.'); } // generate a default random port if (typeof finalConfig.port !== 'number') { finalConfig.port = await getPortPromise({ port: 8000 }); } finalConfig.coverageConfig = { ...defaultCoverageConfig, ...finalConfig.coverageConfig, }; let groupConfigs = await parseConfigGroups(finalConfig, cliArgs); if (groupConfigs.find(g => g.name === 'default')) { throw new TestRunnerStartError( 'Cannot create a group named "default". This named is reserved by the test runner.', ); } if (cliArgs.group != null) { if (cliArgs.group === 'default') { // default group is an alias for the root config groupConfigs = []; } else { const groupConfig = groupConfigs.find(c => c.name === cliArgs.group); if (!groupConfig) { throw new TestRunnerStartError(`Could not find any group named ${cliArgs.group}`); } // when focusing a group, ensure that the "default" group isn't run // we can improve this by relying only on groups inside the test runner itself if (groupConfig.files == null) { groupConfig.files = finalConfig.files; } finalConfig.files = undefined; groupConfigs = [groupConfig]; } } if (cliArgs.puppeteer) { if (finalConfig.browsers && finalConfig.browsers.length > 0) { throw new TestRunnerStartError( 'The --puppeteer flag cannot be used when defining browsers manually in your finalConfig.', ); } finalConfig.browsers = puppeteerLauncher(cliArgs.browsers); } else if (cliArgs.playwright) { if (finalConfig.browsers && finalConfig.browsers.length > 0) { throw new TestRunnerStartError( 'The --playwright flag cannot be used when defining browsers manually in your finalConfig.', ); } finalConfig.browsers = playwrightLauncher(cliArgs.browsers); } else { if (cliArgs.browsers != null) { throw new TestRunnerStartError( `The browsers option must be used along with the puppeteer or playwright option.`, ); } // add default chrome launcher if the user did not configure their own browsers if (!finalConfig.browsers) { finalConfig.browsers = [chromeLauncher()]; } } finalConfig.testFramework = { path: require.resolve('@web/test-runner-mocha/dist/autorun.js'), ...(finalConfig.testFramework ?? {}), }; if (!finalConfig.reporters) { finalConfig.reporters = [defaultReporter()]; } if (!finalConfig.logger) { finalConfig.logger = new TestRunnerLogger(config.debug); } if (finalConfig.plugins == null) { finalConfig.plugins = []; } // plugin with a noop transformImport hook, this will cause the dev server to analyze modules and // catch syntax errors. this way we still report syntax errors when the user has no flags enabled finalConfig.plugins.unshift({ name: 'syntax-checker', transformImport() { return undefined; }, }); finalConfig.plugins.unshift( setViewportPlugin(), emulateMediaPlugin(), setUserAgentPlugin(), selectOptionPlugin(), filePlugin(), sendKeysPlugin(), sendMousePlugin(), snapshotPlugin({ updateSnapshots: !!cliArgs.updateSnapshots }), ); if (finalConfig.nodeResolve) { const userOptions = typeof finalConfig.nodeResolve === 'object' ? finalConfig.nodeResolve : undefined; // do node resolve after user plugins, to allow user plugins to resolve imports finalConfig.plugins!.push( nodeResolvePlugin(finalConfig.rootDir!, finalConfig.preserveSymlinks, userOptions), ); } if (finalConfig?.esbuildTarget) { finalConfig.plugins!.unshift(esbuildPlugin(finalConfig.esbuildTarget)); } if ((!finalConfig.files || finalConfig.files.length === 0) && groupConfigs.length === 0) { throw new TestRunnerStartError( 'Did not find any tests to run. Use the "files" or "groups" option to configure test files.', ); } return { config: finalConfig as TestRunnerCoreConfig, groupConfigs }; }