chore: temporarily remove project and global setup, store (#20181)

This commit is contained in:
Pavel Feldman 2023-01-18 12:56:03 -08:00 committed by GitHub
parent 6d63773965
commit e08168e16e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 52 additions and 522 deletions

View file

@ -1170,24 +1170,6 @@ Test title.
Test function that takes one or two arguments: an object with fixtures and optional [TestInfo]. Test function that takes one or two arguments: an object with fixtures and optional [TestInfo].
## method: Test.projectSetup
* since: v1.30
Declares a project setup function. The function will be run before all other tests in the same project and if it fails the project execution will be aborted.
### param: Test.projectSetup.title
* since: v1.30
- `title` <[string]>
Project setup title.
### param: Test.projectSetup.testFunction
* since: v1.30
- `testFunction` <[function]\([Fixtures], [TestInfo]\)>
Project setup function that takes one or two arguments: an object with fixtures and optional [TestInfo].
## method: Test.setTimeout ## method: Test.setTimeout
* since: v1.10 * since: v1.10

View file

@ -141,18 +141,6 @@ export default defineConfig({
}); });
``` ```
## property: TestConfig.globalScripts
* since: v1.30
- type: ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>>
Files that contain global setup/teardown hooks.
**Details**
[`method: Test.beforeAll`] hooks in the matching files will run before testing starts. [`method: Test.afterAll`] hooks in the matching files will run after testing finishes.
If global setup fails, test execution will be skipped. [`method: Test.afterAll`] hooks will run in the same worker process as [`method: Test.beforeAll`].
## property: TestConfig.globalSetup ## property: TestConfig.globalSetup
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>

View file

@ -201,14 +201,6 @@ Learn more about [automatic screenshots](../test-configuration.md#automatic-scre
## property: TestOptions.storageState = %%-js-python-context-option-storage-state-%% ## property: TestOptions.storageState = %%-js-python-context-option-storage-state-%%
* since: v1.10 * since: v1.10
## property: TestOptions.storageStateName
* since: v1.29
- type: <[string]>
Name of the [TestStore] entry that should be used to initialize [`property: TestOptions.storageState`]. The value must be
written to the test storage before creation of a browser context that uses it (usually in [`property: TestProject.setupMatch`]). If both
this property and [`property: TestOptions.storageState`] are specified, this property will always take precedence.
## property: TestOptions.testIdAttribute ## property: TestOptions.testIdAttribute
* since: v1.27 * since: v1.27

View file

@ -144,7 +144,7 @@ Filter to only run tests with a title matching one of the patterns. For example,
* since: v1.10 * since: v1.10
- type: ?<[RegExp]|[Array]<[RegExp]>> - type: ?<[RegExp]|[Array]<[RegExp]>>
Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of [`property: TestProject.grep`]. Also available globally and in the [command line](../test-cli.md) with the `--grep-invert` option. This filter and its command line counterpart also applies to the setup files. If all [`property: TestProject.setupMatch`] tests match the filter Playwright **will** run all setup files before running the matching tests. Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of [`property: TestProject.grep`]. Also available globally and in the [command line](../test-cli.md) with the `--grep-invert` option.
`grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests). `grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests).
@ -160,18 +160,6 @@ Metadata that will be put directly to the test report serialized as JSON.
Project name is visible in the report and during test execution. Project name is visible in the report and during test execution.
## property: TestProject.setupMatch
* since: v1.29
- type: ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>>
Project setup files that will be executed before all tests in the project.
**Details**
If project setup fails the tests in this project will be skipped. All project setup files will run in every shard if the project is sharded. [`property: TestProject.grep`] and [`property: TestProject.grepInvert`] and their command line counterparts also apply to the setup files. If such filters match only tests in the project, Playwright will run **all** setup files before running the matching tests.
If there is a file that matches both [`property: TestProject.setupMatch`] and [`property: TestProject.testMatch`] filters an error will be thrown.
## property: TestProject.snapshotDir ## property: TestProject.snapshotDir
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>

View file

@ -1,56 +0,0 @@
# class: TestStore
* since: v1.29
* langs: js
Playwright Test provides a global `store` object for passing values between project setup and tests. It is
an error to call store methods outside of setup and tests.
```js tab=js-js
const { setup, store } = require('@playwright/test');
setup('sign in', async ({ page, context }) => {
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await context.storageState();
await store.set('test-user', contextState)
});
```
```js tab=js-ts
import { setup, store } from '@playwright/test';
setup('sign in', async ({ page, context }) => {
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await context.storageState();
await store.set('test-user', contextState)
});
```
## async method: TestStore.get
* since: v1.29
- returns: <[any]>
Get named item from the store. Returns undefined if there is no value with given name.
### param: TestStore.get.name
* since: v1.29
- `name` <[string]>
Item name.
## async method: TestStore.set
* since: v1.29
Set value to the store.
### param: TestStore.set.name
* since: v1.29
- `name` <[string]>
Item name.
### param: TestStore.set.value
* since: v1.29
- `value` <[any]>
Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.

View file

@ -128,7 +128,6 @@ export class ConfigLoader {
this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard); this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard);
this._fullConfig._ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._ignoreSnapshots); this._fullConfig._ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._ignoreSnapshots);
this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig._globalScripts = takeFirst(config.globalScripts, null);
const workers = takeFirst(config.workers, '50%'); const workers = takeFirst(config.workers, '50%');
if (typeof workers === 'string') { if (typeof workers === 'string') {
@ -152,9 +151,8 @@ export class ConfigLoader {
this._fullConfig._webServers = [webServers]; this._fullConfig._webServers = [webServers];
} }
this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata); this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata);
this._fullConfig._globalProject = this._resolveProject(config, this._fullConfig, globalScriptsProject, throwawayArtifactsPath);
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath)); this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath));
this._assignUniqueProjectIds([...this._fullConfig.projects, this._fullConfig._globalProject]); this._assignUniqueProjectIds(this._fullConfig.projects);
} }
private _assignUniqueProjectIds(projects: FullProjectInternal[]) { private _assignUniqueProjectIds(projects: FullProjectInternal[]) {
@ -217,7 +215,6 @@ export class ConfigLoader {
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir); const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(projectConfig.name, config.name, ''); const name = takeFirst(projectConfig.name, config.name, '');
const _setupMatch = takeFirst(projectConfig.setupMatch, []);
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}'; const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
@ -234,7 +231,6 @@ export class ConfigLoader {
metadata: takeFirst(projectConfig.metadata, config.metadata, undefined), metadata: takeFirst(projectConfig.metadata, config.metadata, undefined),
name, name,
testDir, testDir,
_setupMatch,
_respectGitIgnore: respectGitIgnore, _respectGitIgnore: respectGitIgnore,
snapshotDir, snapshotDir,
snapshotPathTemplate, snapshotPathTemplate,
@ -425,7 +421,7 @@ function validateProject(file: string, project: Project, title: string) {
throw errorWithFile(file, `${title}.testDir must be a string`); throw errorWithFile(file, `${title}.testDir must be a string`);
} }
for (const prop of ['testIgnore', 'testMatch', 'setupMatch'] as const) { for (const prop of ['testIgnore', 'testMatch'] as const) {
if (prop in project && project[prop] !== undefined) { if (prop in project && project[prop] !== undefined) {
const value = project[prop]; const value = project[prop];
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -450,11 +446,6 @@ function validateProject(file: string, project: Project, title: string) {
} }
} }
const globalScriptsProject: Project = {
name: 'Global Scripts',
repeatEach: 1,
};
export const baseFullConfig: FullConfigInternal = { export const baseFullConfig: FullConfigInternal = {
forbidOnly: false, forbidOnly: false,
fullyParallel: false, fullyParallel: false,
@ -483,8 +474,6 @@ export const baseFullConfig: FullConfigInternal = {
_storeDir: '', _storeDir: '',
_maxConcurrentTestGroups: 0, _maxConcurrentTestGroups: 0,
_ignoreSnapshots: false, _ignoreSnapshots: false,
_globalScripts: null,
_globalProject: { } as FullProjectInternal,
}; };
function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined { function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {

View file

@ -14,14 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, WatchTestResolvedPayload, RunPayload, SerializedLoaderData } from './ipc'; import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, RunPayload, SerializedLoaderData } from './ipc';
import type { TestResult, Reporter, TestStep, TestError } from '../types/testReporter'; import type { TestResult, Reporter, TestStep, TestError } from '../types/testReporter';
import type { Suite } from './test'; import type { Suite } from './test';
import type { ConfigLoader } from './configLoader'; import type { ConfigLoader } from './configLoader';
import type { ProcessExitData } from './processHost'; import type { ProcessExitData } from './processHost';
import { TestCase } from './test'; import type { TestCase } from './test';
import { ManualPromise } from 'playwright-core/lib/utils'; import { ManualPromise } from 'playwright-core/lib/utils';
import { TestTypeImpl } from './testType';
import { WorkerHost } from './workerHost'; import { WorkerHost } from './workerHost';
export type TestGroup = { export type TestGroup = {
@ -30,8 +29,6 @@ export type TestGroup = {
repeatEachIndex: number; repeatEachIndex: number;
projectId: string; projectId: string;
tests: TestCase[]; tests: TestCase[];
watchMode: boolean;
phase: 'test' | 'projectSetup' | 'globalSetup';
}; };
type TestResultData = { type TestResultData = {
@ -169,15 +166,12 @@ export class Dispatcher {
entries: testGroup.tests.map(test => { entries: testGroup.tests.map(test => {
return { testId: test.id, retry: test.results.length }; return { testId: test.id, retry: test.results.length };
}), }),
watchMode: testGroup.watchMode,
phase: testGroup.phase,
}; };
worker.runTestGroup(runPayload); worker.runTestGroup(runPayload);
let doneCallback = () => {}; let doneCallback = () => {};
const result = new Promise<void>(f => doneCallback = f); const result = new Promise<void>(f => doneCallback = f);
const doneWithJob = () => { const doneWithJob = () => {
worker.removeListener('watchTestResolved', onWatchTestResolved);
worker.removeListener('testBegin', onTestBegin); worker.removeListener('testBegin', onTestBegin);
worker.removeListener('testEnd', onTestEnd); worker.removeListener('testEnd', onTestEnd);
worker.removeListener('stepBegin', onStepBegin); worker.removeListener('stepBegin', onStepBegin);
@ -190,12 +184,6 @@ export class Dispatcher {
const remainingByTestId = new Map(testGroup.tests.map(e => [e.id, e])); const remainingByTestId = new Map(testGroup.tests.map(e => [e.id, e]));
const failedTestIds = new Set<string>(); const failedTestIds = new Set<string>();
const onWatchTestResolved = (params: WatchTestResolvedPayload) => {
const test = new TestCase(params.title, () => {}, new TestTypeImpl([]), params.location);
this._testById.set(params.testId, { test, resultByWorkerIndex: new Map() });
};
worker.addListener('watchTestResolved', onWatchTestResolved);
const onTestBegin = (params: TestBeginPayload) => { const onTestBegin = (params: TestBeginPayload) => {
const data = this._testById.get(params.testId)!; const data = this._testById.get(params.testId)!;
const result = data.test._appendTestResult(); const result = data.test._appendTestResult();

View file

@ -20,14 +20,12 @@ import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOp
import * as playwrightLibrary from 'playwright-core'; import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, removeFolders, addStackIgnoreFilter } from 'playwright-core/lib/utils'; import { createGuid, debugMode, removeFolders, addStackIgnoreFilter } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
import { store as _baseStore } from './store';
import type { TestInfoImpl } from './testInfo'; import type { TestInfoImpl } from './testInfo';
import { rootTestType } from './testType'; import { rootTestType } from './testType';
import { type ContextReuseMode } from './types'; import { type ContextReuseMode } from './types';
export { expect } from './expect'; export { expect } from './expect';
export { addRunnerPlugin as _addRunnerPlugin } from './plugins'; export { addRunnerPlugin as _addRunnerPlugin } from './plugins';
export const _baseTest: TestType<{}, {}> = rootTestType.test; export const _baseTest: TestType<{}, {}> = rootTestType.test;
export const store = _baseStore;
addStackIgnoreFilter((frame: StackFrame) => frame.file.startsWith(path.dirname(require.resolve('../package.json')))); addStackIgnoreFilter((frame: StackFrame) => frame.file.startsWith(path.dirname(require.resolve('../package.json'))));
@ -145,7 +143,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }], permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }], proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }], storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
storageStateName: [undefined, { option: true }],
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }], timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }],
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }], userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }],
viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }], viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }],
@ -175,7 +172,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions, permissions,
proxy, proxy,
storageState, storageState,
storageStateName,
viewport, viewport,
timezoneId, timezoneId,
userAgent, userAgent,
@ -214,14 +210,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
options.permissions = permissions; options.permissions = permissions;
if (proxy !== undefined) if (proxy !== undefined)
options.proxy = proxy; options.proxy = proxy;
if (storageStateName !== undefined) { if (storageState !== undefined)
const value = await store.get(storageStateName);
if (!value)
throw new Error(`Cannot find value in the store for storageStateName: "${storageStateName}"`);
options.storageState = value as any;
} else if (storageState !== undefined) {
options.storageState = storageState; options.storageState = storageState;
}
if (timezoneId !== undefined) if (timezoneId !== undefined)
options.timezoneId = timezoneId; options.timezoneId = timezoneId;
if (userAgent !== undefined) if (userAgent !== undefined)

View file

@ -43,12 +43,6 @@ export type WorkerInitParams = {
loader: SerializedLoaderData; loader: SerializedLoaderData;
}; };
export type WatchTestResolvedPayload = {
testId: string;
title: string;
location: { file: string, line: number, column: number };
};
export type TestBeginPayload = { export type TestBeginPayload = {
testId: string; testId: string;
startWallTime: number; // milliseconds since unix epoch startWallTime: number; // milliseconds since unix epoch
@ -92,8 +86,6 @@ export type TestEntry = {
export type RunPayload = { export type RunPayload = {
file: string; file: string;
entries: TestEntry[]; entries: TestEntry[];
watchMode: boolean;
phase: 'test' | 'projectSetup' | 'globalSetup';
}; };
export type DonePayload = { export type DonePayload = {

View file

@ -200,7 +200,7 @@ export class Runner {
async listTestFiles(projectNames: string[] | undefined): Promise<any> { async listTestFiles(projectNames: string[] | undefined): Promise<any> {
const projects = this._collectProjects(projectNames); const projects = this._collectProjects(projectNames);
const { filesByProject } = await this._collectFiles(projects, []); const filesByProject = await this._collectFiles(projects, []);
const report: any = { const report: any = {
projects: [] projects: []
}; };
@ -239,98 +239,45 @@ export class Runner {
return projects; return projects;
} }
private async _collectFiles(projects: FullProjectInternal[], commandLineFileFilters: TestFileFilter[]): Promise<{filesByProject: Map<FullProjectInternal, string[]>; setupFiles: Set<string>, globalSetupFiles: Set<string>}> { private async _collectFiles(projects: FullProjectInternal[], commandLineFileFilters: TestFileFilter[]): Promise<Map<FullProjectInternal, string[]>> {
const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx']; const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx'];
const testFileExtension = (file: string) => extensions.includes(path.extname(file)); const testFileExtension = (file: string) => extensions.includes(path.extname(file));
const filesByProject = new Map<FullProjectInternal, string[]>(); const filesByProject = new Map<FullProjectInternal, string[]>();
const setupFiles = new Set<string>();
const fileToProjectName = new Map<string, string>(); const fileToProjectName = new Map<string, string>();
const commandLineFileMatcher = commandLineFileFilters.length ? createFileMatcherFromFilters(commandLineFileFilters) : () => true; const commandLineFileMatcher = commandLineFileFilters.length ? createFileMatcherFromFilters(commandLineFileFilters) : () => true;
const config = this._configLoader.fullConfig();
const globalSetupFiles = new Set<string>();
if (config._globalScripts) {
const allFiles = await collectFiles(config.rootDir, true);
const globalScriptMatch = createFileMatcher(config._globalScripts);
const globalScripts = allFiles.filter(file => {
if (!testFileExtension(file) || !globalScriptMatch(file))
return false;
fileToProjectName.set(file, config._globalProject.name);
globalSetupFiles.add(file);
return true;
});
filesByProject.set(config._globalProject, globalScripts);
}
for (const project of projects) { for (const project of projects) {
const allFiles = await collectFiles(project.testDir, project._respectGitIgnore); const allFiles = await collectFiles(project.testDir, project._respectGitIgnore);
const setupMatch = createFileMatcher(project._setupMatch);
const testMatch = createFileMatcher(project.testMatch); const testMatch = createFileMatcher(project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore); const testIgnore = createFileMatcher(project.testIgnore);
const testFiles = allFiles.filter(file => { const testFiles = allFiles.filter(file => {
if (!testFileExtension(file)) if (!testFileExtension(file))
return false; return false;
const isSetup = setupMatch(file);
const isTest = !testIgnore(file) && testMatch(file) && commandLineFileMatcher(file); const isTest = !testIgnore(file) && testMatch(file) && commandLineFileMatcher(file);
if (!isTest && !isSetup) if (!isTest)
return false; return false;
if (isSetup && isTest)
throw new Error(`File "${file}" matches both 'setup' and 'testMatch' filters in project "${project.name}"`);
if (fileToProjectName.has(file)) {
if (isSetup) {
if (!setupFiles.has(file))
throw new Error(`File "${file}" matches 'setup' filter in project "${project.name}" and 'testMatch' filter in project "${fileToProjectName.get(file)}"`);
} else if (setupFiles.has(file)) {
throw new Error(`File "${file}" matches 'setup' filter in project "${fileToProjectName.get(file)}" and 'testMatch' filter in project "${project.name}"`);
}
}
fileToProjectName.set(file, project.name); fileToProjectName.set(file, project.name);
if (isSetup)
setupFiles.add(file);
return true; return true;
}); });
filesByProject.set(project, testFiles); filesByProject.set(project, testFiles);
} }
return { filesByProject, setupFiles, globalSetupFiles }; return filesByProject;
} }
private async _collectTestGroups(options: RunOptions): Promise<{ rootSuite: Suite, globalSetupGroups: TestGroup[], projectSetupGroups: TestGroup[], testGroups: TestGroup[] }> { private async _collectTestGroups(options: RunOptions): Promise<{ rootSuite: Suite, testGroups: TestGroup[] }> {
const config = this._configLoader.fullConfig(); const config = this._configLoader.fullConfig();
const projects = this._collectProjects(options.projectFilter); const projects = this._collectProjects(options.projectFilter);
const { filesByProject, setupFiles, globalSetupFiles } = await this._collectFiles(projects, options.testFileFilters); const filesByProject = await this._collectFiles(projects, options.testFileFilters);
const result = await this._createFilteredRootSuite(options, filesByProject);
let result = await this._createFilteredRootSuite(options, filesByProject, new Set(), !!setupFiles.size, setupFiles, globalSetupFiles);
if (setupFiles.size) {
const allTests = result.rootSuite.allTests();
const tests = allTests.filter(test => test._phase === 'test');
// If >0 tests match and
// - none of the setup files match the filter then we run all setup files,
// - if the filter also matches some of the setup tests, we'll run only
// that maching subset of setup tests.
if (tests.length > 0 && tests.length === allTests.length)
result = await this._createFilteredRootSuite(options, filesByProject, setupFiles, false, setupFiles, globalSetupFiles);
}
this._fatalErrors.push(...result.fatalErrors); this._fatalErrors.push(...result.fatalErrors);
const { rootSuite } = result; const { rootSuite } = result;
const allTestGroups = createTestGroups(rootSuite.suites, config.workers); const testGroups = createTestGroups(rootSuite.suites, config.workers);
const globalSetupGroups = []; return { rootSuite, testGroups };
const projectSetupGroups = [];
const testGroups = [];
for (const group of allTestGroups) {
if (group.phase === 'projectSetup')
projectSetupGroups.push(group);
else if (group.phase === 'globalSetup')
globalSetupGroups.push(group);
else
testGroups.push(group);
}
return { rootSuite, globalSetupGroups, projectSetupGroups, testGroups };
} }
private async _createFilteredRootSuite(options: RunOptions, filesByProject: Map<FullProjectInternal, string[]>, doNotFilterFiles: Set<string>, shouldCloneTests: boolean, setupFiles: Set<string>, globalSetupFiles: Set<string>): Promise<{rootSuite: Suite, fatalErrors: TestError[]}> { private async _createFilteredRootSuite(options: RunOptions, filesByProject: Map<FullProjectInternal, string[]>): Promise<{rootSuite: Suite, fatalErrors: TestError[]}> {
const config = this._configLoader.fullConfig(); const config = this._configLoader.fullConfig();
const fatalErrors: TestError[] = []; const fatalErrors: TestError[] = [];
const allTestFiles = new Set<string>(); const allTestFiles = new Set<string>();
@ -341,23 +288,18 @@ export class Runner {
// Add all tests. // Add all tests.
const preprocessRoot = new Suite('', 'root'); const preprocessRoot = new Suite('', 'root');
for (const file of allTestFiles) { for (const file of allTestFiles) {
let type: 'test' | 'projectSetup' | 'globalSetup' = 'test'; const fileSuite = await testLoader.loadTestFile(file, 'runner');
if (globalSetupFiles.has(file))
type = 'globalSetup';
else if (setupFiles.has(file))
type = 'projectSetup';
const fileSuite = await testLoader.loadTestFile(file, 'runner', type);
if (fileSuite._loadError) if (fileSuite._loadError)
fatalErrors.push(fileSuite._loadError); fatalErrors.push(fileSuite._loadError);
// We have to clone only if there maybe subsequent calls of this method. // We have to clone only if there maybe subsequent calls of this method.
preprocessRoot._addSuite(shouldCloneTests ? fileSuite._deepClone() : fileSuite); preprocessRoot._addSuite(fileSuite);
} }
// Complain about duplicate titles. // Complain about duplicate titles.
fatalErrors.push(...createDuplicateTitlesErrors(config, preprocessRoot)); fatalErrors.push(...createDuplicateTitlesErrors(config, preprocessRoot));
// Filter tests to respect line/column filter. // Filter tests to respect line/column filter.
filterByFocusedLine(preprocessRoot, options.testFileFilters, doNotFilterFiles); filterByFocusedLine(preprocessRoot, options.testFileFilters);
// Complain about only. // Complain about only.
if (config.forbidOnly) { if (config.forbidOnly) {
@ -368,7 +310,7 @@ export class Runner {
// Filter only. // Filter only.
if (!options.listOnly) if (!options.listOnly)
filterOnly(preprocessRoot, doNotFilterFiles); filterOnly(preprocessRoot);
// Generate projects. // Generate projects.
const fileSuites = new Map<string, Suite>(); const fileSuites = new Map<string, Suite>();
@ -381,8 +323,6 @@ export class Runner {
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null; const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
const titleMatcher = (test: TestCase) => { const titleMatcher = (test: TestCase) => {
if (doNotFilterFiles.has(test._requireFile))
return true;
const grepTitle = test.titlePath().join(' '); const grepTitle = test.titlePath().join(' ');
if (grepInvertMatcher?.(grepTitle)) if (grepInvertMatcher?.(grepTitle))
return false; return false;
@ -408,7 +348,7 @@ export class Runner {
return { rootSuite, fatalErrors }; return { rootSuite, fatalErrors };
} }
private _filterForCurrentShard(rootSuite: Suite, projectSetupGroups: TestGroup[], testGroups: TestGroup[]) { private _filterForCurrentShard(rootSuite: Suite, testGroups: TestGroup[]) {
const shard = this._configLoader.fullConfig().shard; const shard = this._configLoader.fullConfig().shard;
if (!shard) if (!shard)
return; return;
@ -447,17 +387,6 @@ export class Runner {
testGroups.length = 0; testGroups.length = 0;
testGroups.push(...shardTestGroups); testGroups.push(...shardTestGroups);
const shardSetupGroups = [];
for (const group of projectSetupGroups) {
if (!shardProjects.has(group.projectId))
continue;
shardSetupGroups.push(group);
for (const test of group.tests)
shardTests.add(test);
}
projectSetupGroups.length = 0;
projectSetupGroups.push(...shardSetupGroups);
if (!shardTests.size) { if (!shardTests.size) {
// Filtering with "only semantics" does not work when we have zero tests - it leaves all the tests. // Filtering with "only semantics" does not work when we have zero tests - it leaves all the tests.
// We need an empty suite in this case. // We need an empty suite in this case.
@ -465,10 +394,7 @@ export class Runner {
rootSuite.suites = []; rootSuite.suites = [];
rootSuite.tests = []; rootSuite.tests = [];
} else { } else {
// Unlike project setup files global setup always run regardless of the selected tests. filterSuiteWithOnlySemantics(rootSuite, () => false, test => shardTests.has(test));
// Because of that we don't add global setup entries to shardTests to avoid running empty
// shards which have only global setup.
filterSuiteWithOnlySemantics(rootSuite, () => false, test => shardTests.has(test) || test._phase === 'globalSetup');
} }
} }
@ -476,15 +402,15 @@ export class Runner {
const config = this._configLoader.fullConfig(); const config = this._configLoader.fullConfig();
// 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, globalSetupGroups, projectSetupGroups, testGroups } = await this._collectTestGroups(options); const { rootSuite, 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)
this._fatalErrors.push(createNoTestsError()); this._fatalErrors.push(createNoTestsError());
this._filterForCurrentShard(rootSuite, projectSetupGroups, testGroups); this._filterForCurrentShard(rootSuite, testGroups);
config._maxConcurrentTestGroups = Math.max(globalSetupGroups.length, projectSetupGroups.length, testGroups.length); config._maxConcurrentTestGroups = testGroups.length;
// Report begin // Report begin
this._reporter.onBegin?.(config, rootSuite); this._reporter.onBegin?.(config, rootSuite);
@ -521,24 +447,7 @@ export class Runner {
// Run tests. // Run tests.
try { try {
// TODO: run only setups, keep workers alive, inherit process.env from global setup workers const dispatchResult = await this._dispatchToWorkers(testGroups);
let dispatchResult = await this._dispatchToWorkers(globalSetupGroups);
if (dispatchResult === 'success') {
if (globalSetupGroups.some(group => group.tests.some(test => !test.ok()))) {
this._skipTestsFromMatchingGroups([...testGroups, ...projectSetupGroups], () => true);
} else {
dispatchResult = await this._dispatchToWorkers(projectSetupGroups);
if (dispatchResult === 'success') {
const failedSetupProjectIds = new Set<string>();
for (const testGroup of projectSetupGroups) {
if (testGroup.tests.some(test => !test.ok()))
failedSetupProjectIds.add(testGroup.projectId);
}
const testGroupsToRun = this._skipTestsFromMatchingGroups(testGroups, group => failedSetupProjectIds.has(group.projectId));
dispatchResult = await this._dispatchToWorkers(testGroupsToRun);
}
}
}
if (dispatchResult === 'signal') { if (dispatchResult === 'signal') {
result.status = 'interrupted'; result.status = 'interrupted';
} else { } else {
@ -694,11 +603,11 @@ export class Runner {
} }
} }
function filterOnly(suite: Suite, doNotFilterFiles: Set<string>) { function filterOnly(suite: Suite) {
if (!suite._getOnlyItems().length) if (!suite._getOnlyItems().length)
return; return;
const suiteFilter = (suite: Suite) => suite._only || doNotFilterFiles.has(suite._requireFile); const suiteFilter = (suite: Suite) => suite._only;
const testFilter = (test: TestCase) => test._only || doNotFilterFiles.has(test._requireFile); const testFilter = (test: TestCase) => test._only;
return filterSuiteWithOnlySemantics(suite, suiteFilter, testFilter); return filterSuiteWithOnlySemantics(suite, suiteFilter, testFilter);
} }
@ -708,13 +617,13 @@ function createFileMatcherFromFilter(filter: TestFileFilter) {
fileMatcher(testFileName) && (filter.line === testLine || filter.line === null) && (filter.column === testColumn || filter.column === null); fileMatcher(testFileName) && (filter.line === testLine || filter.line === null) && (filter.column === testColumn || filter.column === null);
} }
function filterByFocusedLine(suite: Suite, focusedTestFileLines: TestFileFilter[], doNotFilterFiles: Set<string>) { function filterByFocusedLine(suite: Suite, focusedTestFileLines: TestFileFilter[]) {
if (!focusedTestFileLines.length) if (!focusedTestFileLines.length)
return; return;
const matchers = focusedTestFileLines.map(createFileMatcherFromFilter); const matchers = focusedTestFileLines.map(createFileMatcherFromFilter);
const testFileLineMatches = (testFileName: string, testLine: number, testColumn: number) => matchers.some(m => m(testFileName, testLine, testColumn)); const testFileLineMatches = (testFileName: string, testLine: number, testColumn: number) => matchers.some(m => m(testFileName, testLine, testColumn));
const suiteFilter = (suite: Suite) => doNotFilterFiles.has(suite._requireFile) || !!suite.location && testFileLineMatches(suite.location.file, suite.location.line, suite.location.column); const suiteFilter = (suite: Suite) => !!suite.location && testFileLineMatches(suite.location.file, suite.location.line, suite.location.column);
const testFilter = (test: TestCase) => doNotFilterFiles.has(test._requireFile) || testFileLineMatches(test.location.file, test.location.line, test.location.column); const testFilter = (test: TestCase) => testFileLineMatches(test.location.file, test.location.line, test.location.column);
return filterSuite(suite, suiteFilter, testFilter); return filterSuite(suite, suiteFilter, testFilter);
} }
@ -859,8 +768,6 @@ function createTestGroups(projectSuites: Suite[], workers: number): TestGroup[]
repeatEachIndex: test.repeatEachIndex, repeatEachIndex: test.repeatEachIndex,
projectId: test._projectId, projectId: test._projectId,
tests: [], tests: [],
watchMode: false,
phase: test._phase,
}; };
}; };

View file

@ -1,54 +0,0 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import type { TestStore } from '../types/test';
import { currentTestInfo } from './globals';
import { sanitizeForFilePath, trimLongString } from './util';
class JsonStore implements TestStore {
private _toFilePath(name: string) {
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error('store can only be called while test is running');
const fileName = sanitizeForFilePath(trimLongString(name)) + '.json';
return path.join(testInfo.config._storeDir, testInfo.project._id, fileName);
}
async get<T>(name: string) {
const file = this._toFilePath(name);
try {
const data = (await fs.promises.readFile(file)).toString('utf-8');
return JSON.parse(data) as T;
} catch (e) {
return undefined;
}
}
async set<T>(name: string, value: T | undefined) {
const file = this._toFilePath(name);
if (value === undefined) {
await fs.promises.rm(file, { force: true });
return;
}
const data = JSON.stringify(value, undefined, 2);
await fs.promises.mkdir(path.dirname(file), { recursive: true });
await fs.promises.writeFile(file, data);
}
}
export const store = new JsonStore();

View file

@ -23,7 +23,6 @@ class Base {
title: string; title: string;
_only = false; _only = false;
_requireFile: string = ''; _requireFile: string = '';
_phase: 'test' | 'projectSetup' | 'globalSetup' = 'test';
constructor(title: string) { constructor(title: string) {
this.title = title; this.title = title;
@ -121,7 +120,6 @@ export class Suite extends Base implements reporterTypes.Suite {
suite._only = this._only; suite._only = this._only;
suite.location = this.location; suite.location = this.location;
suite._requireFile = this._requireFile; suite._requireFile = this._requireFile;
suite._phase = this._phase;
suite._use = this._use.slice(); suite._use = this._use.slice();
suite._hooks = this._hooks.slice(); suite._hooks = this._hooks.slice();
suite._timeout = this._timeout; suite._timeout = this._timeout;
@ -193,7 +191,6 @@ export class TestCase extends Base implements reporterTypes.TestCase {
const test = new TestCase(this.title, this.fn, this._testType, this.location); const test = new TestCase(this.title, this.fn, this._testType, this.location);
test._only = this._only; test._only = this._only;
test._requireFile = this._requireFile; test._requireFile = this._requireFile;
test._phase = this._phase;
test.expectedStatus = this.expectedStatus; test.expectedStatus = this.expectedStatus;
test.annotations = this.annotations.slice(); test.annotations = this.annotations.slice();
test._annotateWithInheritence = this._annotateWithInheritence; test._annotateWithInheritence = this._annotateWithInheritence;

View file

@ -38,12 +38,11 @@ export class TestLoader {
this._fullConfig = fullConfig; this._fullConfig = fullConfig;
} }
async loadTestFile(file: string, environment: 'runner' | 'worker', phase: 'test' | 'projectSetup' | 'globalSetup') { 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), 'file'); const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file), 'file');
suite._requireFile = file; suite._requireFile = file;
suite._phase = phase;
suite.location = { file, line: 0, column: 0 }; suite.location = { file, line: 0, column: 0 };
setCurrentlyLoadingFileSuite(suite); setCurrentlyLoadingFileSuite(suite);

View file

@ -55,8 +55,6 @@ export class TestTypeImpl {
test.step = wrapFunctionWithLocation(this._step.bind(this)); test.step = wrapFunctionWithLocation(this._step.bind(this));
test.use = wrapFunctionWithLocation(this._use.bind(this)); test.use = wrapFunctionWithLocation(this._use.bind(this));
test.extend = wrapFunctionWithLocation(this._extend.bind(this)); test.extend = wrapFunctionWithLocation(this._extend.bind(this));
test.projectSetup = wrapFunctionWithLocation(this._createTest.bind(this, 'projectSetup'));
(test.projectSetup as any).only = wrapFunctionWithLocation(this._createTest.bind(this, 'projectSetupOnly'));
test._extendTest = wrapFunctionWithLocation(this._extendTest.bind(this)); test._extendTest = wrapFunctionWithLocation(this._extendTest.bind(this));
test.info = () => { test.info = () => {
const result = currentTestInfo(); const result = currentTestInfo();
@ -67,7 +65,7 @@ export class TestTypeImpl {
this.test = test; this.test = test;
} }
private _currentSuite(location: Location, title: string, allowedContext: 'test' | 'projectSetup' | 'any'): Suite | undefined { private _currentSuite(location: Location, title: string): Suite | undefined {
const suite = currentlyLoadingFileSuite(); const suite = currentlyLoadingFileSuite();
if (!suite) { if (!suite) {
addFatalError([ addFatalError([
@ -80,36 +78,19 @@ export class TestTypeImpl {
].join('\n'), location); ].join('\n'), location);
return; return;
} }
if (allowedContext === 'projectSetup' && suite._phase !== 'projectSetup')
addFatalError(`${title} is only allowed in a project setup file.`, location);
else if (allowedContext === 'test' && suite._phase !== 'test' && suite._phase !== 'globalSetup')
addFatalError(`${title} is not allowed in a setup file.`, location);
return suite; return suite;
} }
private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'projectSetup' | 'projectSetupOnly', location: Location, title: string, fn: Function) { private _createTest(type: 'default' | 'only' | 'skip' | 'fixme', location: Location, title: string, fn: Function) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
let functionTitle = 'test()'; const suite = this._currentSuite(location, 'test()');
let allowedContext: 'test' | 'projectSetup' | 'any' = 'any';
switch (type) {
case 'projectSetup':
case 'projectSetupOnly':
functionTitle = 'test.projectSetup()';
allowedContext = 'projectSetup';
break;
case 'default':
allowedContext = 'test';
break;
}
const suite = this._currentSuite(location, functionTitle, allowedContext);
if (!suite) if (!suite)
return; 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._phase = suite._phase;
suite._addTest(test); suite._addTest(test);
if (type === 'only' || type === 'projectSetupOnly') if (type === 'only')
test._only = true; test._only = true;
if (type === 'skip' || type === 'fixme') { if (type === 'skip' || type === 'fixme') {
test.annotations.push({ type }); test.annotations.push({ type });
@ -123,7 +104,7 @@ 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._currentSuite(location, 'test.describe()', 'any'); const suite = this._currentSuite(location, 'test.describe()');
if (!suite) if (!suite)
return; return;
@ -134,7 +115,6 @@ export class TestTypeImpl {
const child = new Suite(title, 'describe'); const child = new Suite(title, 'describe');
child._requireFile = suite._requireFile; child._requireFile = suite._requireFile;
child._phase = suite._phase;
child.location = location; child.location = location;
suite._addSuite(child); suite._addSuite(child);
@ -160,7 +140,7 @@ 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._currentSuite(location, `test.${name}()`, 'test'); const suite = this._currentSuite(location, `test.${name}()`);
if (!suite) if (!suite)
return; return;
suite._hooks.push({ type: name, fn, location }); suite._hooks.push({ type: name, fn, location });
@ -168,7 +148,7 @@ export class TestTypeImpl {
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._currentSuite(location, `test.describe.configure()`, 'any'); const suite = this._currentSuite(location, `test.describe.configure()`);
if (!suite) if (!suite)
return; return;
@ -235,7 +215,7 @@ export class TestTypeImpl {
} }
private _use(location: Location, fixtures: Fixtures) { private _use(location: Location, fixtures: Fixtures) {
const suite = this._currentSuite(location, `test.use()`, 'any'); const suite = this._currentSuite(location, `test.use()`);
if (!suite) if (!suite)
return; return;
suite._use.push({ fixtures, location }); suite._use.push({ fixtures, location });

View file

@ -52,10 +52,6 @@ export interface FullConfigInternal extends FullConfigPublic {
*/ */
webServer: FullConfigPublic['webServer']; webServer: FullConfigPublic['webServer'];
_webServers: Exclude<FullConfigPublic['webServer'], null>[]; _webServers: Exclude<FullConfigPublic['webServer'], null>[];
_globalScripts: string | RegExp | (string | RegExp)[] | null;
// This is an ephemeral project that is not added to `projects` list below.
_globalProject: FullProjectInternal;
// Overrides the public field. // Overrides the public field.
projects: FullProjectInternal[]; projects: FullProjectInternal[];
@ -71,7 +67,6 @@ export interface FullProjectInternal extends FullProjectPublic {
_fullyParallel: boolean; _fullyParallel: boolean;
_expect: Project['expect']; _expect: Project['expect'];
_respectGitIgnore: boolean; _respectGitIgnore: boolean;
_setupMatch: string | RegExp | (string | RegExp)[];
snapshotPathTemplate: string; snapshotPathTemplate: string;
} }

View file

@ -17,7 +17,7 @@
import { colors, rimraf } from 'playwright-core/lib/utilsBundle'; import { colors, rimraf } from 'playwright-core/lib/utilsBundle';
import util from 'util'; import util from 'util';
import { debugTest, formatLocation, relativeFilePath, serializeError } from './util'; import { debugTest, formatLocation, relativeFilePath, serializeError } from './util';
import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, TeardownErrorsPayload, WatchTestResolvedPayload } from './ipc'; import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, TeardownErrorsPayload } from './ipc';
import { setCurrentTestInfo } from './globals'; import { setCurrentTestInfo } from './globals';
import { ConfigLoader } from './configLoader'; import { ConfigLoader } from './configLoader';
import type { Suite, TestCase } from './test'; import type { Suite, TestCase } from './test';
@ -171,11 +171,7 @@ export class WorkerRunner extends ProcessRunner {
this._configLoader = await ConfigLoader.deserialize(this._params.loader); this._configLoader = await ConfigLoader.deserialize(this._params.loader);
this._testLoader = new TestLoader(this._configLoader.fullConfig()); this._testLoader = new TestLoader(this._configLoader.fullConfig());
const globalProject = this._configLoader.fullConfig()._globalProject; this._project = this._configLoader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
if (this._params.projectId === globalProject._id)
this._project = globalProject;
else
this._project = this._configLoader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
} }
async runTestGroup(runPayload: RunPayload) { async runTestGroup(runPayload: RunPayload) {
@ -184,17 +180,8 @@ export class WorkerRunner extends ProcessRunner {
let fatalUnknownTestIds; let fatalUnknownTestIds;
try { try {
await this._loadIfNeeded(); await this._loadIfNeeded();
const fileSuite = await this._testLoader.loadTestFile(runPayload.file, 'worker', runPayload.phase); const fileSuite = await this._testLoader.loadTestFile(runPayload.file, 'worker');
const suite = this._testLoader.buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex, test => { const suite = this._testLoader.buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex, test => {
if (runPayload.watchMode) {
const testResolvedPayload: WatchTestResolvedPayload = {
testId: test.id,
title: test.title,
location: test.location
};
this.dispatchEvent('watchTestResolved', testResolvedPayload);
entries.set(test.id, { testId: test.id, retry: 0 });
}
if (!entries.has(test.id)) if (!entries.has(test.id))
return false; return false;
return true; return true;

View file

@ -190,10 +190,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
/** /**
* Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of * Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of
* [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option. This filter and its command line * and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
* counterpart also applies to the setup files. If all
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match) tests match
* the filter Playwright **will** run all setup files before running the matching tests.
* *
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests). * `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/ */
@ -634,22 +631,6 @@ interface TestConfig {
*/ */
fullyParallel?: boolean; fullyParallel?: boolean;
/**
* Files that contain global setup/teardown hooks.
*
* **Details**
*
* [test.beforeAll(hookFunction)](https://playwright.dev/docs/api/class-test#test-before-all) hooks in the matching
* files will run before testing starts.
* [test.afterAll(hookFunction)](https://playwright.dev/docs/api/class-test#test-after-all) hooks in the matching
* files will run after testing finishes.
*
* If global setup fails, test execution will be skipped.
* [test.afterAll(hookFunction)](https://playwright.dev/docs/api/class-test#test-after-all) hooks will run in the same
* worker process as [test.beforeAll(hookFunction)](https://playwright.dev/docs/api/class-test#test-before-all).
*/
globalScripts?: string|RegExp|Array<string|RegExp>;
/** /**
* Path to the global setup file. This file will be required and run before all the tests. It must export a single * Path to the global setup file. This file will be required and run before all the tests. It must export a single
* function that takes a [`TestConfig`] argument. * function that takes a [`TestConfig`] argument.
@ -3298,35 +3279,6 @@ type ConnectOptions = {
timeout?: number; timeout?: number;
}; };
/**
* Playwright Test provides a global `store` object for passing values between project setup and tests. It is an error
* to call store methods outside of setup and tests.
*
* ```js
* import { setup, store } from '@playwright/test';
*
* setup('sign in', async ({ page, context }) => {
* // Save signed-in state to an entry named 'github-test-user'.
* const contextState = await context.storageState();
* await store.set('test-user', contextState)
* });
* ```
*
*/
export interface TestStore {
/**
* Get named item from the store. Returns undefined if there is no value with given name.
* @param name Item name.
*/
get<T>(name: string): Promise<T | undefined>;
/**
* Set value to the store.
* @param name Item name.
* @param value Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.
*/
set<T>(name: string, value: T | undefined): Promise<void>;
}
/** /**
* Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more. * Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more.
* *
@ -3560,16 +3512,6 @@ export interface PlaywrightTestOptions {
* Either a path to the file with saved storage, or an object with the following fields: * Either a path to the file with saved storage, or an object with the following fields:
*/ */
storageState: StorageState | undefined; storageState: StorageState | undefined;
/**
* Name of the [TestStore] entry that should be used to initialize
* [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state). The value
* must be written to the test storage before creation of a browser context that uses it (usually in
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match)). If both this
* property and
* [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state) are
* specified, this property will always take precedence.
*/
storageStateName: string | undefined;
/** /**
* Changes the timezone of the context. See * Changes the timezone of the context. See
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) * [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
@ -3877,7 +3819,6 @@ export default test;
export const _baseTest: TestType<{}, {}>; export const _baseTest: TestType<{}, {}>;
export const expect: Expect; export const expect: Expect;
export const store: TestStore;
/** /**
* Defines Playwright config * Defines Playwright config
@ -5198,10 +5139,7 @@ interface TestProject {
/** /**
* Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of * Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of
* [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option. This filter and its command line * and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
* counterpart also applies to the setup files. If all
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match) tests match
* the filter Playwright **will** run all setup files before running the matching tests.
* *
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests). * `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/ */
@ -5217,24 +5155,6 @@ interface TestProject {
*/ */
name?: string; name?: string;
/**
* Project setup files that will be executed before all tests in the project.
*
* **Details**
*
* If project setup fails the tests in this project will be skipped. All project setup files will run in every shard
* if the project is sharded. [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep)
* and [testProject.grepInvert](https://playwright.dev/docs/api/class-testproject#test-project-grep-invert) and their
* command line counterparts also apply to the setup files. If such filters match only tests in the project,
* Playwright will run **all** setup files before running the matching tests.
*
* If there is a file that matches both
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match) and
* [testProject.testMatch](https://playwright.dev/docs/api/class-testproject#test-project-test-match) filters an error
* will be thrown.
*/
setupMatch?: string|RegExp|Array<string|RegExp>;
/** /**
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to * The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir). * [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).

View file

@ -478,41 +478,3 @@ test('should have correct types for the config', async ({ runTSC }) => {
}); });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });
test('should throw when project.setupMatch has wrong type', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', setupMatch: 100 },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Error: playwright.config.ts: config.projects[0].setupMatch must be a string or a RegExp`);
});
test('should throw when project.setupMatch has wrong array type', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', setupMatch: [/100/, 100] },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Error: playwright.config.ts: config.projects[0].setupMatch[1] must be a string or a RegExp`);
});

View file

@ -16,6 +16,8 @@
import path from 'path'; import path from 'path';
import { test, expect } from './playwright-test-fixtures'; import { test, expect } from './playwright-test-fixtures';
test.fixme(true, 'Restore this');
type Timeline = { titlePath: string[], event: 'begin' | 'end' }[]; type Timeline = { titlePath: string[], event: 'begin' | 'end' }[];
function formatTimeline(timeline: Timeline) { function formatTimeline(timeline: Timeline) {

View file

@ -17,6 +17,8 @@ import type { PlaywrightTestConfig, TestInfo, PlaywrightTestProject } from '@pla
import path from 'path'; import path from 'path';
import { test, expect } from './playwright-test-fixtures'; import { test, expect } from './playwright-test-fixtures';
test.fixme(true, 'Restore this');
function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTemplates?: { [name: string]: PlaywrightTestProject }): Record<string, string> { function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTemplates?: { [name: string]: PlaywrightTestProject }): Record<string, string> {
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })), projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })),

View file

@ -16,6 +16,8 @@
import { expect, test } from './playwright-test-fixtures'; import { expect, test } from './playwright-test-fixtures';
test.fixme(true, 'Restore this');
test('should provide store fixture', async ({ runInlineTest }) => { test('should provide store fixture', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.js': ` 'playwright.config.js': `

View file

@ -188,18 +188,3 @@ test('config should allow void/empty options', async ({ runTSC }) => {
}); });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });
test('should provide store interface', async ({ runTSC }) => {
const result = await runTSC({
'a.spec.ts': `
const { test, store } = pwt;
test('my test', async () => {
await store.set('foo', 'bar');
const val = await store.get('foo');
// @ts-expect-error
await store.unknown();
});
`
});
expect(result.exitCode).toBe(0);
});

View file

@ -196,11 +196,6 @@ type ConnectOptions = {
timeout?: number; timeout?: number;
}; };
export interface TestStore {
get<T>(name: string): Promise<T | undefined>;
set<T>(name: string, value: T | undefined): Promise<void>;
}
export interface PlaywrightWorkerOptions { export interface PlaywrightWorkerOptions {
browserName: BrowserName; browserName: BrowserName;
defaultBrowserType: BrowserName; defaultBrowserType: BrowserName;
@ -234,7 +229,6 @@ export interface PlaywrightTestOptions {
permissions: string[] | undefined; permissions: string[] | undefined;
proxy: Proxy | undefined; proxy: Proxy | undefined;
storageState: StorageState | undefined; storageState: StorageState | undefined;
storageStateName: string | undefined;
timezoneId: string | undefined; timezoneId: string | undefined;
userAgent: string | undefined; userAgent: string | undefined;
viewport: ViewportSize | null | undefined; viewport: ViewportSize | null | undefined;
@ -357,7 +351,6 @@ export default test;
export const _baseTest: TestType<{}, {}>; export const _baseTest: TestType<{}, {}>;
export const expect: Expect; export const expect: Expect;
export const store: TestStore;
/** /**
* Defines Playwright config * Defines Playwright config