| // 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; | |
| } | |
| } |