mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: group filter (#17646)
This commit is contained in:
parent
cadd4d1dd0
commit
9f17ee6871
@ -236,6 +236,10 @@ Filter to only run tests with a title **not** matching one of the patterns. This
|
|||||||
* since: v1.27
|
* since: v1.27
|
||||||
- type: ?<[Object]<[string],[Array]<[string]|[Array]<[string]|[Object]>>>>
|
- type: ?<[Object]<[string],[Array]<[string]|[Array]<[string]|[Object]>>>>
|
||||||
- `project` <[string]|[Array]<[string]>> Project name(s).
|
- `project` <[string]|[Array]<[string]>> Project name(s).
|
||||||
|
- `grep` ?<[RegExp]|[Array]<[RegExp]>> Filter to only run tests with a title matching one of the patterns.
|
||||||
|
- `grepInvert` ?<[RegExp]|[Array]<[RegExp]>> Filter to only run tests with a title **not** matching one of the patterns.
|
||||||
|
- `testMatch` ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>> Only the files matching one of these patterns are executed as test files. Matching is performed against the absolute file path. Strings are treated as glob patterns.
|
||||||
|
- `testIgnore` ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>> Files matching one of these patterns are not executed as test files. Matching is performed against the absolute file path. Strings are treated as glob patterns.
|
||||||
|
|
||||||
Project groups that control project execution order.
|
Project groups that control project execution order.
|
||||||
|
|
||||||
|
|||||||
@ -515,7 +515,7 @@ function validateConfig(file: string, config: Config) {
|
|||||||
throw errorWithFile(file, `config.grepInvert[${index}] must be a RegExp`);
|
throw errorWithFile(file, `config.grepInvert[${index}] must be a RegExp`);
|
||||||
});
|
});
|
||||||
} else if (!isRegExp(config.grepInvert)) {
|
} else if (!isRegExp(config.grepInvert)) {
|
||||||
throw errorWithFile(file, `config.grep must be a RegExp`);
|
throw errorWithFile(file, `config.grepInvert must be a RegExp`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,40 +656,73 @@ function validateProjectGroups(file: string, config: Config) {
|
|||||||
if (!projectNames.has(projectName))
|
if (!projectNames.has(projectName))
|
||||||
throw errorWithFile(file, `config.groups.${groupName} refers to an unknown project '${projectName}'`);
|
throw errorWithFile(file, `config.groups.${groupName} refers to an unknown project '${projectName}'`);
|
||||||
}
|
}
|
||||||
for (const step of group) {
|
group.forEach((step, stepIndex) => {
|
||||||
if (isString(step)) {
|
if (isString(step)) {
|
||||||
validateProjectReference(step);
|
validateProjectReference(step);
|
||||||
} else if (Array.isArray(step)) {
|
} else if (Array.isArray(step)) {
|
||||||
const parallelProjectNames = new Set();
|
const parallelProjectNames = new Set();
|
||||||
for (const item of step) {
|
step.forEach((item, itemIndex) => {
|
||||||
let projectName;
|
let projectName;
|
||||||
if (isString(item)) {
|
if (isString(item)) {
|
||||||
validateProjectReference(item);
|
validateProjectReference(item);
|
||||||
projectName = item;
|
projectName = item;
|
||||||
} else if (isObject(item)) {
|
} else if (isObject(item)) {
|
||||||
const project = (item as any).project;
|
const project = item.project;
|
||||||
if (isString(project)) {
|
if (isString(project)) {
|
||||||
validateProjectReference(project);
|
validateProjectReference(project);
|
||||||
} else if (Array.isArray(project)) {
|
} else if (Array.isArray(project)) {
|
||||||
project.forEach(name => {
|
project.forEach((name, projectIndex) => {
|
||||||
if (!isString(name))
|
if (!isString(name))
|
||||||
throw errorWithFile(file, `config.groups.${groupName}[*].project contains non string value.`);
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].project[${projectIndex}] contains non string value.`);
|
||||||
validateProjectReference(name);
|
validateProjectReference(name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
projectName = project;
|
projectName = project;
|
||||||
|
if ('grep' in item) {
|
||||||
|
if (Array.isArray(item.grep)) {
|
||||||
|
item.grep.forEach((item, grepIndex) => {
|
||||||
|
if (!isRegExp(item))
|
||||||
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].grep[${grepIndex}] must be a RegExp`);
|
||||||
|
});
|
||||||
|
} else if (!isRegExp(item.grep)) {
|
||||||
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].grep must be a RegExp`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('grepInvert' in item) {
|
||||||
|
if (Array.isArray(item.grepInvert)) {
|
||||||
|
item.grepInvert.forEach((item, index) => {
|
||||||
|
if (!isRegExp(item))
|
||||||
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].grepInvert[${index}] must be a RegExp`);
|
||||||
|
});
|
||||||
|
} else if (!isRegExp(item.grepInvert)) {
|
||||||
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].grepInvert must be a RegExp`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const prop of ['testIgnore', 'testMatch'] as const) {
|
||||||
|
if (prop in item) {
|
||||||
|
const value = item[prop];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
if (typeof item !== 'string' && !isRegExp(item))
|
||||||
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].${prop}[${index}] must be a string or a RegExp`);
|
||||||
|
});
|
||||||
|
} else if (typeof value !== 'string' && !isRegExp(value)) {
|
||||||
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}].${prop} must be a string or a RegExp`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw errorWithFile(file, `config.groups.${groupName} unexpected group entry ${JSON.stringify(step, null, 2)}`);
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}] unexpected group entry ${JSON.stringify(step, null, 2)}`);
|
||||||
}
|
}
|
||||||
// We can relax this later.
|
// We can relax this later.
|
||||||
if (parallelProjectNames.has(projectName))
|
if (parallelProjectNames.has(projectName))
|
||||||
throw errorWithFile(file, `config.groups.${groupName} group mentions project '${projectName}' twice in one parallel group`);
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}][${itemIndex}] group mentions project '${projectName}' twice in one parallel group`);
|
||||||
parallelProjectNames.add(projectName);
|
parallelProjectNames.add(projectName);
|
||||||
}
|
});
|
||||||
} else {
|
} else {
|
||||||
throw errorWithFile(file, `config.groups.${groupName} unexpected group entry ${JSON.stringify(step, null, 2)}`);
|
throw errorWithFile(file, `config.groups.${groupName}[${stepIndex}] unexpected group entry ${JSON.stringify(step, null, 2)}`);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import * as path from 'path';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import type { TestGroup } from './dispatcher';
|
import type { TestGroup } from './dispatcher';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import type { TestFileFilter } from './util';
|
import type { Matcher, TestFileFilter } from './util';
|
||||||
import { createFileMatcher, createTitleMatcher, serializeError } from './util';
|
import { createFileMatcher, createTitleMatcher, serializeError } from './util';
|
||||||
import type { TestCase } from './test';
|
import type { TestCase } from './test';
|
||||||
import { Suite } from './test';
|
import { Suite } from './test';
|
||||||
@ -56,9 +56,10 @@ export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.j
|
|||||||
|
|
||||||
// Project group is a sequence of run phases.
|
// Project group is a sequence of run phases.
|
||||||
type RunPhase = {
|
type RunPhase = {
|
||||||
testFileFilters: TestFileFilter[];
|
projectName: string;
|
||||||
projectFilter?: string[];
|
testFileMatcher: Matcher;
|
||||||
};
|
testTitleMatcher: Matcher;
|
||||||
|
}[];
|
||||||
|
|
||||||
type RunOptions = {
|
type RunOptions = {
|
||||||
listOnly?: boolean;
|
listOnly?: boolean;
|
||||||
@ -220,7 +221,13 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listTestFiles(configFile: string, projectNames: string[] | undefined): Promise<any> {
|
async listTestFiles(configFile: string, projectNames: string[] | undefined): Promise<any> {
|
||||||
const filesByProject = await this._collectFiles([], projectNames);
|
const projects = projectNames ?? this._loader.fullConfig().projects.map(p => p.name);
|
||||||
|
const phase: RunPhase = projects.map(projectName => ({
|
||||||
|
projectName,
|
||||||
|
testFileMatcher: () => true,
|
||||||
|
testTitleMatcher: () => true,
|
||||||
|
}));
|
||||||
|
const filesByProject = await this._collectFiles(phase);
|
||||||
const report: any = {
|
const report: any = {
|
||||||
projects: []
|
projects: []
|
||||||
};
|
};
|
||||||
@ -235,7 +242,6 @@ export class Runner {
|
|||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private _collectRunPhases(options: RunOptions) {
|
private _collectRunPhases(options: RunOptions) {
|
||||||
const config = this._loader.fullConfig();
|
const config = this._loader.fullConfig();
|
||||||
|
|
||||||
@ -258,64 +264,82 @@ export class Runner {
|
|||||||
if (!group)
|
if (!group)
|
||||||
throw new Error(`Cannot find project group '${projectGroup}' in the config`);
|
throw new Error(`Cannot find project group '${projectGroup}' in the config`);
|
||||||
for (const entry of group) {
|
for (const entry of group) {
|
||||||
const projectFilter: string[] = [];
|
|
||||||
const testFileFilters: TestFileFilter[] = [];
|
|
||||||
if (isString(entry)) {
|
if (isString(entry)) {
|
||||||
projectFilter.push(entry);
|
phases.push([{
|
||||||
|
projectName: entry,
|
||||||
|
testFileMatcher: () => true,
|
||||||
|
testTitleMatcher: () => true,
|
||||||
|
}]);
|
||||||
} else {
|
} else {
|
||||||
|
const phase: RunPhase = [];
|
||||||
|
phases.push(phase);
|
||||||
for (const p of entry) {
|
for (const p of entry) {
|
||||||
if (isString(p))
|
if (isString(p)) {
|
||||||
projectFilter.push(p);
|
phase.push({
|
||||||
else if (isString(p.project))
|
projectName: p,
|
||||||
projectFilter.push(p.project);
|
testFileMatcher: () => true,
|
||||||
else
|
testTitleMatcher: () => true,
|
||||||
projectFilter.push(...p.project);
|
});
|
||||||
|
} else {
|
||||||
|
const testMatch = p.testMatch ? createFileMatcher(p.testMatch) : () => true;
|
||||||
|
const testIgnore = p.testIgnore ? createFileMatcher(p.testIgnore) : () => false;
|
||||||
|
const grep = p.grep ? createTitleMatcher(p.grep) : () => true;
|
||||||
|
const grepInvert = p.grepInvert ? createTitleMatcher(p.grepInvert) : () => false;
|
||||||
|
const projects = isString(p.project) ? [p.project] : p.project;
|
||||||
|
phase.push(...projects.map(projectName => ({
|
||||||
|
projectName,
|
||||||
|
testFileMatcher: (file: string) => !testIgnore(file) && testMatch(file),
|
||||||
|
testTitleMatcher: (title: string) => !grepInvert(title) && grep(title),
|
||||||
|
})));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: filter per project set.
|
|
||||||
phases.push({
|
|
||||||
testFileFilters,
|
|
||||||
projectFilter
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
phases.push({
|
phases.push(this._runPhaseFromOptions(options));
|
||||||
projectFilter: options.projectFilter,
|
|
||||||
testFileFilters: options.testFileFilters || [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return phases;
|
return phases;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _collectFiles(testFileFilters: TestFileFilter[], projectNames?: string[]): Promise<Map<FullProjectInternal, string[]>> {
|
private _runPhaseFromOptions(options: RunOptions): RunPhase {
|
||||||
const testFileFilter = testFileFilters.length ? createFileMatcher(testFileFilters.map(e => e.re || e.exact || '')) : () => true;
|
const testFileMatcher = fileMatcherFrom(options.testFileFilters);
|
||||||
let projectsToFind: Set<string> | undefined;
|
const testTitleMatcher = () => true;
|
||||||
let unknownProjects: Map<string, string> | undefined;
|
const projects = options.projectFilter ?? this._loader.fullConfig().projects.map(p => p.name);
|
||||||
if (projectNames) {
|
return projects.map(projectName => ({
|
||||||
projectsToFind = new Set();
|
projectName,
|
||||||
unknownProjects = new Map();
|
testFileMatcher,
|
||||||
projectNames.forEach(n => {
|
testTitleMatcher
|
||||||
const name = n.toLocaleLowerCase();
|
}));
|
||||||
projectsToFind!.add(name);
|
}
|
||||||
unknownProjects!.set(name, n);
|
|
||||||
});
|
private _collectProjects(projectNames: string[]): FullProjectInternal[] {
|
||||||
}
|
const projectsToFind = new Set<string>();
|
||||||
|
const unknownProjects = new Map<string, string>();
|
||||||
|
projectNames.forEach(n => {
|
||||||
|
const name = n.toLocaleLowerCase();
|
||||||
|
projectsToFind.add(name);
|
||||||
|
unknownProjects.set(name, n);
|
||||||
|
});
|
||||||
const fullConfig = this._loader.fullConfig();
|
const fullConfig = this._loader.fullConfig();
|
||||||
const projects = fullConfig.projects.filter(project => {
|
const projects = fullConfig.projects.filter(project => {
|
||||||
if (!projectsToFind)
|
|
||||||
return true;
|
|
||||||
const name = project.name.toLocaleLowerCase();
|
const name = project.name.toLocaleLowerCase();
|
||||||
unknownProjects!.delete(name);
|
unknownProjects.delete(name);
|
||||||
return projectsToFind.has(name);
|
return projectsToFind.has(name);
|
||||||
});
|
});
|
||||||
if (unknownProjects && unknownProjects.size) {
|
if (unknownProjects.size) {
|
||||||
const names = fullConfig.projects.map(p => p.name).filter(name => !!name);
|
const names = fullConfig.projects.map(p => p.name).filter(name => !!name);
|
||||||
if (!names.length)
|
if (!names.length)
|
||||||
throw new Error(`No named projects are specified in the configuration file`);
|
throw new Error(`No named projects are specified in the configuration file`);
|
||||||
const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', ');
|
const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', ');
|
||||||
throw new Error(`Project(s) ${unknownProjectNames} not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`);
|
throw new Error(`Project(s) ${unknownProjectNames} not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`);
|
||||||
}
|
}
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _collectFiles(runPhase: RunPhase): Promise<Map<FullProjectInternal, string[]>> {
|
||||||
|
const projectNames = runPhase.map(p => p.projectName);
|
||||||
|
const projects = this._collectProjects(projectNames);
|
||||||
|
const projectToGroupEntry = new Map(runPhase.map(p => [p.projectName.toLocaleLowerCase(), p]));
|
||||||
const files = new Map<FullProjectInternal, string[]>();
|
const files = new Map<FullProjectInternal, string[]>();
|
||||||
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);
|
||||||
@ -323,6 +347,7 @@ export class Runner {
|
|||||||
const testIgnore = createFileMatcher(project.testIgnore);
|
const testIgnore = createFileMatcher(project.testIgnore);
|
||||||
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 testFileFilter = projectToGroupEntry.get(project.name.toLocaleLowerCase())!.testFileMatcher;
|
||||||
const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file) && testFileExtension(file));
|
const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file) && testFileExtension(file));
|
||||||
files.set(project, testFiles);
|
files.set(project, testFiles);
|
||||||
}
|
}
|
||||||
@ -338,9 +363,9 @@ export class Runner {
|
|||||||
const rootSuite = new Suite('', 'root');
|
const rootSuite = new Suite('', 'root');
|
||||||
const runPhases = this._collectRunPhases(options);
|
const runPhases = this._collectRunPhases(options);
|
||||||
assert(runPhases.length > 0);
|
assert(runPhases.length > 0);
|
||||||
for (const { projectFilter, testFileFilters } of runPhases) {
|
for (const phase of runPhases) {
|
||||||
// TODO: do not collect files for each project multiple times.
|
// TODO: do not collect files for each project multiple times.
|
||||||
const filesByProject = await this._collectFiles(testFileFilters, projectFilter);
|
const filesByProject = await this._collectFiles(phase);
|
||||||
|
|
||||||
const allTestFiles = new Set<string>();
|
const allTestFiles = new Set<string>();
|
||||||
for (const files of filesByProject.values())
|
for (const files of filesByProject.values())
|
||||||
@ -363,7 +388,8 @@ export class Runner {
|
|||||||
|
|
||||||
// 3. Filter tests to respect line/column filter.
|
// 3. Filter tests to respect line/column filter.
|
||||||
// TODO: figure out how this is supposed to work with groups.
|
// TODO: figure out how this is supposed to work with groups.
|
||||||
filterByFocusedLine(preprocessRoot, testFileFilters);
|
if (options.testFileFilters?.length)
|
||||||
|
filterByFocusedLine(preprocessRoot, options.testFileFilters);
|
||||||
|
|
||||||
// 4. Complain about only.
|
// 4. Complain about only.
|
||||||
if (config.forbidOnly) {
|
if (config.forbidOnly) {
|
||||||
@ -385,6 +411,7 @@ export class Runner {
|
|||||||
for (const [project, files] of filesByProject) {
|
for (const [project, files] of filesByProject) {
|
||||||
const grepMatcher = createTitleMatcher(project.grep);
|
const grepMatcher = createTitleMatcher(project.grep);
|
||||||
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
|
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
|
||||||
|
const groupTitleMatcher = phase.find(p => p.projectName.toLocaleLowerCase() === project.name.toLocaleLowerCase())!.testTitleMatcher;
|
||||||
const projectSuite = new Suite(project.name, 'project');
|
const projectSuite = new Suite(project.name, 'project');
|
||||||
projectSuite._projectConfig = project;
|
projectSuite._projectConfig = project;
|
||||||
if (project._fullyParallel)
|
if (project._fullyParallel)
|
||||||
@ -399,7 +426,7 @@ export class Runner {
|
|||||||
const grepTitle = test.titlePath().join(' ');
|
const grepTitle = test.titlePath().join(' ');
|
||||||
if (grepInvertMatcher?.(grepTitle))
|
if (grepInvertMatcher?.(grepTitle))
|
||||||
return false;
|
return false;
|
||||||
return grepMatcher(grepTitle);
|
return grepMatcher(grepTitle) && groupTitleMatcher(grepTitle);
|
||||||
});
|
});
|
||||||
if (builtSuite)
|
if (builtSuite)
|
||||||
projectSuite._addSuite(builtSuite);
|
projectSuite._addSuite(builtSuite);
|
||||||
@ -546,8 +573,7 @@ export class Runner {
|
|||||||
|
|
||||||
const runAndWatch = async () => {
|
const runAndWatch = async () => {
|
||||||
// 5. Collect all files.
|
// 5. Collect all files.
|
||||||
const testFileFilters = options.testFileFilters || [];
|
const filesByProject = await this._collectFiles(this._runPhaseFromOptions(options));
|
||||||
const filesByProject = await this._collectFiles(testFileFilters, options.projectFilter);
|
|
||||||
|
|
||||||
const allTestFiles = new Set<string>();
|
const allTestFiles = new Set<string>();
|
||||||
for (const files of filesByProject.values())
|
for (const files of filesByProject.values())
|
||||||
@ -579,12 +605,10 @@ export class Runner {
|
|||||||
if (progress.canceled)
|
if (progress.canceled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const testFileFilters: TestFileFilter[] = [...testFiles].map(f => ({
|
const fileMatcher = createFileMatcher([...testFiles]);
|
||||||
exact: f,
|
const phase = this._runPhaseFromOptions(options);
|
||||||
line: null,
|
phase.forEach(p => p.testFileMatcher = fileMatcher);
|
||||||
column: null,
|
const filesByProject = await this._collectFiles(phase);
|
||||||
}));
|
|
||||||
const filesByProject = await this._collectFiles(testFileFilters, options.projectFilter);
|
|
||||||
|
|
||||||
if (progress.canceled)
|
if (progress.canceled)
|
||||||
return;
|
return;
|
||||||
@ -1023,6 +1047,12 @@ class ListModeReporter implements Reporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileMatcherFrom(testFileFilters?: TestFileFilter[]): Matcher {
|
||||||
|
if (testFileFilters?.length)
|
||||||
|
return createFileMatcher(testFileFilters.map(e => e.re || e.exact || ''));
|
||||||
|
return () => true;
|
||||||
|
}
|
||||||
|
|
||||||
function createForbidOnlyError(config: FullConfigInternal, onlyTestsAndSuites: (TestCase | Suite)[]): TestError {
|
function createForbidOnlyError(config: FullConfigInternal, onlyTestsAndSuites: (TestCase | Suite)[]): TestError {
|
||||||
const errorMessage = [
|
const errorMessage = [
|
||||||
'=====================================',
|
'=====================================',
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export function createFileMatcher(patterns: string | RegExp | (string | RegExp)[
|
|||||||
if (isRegExp(pattern)) {
|
if (isRegExp(pattern)) {
|
||||||
reList.push(pattern);
|
reList.push(pattern);
|
||||||
} else {
|
} else {
|
||||||
if (!pattern.startsWith('**/') && !pattern.startsWith('**/'))
|
if (!pattern.startsWith('**/'))
|
||||||
filePatterns.push('**/' + pattern);
|
filePatterns.push('**/' + pattern);
|
||||||
else
|
else
|
||||||
filePatterns.push(pattern);
|
filePatterns.push(pattern);
|
||||||
|
|||||||
22
packages/playwright-test/types/test.d.ts
vendored
22
packages/playwright-test/types/test.d.ts
vendored
@ -695,6 +695,28 @@ interface TestConfig {
|
|||||||
* Project name(s).
|
* Project name(s).
|
||||||
*/
|
*/
|
||||||
project: string|Array<string>;
|
project: string|Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to only run tests with a title matching one of the patterns.
|
||||||
|
*/
|
||||||
|
grep?: RegExp|Array<RegExp>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to only run tests with a title **not** matching one of the patterns.
|
||||||
|
*/
|
||||||
|
grepInvert?: RegExp|Array<RegExp>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only the files matching one of these patterns are executed as test files. Matching is performed against the absolute
|
||||||
|
* file path. Strings are treated as glob patterns.
|
||||||
|
*/
|
||||||
|
testMatch?: string|RegExp|Array<string|RegExp>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Files matching one of these patterns are not executed as test files. Matching is performed against the absolute file
|
||||||
|
* path. Strings are treated as glob patterns.
|
||||||
|
*/
|
||||||
|
testIgnore?: string|RegExp|Array<string|RegExp>;
|
||||||
}>>; };
|
}>>; };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -502,7 +502,103 @@ test('should throw when group has duplicate project references', async ({ runInl
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain(`config.groups.default group mentions project 'a' twice in one parallel group`);
|
expect(result.output).toContain(`config.groups.default[0][1] group mentions project 'a' twice in one parallel group`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw when group grep has invalid type', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a' },
|
||||||
|
],
|
||||||
|
groups: {
|
||||||
|
default: [
|
||||||
|
[{ project: 'a', grep: 2022 }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.groups.default[0][0].grep must be a RegExp`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw when group grepInvert has invalid type', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a' },
|
||||||
|
],
|
||||||
|
groups: {
|
||||||
|
default: [
|
||||||
|
[{ project: 'a', grepInvert: [{}] }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.groups.default[0][0].grepInvert[0] must be a RegExp`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw when group testMatch has invalid type', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a' },
|
||||||
|
],
|
||||||
|
groups: {
|
||||||
|
all: [
|
||||||
|
[{ project: 'a', testMatch: [{}] }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.groups.all[0][0].testMatch[0] must be a string or a RegEx`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw when group testIgnore has invalid type', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a' },
|
||||||
|
],
|
||||||
|
groups: {
|
||||||
|
all: [
|
||||||
|
[{ project: 'a', testIgnore: [2022] }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.groups.all[0][0].testIgnore[0] must be a string or a RegEx`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw when group has unknown project reference', async ({ runInlineTest }) => {
|
test('should throw when group has unknown project reference', async ({ runInlineTest }) => {
|
||||||
|
|||||||
@ -13,13 +13,13 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import type { PlaywrightTestConfig, TestInfo } from '@playwright/test';
|
import type { PlaywrightTestConfig, TestInfo, PlaywrightTestProject } from '@playwright/test';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
import { test, expect } from './playwright-test-fixtures';
|
||||||
|
|
||||||
function createConfigWithProjects(names: string[], testInfo: TestInfo, groups: PlaywrightTestConfig['groups']): Record<string, string> {
|
function createConfigWithProjects(names: string[], testInfo: TestInfo, groups: PlaywrightTestConfig['groups'], projectTemplates?: { [name: string]: PlaywrightTestProject }): Record<string, string> {
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
projects: names.map(name => ({ name, testDir: testInfo.outputPath(name) })),
|
projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })),
|
||||||
groups
|
groups
|
||||||
};
|
};
|
||||||
const files = {};
|
const files = {};
|
||||||
@ -30,10 +30,16 @@ function createConfigWithProjects(names: string[], testInfo: TestInfo, groups: P
|
|||||||
await new Promise(f => setTimeout(f, 100));
|
await new Promise(f => setTimeout(f, 100));
|
||||||
});`;
|
});`;
|
||||||
}
|
}
|
||||||
|
function replacer(key, value) {
|
||||||
|
if (value instanceof RegExp)
|
||||||
|
return `RegExp(${value.toString()})`;
|
||||||
|
else
|
||||||
|
return value;
|
||||||
|
}
|
||||||
files['playwright.config.ts'] = `
|
files['playwright.config.ts'] = `
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
module.exports = ${JSON.stringify(config)};
|
module.exports = ${JSON.stringify(config, replacer, 2)};
|
||||||
`;
|
`.replace(/"RegExp\((.*)\)"/g, '$1');
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +49,12 @@ function formatTimeline(timeline: Timeline) {
|
|||||||
return timeline.map(e => `${e.titlePath.slice(1).join(' > ')} [${e.event}]`).join('\n');
|
return timeline.map(e => `${e.titlePath.slice(1).join(' > ')} [${e.event}]`).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function projectNames(timeline: Timeline) {
|
||||||
|
const projectNames = Array.from(new Set(timeline.map(({ titlePath }) => titlePath[1])).keys());
|
||||||
|
projectNames.sort();
|
||||||
|
return projectNames;
|
||||||
|
}
|
||||||
|
|
||||||
function expectRunBefore(timeline: Timeline, before: string[], after: string[]) {
|
function expectRunBefore(timeline: Timeline, before: string[], after: string[]) {
|
||||||
const begin = new Map<string, number>();
|
const begin = new Map<string, number>();
|
||||||
const end = new Map<string, number>();
|
const end = new Map<string, number>();
|
||||||
@ -258,3 +270,97 @@ test('should throw when unknown --group is passed', async ({ runGroups }, testIn
|
|||||||
expect(exitCode).toBe(1);
|
expect(exitCode).toBe(1);
|
||||||
expect(output).toContain(`Cannot find project group 'bar' in the config`);
|
expect(output).toContain(`Cannot find project group 'bar' in the config`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should support testMatch and testIgnore', async ({ runGroups }, testInfo) => {
|
||||||
|
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, {
|
||||||
|
default: [
|
||||||
|
[
|
||||||
|
{ project: ['a', 'b'], testMatch: ['**/a.spec.ts'] },
|
||||||
|
{ project: ['c', 'd'], testMatch: [/.*c.spec.ts/, '**/*d*'] },
|
||||||
|
{ project: ['e'], testIgnore: [/.*e.spec.ts/] },
|
||||||
|
{ project: ['f'], testMatch: /does not match/ },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(passed).toBe(3);
|
||||||
|
expect(projectNames(timeline)).toEqual(['a', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support grep and grepInvert', async ({ runGroups }, testInfo) => {
|
||||||
|
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, {
|
||||||
|
default: [
|
||||||
|
[
|
||||||
|
{ project: ['a', 'b'], grep: /.*a test/ },
|
||||||
|
{ project: ['c', 'd'], grepInvert: [/.*c test/] },
|
||||||
|
{ project: ['e', 'f'], grep: /.*(e|f) test/, grepInvert: [/.*f test/] },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(passed).toBe(3);
|
||||||
|
expect(projectNames(timeline)).toEqual(['a', 'd', 'e']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should intercect gpoup and project level grep and grepInvert', async ({ runGroups }, testInfo) => {
|
||||||
|
const projectTemplates = {
|
||||||
|
'a': {
|
||||||
|
grep: /a test/,
|
||||||
|
grepInvert: [/no test/],
|
||||||
|
},
|
||||||
|
'b': {
|
||||||
|
grep: /.*b te.*/,
|
||||||
|
grepInvert: [/.*a test/],
|
||||||
|
},
|
||||||
|
'c': {
|
||||||
|
grepInvert: [/.*test/],
|
||||||
|
},
|
||||||
|
'd': {
|
||||||
|
grep: [/.*unkwnown test/],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, {
|
||||||
|
default: [
|
||||||
|
[
|
||||||
|
{ project: ['a', 'b', 'c', 'd', 'e'], grep: /.*(b|c|d|e) test/, grepInvert: /.*d test/ },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}, projectTemplates);
|
||||||
|
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(passed).toBe(2);
|
||||||
|
expect(projectNames(timeline)).toEqual(['b', 'e']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should intercect gpoup and project level testMatch and testIgnore', async ({ runGroups }, testInfo) => {
|
||||||
|
const projectTemplates = {
|
||||||
|
'a': {
|
||||||
|
testMatch: /.*a.spec.ts/,
|
||||||
|
testIgnore: [/no test/],
|
||||||
|
},
|
||||||
|
'b': {
|
||||||
|
testMatch: '**/b.spec.ts',
|
||||||
|
testIgnore: [/.*a.spec.ts/],
|
||||||
|
},
|
||||||
|
'c': {
|
||||||
|
testIgnore: [/.*no-match.spec.ts/],
|
||||||
|
},
|
||||||
|
'd': {
|
||||||
|
testMatch: [/.*unkwnown/],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, {
|
||||||
|
default: [
|
||||||
|
[
|
||||||
|
{ project: ['a', 'b', 'c', 'd'], testMatch: /.*(b|c|d).spec.ts/, testIgnore: /.*c.spec.ts/ },
|
||||||
|
{ project: ['c', 'd', 'e', 'f'], testIgnore: /.*[^ef].spec.ts/ },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}, projectTemplates);
|
||||||
|
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(passed).toBe(3);
|
||||||
|
expect(projectNames(timeline)).toEqual(['b', 'e', 'f']);
|
||||||
|
});
|
||||||
|
|||||||
@ -254,7 +254,7 @@ function parseVariable(line) {
|
|||||||
if (depth === 0)
|
if (depth === 0)
|
||||||
return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2), optional, experimental };
|
return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2), optional, experimental };
|
||||||
}
|
}
|
||||||
throw new Error('Should not be reached');
|
throw new Error('Should not be reached, line: ' + line);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user