chore: report more fatal errors via reporter (#19640)

This commit is contained in:
Pavel Feldman 2022-12-22 17:31:02 -08:00 committed by GitHub
parent 8b80e22a03
commit 233664bd30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 120 additions and 88 deletions

View file

@ -20,6 +20,7 @@ import type { FixturesWithLocation, Location, WorkerInfo } from './types';
import { ManualPromise } from 'playwright-core/lib/utils/manualPromise'; import { ManualPromise } from 'playwright-core/lib/utils/manualPromise';
import type { TestInfoImpl } from './testInfo'; import type { TestInfoImpl } from './testInfo';
import type { FixtureDescription, TimeoutManager } from './timeoutManager'; import type { FixtureDescription, TimeoutManager } from './timeoutManager';
import { addFatalError } from './globals';
type FixtureScope = 'test' | 'worker'; type FixtureScope = 'test' | 'worker';
type FixtureAuto = boolean | 'all-hooks-included'; type FixtureAuto = boolean | 'all-hooks-included';
@ -209,20 +210,28 @@ export class FixturePool {
const previous = this.registrations.get(name); const previous = this.registrations.get(name);
if (previous && options) { if (previous && options) {
if (previous.scope !== options.scope) if (previous.scope !== options.scope) {
throw errorWithLocations(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture.`, { location, name }, previous); addFatalError(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture defined in ${formatLocation(previous.location)}.`, location);
if (previous.auto !== options.auto) continue;
throw errorWithLocations(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture.`, { location, name }, previous); }
if (previous.auto !== options.auto) {
addFatalError(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture defined in ${formatLocation(previous.location)}.`, location);
continue;
}
} else if (previous) { } else if (previous) {
options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle }; options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle };
} else if (!options) { } else if (!options) {
options = { auto: false, scope: 'test', option: false, timeout: undefined, customTitle: undefined }; options = { auto: false, scope: 'test', option: false, timeout: undefined, customTitle: undefined };
} }
if (!kScopeOrder.includes(options.scope)) if (!kScopeOrder.includes(options.scope)) {
throw errorWithLocations(`Fixture "${name}" has unknown { scope: '${options.scope}' }.`, { location, name }); addFatalError(`Fixture "${name}" has unknown { scope: '${options.scope}' }.`, location);
if (options.scope === 'worker' && disallowWorkerFixtures) continue;
throw errorWithLocations(`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, name }); }
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);
continue;
}
// Overriding option with "undefined" value means setting it to the default value // Overriding option with "undefined" value means setting it to the default value
// from the config or from the original declaration of the option. // from the config or from the original declaration of the option.
@ -253,19 +262,23 @@ export class FixturePool {
const dep = this.resolveDependency(registration, name); const dep = this.resolveDependency(registration, name);
if (!dep) { if (!dep) {
if (name === registration.name) if (name === registration.name)
throw errorWithLocations(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration); addFatalError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
else else
throw errorWithLocations(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration); addFatalError(`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);
continue;
} }
if (kScopeOrder.indexOf(registration.scope) > kScopeOrder.indexOf(dep.scope))
throw errorWithLocations(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}".`, registration, dep);
if (!markers.has(dep)) { if (!markers.has(dep)) {
visit(dep); visit(dep);
} else if (markers.get(dep) === 'visiting') { } else if (markers.get(dep) === 'visiting') {
const index = stack.indexOf(dep); const index = stack.indexOf(dep);
const regs = stack.slice(index, stack.length); const regs = stack.slice(index, stack.length);
const names = regs.map(r => `"${r.name}"`); const names = regs.map(r => `"${r.name}"`);
throw errorWithLocations(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle.`, ...regs); addFatalError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatLocation(r.location)).join(' -> ')}`, dep.location);
continue;
} }
} }
markers.set(registration, 'visited'); markers.set(registration, 'visited');
@ -287,7 +300,7 @@ export class FixturePool {
for (const name of fixtureParameterNames(fn, location)) { for (const name of fixtureParameterNames(fn, location)) {
const registration = this.registrations.get(name); const registration = this.registrations.get(name);
if (!registration) if (!registration)
throw errorWithLocations(`${prefix} has unknown parameter "${name}".`, { location, name: prefix, quoted: false }); addFatalError(`${prefix} has unknown parameter "${name}".`, location);
} }
} }
@ -421,7 +434,7 @@ function innerFixtureParameterNames(fn: Function, location: Location): string[]
return []; return [];
const [firstParam] = splitByComma(trimmedParams); const [firstParam] = splitByComma(trimmedParams);
if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}') if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}')
throw errorWithLocations('First argument must use the object destructuring pattern: ' + firstParam, { location }); addFatalError('First argument must use the object destructuring pattern: ' + firstParam, location);
const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => { const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => {
const colon = prop.indexOf(':'); const colon = prop.indexOf(':');
return colon === -1 ? prop : prop.substring(0, colon).trim(); return colon === -1 ? prop : prop.substring(0, colon).trim();
@ -469,15 +482,3 @@ function registrationId(registration: FixtureRegistration): string {
registration.id = map.get(registration.fn)!; registration.id = map.get(registration.fn)!;
return registration.id; return registration.id;
} }
function errorWithLocations(message: string, ...defined: { location: Location, name?: string, quoted?: boolean }[]): Error {
for (const { name, location, quoted } of defined) {
let prefix = '';
if (name && quoted === false)
prefix = name + ' ';
else if (name)
prefix = `"${name}" `;
message += `\n ${prefix}defined at ${formatLocation(location)}`;
}
return new Error(message);
}

View file

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

View file

@ -44,6 +44,7 @@ import { Suite } from './test';
import type { Config, FullConfigInternal, FullProjectInternal, ReporterInternal } from './types'; import type { Config, FullConfigInternal, FullProjectInternal, ReporterInternal } from './types';
import { createFileMatcher, createFileMatcherFromFilters, createTitleMatcher, serializeError } from './util'; import { createFileMatcher, createFileMatcherFromFilters, createTitleMatcher, serializeError } from './util';
import type { Matcher, TestFileFilter } from './util'; import type { Matcher, TestFileFilter } from './util';
import { setFatalErrorSink } from './globals';
const removeFolderAsync = promisify(rimraf); const removeFolderAsync = promisify(rimraf);
const readDirAsync = promisify(fs.readdir); const readDirAsync = promisify(fs.readdir);
@ -81,10 +82,12 @@ export class Runner {
private _loader: Loader; private _loader: Loader;
private _reporter!: ReporterInternal; private _reporter!: ReporterInternal;
private _plugins: TestRunnerPlugin[] = []; private _plugins: TestRunnerPlugin[] = [];
private _fatalErrors: TestError[] = [];
constructor(configCLIOverrides?: ConfigCLIOverrides) { constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._loader = new Loader(configCLIOverrides); this._loader = new Loader(configCLIOverrides);
setRunnerToAddPluginsTo(this); setRunnerToAddPluginsTo(this);
setFatalErrorSink(this._fatalErrors);
} }
addPlugin(plugin: TestRunnerPlugin) { addPlugin(plugin: TestRunnerPlugin) {
@ -275,7 +278,7 @@ export class Runner {
return { filesByProject, setupFiles }; return { filesByProject, setupFiles };
} }
private async _collectTestGroups(options: RunOptions, fatalErrors: TestError[]): Promise<{ rootSuite: Suite, projectSetupGroups: TestGroup[], testGroups: TestGroup[] }> { private async _collectTestGroups(options: RunOptions): Promise<{ rootSuite: Suite, projectSetupGroups: TestGroup[], testGroups: TestGroup[] }> {
const config = this._loader.fullConfig(); const config = this._loader.fullConfig();
const projects = this._collectProjects(options.projectFilter); const projects = this._collectProjects(options.projectFilter);
const { filesByProject, setupFiles } = await this._collectFiles(projects, options.testFileFilters); const { filesByProject, setupFiles } = await this._collectFiles(projects, options.testFileFilters);
@ -292,7 +295,7 @@ export class Runner {
result = await this._createFilteredRootSuite(options, filesByProject, setupFiles, false, setupFiles); result = await this._createFilteredRootSuite(options, filesByProject, setupFiles, false, setupFiles);
} }
fatalErrors.push(...result.fatalErrors); this._fatalErrors.push(...result.fatalErrors);
const { rootSuite } = result; const { rootSuite } = result;
const allTestGroups = createTestGroups(rootSuite.suites, config.workers); const allTestGroups = createTestGroups(rootSuite.suites, config.workers);
@ -442,14 +445,13 @@ export class Runner {
private async _run(options: RunOptions): Promise<FullResult> { private async _run(options: RunOptions): Promise<FullResult> {
const config = this._loader.fullConfig(); const config = this._loader.fullConfig();
const fatalErrors: TestError[] = [];
// Each entry is an array of test groups that can be run concurrently. All // Each entry is an array of test groups that can be run concurrently. All
// test groups from the previos entries must finish before entry starts. // test groups from the previos entries must finish before entry starts.
const { rootSuite, projectSetupGroups, testGroups } = await this._collectTestGroups(options, fatalErrors); const { rootSuite, projectSetupGroups, testGroups } = await this._collectTestGroups(options);
// Fail when no tests. // Fail when no tests.
if (!rootSuite.allTests().length && !options.passWithNoTests) if (!rootSuite.allTests().length && !options.passWithNoTests)
fatalErrors.push(createNoTestsError()); this._fatalErrors.push(createNoTestsError());
this._filterForCurrentShard(rootSuite, projectSetupGroups, testGroups); this._filterForCurrentShard(rootSuite, projectSetupGroups, testGroups);
@ -459,8 +461,8 @@ export class Runner {
this._reporter.onBegin?.(config, rootSuite); this._reporter.onBegin?.(config, rootSuite);
// Bail out on errors prior to running global setup. // Bail out on errors prior to running global setup.
if (fatalErrors.length) { if (this._fatalErrors.length) {
for (const error of fatalErrors) for (const error of this._fatalErrors)
this._reporter.onError?.(error); this._reporter.onError?.(error);
return { status: 'failed' }; return { status: 'failed' };
} }

View file

@ -15,11 +15,11 @@
*/ */
import { expect } from './expect'; import { expect } from './expect';
import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals'; import { currentlyLoadingFileSuite, currentTestInfo, addFatalError, setCurrentlyLoadingFileSuite } from './globals';
import { TestCase, Suite } from './test'; import { TestCase, Suite } from './test';
import { wrapFunctionWithLocation } from './transform'; import { wrapFunctionWithLocation } from './transform';
import type { Fixtures, FixturesWithLocation, Location, TestType } from './types'; import type { Fixtures, FixturesWithLocation, Location, TestType } from './types';
import { errorWithLocation, serializeError } from './util'; import { serializeError } from './util';
const testTypeSymbol = Symbol('testType'); const testTypeSymbol = Symbol('testType');
@ -67,29 +67,33 @@ export class TestTypeImpl {
this.test = test; this.test = test;
} }
private _ensureCurrentSuite(location: Location, title: string): Suite { private _currentSuite(location: Location, title: string): Suite | undefined {
const suite = currentlyLoadingFileSuite(); const suite = currentlyLoadingFileSuite();
if (!suite) { if (!suite) {
throw errorWithLocation(location, [ addFatalError([
`Playwright Test did not expect ${title} to be called here.`, `Playwright Test did not expect ${title} to be called here.`,
`Most common reasons include:`, `Most common reasons include:`,
`- You are calling ${title} in a configuration file.`, `- You are calling ${title} in a configuration file.`,
`- You are calling ${title} in a file that is imported by the 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`, `- You have two different versions of @playwright/test. This usually happens`,
` when one of the dependencies in your package.json depends on @playwright/test.`, ` when one of the dependencies in your package.json depends on @playwright/test.`,
].join('\n')); ].join('\n'), location);
return;
} }
if (this._projectSetup !== suite._isProjectSetup) { if (this._projectSetup !== suite._isProjectSetup) {
if (this._projectSetup) if (this._projectSetup)
throw errorWithLocation(location, `${title} is called in a file which is not a part of project setup.`); addFatalError(`${title} is called in a file which is not a part of project setup.`, location);
throw errorWithLocation(location, `${title} is called in a project setup file (use '_setup' instead of 'test').`); else
addFatalError(`${title} is called in a project setup file (use '_setup' instead of 'test').`, location);
} }
return suite; return suite;
} }
private _createTest(type: 'default' | 'only' | 'skip' | 'fixme', location: Location, title: string, fn: Function) { private _createTest(type: 'default' | 'only' | 'skip' | 'fixme', location: Location, title: string, fn: Function) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
const suite = this._ensureCurrentSuite(location, this._projectSetup ? '_setup()' : 'test()'); const suite = this._currentSuite(location, this._projectSetup ? '_setup()' : 'test()');
if (!suite)
return;
const test = new TestCase(title, fn, this, location); const test = new TestCase(title, fn, this, location);
test._requireFile = suite._requireFile; test._requireFile = suite._requireFile;
test._isProjectSetup = suite._isProjectSetup; test._isProjectSetup = suite._isProjectSetup;
@ -109,7 +113,9 @@ export class TestTypeImpl {
private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, title: string | Function, fn?: Function) { private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, title: string | Function, fn?: Function) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
const suite = this._ensureCurrentSuite(location, this._projectSetup ? 'setup.describe()' : 'test.describe()'); const suite = this._currentSuite(location, this._projectSetup ? 'setup.describe()' : 'test.describe()');
if (!suite)
return;
if (typeof title === 'function') { if (typeof title === 'function') {
fn = title; fn = title;
@ -135,7 +141,7 @@ export class TestTypeImpl {
for (let parent: Suite | undefined = suite; parent; parent = parent.parent) { for (let parent: Suite | undefined = suite; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel') if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel')
throw errorWithLocation(location, 'describe.parallel cannot be nested inside describe.serial'); addFatalError('describe.parallel cannot be nested inside describe.serial', location);
} }
setCurrentlyLoadingFileSuite(child); setCurrentlyLoadingFileSuite(child);
@ -144,13 +150,17 @@ export class TestTypeImpl {
} }
private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, fn: Function) { private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, fn: Function) {
const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.${name}()`); const suite = this._currentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.${name}()`);
if (!suite)
return;
suite._hooks.push({ type: name, fn, location }); suite._hooks.push({ type: name, fn, location });
} }
private _configure(location: Location, options: { mode?: 'parallel' | 'serial', retries?: number, timeout?: number }) { private _configure(location: Location, options: { mode?: 'parallel' | 'serial', retries?: number, timeout?: number }) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.describe.configure()`); const suite = this._currentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.describe.configure()`);
if (!suite)
return;
if (options.timeout !== undefined) if (options.timeout !== undefined)
suite._timeout = options.timeout; suite._timeout = options.timeout;
@ -160,11 +170,11 @@ export class TestTypeImpl {
if (options.mode !== undefined) { if (options.mode !== undefined) {
if (suite._parallelMode !== 'default') if (suite._parallelMode !== 'default')
throw errorWithLocation(location, 'Parallel mode is already assigned for the enclosing scope.'); addFatalError('Parallel mode is already assigned for the enclosing scope.', location);
suite._parallelMode = options.mode; suite._parallelMode = options.mode;
for (let parent: Suite | undefined = suite.parent; parent; parent = parent.parent) { for (let parent: Suite | undefined = suite.parent; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial' && suite._parallelMode === 'parallel') if (parent._parallelMode === 'serial' && suite._parallelMode === 'parallel')
throw errorWithLocation(location, 'describe.parallel cannot be nested inside describe.serial'); addFatalError('describe.parallel cannot be nested inside describe.serial', location);
} }
} }
} }
@ -190,10 +200,12 @@ export class TestTypeImpl {
} }
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo) {
throw errorWithLocation(location, `test.${type}() can only be called inside test, describe block or fixture`); addFatalError(`test.${type}() can only be called inside test, describe block or fixture`, location);
return;
}
if (typeof modifierArgs[0] === 'function') if (typeof modifierArgs[0] === 'function')
throw errorWithLocation(location, `test.${type}() with a function can only be called inside describe block`); addFatalError(`test.${type}() with a function can only be called inside describe block`, location);
testInfo[type](...modifierArgs as [any, any]); testInfo[type](...modifierArgs as [any, any]);
} }
@ -205,20 +217,26 @@ export class TestTypeImpl {
} }
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo) {
throw errorWithLocation(location, `test.setTimeout() can only be called from a test`); addFatalError(`test.setTimeout() can only be called from a test`, location);
return;
}
testInfo.setTimeout(timeout); testInfo.setTimeout(timeout);
} }
private _use(location: Location, fixtures: Fixtures) { private _use(location: Location, fixtures: Fixtures) {
const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.use()`); const suite = this._currentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.use()`);
if (!suite)
return;
suite._use.push({ fixtures, location }); suite._use.push({ fixtures, location });
} }
private async _step<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> { private async _step<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo) {
throw errorWithLocation(location, `${this._projectSetup ? '_setup' : 'test'}.step() can only be called from a test`); addFatalError(`${this._projectSetup ? '_setup' : 'test'}.step() can only be called from a test`, location);
return undefined as any;
}
const step = testInfo._addStep({ const step = testInfo._addStep({
category: 'test.step', category: 'test.step',
title, title,

View file

@ -193,10 +193,6 @@ export function errorWithFile(file: string, message: string) {
return new Error(`${relativeFilePath(file)}: ${message}`); return new Error(`${relativeFilePath(file)}: ${message}`);
} }
export function errorWithLocation(location: Location, message: string) {
return new Error(`${formatLocation(location)}: ${message}`);
}
export function expectTypes(receiver: any, types: string[], matcherName: string) { export function expectTypes(receiver: any, types: string[], matcherName: string) {
if (typeof receiver !== 'object' || !types.includes(receiver.constructor.name)) { if (typeof receiver !== 'object' || !types.includes(receiver.constructor.name)) {
const commaSeparated = types.slice(); const commaSeparated = types.slice();

View file

@ -128,7 +128,8 @@ test('should throw when using non-defined super worker fixture', async ({ runInl
` `
}); });
expect(result.output).toContain(`Fixture "foo" references itself, but does not have a base implementation.`); expect(result.output).toContain(`Fixture "foo" references itself, but does not have a base implementation.`);
expect(result.output).toContain('a.spec.ts:5:29'); expect(result.output).toContain('a.spec.ts:5');
expect(stripAnsi(result.output)).toContain('const test = pwt.test.extend');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -149,9 +150,9 @@ test('should throw when defining test fixture with the same name as a worker fix
test2('works', async ({foo}) => {}); test2('works', async ({foo}) => {});
`, `,
}); });
expect(result.output).toContain(`Fixture "foo" has already been registered as a { scope: 'worker' } fixture.`); expect(result.output).toContain(`Fixture "foo" has already been registered as a { scope: 'worker' } fixture defined in e.spec.ts:5:30.`);
expect(result.output).toContain(`e.spec.ts:10`); expect(result.output).toContain(`e.spec.ts:10`);
expect(result.output).toContain(`e.spec.ts:5`); expect(stripAnsi(result.output)).toContain('const test2 = test1.extend');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -172,9 +173,9 @@ test('should throw when defining worker fixture with the same name as a test fix
test2('works', async ({foo}) => {}); test2('works', async ({foo}) => {});
`, `,
}); });
expect(result.output).toContain(`Fixture "foo" has already been registered as a { scope: 'test' } fixture.`); expect(result.output).toContain(`Fixture "foo" has already been registered as a { scope: 'test' } fixture defined in e.spec.ts:5:30.`);
expect(result.output).toContain(`e.spec.ts:10`); expect(result.output).toContain(`e.spec.ts:10`);
expect(result.output).toContain(`e.spec.ts:5`); expect(stripAnsi(result.output)).toContain('const test2 = test1.extend');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -194,8 +195,7 @@ test('should throw when worker fixture depends on a test fixture', async ({ runI
test('works', async ({bar}) => {}); test('works', async ({bar}) => {});
`, `,
}); });
expect(result.output).toContain('worker fixture "bar" cannot depend on a test fixture "foo".'); expect(result.output).toContain('worker fixture "bar" cannot depend on a test fixture "foo" defined in f.spec.ts:5:29.');
expect(result.output).toContain(`f.spec.ts:5`);
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -239,12 +239,8 @@ test('should detect fixture dependency cycle', async ({ runInlineTest }) => {
test('works', async ({foo}) => {}); test('works', async ({foo}) => {});
`, `,
}); });
expect(result.output).toContain('Fixtures "bar" -> "baz" -> "qux" -> "foo" -> "bar" form a dependency cycle.'); expect(result.output).toContain('Fixtures "bar" -> "baz" -> "qux" -> "foo" -> "bar" form a dependency cycle:');
expect(result.output).toContain('"foo" defined at'); expect(result.output).toContain('x.spec.ts:5:29 -> x.spec.ts:5:29 -> x.spec.ts:5:29 -> x.spec.ts:5:29');
expect(result.output).toContain('"bar" defined at');
expect(result.output).toContain('"baz" defined at');
expect(result.output).toContain('"qux" defined at');
expect(result.output).toContain('x.spec.ts:5:29');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -261,7 +257,8 @@ test('should not reuse fixtures from one file in another one', async ({ runInlin
`, `,
}); });
expect(result.output).toContain('Test has unknown parameter "foo".'); expect(result.output).toContain('Test has unknown parameter "foo".');
expect(result.output).toContain('b.spec.ts:7:7'); expect(result.output).toContain('b.spec.ts:7');
expect(stripAnsi(result.output)).toContain(`test('test2', async ({foo}) => {})`);
}); });
test('should throw for cycle in two overrides', async ({ runInlineTest }) => { test('should throw for cycle in two overrides', async ({ runInlineTest }) => {
@ -283,9 +280,8 @@ test('should throw for cycle in two overrides', async ({ runInlineTest }) => {
}); });
`, `,
}); });
expect(result.output).toContain('Fixtures "bar" -> "foo" -> "bar" form a dependency cycle.'); expect(result.output).toContain('Fixtures "bar" -> "foo" -> "bar" form a dependency cycle:');
expect(result.output).toContain('a.test.js:9'); expect(result.output).toContain('a.test.js:12:27 -> a.test.js:9:27');
expect(result.output).toContain('a.test.js:12');
}); });
test('should throw when overridden worker fixture depends on a test fixture', async ({ runInlineTest }) => { test('should throw when overridden worker fixture depends on a test fixture', async ({ runInlineTest }) => {
@ -302,7 +298,8 @@ test('should throw when overridden worker fixture depends on a test fixture', as
test2('works', async ({bar}) => {}); test2('works', async ({bar}) => {});
`, `,
}); });
expect(result.output).toContain('worker fixture "bar" cannot depend on a test fixture "foo".'); expect(result.output).toContain('worker fixture "bar" cannot depend on a test fixture "foo" defined in f.spec.ts:5:30.');
expect(result.output).toContain('f.spec.ts:9');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -317,7 +314,8 @@ test('should throw for unknown fixture parameter', async ({ runInlineTest }) =>
`, `,
}); });
expect(result.output).toContain('Fixture "foo" has unknown parameter "bar".'); expect(result.output).toContain('Fixture "foo" has unknown parameter "bar".');
expect(result.output).toContain('f.spec.ts:5:29'); expect(result.output).toContain('f.spec.ts:5');
expect(stripAnsi(result.output)).toContain('const test = pwt.test.extend');
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
@ -386,7 +384,7 @@ test('should error for unsupported scope', async ({ runInlineTest }) => {
` `
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Error: Fixture "failure" has unknown { scope: 'foo' }`); expect(result.output).toContain(`Fixture "failure" has unknown { scope: 'foo' }`);
}); });
test('should give enough time for fixture teardown', async ({ runInlineTest }) => { test('should give enough time for fixture teardown', async ({ runInlineTest }) => {

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { test, expect } from './playwright-test-fixtures'; import { test, expect, stripAnsi } from './playwright-test-fixtures';
test('should work', async ({ runInlineTest }) => { test('should work', async ({ runInlineTest }) => {
const { results } = await runInlineTest({ const { results } = await runInlineTest({
@ -167,7 +167,8 @@ test('should fail if parameters are not destructured', async ({ runInlineTest })
`, `,
}); });
expect(result.output).toContain('First argument must use the object destructuring pattern: abc'); expect(result.output).toContain('First argument must use the object destructuring pattern: abc');
expect(result.output).toContain('a.test.js:11:7'); expect(result.output).toContain('a.test.js:11');
expect(stripAnsi(result.output)).toContain('function (abc)');
expect(result.results.length).toBe(0); expect(result.results.length).toBe(0);
}); });
@ -180,7 +181,8 @@ test('should fail with an unknown fixture', async ({ runInlineTest }) => {
`, `,
}); });
expect(result.output).toContain('Test has unknown parameter "asdf".'); expect(result.output).toContain('Test has unknown parameter "asdf".');
expect(result.output).toContain('a.test.js:5:11'); expect(result.output).toContain('a.test.js:5');
expect(stripAnsi(result.output)).toContain('async ({asdf})');
expect(result.results.length).toBe(0); expect(result.results.length).toBe(0);
}); });

View file

@ -27,7 +27,8 @@ test('test.describe.parallel should throw inside test.describe.serial', async ({
`, `,
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.output).toContain('a.test.ts:7:23: describe.parallel cannot be nested inside describe.serial'); expect(result.output).toContain('Error: describe.parallel cannot be nested inside describe.serial');
expect(result.output).toContain('a.test.ts:7');
}); });
test('test.describe.parallel should work', async ({ runInlineTest }) => { test('test.describe.parallel should work', async ({ runInlineTest }) => {

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { test, expect } from './playwright-test-fixtures'; import { test, expect, stripAnsi } from './playwright-test-fixtures';
test('should merge options', async ({ runInlineTest }) => { test('should merge options', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
@ -89,8 +89,8 @@ test('should throw when setting worker options in describe', async ({ runInlineT
expect(result.output).toContain([ expect(result.output).toContain([
`Cannot use({ foo }) in a describe group, because it forces a new worker.`, `Cannot use({ foo }) in a describe group, because it forces a new worker.`,
`Make it top-level in the test file or put in the configuration file.`, `Make it top-level in the test file or put in the configuration file.`,
` "foo" defined at a.test.ts:9:14`,
].join('\n')); ].join('\n'));
expect(stripAnsi(result.output)).toContain(`{ foo: 'bar' }`);
}); });
test('should run tests with different worker options', async ({ runInlineTest }) => { test('should run tests with different worker options', async ({ runInlineTest }) => {

View file

@ -73,7 +73,7 @@ test('should respect test.setTimeout outside of the test', async ({ runInlineTes
'a.spec.ts': ` 'a.spec.ts': `
const { test } = pwt; const { test } = pwt;
test.setTimeout(500); test.setTimeout(1000);
test('fails', async ({}) => { test('fails', async ({}) => {
await new Promise(f => setTimeout(f, 1000)); await new Promise(f => setTimeout(f, 1000));
}); });
@ -94,7 +94,7 @@ test('should respect test.setTimeout outside of the test', async ({ runInlineTes
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(2); expect(result.failed).toBe(2);
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
expect(result.output).toContain('Test timeout of 500ms exceeded.'); expect(result.output).toContain('Test timeout of 1000ms exceeded.');
}); });
test('should timeout when calling test.setTimeout too late', async ({ runInlineTest }) => { test('should timeout when calling test.setTimeout too late', async ({ runInlineTest }) => {