blob: d0a052ac2a6200c9e852a91b1d7c8a55cfb2262b [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import Mocha from 'mocha';
import * as path from 'node:path';
import {pathToFileURL} from 'node:url';
import {
checkForDuplicateTests,
createTestIdMap,
duplicateTests,
pruneSuite,
} from '../../front_end/testing/MochaHelpers.js';
import {TEST_ID_REGEX} from '../conductor/paths.js';
import {TestConfig} from '../conductor/test_config.js';
import {getSkippedTests} from '../conductor/test_expectations.js';
import {ResultsDbReporter} from './mocha-resultsdb-reporter.js';
type Options = Mocha.MochaOptions&{spec?: string[], suiteName?: string};
type OmitOptions = 'reporter'|'allowUncaught'|'retries';
function optionsWithDefalts(options: Options): Options {
const withDefaults = {
// Setting allowUncaught to true causes Mocha to silently abort test runs
// when a synchronous test fails after an async hook (crbug.com/505974404).
allowUncaught: false,
retries: TestConfig.retries,
reporter: ResultsDbReporter,
slow: 1000,
failZero: TestConfig.dryRun,
dryRun: TestConfig.dryRun,
bail: TestConfig.bail,
...options,
...TestConfig.mochaGrep,
};
if (TestConfig.debug) {
withDefaults.timeout = 0;
}
return withDefaults;
}
export async function run(options: Omit<Options, OmitOptions>): Promise<void> {
const mocha = new Mocha(optionsWithDefalts(options));
// Load files
const files = options.spec;
if (Array.isArray(files)) {
for (const file of files) {
mocha.addFile(file);
}
}
// Load requires if necessary (Mocha programmatic API might not automatically load requires,
// we may need to import them manually).
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const globalSetups: Function[] = [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const globalTeardowns: Function[] = [];
if (options.require) {
const requires = Array.isArray(options.require) ? options.require : [options.require];
for (const req of requires) {
const importTarget = path.isAbsolute(req) ? pathToFileURL(req).href : req;
const mod = await import(importTarget);
const exportsObj = mod.default || mod;
if (exportsObj.mochaGlobalSetup) {
globalSetups.push(exportsObj.mochaGlobalSetup);
}
if (exportsObj.mochaGlobalTeardown) {
globalTeardowns.push(exportsObj.mochaGlobalTeardown);
}
if (exportsObj.mochaHooks) {
mocha.rootHooks(exportsObj.mochaHooks);
}
}
}
if (globalSetups.length > 0) {
// @ts-expect-error unknown types
mocha.globalSetup(globalSetups);
}
if (globalTeardowns.length > 0) {
// @ts-expect-error unknown types
mocha.globalTeardown(globalTeardowns);
}
// Mocha passes `globalThis` as the context of the pre-require event; its
// interfaces (e.g. BDD) install `describe`, `it`, ... on that context, and
// the functions exported from 'mocha' forward to it. Pass a private context
// instead, so test files have to import them from 'mocha'; using them as
// globals fails with a ReferenceError when the file is loaded.
const emit = mocha.suite.emit.bind(mocha.suite);
mocha.suite.emit = (event: string, ...args: unknown[]) => {
if (event === Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE && args[0] === globalThis) {
args[0] = {};
}
return emit(event, ...args);
};
// Errors thrown while importing a test file (or any of its transitive
// dependencies) don't mention the test file, which makes them hard to
// track down. Remember which file Mocha is loading and report it.
let currentFile: string|undefined;
const onPreRequire = (_context: unknown, file: string) => {
currentFile = file;
};
mocha.suite.on(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, onPreRequire);
try {
await mocha.loadFilesAsync();
} catch (error) {
// Node does not print the cause of an unhandled rejection, so include it
// in the message (e.g. the ReferenceError for a mocha global that was not
// imported).
throw new Error(`Failed to load test file ${currentFile ?? '<unknown>'}: ${error}`, {cause: error});
} finally {
mocha.suite.off(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, onPreRequire);
}
const testIds = new Set(
TestConfig.tests.filter(testId => TEST_ID_REGEX.test(testId)),
);
const skippedTests = TestConfig.dryRun ? [] : getSkippedTests();
const testIdMap = createTestIdMap(mocha.suite);
checkForDuplicateTests(testIdMap);
pruneSuite(mocha.suite, testIdMap, {testIds, skippedTests});
duplicateTests(mocha.suite, TestConfig.repetitions);
mocha.enableGlobalSetup(!TestConfig.dryRun);
mocha.enableGlobalTeardown(!TestConfig.dryRun);
const failures = await new Promise<number>(resolve => {
mocha.run(resolve);
});
if (process.exitCode === undefined) {
process.exitCode = failures > 0 ? 1 : 0;
}
}