feat(breaking): always report onBegin/onEnd, report file errors (#11758)

This commit is contained in:
Pavel Feldman 2022-01-31 17:09:04 -08:00 committed by GitHub
parent 1dc0ddffce
commit 2b55adaafa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 240 additions and 207 deletions

View File

@ -24,6 +24,11 @@ Reporter is given a root suite in the [`method: Reporter.onBegin`] method.
Returns the list of all test cases in this suite and its descendants, as opposite to [`property: Suite.tests`]. Returns the list of all test cases in this suite and its descendants, as opposite to [`property: Suite.tests`].
## property: Suite.loadError
- type: <[void]|[TestError]>
For file suites, contains errors that occurred while loading this file.
## property: Suite.location ## property: Suite.location
- type: <[void]|[Location]> - type: <[void]|[Location]>

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { formatLocation, wrapInPromise, debugTest } from './util'; import { formatLocation, debugTest } from './util';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { FixturesWithLocation, Location, WorkerInfo, TestInfo } from './types'; import { FixturesWithLocation, Location, WorkerInfo, TestInfo } from './types';
import { ManualPromise } from 'playwright-core/lib/utils/async'; import { ManualPromise } from 'playwright-core/lib/utils/async';
@ -78,7 +78,7 @@ class Fixture {
await this._useFuncFinished; await this._useFuncFinished;
}; };
const info = this.registration.scope === 'worker' ? workerInfo : testInfo; const info = this.registration.scope === 'worker' ? workerInfo : testInfo;
this._selfTeardownComplete = wrapInPromise(this.registration.fn(params, useFunc, info)).catch((e: any) => { this._selfTeardownComplete = Promise.resolve().then(() => this.registration.fn(params, useFunc, info)).catch((e: any) => {
if (!useFuncStarted.isDone()) if (!useFuncStarted.isDone())
useFuncStarted.reject(e); useFuncStarted.reject(e);
else else

View File

@ -27,6 +27,7 @@ import { ProjectImpl } from './project';
import { Reporter } from '../types/testReporter'; import { Reporter } from '../types/testReporter';
import { BuiltInReporter, builtInReporters } from './runner'; import { BuiltInReporter, builtInReporters } from './runner';
import { isRegExp } from 'playwright-core/lib/utils/utils'; import { isRegExp } from 'playwright-core/lib/utils/utils';
import { serializeError } from './util';
// To allow multiple loaders in the same process without clearing require cache, // To allow multiple loaders in the same process without clearing require cache,
// we make these maps global. // we make these maps global.
@ -113,20 +114,25 @@ export class Loader {
this._fullConfig.projects = this._projects.map(p => p.config); this._fullConfig.projects = this._projects.map(p => p.config);
} }
async loadTestFile(file: string) { async loadTestFile(file: string, environment: 'runner' | 'worker') {
if (cachedFileSuites.has(file)) if (cachedFileSuites.has(file))
return cachedFileSuites.get(file)!; return cachedFileSuites.get(file)!;
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file));
suite._requireFile = file;
suite.location = { file, line: 0, column: 0 };
setCurrentlyLoadingFileSuite(suite);
try { try {
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file));
suite._requireFile = file;
suite.location = { file, line: 0, column: 0 };
setCurrentlyLoadingFileSuite(suite);
await this._requireOrImport(file); await this._requireOrImport(file);
cachedFileSuites.set(file, suite); cachedFileSuites.set(file, suite);
return suite; } catch (e) {
if (environment === 'worker')
throw e;
suite.loadError = serializeError(e);
} finally { } finally {
setCurrentlyLoadingFileSuite(undefined); setCurrentlyLoadingFileSuite(undefined);
} }
return suite;
} }
async loadGlobalHook(file: string, name: string): Promise<(config: FullConfig) => any> { async loadGlobalHook(file: string, name: string): Promise<(config: FullConfig) => any> {

View File

@ -34,37 +34,37 @@ export class Multiplexer implements Reporter {
onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
reporter.onTestBegin?.(test, result); wrap(() => reporter.onTestBegin?.(test, result));
} }
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
reporter.onStdOut?.(chunk, test, result); wrap(() => reporter.onStdOut?.(chunk, test, result));
} }
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
reporter.onStdErr?.(chunk, test, result); wrap(() => reporter.onStdErr?.(chunk, test, result));
} }
onTestEnd(test: TestCase, result: TestResult) { onTestEnd(test: TestCase, result: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
reporter.onTestEnd?.(test, result); wrap(() => reporter.onTestEnd?.(test, result));
} }
async onEnd(result: FullResult) { async onEnd(result: FullResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
await reporter.onEnd?.(result); await Promise.resolve().then(() => reporter.onEnd?.(result)).catch(e => console.error('Error in reporter', e));
} }
onError(error: TestError) { onError(error: TestError) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
reporter.onError?.(error); wrap(() => reporter.onError?.(error));
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep) { onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
(reporter as any).onStepBegin?.(test, result, step); wrap(() => (reporter as any).onStepBegin?.(test, result, step));
} }
onStepEnd(test: TestCase, result: TestResult, step: TestStep) { onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
@ -72,3 +72,11 @@ export class Multiplexer implements Reporter {
(reporter as any).onStepEnd?.(test, result, step); (reporter as any).onStepEnd?.(test, result, step);
} }
} }
function wrap(callback: () => void) {
try {
callback();
} catch (e) {
console.error('Error in reporter', e);
}
}

