feat(test-runner): test filter

This commit is contained in:
Mathias Leppich 2024-10-10 20:02:08 +02:00
parent bc30cc795e
commit 58242e3592
8 changed files with 91 additions and 6 deletions

View file

@ -40,6 +40,12 @@ See [`property: TestConfig.globalTeardown`].
See [`property: TestConfig.globalTimeout`].
## property: FullConfig.filter
* since: v1.49
- type: <[null]|[TestFilter]|[Array]<[TestFilter]>>
See [`property: TestConfig.filter`].
## property: FullConfig.grep
* since: v1.10
- type: <[RegExp]|[Array]<[RegExp]>>

View file

@ -160,6 +160,13 @@ export default defineConfig({
});
```
## property: TestConfig.filter
* since: v1.49
- type: ?<[TestFilter]|[Array]<[TestFilter]>>
Filter tests by passing a function.
## property: TestConfig.grep
* since: v1.10
- type: ?<[RegExp]|[Array]<[RegExp]>>

View file

@ -78,6 +78,7 @@ export class FullConfigInternal {
globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null),
globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null),
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
filter: takeFirst(userConfig.filter, undefined),
grep: takeFirst(userConfig.grep, defaultGrep),
grepInvert: takeFirst(userConfig.grepInvert, null),
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : undefined, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
@ -289,4 +290,4 @@ const configInternalSymbol = Symbol('configInternalSymbol');
export function getProjectId(project: FullProject): string {
return (project as any).__projectId!;
}
}

View file

@ -581,6 +581,7 @@ export const baseFullConfig: reporterTypes.FullConfig = {
globalSetup: null,
globalTeardown: null,
globalTimeout: 0,
filter: null,
grep: /.*/,
grepInvert: null,
maxFailures: 0,

View file

@ -16,6 +16,7 @@
import path from 'path';
import type { FullConfig, Reporter, TestError } from '../../types/testReporter';
import type * as reporterTypes from '../../types/testReporter';
import { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost';
import { Suite } from '../common/test';
import type { TestCase } from '../common/test';
@ -173,22 +174,36 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
}
// Shard only the top-level projects.
if (config.config.shard) {
if (config.config.shard || config.config.filter) {
// Create test groups for top-level projects.
const testGroups: TestGroup[] = [];
for (const projectSuite of rootSuite.suites)
testGroups.push(...createTestGroups(projectSuite, config.config.workers));
if (config.config.filter) {
const filters = Array.isArray(config.config.filter) ? config.config.filter : [config.config.filter];
const allTests = new Set<reporterTypes.TestCase>(testGroups.flatMap(group => group.tests));
let filteredTests = [...allTests.values()];
for (const filter of filters)
filteredTests = filter(filteredTests);
const filteredTestSet = new Set(filteredTests);
for (const group of testGroups)
group.tests = group.tests.filter(test => filteredTestSet.has(test));
}
// Shard test groups.
const testGroupsInThisShard = filterForShard(config.config.shard, testGroups);
const testsInThisShard = new Set<TestCase>();
const testGroupsInThisShard = config.config.shard ? filterForShard(config.config.shard, testGroups) : new Set(testGroups);
const testsInThisRun = new Set<TestCase>();
for (const group of testGroupsInThisShard) {
for (const test of group.tests)
testsInThisShard.add(test);
testsInThisRun.add(test);
}
// Update project suites, removing empty ones.
filterTestsRemoveEmptySuites(rootSuite, test => testsInThisShard.has(test));
filterTestsRemoveEmptySuites(rootSuite, test => testsInThisRun.has(test));
}
// Now prepend dependency projects without filtration.

View file

@ -18,6 +18,8 @@
import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core';
export * from 'playwright-core';
import type { TestCase } from './testReporter';
export type ReporterDescription = Readonly<
['blob'] | ['blob', { outputDir?: string, fileName?: string }] |
['dot'] |
@ -1034,6 +1036,11 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
};
};
/**
* Filter tests by passing a function.
*/
filter?: TestFilter|Array<TestFilter>;
/**
* Whether to exit with an error if any tests or groups are marked as
* [test.only(title[, details, body])](https://playwright.dev/docs/api/class-test#test-only) or
@ -1720,6 +1727,11 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
*/
configFile?: string;
/**
* See [testConfig.filter](https://playwright.dev/docs/api/class-testconfig#test-config-filter).
*/
filter: null|TestFilter|Array<TestFilter>;
/**
* See [testConfig.forbidOnly](https://playwright.dev/docs/api/class-testconfig#test-config-forbid-only).
*/
@ -1838,6 +1850,8 @@ export type TestDetails = {
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
}
export type TestFilter = (tests: TestCase[]) => TestCase[];
interface SuiteFunction {
/**
* Declares a group of tests.

View file

@ -0,0 +1,37 @@
/**
* 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 { test, expect } from './playwright-test-fixtures';
test('config.filter should work', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
filter: (tests) => tests.filter(test => test.title === 'test1'),
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('test1', async () => { console.log('\\n%% test1'); });
test('test2', async () => { console.log('\\n%% test2'); });
`,
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.outputLines).toEqual([
'test1',
]);
});

View file

@ -17,6 +17,8 @@
import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core';
export * from 'playwright-core';
import type { TestCase } from './testReporter';
export type ReporterDescription = Readonly<
['blob'] | ['blob', { outputDir?: string, fileName?: string }] |
['dot'] |
@ -75,6 +77,8 @@ export type TestDetails = {
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
}
export type TestFilter = (tests: TestCase[]) => TestCase[];
interface SuiteFunction {
(title: string, callback: () => void): void;
(callback: () => void): void;