chore: remove addFatalError (#20383)

This commit is contained in:
Pavel Feldman 2023-01-25 17:26:30 -08:00 committed by GitHub
parent fe1dd7818d
commit f7ff252455
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 111 additions and 132 deletions

View file

@ -20,7 +20,6 @@ import type { FixturesWithLocation, Location, WorkerInfo } from './types';
import { ManualPromise } from 'playwright-core/lib/utils';
import type { TestInfoImpl } from './testInfo';
import type { FixtureDescription, TimeoutManager } from './timeoutManager';
import { addFatalError } from './globals';
type FixtureScope = 'test' | 'worker';
type FixtureAuto = boolean | 'all-hooks-included';
@ -52,6 +51,10 @@ type FixtureRegistration = {
// Whether this fixture is an option value set from the config.
fromConfig?: boolean;
};
export type LoadError = {
message: string;
location: Location;
};
class Fixture {
runner: FixtureRunner;
@ -187,9 +190,11 @@ export function isFixtureOption(value: any): value is FixtureTuple {
export class FixturePool {
readonly digest: string;
readonly registrations: Map<string, FixtureRegistration>;
private _onLoadError: (error: LoadError) => void;
constructor(fixturesList: FixturesWithLocation[], parentPool?: FixturePool, disallowWorkerFixtures?: boolean) {
constructor(fixturesList: FixturesWithLocation[], onLoadError: (error: LoadError) => void, parentPool?: FixturePool, disallowWorkerFixtures?: boolean) {
this.registrations = new Map(parentPool ? parentPool.registrations : []);
this._onLoadError = onLoadError;
for (const { fixtures, location, fromConfig } of fixturesList) {
for (const entry of Object.entries(fixtures)) {
@ -211,11 +216,11 @@ export class FixturePool {
const previous = this.registrations.get(name);
if (previous && options) {
if (previous.scope !== options.scope) {
addFatalError(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture defined in ${formatLocation(previous.location)}.`, location);
this._addLoadError(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture defined in ${formatLocation(previous.location)}.`, location);
continue;
}
if (previous.auto !== options.auto) {
addFatalError(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture defined in ${formatLocation(previous.location)}.`, location);
this._addLoadError(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture defined in ${formatLocation(previous.location)}.`, location);
continue;
}
} else if (previous) {
@ -225,11 +230,11 @@ export class FixturePool {
}
if (!kScopeOrder.includes(options.scope)) {
addFatalError(`Fixture "${name}" has unknown { scope: '${options.scope}' }.`, location);
this._addLoadError(`Fixture "${name}" has unknown { scope: '${options.scope}' }.`, location);
continue;
}
if (options.scope === 'worker' && disallowWorkerFixtures) {
addFatalError(`Cannot use({ ${name} }) in a describe group, because it forces a new worker.\nMake it top-level in the test file or put in the configuration file.`, location);
this._addLoadError(`Cannot use({ ${name} }) in a describe group, because it forces a new worker.\nMake it top-level in the test file or put in the configuration file.`, location);
continue;
}
@ -242,7 +247,7 @@ export class FixturePool {
fn = original.fn;
}
const deps = fixtureParameterNames(fn, location);
const deps = fixtureParameterNames(fn, location, e => this._onLoadError(e));
const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, deps, super: previous, fromConfig };
registrationId(registration);
this.registrations.set(name, registration);
@ -262,13 +267,13 @@ export class FixturePool {
const dep = this.resolveDependency(registration, name);
if (!dep) {
if (name === registration.name)
addFatalError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
this._addLoadError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
else
addFatalError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location);
this._addLoadError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location);
continue;
}
if (kScopeOrder.indexOf(registration.scope) > kScopeOrder.indexOf(dep.scope)) {
addFatalError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatLocation(dep.location)}.`, registration.location);
this._addLoadError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatLocation(dep.location)}.`, registration.location);
continue;
}
if (!markers.has(dep)) {
@ -277,7 +282,7 @@ export class FixturePool {
const index = stack.indexOf(dep);
const regs = stack.slice(index, stack.length);
const names = regs.map(r => `"${r.name}"`);
addFatalError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatLocation(r.location)).join(' -> ')}`, dep.location);
this._addLoadError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatLocation(r.location)).join(' -> ')}`, dep.location);
continue;
}
}
@ -297,10 +302,10 @@ export class FixturePool {
}
validateFunction(fn: Function, prefix: string, location: Location) {
for (const name of fixtureParameterNames(fn, location)) {
for (const name of fixtureParameterNames(fn, location, e => this._onLoadError(e))) {
const registration = this.registrations.get(name);
if (!registration)
addFatalError(`${prefix} has unknown parameter "${name}".`, location);
this._addLoadError(`${prefix} has unknown parameter "${name}".`, location);
}
}
@ -309,6 +314,10 @@ export class FixturePool {
return registration.super;
return this.registrations.get(name);
}
private _addLoadError(message: string, location: Location) {
this._onLoadError({ message, location });
}
}
export class FixtureRunner {
@ -368,7 +377,7 @@ export class FixtureRunner {
}
// Install used fixtures.
const names = fixtureParameterNames(fn, { file: '<unused>', line: 1, column: 1 });
const names = fixtureParameterNames(fn, { file: '<unused>', line: 1, column: 1 }, serializeAndThrowError);
const params: { [key: string]: any } = {};
for (const name of names) {
const registration = this.pool!.registrations.get(name)!;
@ -404,7 +413,7 @@ export class FixtureRunner {
}
dependsOnWorkerFixturesOnly(fn: Function, location: Location): boolean {
const names = fixtureParameterNames(fn, location);
const names = fixtureParameterNames(fn, location, serializeAndThrowError);
for (const name of names) {
const registration = this.pool!.registrations.get(name)!;
if (registration.scope !== 'worker')
@ -414,17 +423,21 @@ export class FixtureRunner {
}
}
function serializeAndThrowError(e: LoadError) {
throw new Error(`${formatLocation(e.location!)}: ${e.message}`);
}
const signatureSymbol = Symbol('signature');
function fixtureParameterNames(fn: Function | any, location: Location): string[] {
function fixtureParameterNames(fn: Function | any, location: Location, onError: (error: LoadError) => void): string[] {
if (typeof fn !== 'function')
return [];
if (!fn[signatureSymbol])
fn[signatureSymbol] = innerFixtureParameterNames(fn, location);
fn[signatureSymbol] = innerFixtureParameterNames(fn, location, onError);
return fn[signatureSymbol];
}
function innerFixtureParameterNames(fn: Function, location: Location): string[] {
function innerFixtureParameterNames(fn: Function, location: Location, onError: (error: LoadError) => void): string[] {
const text = fn.toString();
const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);
if (!match)
@ -434,7 +447,7 @@ function innerFixtureParameterNames(fn: Function, location: Location): string[]
return [];
const [firstParam] = splitByComma(trimmedParams);
if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}')
addFatalError('First argument must use the object destructuring pattern: ' + firstParam, location);
onError({ message: 'First argument must use the object destructuring pattern: ' + firstParam, location });
const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => {
const colon = prop.indexOf(':');
return colon === -1 ? prop : prop.substring(0, colon).trim();

View file

@ -16,8 +16,6 @@
import type { TestInfoImpl } from './testInfo';
import type { Suite } from './test';
import type { TestError, Location } from '../types/testReporter';
import { formatLocation } from './util';
let currentTestInfoValue: TestInfoImpl | null = null;
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
@ -34,15 +32,3 @@ export function setCurrentlyLoadingFileSuite(suite: Suite | undefined) {
export function currentlyLoadingFileSuite() {
return currentFileSuite;
}
let _fatalErrorSink: ((fatalError: TestError) => void) | undefined;
export function setFatalErrorSink(fatalErrorSink: (fatalError: TestError) => void) {
_fatalErrorSink = fatalErrorSink;
}
export function addFatalError(message: string, location: Location) {
if (_fatalErrorSink)
_fatalErrorSink({ message: `Error: ${message}`, location });
else
throw new Error(`${formatLocation(location)}: ${message}`);
}

View file

@ -15,11 +15,10 @@
*/
import type { SerializedConfig } from './ipc';
import type { TestError } from '../reporter';
import { ConfigLoader } from './configLoader';
import { ProcessRunner } from './process';
import { loadTestFilesInProcess } from './testLoader';
import { setFatalErrorSink } from './globals';
import type { LoadError } from './fixtures';
export class LoaderMain extends ProcessRunner {
private _config: SerializedConfig;
@ -37,8 +36,7 @@ export class LoaderMain extends ProcessRunner {
}
async loadTestFiles(params: { files: string[] }) {
const loadErrors: TestError[] = [];
setFatalErrorSink(error => loadErrors.push(error));
const loadErrors: LoadError[] = [];
const configLoader = await this._configLoader();
const rootSuite = await loadTestFilesInProcess(configLoader.fullConfig(), params.files, loadErrors);
return { rootSuite: rootSuite._deepSerialize(), loadErrors };

View file

@ -15,25 +15,29 @@
*/
import { FixturePool, isFixtureOption } from './fixtures';
import type { LoadError } from './fixtures';
import type { Suite, TestCase } from './test';
import type { TestTypeImpl } from './testType';
import type { Fixtures, FixturesWithLocation, FullProjectInternal } from './types';
import { formatLocation } from './util';
export class PoolBuilder {
private _project: FullProjectInternal | undefined;
private _testTypePools = new Map<TestTypeImpl, FixturePool>();
private _type: 'loader' | 'worker';
private _loadErrors: LoadError[] | undefined;
static buildForLoader(suite: Suite) {
new PoolBuilder('loader').buildPools(suite);
static buildForLoader(suite: Suite, loadErrors: LoadError[]) {
new PoolBuilder('loader', loadErrors).buildPools(suite);
}
static createForWorker(project: FullProjectInternal) {
return new PoolBuilder('worker', project);
return new PoolBuilder('worker', undefined, project);
}
private constructor(type: 'loader' | 'worker', project?: FullProjectInternal) {
private constructor(type: 'loader' | 'worker', loadErrors?: LoadError[], project?: FullProjectInternal) {
this._type = type;
this._loadErrors = loadErrors;
this._project = project;
}
@ -57,7 +61,7 @@ export class PoolBuilder {
for (const parent of parents) {
if (parent._use.length)
pool = new FixturePool(parent._use, pool, parent._type === 'describe');
pool = new FixturePool(parent._use, e => this._onLoadError(e), pool, parent._type === 'describe');
for (const hook of parent._hooks)
pool.validateFunction(hook.fn, hook.type + ' hook', hook.location);
for (const modifier of parent._modifiers)
@ -71,12 +75,19 @@ export class PoolBuilder {
private _buildTestTypePool(testType: TestTypeImpl): FixturePool {
if (!this._testTypePools.has(testType)) {
const fixtures = this._project ? this._applyConfigUseOptions(this._project, testType) : testType.fixtures;
const pool = new FixturePool(fixtures);
const pool = new FixturePool(fixtures, e => this._onLoadError(e));
this._testTypePools.set(testType, pool);
}
return this._testTypePools.get(testType)!;
}
private _onLoadError(e: LoadError): void {
if (this._loadErrors)
this._loadErrors.push(e);
else
throw new Error(`${formatLocation(e.location)}: ${e.message}`);
}
private _applyConfigUseOptions(project: FullProjectInternal, testType: TestTypeImpl): FixturesWithLocation[] {
const projectUse = project.use || {};
const configKeys = new Set(Object.keys(projectUse));

View file

@ -28,7 +28,6 @@ export class Multiplexer implements Reporter {
private _reporters: Reporter[];
private _deferredErrors: TestError[] | null = [];
private _deferredStdIO: StdIOChunk[] | null = [];
hasErrors = false;
private _config!: FullConfig;
constructor(reporters: Reporter[]) {
@ -107,8 +106,6 @@ export class Multiplexer implements Reporter {
}
onError(error: TestError) {
this.hasErrors = true;
if (this._deferredErrors) {
this._deferredErrors.push(error);
return;

View file

@ -41,13 +41,13 @@ import { Multiplexer } from './reporters/multiplexer';
import type { TestCase } from './test';
import { Suite } from './test';
import type { Config, FullConfigInternal, FullProjectInternal } from './types';
import { createFileMatcher, createFileMatcherFromFilters, createTitleMatcher, serializeError } from './util';
import { createFileMatcher, createFileMatcherFromFilters, createTitleMatcher } from './util';
import type { Matcher, TestFileFilter } from './util';
import { setFatalErrorSink } from './globals';
import { buildFileSuiteForProject, filterOnly, filterSuite, filterSuiteWithOnlySemantics, filterTestsRemoveEmptySuites } from './suiteUtils';
import { LoaderHost } from './loaderHost';
import { loadTestFilesInProcess } from './testLoader';
import { TaskRunner } from './taskRunner';
import type { LoadError } from './fixtures';
const removeFolderAsync = promisify(rimraf);
const readDirAsync = promisify(fs.readdir);
@ -240,7 +240,7 @@ export class Runner {
return filesByProject;
}
private async _loadAllTests(options: RunOptions): Promise<{ rootSuite: Suite, testGroups: TestGroup[] }> {
private async _loadAllTests(options: RunOptions, errors: TestError[]): Promise<{ rootSuite: Suite, testGroups: TestGroup[] }> {
const config = this._configLoader.fullConfig();
const projects = this._collectProjects(options.projectFilter);
const filesByProject = await this._collectFiles(projects, options.testFileFilters);
@ -249,10 +249,10 @@ export class Runner {
files.forEach(file => allTestFiles.add(file));
// Load all tests.
const preprocessRoot = await this._loadTests(allTestFiles);
const preprocessRoot = await this._loadTests(allTestFiles, errors);
// Complain about duplicate titles.
createDuplicateTitlesErrors(config, preprocessRoot).forEach(e => this._reporter.onError(e));
errors.push(...createDuplicateTitlesErrors(config, preprocessRoot));
// Filter tests to respect line/column filter.
filterByFocusedLine(preprocessRoot, options.testFileFilters);
@ -261,7 +261,7 @@ export class Runner {
if (config.forbidOnly) {
const onlyTestsAndSuites = preprocessRoot._getOnlyItems();
if (onlyTestsAndSuites.length > 0)
createForbidOnlyErrors(config, onlyTestsAndSuites).forEach(e => this._reporter.onError(e));
errors.push(...createForbidOnlyErrors(config, onlyTestsAndSuites));
}
// Filter only.
@ -316,7 +316,7 @@ export class Runner {
return rootSuite;
}
private async _loadTests(testFiles: Set<string>): Promise<Suite> {
private async _loadTests(testFiles: Set<string>, errors: TestError[]): Promise<Suite> {
if (process.env.PW_TEST_OOP_LOADER) {
const loaderHost = new LoaderHost();
await loaderHost.start(this._configLoader.serializedConfig());
@ -326,11 +326,11 @@ export class Runner {
await loaderHost.stop();
}
}
const loadErrors: TestError[] = [];
const loadErrors: LoadError[] = [];
try {
return await loadTestFilesInProcess(this._configLoader.fullConfig(), [...testFiles], loadErrors);
} finally {
loadErrors.forEach(e => this._reporter.onError(e));
errors.push(...loadErrors);
}
}
@ -390,7 +390,6 @@ export class Runner {
this._plugins.push(dockerPlugin);
this._reporter = await this._createReporter(options.listOnly);
setFatalErrorSink(error => this._reporter.onError(error));
const taskRunner = new TaskRunner(this._reporter, config.globalTimeout);
// Setup the plugins.
@ -419,21 +418,14 @@ export class Runner {
// Load tests.
let loadedTests!: { rootSuite: Suite, testGroups: TestGroup[] };
taskRunner.addTask('load tests', async () => {
loadedTests = await this._loadAllTests(options);
if (this._reporter.hasErrors) {
status = 'failed';
taskRunner.stop();
taskRunner.addTask('load tests', async ({ errors }) => {
loadedTests = await this._loadAllTests(options, errors);
if (errors.length)
return;
}
// Fail when no tests.
if (!loadedTests.rootSuite.allTests().length && !options.passWithNoTests) {
this._reporter.onError(createNoTestsError());
status = 'failed';
taskRunner.stop();
return;
}
if (!loadedTests.rootSuite.allTests().length && !options.passWithNoTests)
throw new Error(`No tests found`);
if (!options.listOnly) {
this._filterForCurrentShard(loadedTests.rootSuite, loadedTests.testGroups);
@ -444,11 +436,7 @@ export class Runner {
if (!options.listOnly) {
taskRunner.addTask('prepare to run', async () => {
// Remove output directores.
if (!await this._removeOutputDirs(options)) {
status = 'failed';
taskRunner.stop();
return;
}
await this._removeOutputDirs(options);
});
taskRunner.addTask('plugin begin', async () => {
@ -509,7 +497,7 @@ export class Runner {
return status;
}
private async _removeOutputDirs(options: RunOptions): Promise<boolean> {
private async _removeOutputDirs(options: RunOptions) {
const config = this._configLoader.fullConfig();
const outputDirs = new Set<string>();
for (const p of config.projects) {
@ -517,23 +505,17 @@ export class Runner {
outputDirs.add(p.outputDir);
}
try {
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async (error: any) => {
if ((error as any).code === 'EBUSY') {
// We failed to remove folder, might be due to the whole folder being mounted inside a container:
// https://github.com/microsoft/playwright/issues/12106
// Do a best-effort to remove all files inside of it instead.
const entries = await readDirAsync(outputDir).catch(e => []);
await Promise.all(entries.map(entry => removeFolderAsync(path.join(outputDir, entry))));
} else {
throw error;
}
})));
} catch (e) {
this._reporter.onError(serializeError(e));
return false;
}
return true;
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async (error: any) => {
if ((error as any).code === 'EBUSY') {
// We failed to remove folder, might be due to the whole folder being mounted inside a container:
// https://github.com/microsoft/playwright/issues/12106
// Do a best-effort to remove all files inside of it instead.
const entries = await readDirAsync(outputDir).catch(e => []);
await Promise.all(entries.map(entry => removeFolderAsync(path.join(outputDir, entry))));
} else {
throw error;
}
})));
}
}
@ -807,14 +789,6 @@ function createDuplicateTitlesErrors(config: FullConfigInternal, rootSuite: Suit
return errors;
}
function createNoTestsError(): TestError {
return createStacklessError(`=================\n no tests found.\n=================`);
}
function createStacklessError(message: string, location?: TestError['location']): TestError {
return { message, location };
}
function sanitizeConfigForJSON(object: any, visited: Set<any>): any {
const type = typeof object;
if (type === 'function' || type === 'symbol')

View file

@ -16,12 +16,12 @@
import { debug } from 'playwright-core/lib/utilsBundle';
import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, Reporter } from '../reporter';
import type { FullResult, Reporter, TestError } from '../reporter';
import { SigIntWatcher } from './sigIntWatcher';
import { serializeError } from './util';
type TaskTeardown = () => Promise<any> | undefined;
type Task = () => Promise<TaskTeardown | void> | undefined;
type Task = (params: { errors: TestError[] }) => Promise<TaskTeardown | void> | undefined;
export class TaskRunner {
private _tasks: { name: string, task: Task }[] = [];
@ -58,16 +58,22 @@ export class TaskRunner {
if (this._interrupted)
break;
debug('pw:test:task')(`"${name}" started`);
const errors: TestError[] = [];
try {
const teardown = await task();
const teardown = await task({ errors });
if (teardown)
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: teardown });
} catch (e) {
debug('pw:test:task')(`error in "${name}": `, e);
this._reporter.onError?.(serializeError(e));
if (!this._isTearDown)
this._interrupted = true;
this._hasErrors = true;
errors.push(serializeError(e));
} finally {
for (const error of errors)
this._reporter.onError?.(error);
if (errors.length) {
if (!this._isTearDown)
this._interrupted = true;
this._hasErrors = true;
}
}
debug('pw:test:task')(`"${name}" finished`);
}

View file

@ -17,6 +17,7 @@
import path from 'path';
import type { TestError } from '../reporter';
import type { FullConfigInternal } from './types';
import type { LoadError } from './fixtures';
import { setCurrentlyLoadingFileSuite } from './globals';
import { PoolBuilder } from './poolBuilder';
import { Suite } from './test';
@ -79,7 +80,7 @@ export class TestLoader {
}
}
export async function loadTestFilesInProcess(config: FullConfigInternal, testFiles: string[], loadErrors: TestError[]): Promise<Suite> {
export async function loadTestFilesInProcess(config: FullConfigInternal, testFiles: string[], loadErrors: LoadError[]): Promise<Suite> {
const testLoader = new TestLoader(config);
const rootSuite = new Suite('', 'root');
for (const file of testFiles) {
@ -87,6 +88,6 @@ export async function loadTestFilesInProcess(config: FullConfigInternal, testFil
rootSuite._addSuite(fileSuite);
}
// Generate hashes.
PoolBuilder.buildForLoader(rootSuite);
PoolBuilder.buildForLoader(rootSuite, loadErrors);
return rootSuite;
}

View file

@ -15,7 +15,7 @@
*/
import { expect } from './expect';
import { currentlyLoadingFileSuite, currentTestInfo, addFatalError, setCurrentlyLoadingFileSuite } from './globals';
import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals';
import { TestCase, Suite } from './test';
import { wrapFunctionWithLocation } from './transform';
import type { Fixtures, FixturesWithLocation, Location, TestType } from './types';
@ -68,15 +68,14 @@ export class TestTypeImpl {
private _currentSuite(location: Location, title: string): Suite | undefined {
const suite = currentlyLoadingFileSuite();
if (!suite) {
addFatalError([
throw new Error([
`Playwright Test did not expect ${title} to be called here.`,
`Most common reasons include:`,
`- You are calling ${title} in a configuration file.`,
`- You are calling ${title} in a file that is imported by the configuration file.`,
`- You have two different versions of @playwright/test. This usually happens`,
` when one of the dependencies in your package.json depends on @playwright/test.`,
].join('\n'), location);
return;
].join('\n'));
}
return suite;
}
@ -123,7 +122,7 @@ export class TestTypeImpl {
for (let parent: Suite | undefined = suite; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel')
addFatalError('describe.parallel cannot be nested inside describe.serial', location);
throw new Error('describe.parallel cannot be nested inside describe.serial');
}
setCurrentlyLoadingFileSuite(child);
@ -152,11 +151,11 @@ export class TestTypeImpl {
if (options.mode !== undefined) {
if (suite._parallelMode !== 'default')
addFatalError('Parallel mode is already assigned for the enclosing scope.', location);
throw new Error('Parallel mode is already assigned for the enclosing scope.');
suite._parallelMode = options.mode;
for (let parent: Suite | undefined = suite.parent; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial' && suite._parallelMode === 'parallel')
addFatalError('describe.parallel cannot be nested inside describe.serial', location);
throw new Error('describe.parallel cannot be nested inside describe.serial');
}
}
}
@ -182,12 +181,10 @@ export class TestTypeImpl {
}
const testInfo = currentTestInfo();
if (!testInfo) {
addFatalError(`test.${type}() can only be called inside test, describe block or fixture`, location);
return;
}
if (!testInfo)
throw new Error(`test.${type}() can only be called inside test, describe block or fixture`);
if (typeof modifierArgs[0] === 'function')
addFatalError(`test.${type}() with a function can only be called inside describe block`, location);
throw new Error(`test.${type}() with a function can only be called inside describe block`);
testInfo[type](...modifierArgs as [any, any]);
}
@ -199,10 +196,8 @@ export class TestTypeImpl {
}
const testInfo = currentTestInfo();
if (!testInfo) {
addFatalError(`test.setTimeout() can only be called from a test`, location);
return;
}
if (!testInfo)
throw new Error(`test.setTimeout() can only be called from a test`);
testInfo.setTimeout(timeout);
}
@ -215,10 +210,8 @@ export class TestTypeImpl {
private async _step<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> {
const testInfo = currentTestInfo();
if (!testInfo) {
addFatalError(`test.step() can only be called from a test`, location);
return undefined as any;
}
if (!testInfo)
throw new Error(`test.step() can only be called from a test`);
const step = testInfo._addStep({
category: 'test.step',
title,

View file

@ -141,7 +141,7 @@ test('should exit with code 1 if the specified folder does not exist', async ({
`,
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`no tests found.`);
expect(result.output).toContain(`No tests found`);
});
test('should exit with code 1 if passed a file name', async ({ runInlineTest }) => {
@ -153,7 +153,7 @@ test('should exit with code 1 if passed a file name', async ({ runInlineTest })
`,
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`no tests found.`);
expect(result.output).toContain(`No tests found`);
});
test('should exit with code 0 with --pass-with-no-tests', async ({ runInlineTest }) => {

View file

@ -298,7 +298,7 @@ test('should print errors with inconsistent message/stack', async ({ runInlineTe
test('should print "no tests found" error', async ({ runInlineTest }) => {
const result = await runInlineTest({ });
expect(result.exitCode).toBe(1);
expect(result.output).toContain('no tests found.');
expect(result.output).toContain('No tests found');
});
test('should not crash on undefined body with manual attachments', async ({ runInlineTest }) => {

View file

@ -489,7 +489,7 @@ test('should report no-tests error to reporter', async ({ runInlineTest }) => {
}, { 'reporter': '' });
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`%%got error: =================\n no tests found.`);
expect(result.output).toContain(`%%got error: No tests found`);
});
test('should report require error to reporter', async ({ runInlineTest }) => {