View File

@ -20,7 +20,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { promisify } from 'util'; import { promisify } from 'util';
import { Dispatcher, TestGroup } from './dispatcher'; import { Dispatcher, TestGroup } from './dispatcher';
import { createFileMatcher, createTitleMatcher, FilePatternFilter, serializeError } from './util'; import { createFileMatcher, createTitleMatcher, FilePatternFilter } from './util';
import { TestCase, Suite } from './test'; import { TestCase, Suite } from './test';
import { Loader } from './loader'; import { Loader } from './loader';
import { FullResult, Reporter, TestError } from '../types/testReporter'; import { FullResult, Reporter, TestError } from '../types/testReporter';
@ -39,6 +39,7 @@ import { Config, FullConfig } from './types';
import { WebServer } from './webServer'; import { WebServer } from './webServer';
import { raceAgainstTimeout } from 'playwright-core/lib/utils/async'; import { raceAgainstTimeout } from 'playwright-core/lib/utils/async';
import { SigIntWatcher } from 'playwright-core/lib/utils/utils'; import { SigIntWatcher } from 'playwright-core/lib/utils/utils';
import { serializeError } from './util';
const removeFolderAsync = promisify(rimraf); const removeFolderAsync = promisify(rimraf);
const readDirAsync = promisify(fs.readdir); const readDirAsync = promisify(fs.readdir);
@ -56,7 +57,6 @@ type RunOptions = {
export class Runner { export class Runner {
private _loader: Loader; private _loader: Loader;
private _reporter!: Reporter; private _reporter!: Reporter;
private _didBegin = false;
private _internalGlobalSetups: Array<InternalGlobalSetupFunction> = []; private _internalGlobalSetups: Array<InternalGlobalSetupFunction> = [];
constructor(configOverrides: Config, options: { defaultConfig?: Config } = {}) { constructor(configOverrides: Config, options: { defaultConfig?: Config } = {}) {
@ -135,34 +135,24 @@ export class Runner {
async runAllTests(options: RunOptions = {}): Promise<FullResult> { async runAllTests(options: RunOptions = {}): Promise<FullResult> {
this._reporter = await this._createReporter(!!options.listOnly); this._reporter = await this._createReporter(!!options.listOnly);
try { const config = this._loader.fullConfig();
const config = this._loader.fullConfig(); const result = await raceAgainstTimeout(() => this._run(!!options.listOnly, options.filePatternFilter || [], options.projectFilter), config.globalTimeout);
const result = await raceAgainstTimeout(() => this._run(!!options.listOnly, options.filePatternFilter || [], options.projectFilter), config.globalTimeout); let fullResult: FullResult;
if (result.timedOut) { if (result.timedOut) {
const actualResult: FullResult = { status: 'timedout' }; this._reporter.onError?.(createStacklessError(`Timed out waiting ${config.globalTimeout / 1000}s for the entire test run`));
if (this._didBegin) fullResult = { status: 'timedout' };
await this._reporter.onEnd?.(actualResult); } else {
else fullResult = result.result;
this._reporter.onError?.(createStacklessError(`Timed out waiting ${config.globalTimeout / 1000}s for the entire test run`));
return actualResult;
} else if (this._didBegin) {
await this._reporter.onEnd?.(result.result);
}
return result.result;
} catch (e) {
const result: FullResult = { status: 'failed' };
try {
this._reporter.onError?.(serializeError(e));
} catch (ignored) {
}
return result;
} finally {
// Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456.
// See https://github.com/nodejs/node/issues/12921
await new Promise<void>(resolve => process.stdout.write('', () => resolve()));
await new Promise<void>(resolve => process.stderr.write('', () => resolve()));
} }
await this._reporter.onEnd?.(fullResult);
// Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456.
// See https://github.com/nodejs/node/issues/12921
await new Promise<void>(resolve => process.stdout.write('', () => resolve()));
await new Promise<void>(resolve => process.stderr.write('', () => resolve()));
return fullResult;
} }
async listAllTestFiles(config: Config, projectNames: string[] | undefined): Promise<any> { async listAllTestFiles(config: Config, projectNames: string[] | undefined): Promise<any> {
@ -237,148 +227,194 @@ export class Runner {
files.forEach(file => allTestFiles.add(file)); files.forEach(file => allTestFiles.add(file));
const config = this._loader.fullConfig(); const config = this._loader.fullConfig();
const fatalErrors: TestError[] = [];
// 1. Add all tests.
const preprocessRoot = new Suite('');
for (const file of allTestFiles) {
const fileSuite = await this._loader.loadTestFile(file, 'runner');
if (fileSuite.loadError)
fatalErrors.push(fileSuite.loadError);
preprocessRoot._addSuite(fileSuite);
}
// 2. Filter tests to respect column filter.
filterByFocusedLine(preprocessRoot, testFileReFilters);
// 3. Complain about only.
if (config.forbidOnly) {
const onlyTestsAndSuites = preprocessRoot._getOnlyItems();
if (onlyTestsAndSuites.length > 0)
fatalErrors.push(createForbidOnlyError(config, onlyTestsAndSuites));
}
// 4. Filter only
if (!list)
filterOnly(preprocessRoot);
// 5. Complain about clashing.
const clashingTests = getClashingTestsPerSuite(preprocessRoot);
if (clashingTests.size > 0)
fatalErrors.push(createDuplicateTitlesError(config, clashingTests));
// 6. Generate projects.
const fileSuites = new Map<string, Suite>();
for (const fileSuite of preprocessRoot.suites)
fileSuites.set(fileSuite._requireFile, fileSuite);
const outputDirs = new Set<string>();
const grepMatcher = createTitleMatcher(config.grep);
const grepInvertMatcher = config.grepInvert ? createTitleMatcher(config.grepInvert) : null;
const rootSuite = new Suite('');
for (const [project, files] of filesByProject) {
const projectSuite = new Suite(project.config.name);
projectSuite._projectConfig = project.config;
rootSuite._addSuite(projectSuite);
for (const file of files) {
const fileSuite = fileSuites.get(file);
if (!fileSuite)
continue;
for (let repeatEachIndex = 0; repeatEachIndex < project.config.repeatEach; repeatEachIndex++) {
const cloned = project.cloneFileSuite(fileSuite, repeatEachIndex, test => {
const grepTitle = test.titlePath().join(' ');
if (grepInvertMatcher?.(grepTitle))
return false;
return grepMatcher(grepTitle);
});
if (cloned)
projectSuite._addSuite(cloned);
}
}
outputDirs.add(project.config.outputDir);
}
// 7. Fail when no tests.
let total = rootSuite.allTests().length;
if (!total)
fatalErrors.push(createNoTestsError());
// 8. Fail when output fails.
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(e => {
fatalErrors.push(serializeError(e));
})));
// 9. Compute shards.
let testGroups = createTestGroups(rootSuite);
const shard = config.shard;
if (shard) {
const shardGroups: TestGroup[] = [];
const shardTests = new Set<TestCase>();
// Each shard gets some tests.
const shardSize = Math.floor(total / shard.total);
// First few shards get one more test each.
const extraOne = total - shardSize * shard.total;
const currentShard = shard.current - 1; // Make it zero-based for calculations.
const from = shardSize * currentShard + Math.min(extraOne, currentShard);
const to = from + shardSize + (currentShard < extraOne ? 1 : 0);
let current = 0;
for (const group of testGroups) {
// Any test group goes to the shard that contains the first test of this group.
// So, this shard gets any group that starts at [from; to)
if (current >= from && current < to) {
shardGroups.push(group);
for (const test of group.tests)
shardTests.add(test);
}
current += group.tests.length;
}
testGroups = shardGroups;
filterSuiteWithOnlySemantics(rootSuite, () => false, test => shardTests.has(test));
total = rootSuite.allTests().length;
}
(config as any).__testGroupsCount = testGroups.length;
// 10. Report begin
this._reporter.onBegin?.(config, rootSuite);
// 11. Bail out on errors prior to running global setup.
if (fatalErrors.length) {
for (const error of fatalErrors)
this._reporter.onError?.(error);
return { status: 'failed' };
}
// 12. Bail out if list mode only, don't do any work.
if (list)
return { status: 'passed' };
// 13. Declare global setup to tear down in finally.
const internalGlobalTeardowns: (() => Promise<void>)[] = []; const internalGlobalTeardowns: (() => Promise<void>)[] = [];
if (!list) { let webServer: WebServer | undefined;
let globalSetupResult: any;
const result: FullResult = { status: 'passed' };
try {
// 14. Perform global setup.
for (const internalGlobalSetup of this._internalGlobalSetups) for (const internalGlobalSetup of this._internalGlobalSetups)
internalGlobalTeardowns.push(await internalGlobalSetup()); internalGlobalTeardowns.push(await internalGlobalSetup());
} webServer = config.webServer ? await WebServer.create(config.webServer) : undefined;
const webServer = (!list && config.webServer) ? await WebServer.create(config.webServer) : undefined; if (config.globalSetup)
let globalSetupResult: any; globalSetupResult = await (await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'))(this._loader.fullConfig());
if (config.globalSetup && !list)
globalSetupResult = await (await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'))(this._loader.fullConfig());
try {
// 1. Add all tests.
const preprocessRoot = new Suite('');
for (const file of allTestFiles)
preprocessRoot._addSuite(await this._loader.loadTestFile(file));
// 2. Filter tests to respect column filter.
filterByFocusedLine(preprocessRoot, testFileReFilters);
// 3. Complain about only.
if (config.forbidOnly) {
const onlyTestsAndSuites = preprocessRoot._getOnlyItems();
if (onlyTestsAndSuites.length > 0) {
this._reporter.onError?.(createForbidOnlyError(config, onlyTestsAndSuites));
return { status: 'failed' };
}
}
// 4. Filter only
if (!list)
filterOnly(preprocessRoot);
// 5. Complain about clashing.
const clashingTests = getClashingTestsPerSuite(preprocessRoot);
if (clashingTests.size > 0) {
this._reporter.onError?.(createDuplicateTitlesError(config, clashingTests));
return { status: 'failed' };
}
const fileSuites = new Map<string, Suite>();
for (const fileSuite of preprocessRoot.suites)
fileSuites.set(fileSuite._requireFile, fileSuite);
const outputDirs = new Set<string>();
const grepMatcher = createTitleMatcher(config.grep);
const grepInvertMatcher = config.grepInvert ? createTitleMatcher(config.grepInvert) : null;
const rootSuite = new Suite('');
for (const [project, files] of filesByProject) {
const projectSuite = new Suite(project.config.name);
projectSuite._projectConfig = project.config;
rootSuite._addSuite(projectSuite);
for (const file of files) {
const fileSuite = fileSuites.get(file);
if (!fileSuite)
continue;
for (let repeatEachIndex = 0; repeatEachIndex < project.config.repeatEach; repeatEachIndex++) {
const cloned = project.cloneFileSuite(fileSuite, repeatEachIndex, test => {
const grepTitle = test.titlePath().join(' ');
if (grepInvertMatcher?.(grepTitle))
return false;
return grepMatcher(grepTitle);
});
if (cloned)
projectSuite._addSuite(cloned);
}
}
outputDirs.add(project.config.outputDir);
}
let total = rootSuite.allTests().length;
if (!total) {
this._reporter.onError?.(createNoTestsError());
return { status: 'failed' };
}
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(e => {})));
let testGroups = createTestGroups(rootSuite);
const shard = config.shard;
if (shard) {
const shardGroups: TestGroup[] = [];
const shardTests = new Set<TestCase>();
// Each shard gets some tests.
const shardSize = Math.floor(total / shard.total);
// First few shards get one more test each.
const extraOne = total - shardSize * shard.total;
const currentShard = shard.current - 1; // Make it zero-based for calculations.
const from = shardSize * currentShard + Math.min(extraOne, currentShard);
const to = from + shardSize + (currentShard < extraOne ? 1 : 0);
let current = 0;
for (const group of testGroups) {
// Any test group goes to the shard that contains the first test of this group.
// So, this shard gets any group that starts at [from; to)
if (current >= from && current < to) {
shardGroups.push(group);
for (const test of group.tests)
shardTests.add(test);
}
current += group.tests.length;
}
testGroups = shardGroups;
filterSuiteWithOnlySemantics(rootSuite, () => false, test => shardTests.has(test));
total = rootSuite.allTests().length;
}
(config as any).__testGroupsCount = testGroups.length;
const sigintWatcher = new SigIntWatcher(); const sigintWatcher = new SigIntWatcher();
this._reporter.onBegin?.(config, rootSuite);
this._didBegin = true;
let hasWorkerErrors = false; let hasWorkerErrors = false;
if (!list) { const dispatcher = new Dispatcher(this._loader, testGroups, this._reporter);
const dispatcher = new Dispatcher(this._loader, testGroups, this._reporter); await Promise.race([dispatcher.run(), sigintWatcher.promise()]);
await Promise.race([dispatcher.run(), sigintWatcher.promise()]); if (!sigintWatcher.hadSignal()) {
if (!sigintWatcher.hadSignal()) { // We know for sure there was no Ctrl+C, so we remove custom SIGINT handler
// We know for sure there was no Ctrl+C, so we remove custom SIGINT handler // as soon as we can.
// as soon as we can. sigintWatcher.disarm();
sigintWatcher.disarm();
}
await dispatcher.stop();
hasWorkerErrors = dispatcher.hasWorkerErrors();
} }
await dispatcher.stop();
hasWorkerErrors = dispatcher.hasWorkerErrors();
if (sigintWatcher.hadSignal()) { if (!sigintWatcher.hadSignal()) {
const result: FullResult = { status: 'interrupted' }; const failed = hasWorkerErrors || rootSuite.allTests().some(test => !test.ok());
return result; result.status = failed ? 'failed' : 'passed';
} else {
result.status = 'interrupted';
} }
} catch (e) {
const failed = hasWorkerErrors || rootSuite.allTests().some(test => !test.ok()); this._reporter.onError?.(serializeError(e));
const result: FullResult = { status: failed ? 'failed' : 'passed' }; return { status: 'failed' };
return result;
} finally { } finally {
if (globalSetupResult && typeof globalSetupResult === 'function')
await globalSetupResult(this._loader.fullConfig()); await this._runAndAssignError(async () => {
if (config.globalTeardown && !list) if (globalSetupResult && typeof globalSetupResult === 'function')
await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig()); await globalSetupResult(this._loader.fullConfig());
await webServer?.kill(); }, result);
for (const internalGlobalTeardown of internalGlobalTeardowns)
await internalGlobalTeardown(); await this._runAndAssignError(async () => {
if (config.globalTeardown)
await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig());
}, result);
await this._runAndAssignError(async () => {
await webServer?.kill();
}, result);
await this._runAndAssignError(async () => {
for (const internalGlobalTeardown of internalGlobalTeardowns)
await internalGlobalTeardown();
}, result);
}
return result;
}
private async _runAndAssignError(callback: () => Promise<void>, result: FullResult) {
try {
await callback();
} catch (e) {
if (result.status === 'passed')
result.status = 'failed';
this._reporter.onError?.(serializeError(e));
} }
} }
} }

View File

@ -40,6 +40,7 @@ export type Modifier = {
export class Suite extends Base implements reporterTypes.Suite { export class Suite extends Base implements reporterTypes.Suite {
suites: Suite[] = []; suites: Suite[] = [];
tests: TestCase[] = []; tests: TestCase[] = [];
loadError?: reporterTypes.TestError;
location?: Location; location?: Location;
parent?: Suite; parent?: Suite;
_use: FixturesWithLocation[] = []; _use: FixturesWithLocation[] = [];

View File

@ -106,10 +106,6 @@ export function mergeObjects<A extends object, B extends object>(a: A | undefine
return result as any; return result as any;
} }
export async function wrapInPromise(value: any) {
return value;
}
export function forceRegExp(pattern: string): RegExp { export function forceRegExp(pattern: string): RegExp {
const match = pattern.match(/^\/(.*)\/([gi]*)$/); const match = pattern.match(/^\/(.*)\/([gi]*)$/);
if (match) if (match)

View File

@ -125,7 +125,7 @@ export class WorkerRunner extends EventEmitter {
try { try {
this._entries = new Map(runPayload.entries.map(e => [ e.testId, e ])); this._entries = new Map(runPayload.entries.map(e => [ e.testId, e ]));
await this._loadIfNeeded(); await this._loadIfNeeded();
const fileSuite = await this._loader.loadTestFile(runPayload.file); const fileSuite = await this._loader.loadTestFile(runPayload.file, 'worker');
const suite = this._project.cloneFileSuite(fileSuite, this._params.repeatEachIndex, test => { const suite = this._project.cloneFileSuite(fileSuite, this._params.repeatEachIndex, test => {
if (!this._entries.has(test._id)) if (!this._entries.has(test._id))
return false; return false;

View File

@ -70,6 +70,10 @@ export interface Suite {
* group suite. * group suite.
*/ */
title: string; title: string;
/**
* For file suites, contains errors that occurred while loading this file.
*/
loadError?: TestError;
/** /**
* Location in the source where the suite is defined. Missing for root and project suites. * Location in the source where the suite is defined. Missing for root and project suites.
*/ */

View File

@ -106,38 +106,14 @@ test('globalTeardown does not run when globalSetup times out', async ({ runInlin
}); });
`, `,
}); });
// We did not collect tests, so everything should be zero. // We did not run tests, so we should only have 1 skipped test.
expect(result.skipped).toBe(0); expect(result.skipped).toBe(1);
expect(result.passed).toBe(0); expect(result.passed).toBe(0);
expect(result.failed).toBe(0); expect(result.failed).toBe(0);
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.output).not.toContain('teardown='); expect(result.output).not.toContain('teardown=');
}); });
test('globalSetup should be run before requiring tests', async ({ runInlineTest }) => {
const { passed } = await runInlineTest({
'playwright.config.ts': `
import * as path from 'path';
module.exports = {
globalSetup: './globalSetup.ts',
};
`,
'globalSetup.ts': `
module.exports = async () => {
process.env.FOO = JSON.stringify({ foo: 'bar' });
};
`,
'a.test.js': `
const { test } = pwt;
let value = JSON.parse(process.env.FOO);
test('should work', async ({}) => {
expect(value).toEqual({ foo: 'bar' });
});
`,
});
expect(passed).toBe(1);
});
test('globalSetup should work with sync function', async ({ runInlineTest }) => { test('globalSetup should work with sync function', async ({ runInlineTest }) => {
const { passed } = await runInlineTest({ const { passed } = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
@ -153,8 +129,8 @@ test('globalSetup should work with sync function', async ({ runInlineTest }) =>
`, `,
'a.test.js': ` 'a.test.js': `
const { test } = pwt; const { test } = pwt;
let value = JSON.parse(process.env.FOO);
test('should work', async ({}) => { test('should work', async ({}) => {
const value = JSON.parse(process.env.FOO);
expect(value).toEqual({ foo: 'bar' }); expect(value).toEqual({ foo: 'bar' });
}); });
`, `,

View File

@ -26,6 +26,7 @@ export interface Location {
export interface Suite { export interface Suite {
parent?: Suite; parent?: Suite;
title: string; title: string;
loadError?: TestError;
location?: Location; location?: Location;
suites: Suite[]; suites: Suite[];
tests: TestCase[]; tests: TestCase[];