chore: report more fatal errors via reporter (#19640)
This commit is contained in:
parent
8b80e22a03
commit
233664bd30
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue