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].
## 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
* 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
* since: v1.10
- 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-%%
* 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
* 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
- 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).
@ -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.
## 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
* since: v1.10
- 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._ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._ignoreSnapshots);
this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig._globalScripts = takeFirst(config.globalScripts, null);
const workers = takeFirst(config.workers, '50%');
if (typeof workers === 'string') {
@ -152,9 +151,8 @@ export class ConfigLoader {
this._fullConfig._webServers = [webServers];
}
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._assignUniqueProjectIds([...this._fullConfig.projects, this._fullConfig._globalProject]);
this._assignUniqueProjectIds(this._fullConfig.projects);
}
private _assignUniqueProjectIds(projects: FullProjectInternal[]) {
@ -217,7 +215,6 @@ export class ConfigLoader {
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(projectConfig.name, config.name, '');
const _setupMatch = takeFirst(projectConfig.setupMatch, []);
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
@ -234,7 +231,6 @@ export class ConfigLoader {
metadata: takeFirst(projectConfig.metadata, config.metadata, undefined),
name,
testDir,
_setupMatch,
_respectGitIgnore: respectGitIgnore,
snapshotDir,
snapshotPathTemplate,
@ -425,7 +421,7 @@ function validateProject(file: string, project: Project, title: 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) {
const value = project[prop];
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 = {
forbidOnly: false,
fullyParallel: false,
@ -483,8 +474,6 @@ export const baseFullConfig: FullConfigInternal = {
_storeDir: '',
_maxConcurrentTestGroups: 0,
_ignoreSnapshots: false,
_globalScripts: null,
_globalProject: { } as FullProjectInternal,
};
function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {

View file

@ -14,14 +14,13 @@
* 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 { Suite } from './test';
import type { ConfigLoader } from './configLoader';
import type { ProcessExitData } from './processHost';
import { TestCase } from './test';
import type { TestCase } from './test';
import { ManualPromise } from 'playwright-core/lib/utils';
import { TestTypeImpl } from './testType';
import { WorkerHost } from './workerHost';
export type TestGroup = {
@ -30,8 +29,6 @@ export type TestGroup = {
repeatEachIndex: number;
projectId: string;
tests: TestCase[];
watchMode: boolean;
phase: 'test' | 'projectSetup' | 'globalSetup';
};
type TestResultData = {
@ -169,15 +166,12 @@ export class Dispatcher {
entries: testGroup.tests.map(test => {
return { testId: test.id, retry: test.results.length };
}),
watchMode: testGroup.watchMode,
phase: testGroup.phase,
};
worker.runTestGroup(runPayload);
let doneCallback = () => {};
const result = new Promise<void>(f => doneCallback = f);
const doneWithJob = () => {
worker.removeListener('watchTestResolved', onWatchTestResolved);
worker.removeListener('testBegin', onTestBegin);
worker.removeListener('testEnd', onTestEnd);
worker.removeListener('stepBegin', onStepBegin);
@ -190,12 +184,6 @@ export class Dispatcher {
const remainingByTestId = new Map(testGroup.tests.map(e => [e.id, e]));
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 data = this._testById.get(params.testId)!;
const result = data.test._appendTestResult();

View file

@ -20,14 +20,12 @@ import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOp
import * as playwrightLibrary from 'playwright-core';
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 { store as _baseStore } from './store';
import type { TestInfoImpl } from './testInfo';
import { rootTestType } from './testType';
import { type ContextReuseMode } from './types';
export { expect } from './expect';
export { addRunnerPlugin as _addRunnerPlugin } from './plugins';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
export const store = _baseStore;
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 }],
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
storageStateName: [undefined, { option: true }],
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { 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 }],
@ -175,7 +172,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions,
proxy,
storageState,
storageStateName,
viewport,
timezoneId,
userAgent,
@ -214,14 +210,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
options.permissions = permissions;
if (proxy !== undefined)
options.proxy = proxy;
if (storageStateName !== 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) {
if (storageState !== undefined)
options.storageState = storageState;
}
if (timezoneId !== undefined)
options.timezoneId = timezoneId;
if (userAgent !== undefined)

View file

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

View file

@ -200,7 +200,7 @@ export class Runner {
async listTestFiles(projectNames: string[] | undefined): Promise<any> {
const projects = this._collectProjects(projectNames);
const { filesByProject } = await this._collectFiles(projects, []);
const filesByProject = await this._collectFiles(projects, []);
const report: any = {
projects: []
};
@ -239,98 +239,45 @@ export class Runner {
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 testFileExtension = (file: string) => extensions.includes(path.extname(file));
const filesByProject = new Map<FullProjectInternal, string[]>();
const setupFiles = new Set<string>();
const fileToProjectName = new Map<string, string>();
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) {
const allFiles = await collectFiles(project.testDir, project._respectGitIgnore);
const setupMatch = createFileMatcher(project._setupMatch);
const testMatch = createFileMatcher(project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore);
const testFiles = allFiles.filter(file => {
if (!testFileExtension(file))
return false;
const isSetup = setupMatch(file);
const isTest = !testIgnore(file) && testMatch(file) && commandLineFileMatcher(file);
if (!isTest && !isSetup)
if (!isTest)
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);
if (isSetup)
setupFiles.add(file);
return true;
});
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 projects = this._collectProjects(options.projectFilter);
const { filesByProject, setupFiles, globalSetupFiles } = await this._collectFiles(projects, options.testFileFilters);
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);
}
const filesByProject = await this._collectFiles(projects, options.testFileFilters);
const result = await this._createFilteredRootSuite(options, filesByProject);
this._fatalErrors.push(...result.fatalErrors);
const { rootSuite } = result;
const allTestGroups = createTestGroups(rootSuite.suites, config.workers);
const globalSetupGroups = [];
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 };
const testGroups = createTestGroups(rootSuite.suites, config.workers);
return { rootSuite, 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 fatalErrors: TestError[] = [];
const allTestFiles = new Set<string>();
@ -341,23 +288,18 @@ export class Runner {
// Add all tests.
const preprocessRoot = new Suite('', 'root');
for (const file of allTestFiles) {
let type: 'test' | 'projectSetup' | 'globalSetup' = 'test';
if (globalSetupFiles.has(file))
type = 'globalSetup';
else if (setupFiles.has(file))
type = 'projectSetup';
const fileSuite = await testLoader.loadTestFile(file, 'runner', type);
const fileSuite = await testLoader.loadTestFile(file, 'runner');
if (fileSuite._loadError)
fatalErrors.push(fileSuite._loadError);
// 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.
fatalErrors.push(...createDuplicateTitlesErrors(config, preprocessRoot));
// Filter tests to respect line/column filter.
filterByFocusedLine(preprocessRoot, options.testFileFilters, doNotFilterFiles);
filterByFocusedLine(preprocessRoot, options.testFileFilters);
// Complain about only.
if (config.forbidOnly) {
@ -368,7 +310,7 @@ export class Runner {
// Filter only.
if (!options.listOnly)
filterOnly(preprocessRoot, doNotFilterFiles);
filterOnly(preprocessRoot);
// Generate projects.
const fileSuites = new Map<string, Suite>();
@ -381,8 +323,6 @@ export class Runner {
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
const titleMatcher = (test: TestCase) => {
if (doNotFilterFiles.has(test._requireFile))
return true;
const grepTitle = test.titlePath().join(' ');
if (grepInvertMatcher?.(grepTitle))
return false;
@ -408,7 +348,7 @@ export class Runner {
return { rootSuite, fatalErrors };
}
private _filterForCurrentShard(rootSuite: Suite, projectSetupGroups: TestGroup[], testGroups: TestGroup[]) {
private _filterForCurrentShard(rootSuite: Suite, testGroups: TestGroup[]) {
const shard = this._configLoader.fullConfig().shard;
if (!shard)
return;
@ -447,17 +387,6 @@ export class Runner {
testGroups.length = 0;
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) {
// 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.
@ -465,10 +394,7 @@ export class Runner {
rootSuite.suites = [];
rootSuite.tests = [];
} else {
// Unlike project setup files global setup always run regardless of the selected tests.
// 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');
filterSuiteWithOnlySemantics(rootSuite, () => false, test => shardTests.has(test));
}
}
@ -476,15 +402,15 @@ export class Runner {
const config = this._configLoader.fullConfig();
// 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.
const { rootSuite, globalSetupGroups, projectSetupGroups, testGroups } = await this._collectTestGroups(options);
const { rootSuite, testGroups } = await this._collectTestGroups(options);
// Fail when no tests.
if (!rootSuite.allTests().length && !options.passWithNoTests)
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
this._reporter.onBegin?.(config, rootSuite);
@ -521,24 +447,7 @@ export class Runner {
// Run tests.
try {
// TODO: run only setups, keep workers alive, inherit process.env from global setup workers
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);
}
}
}
const dispatchResult = await this._dispatchToWorkers(testGroups);
if (dispatchResult === 'signal') {
result.status = 'interrupted';
} else {
@ -694,11 +603,11 @@ export class Runner {
}
}
function filterOnly(suite: Suite, doNotFilterFiles: Set<string>) {
function filterOnly(suite: Suite) {
if (!suite._getOnlyItems().length)
return;
const suiteFilter = (suite: Suite) => suite._only || doNotFilterFiles.has(suite._requireFile);
const testFilter = (test: TestCase) => test._only || doNotFilterFiles.has(test._requireFile);
const suiteFilter = (suite: Suite) => suite._only;
const testFilter = (test: TestCase) => test._only;
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);
}
function filterByFocusedLine(suite: Suite, focusedTestFileLines: TestFileFilter[], doNotFilterFiles: Set<string>) {
function filterByFocusedLine(suite: Suite, focusedTestFileLines: TestFileFilter[]) {
if (!focusedTestFileLines.length)
return;
const matchers = focusedTestFileLines.map(createFileMatcherFromFilter);
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 testFilter = (test: TestCase) => doNotFilterFiles.has(test._requireFile) || testFileLineMatches(test.location.file, test.location.line, test.location.column);
const suiteFilter = (suite: Suite) => !!suite.location && testFileLineMatches(suite.location.file, suite.location.line, suite.location.column);
const testFilter = (test: TestCase) => testFileLineMatches(test.location.file, test.location.line, test.location.column);
return filterSuite(suite, suiteFilter, testFilter);
}
@ -859,8 +768,6 @@ function createTestGroups(projectSuites: Suite[], workers: number): TestGroup[]
repeatEachIndex: test.repeatEachIndex,
projectId: test._projectId,
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;
_only = false;
_requireFile: string = '';
_phase: 'test' | 'projectSetup' | 'globalSetup' = 'test';
constructor(title: string) {
this.title = title;
@ -121,7 +120,6 @@ export class Suite extends Base implements reporterTypes.Suite {
suite._only = this._only;
suite.location = this.location;
suite._requireFile = this._requireFile;
suite._phase = this._phase;
suite._use = this._use.slice();
suite._hooks = this._hooks.slice();
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);
test._only = this._only;
test._requireFile = this._requireFile;
test._phase = this._phase;
test.expectedStatus = this.expectedStatus;
test.annotations = this.annotations.slice();
test._annotateWithInheritence = this._annotateWithInheritence;

View file

@ -38,12 +38,11 @@ export class TestLoader {
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))
return cachedFileSuites.get(file)!;
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file), 'file');
suite._requireFile = file;
suite._phase = phase;
suite.location = { file, line: 0, column: 0 };
setCurrentlyLoadingFileSuite(suite);

View file

@ -55,8 +55,6 @@ export class TestTypeImpl {
test.step = wrapFunctionWithLocation(this._step.bind(this));
test.use = wrapFunctionWithLocation(this._use.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.info = () => {
const result = currentTestInfo();
@ -67,7 +65,7 @@ export class TestTypeImpl {
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();
if (!suite) {
addFatalError([
@ -80,36 +78,19 @@ export class TestTypeImpl {
].join('\n'), location);
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;
}
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();
let functionTitle = '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);
const suite = this._currentSuite(location, 'test()');
if (!suite)
return;
const test = new TestCase(title, fn, this, location);
test._requireFile = suite._requireFile;
test._phase = suite._phase;
suite._addTest(test);
if (type === 'only' || type === 'projectSetupOnly')
if (type === 'only')
test._only = true;
if (type === 'skip' || type === 'fixme') {
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) {
throwIfRunningInsideJest();
const suite = this._currentSuite(location, 'test.describe()', 'any');
const suite = this._currentSuite(location, 'test.describe()');
if (!suite)
return;
@ -134,7 +115,6 @@ export class TestTypeImpl {
const child = new Suite(title, 'describe');
child._requireFile = suite._requireFile;
child._phase = suite._phase;
child.location = location;
suite._addSuite(child);
@ -160,7 +140,7 @@ export class TestTypeImpl {
}
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)
return;
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 }) {
throwIfRunningInsideJest();
const suite = this._currentSuite(location, `test.describe.configure()`, 'any');
const suite = this._currentSuite(location, `test.describe.configure()`);
if (!suite)
return;
@ -235,7 +215,7 @@ export class TestTypeImpl {
}
private _use(location: Location, fixtures: Fixtures) {
const suite = this._currentSuite(location, `test.use()`, 'any');
const suite = this._currentSuite(location, `test.use()`);
if (!suite)
return;
suite._use.push({ fixtures, location });

View file

@ -52,10 +52,6 @@ export interface FullConfigInternal extends FullConfigPublic {
*/
webServer: FullConfigPublic['webServer'];
_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.
projects: FullProjectInternal[];
@ -71,7 +67,6 @@ export interface FullProjectInternal extends FullProjectPublic {
_fullyParallel: boolean;
_expect: Project['expect'];
_respectGitIgnore: boolean;
_setupMatch: string | RegExp | (string | RegExp)[];
snapshotPathTemplate: string;
}

View file

@ -17,7 +17,7 @@
import { colors, rimraf } from 'playwright-core/lib/utilsBundle';
import util 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 { ConfigLoader } from './configLoader';
import type { Suite, TestCase } from './test';
@ -171,10 +171,6 @@ export class WorkerRunner extends ProcessRunner {
this._configLoader = await ConfigLoader.deserialize(this._params.loader);
this._testLoader = new TestLoader(this._configLoader.fullConfig());
const globalProject = this._configLoader.fullConfig()._globalProject;
if (this._params.projectId === globalProject._id)
this._project = globalProject;
else
this._project = this._configLoader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
}
@ -184,17 +180,8 @@ export class WorkerRunner extends ProcessRunner {
let fatalUnknownTestIds;
try {
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 => {
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))
return false;
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
* [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
* 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.
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
*
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/
@ -634,22 +631,6 @@ interface TestConfig {
*/
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
* function that takes a [`TestConfig`] argument.
@ -3298,35 +3279,6 @@ type ConnectOptions = {
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.
*
@ -3560,16 +3512,6 @@ export interface PlaywrightTestOptions {
* Either a path to the file with saved storage, or an object with the following fields:
*/
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
* [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 expect: Expect;
export const store: TestStore;
/**
* 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
* [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
* 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.
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
*
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/
@ -5217,24 +5155,6 @@ interface TestProject {
*/
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
* [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);
});
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 { test, expect } from './playwright-test-fixtures';
test.fixme(true, 'Restore this');
type Timeline = { titlePath: string[], event: 'begin' | 'end' }[];
function formatTimeline(timeline: Timeline) {

View file

@ -17,6 +17,8 @@ import type { PlaywrightTestConfig, TestInfo, PlaywrightTestProject } from '@pla
import path from 'path';
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> {
const config: PlaywrightTestConfig = {
projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })),

View file

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

View file

@ -188,18 +188,3 @@ test('config should allow void/empty options', async ({ runTSC }) => {
});
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;
};
export interface TestStore {
get<T>(name: string): Promise<T | undefined>;
set<T>(name: string, value: T | undefined): Promise<void>;
}
export interface PlaywrightWorkerOptions {
browserName: BrowserName;
defaultBrowserType: BrowserName;
@ -234,7 +229,6 @@ export interface PlaywrightTestOptions {
permissions: string[] | undefined;
proxy: Proxy | undefined;
storageState: StorageState | undefined;
storageStateName: string | undefined;
timezoneId: string | undefined;
userAgent: string | undefined;
viewport: ViewportSize | null | undefined;
@ -357,7 +351,6 @@ export default test;
export const _baseTest: TestType<{}, {}>;
export const expect: Expect;
export const store: TestStore;
/**
* Defines Playwright config