diff --git a/docs/src/test-cli-js.md b/docs/src/test-cli-js.md
index b88766a58b..ebdc814d25 100644
--- a/docs/src/test-cli-js.md
+++ b/docs/src/test-cli-js.md
@@ -92,8 +92,7 @@ Complete set of Playwright Test options is available in the [configuration file]
| `--no-deps` | Ignore the dependencies between projects and behave as if they were not specified. |
| `--output
` | Directory for artifacts produced by tests, defaults to `test-results`. |
| `--pass-with-no-tests` | Allows the test suite to pass when no files are found. |
-| `--project ` | Only run tests from the specified [projects](./test-projects.md). Defaults to running all projects defined in the configuration file.|
-| `--project-grep ` | Only run tests from the projects matching this regular expression. Defaults to running all projects defined in the configuration file.|
+| `--project ` | Only run tests from the specified [projects](./test-projects.md), supports '*' wildcard. Defaults to running all projects defined in the configuration file.|
| `--quiet` | Whether to suppress stdout and stderr from the tests. |
| `--repeat-each ` | Run each test `N` times, defaults to one. |
| `--reporter ` | Choose a reporter: minimalist `dot`, concise `line` or detailed `list`. See [reporters](./test-reporters.md) for more information. |
diff --git a/packages/playwright-core/src/server/injected/selectorGenerator.ts b/packages/playwright-core/src/server/injected/selectorGenerator.ts
index b629db04be..b88b25acf4 100644
--- a/packages/playwright-core/src/server/injected/selectorGenerator.ts
+++ b/packages/playwright-core/src/server/injected/selectorGenerator.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, quoteCSSAttributeValue } from '../../utils/isomorphic/stringUtils';
+import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, escapeRegExp, quoteCSSAttributeValue } from '../../utils/isomorphic/stringUtils';
import { closestCrossShadow, isInsideScope, parentElementOrShadowHost } from './domUtils';
import type { InjectedScript } from './injectedScript';
import { getAriaRole, getElementAccessibleName, beginAriaCaches, endAriaCaches } from './roleUtils';
@@ -508,11 +508,6 @@ function isGuidLike(id: string): boolean {
return transitionCount >= id.length / 4;
}
-function escapeRegExp(s: string) {
- // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
-}
-
function trimWordBoundary(text: string, maxLength: number) {
if (text.length <= maxLength)
return text;
diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts
index 2595c40ff2..16642ae042 100644
--- a/packages/playwright-core/src/utils/index.ts
+++ b/packages/playwright-core/src/utils/index.ts
@@ -43,3 +43,4 @@ export * from './userAgent';
export * from './zipFile';
export * from './zones';
export * from './isomorphic/locatorGenerators';
+export * from './isomorphic/stringUtils';
diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
index d51ecf8d41..19953ef5bb 100644
--- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
+++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
@@ -115,4 +115,9 @@ export function trimString(input: string, cap: number, suffix: string = ''): str
export function trimStringWithEllipsis(input: string, cap: number): string {
return trimString(input, cap, '\u2026');
-}
\ No newline at end of file
+}
+
+export function escapeRegExp(s: string) {
+ // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
diff --git a/packages/playwright-core/src/utils/rtti.ts b/packages/playwright-core/src/utils/rtti.ts
index f096ae8ee4..a18d3a450a 100644
--- a/packages/playwright-core/src/utils/rtti.ts
+++ b/packages/playwright-core/src/utils/rtti.ts
@@ -14,9 +14,7 @@
* limitations under the License.
*/
-export function isString(obj: any): obj is string {
- return typeof obj === 'string' || obj instanceof String;
-}
+export { isString } from './isomorphic/stringUtils';
export function isRegExp(obj: any): obj is RegExp {
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts
index 4262d4f889..5aa4a9abe0 100644
--- a/packages/playwright/src/common/config.ts
+++ b/packages/playwright/src/common/config.ts
@@ -47,7 +47,6 @@ export class FullConfigInternal {
cliGrep: string | undefined;
cliGrepInvert: string | undefined;
cliProjectFilter?: string[];
- cliProjectGrep?: string;
cliListOnly = false;
cliPassWithNoTests?: boolean;
testIdMatcher?: Matcher;
diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts
index 64c00d9db6..705ee4091f 100644
--- a/packages/playwright/src/program.ts
+++ b/packages/playwright/src/program.ts
@@ -64,8 +64,7 @@ function addListFilesCommand(program: Command) {
const command = program.command('list-files [file-filter...]', { hidden: true });
command.description('List files with Playwright Test tests');
command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
- command.option('--project ', `Only run tests from the specified list of projects (default: list all projects)`);
- command.option('--project-grep ', `Only run tests from the projects matching this regular expression (default: list all projects)`);
+ command.option('--project ', `Only run tests from the specified list of projects, supports '*' wildcard (default: list all projects)`);
command.action(async (args, opts) => listTestFiles(opts));
}
@@ -159,15 +158,11 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
if (!config)
return;
- if (opts.project && opts.projectGrep)
- throw new Error('Only one of --project and --project-grep can be specified.');
-
config.cliArgs = args;
config.cliGrep = opts.grep as string | undefined;
config.cliGrepInvert = opts.grepInvert as string | undefined;
config.cliListOnly = !!opts.list;
config.cliProjectFilter = opts.project || undefined;
- config.cliProjectGrep = opts.projectGrep || undefined;
config.cliPassWithNoTests = !!opts.passWithNoTests;
const runner = new Runner(config);
@@ -206,11 +201,9 @@ export async function withRunnerAndMutedWrite(configFile: string | undefined, ca
}
async function listTestFiles(opts: { [key: string]: any }) {
- if (opts.project && opts.projectGrep)
- throw new Error('Only one of --project and --project-grep can be specified.');
await withRunnerAndMutedWrite(opts.config, async (runner, config) => {
const frameworkPackage = (config as any)['@playwright/test']?.['packageJSON'];
- return await runner.listTestFiles(frameworkPackage, opts.project, opts.projectGrep);
+ return await runner.listTestFiles(frameworkPackage, opts.project);
});
}
@@ -324,8 +317,7 @@ const testOptions: [string, string][] = [
['--no-deps', 'Do not run project dependencies'],
['--output ', `Folder for output artifacts (default: "test-results")`],
['--pass-with-no-tests', `Makes test run succeed even if no tests were found`],
- ['--project ', `Only run tests from the specified list of projects(default: run all projects)`],
- ['--project-grep ', `Only run tests from the projects matching this regular expression (default: run all projects)`],
+ ['--project ', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`],
['--quiet', `Suppress stdio`],
['--repeat-each ', `Run each test N times (default: 1)`],
['--reporter ', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`],
diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts
index cc88d426c1..bd2c45e35f 100644
--- a/packages/playwright/src/runner/loadUtils.ts
+++ b/packages/playwright/src/runner/loadUtils.ts
@@ -40,7 +40,7 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
// First collect all files for the projects in the command line, don't apply any file filters.
const allFilesForProject = new Map();
- const filteredProjects = filterProjects(config.projects, config.cliProjectFilter, config.cliProjectGrep);
+ const filteredProjects = filterProjects(config.projects, config.cliProjectFilter);
for (const project of filteredProjects) {
const files = await collectFilesForProject(project, fsCache);
allFilesForProject.set(project, files);
diff --git a/packages/playwright/src/runner/projectUtils.ts b/packages/playwright/src/runner/projectUtils.ts
index 591e7f5966..49a6c89d84 100644
--- a/packages/playwright/src/runner/projectUtils.ts
+++ b/packages/playwright/src/runner/projectUtils.ts
@@ -16,35 +16,34 @@
import fs from 'fs';
import path from 'path';
+import { escapeRegExp } from 'playwright-core/lib/utils';
import { minimatch } from 'playwright-core/lib/utilsBundle';
import { promisify } from 'util';
import type { FullProjectInternal } from '../common/config';
-import { createFileMatcher, forceRegExp } from '../util';
+import { createFileMatcher } from '../util';
const readFileAsync = promisify(fs.readFile);
const readDirAsync = promisify(fs.readdir);
-export function filterProjects(projects: FullProjectInternal[], projectNames?: string[], projectGrep?: string): FullProjectInternal[] {
- if (!projectNames && !projectGrep)
- return [...projects];
+function wildcardPatternToRegExp(pattern: string): RegExp {
+ return new RegExp('^' + pattern.split('*').map(escapeRegExp).join('.*') + '$', 'ig');
+}
- if (projectGrep) {
- const regex = forceRegExp(projectGrep);
- const result = projects.filter(project => {
- regex.lastIndex = 0;
- return regex.test(project.project.name);
- });
- if (!result.length)
- throw new Error(`Projects matching "${projectGrep}" not found. Available projects: ${projects.map(p => `"${p.project.name}"`).join(', ')}`);
- return result;
- }
+export function filterProjects(projects: FullProjectInternal[], projectNames?: string[]): FullProjectInternal[] {
+ if (!projectNames)
+ return [...projects];
const projectNamesToFind = new Set();
const unmatchedProjectNames = new Map();
+ const patterns = new Set();
for (const name of projectNames!) {
const lowerCaseName = name.toLocaleLowerCase();
- projectNamesToFind.add(lowerCaseName);
- unmatchedProjectNames.set(lowerCaseName, name);
+ if (lowerCaseName.includes('*')) {
+ patterns.add(wildcardPatternToRegExp(lowerCaseName));
+ } else {
+ projectNamesToFind.add(lowerCaseName);
+ unmatchedProjectNames.set(lowerCaseName, name);
+ }
}
const result = projects.filter(project => {
@@ -53,6 +52,11 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
unmatchedProjectNames.delete(lowerCaseName);
return true;
}
+ for (const regex of patterns) {
+ regex.lastIndex = 0;
+ if (regex.test(lowerCaseName))
+ return true;
+ }
return false;
});
@@ -61,6 +65,12 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
throw new Error(`Project(s) ${unknownProjectNames} not found. Available projects: ${projects.map(p => `"${p.project.name}"`).join(', ')}`);
}
+ if (!result.length) {
+ const allProjects = projects.map(p => `"${p.project.name}"`).join(', ');
+ throw new Error(`No projects matched. Available projects: ${allProjects}`);
+ }
+
+
return result;
}
diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts
index f8fa2cac0e..2f3fa739ab 100644
--- a/packages/playwright/src/runner/runner.ts
+++ b/packages/playwright/src/runner/runner.ts
@@ -51,8 +51,8 @@ export class Runner {
this._config = config;
}
- async listTestFiles(frameworkPackage: string | undefined, projectNames: string[] | undefined, projectGrep: string | undefined): Promise {
- const projects = filterProjects(this._config.projects, projectNames, projectGrep);
+ async listTestFiles(frameworkPackage: string | undefined, projectNames: string[] | undefined): Promise {
+ const projects = filterProjects(this._config.projects, projectNames);
const report: ConfigListFilesReport = {
projects: [],
cliEntryPoint: frameworkPackage ? path.join(path.dirname(frameworkPackage), 'cli.js') : undefined,
diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts
index 3862cad76f..28f363d58f 100644
--- a/packages/playwright/src/runner/tasks.ts
+++ b/packages/playwright/src/runner/tasks.ts
@@ -168,7 +168,7 @@ function createRemoveOutputDirsTask(): Task {
if (process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
return;
const outputDirs = new Set();
- const projects = filterProjects(config.projects, config.cliProjectFilter, config.cliProjectGrep);
+ const projects = filterProjects(config.projects, config.cliProjectFilter);
projects.forEach(p => outputDirs.add(p.project.outputDir));
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolders([outputDir]).then(async ([error]) => {
diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts
index 8ce7972f87..575653d3e2 100644
--- a/packages/playwright/src/runner/watchMode.ts
+++ b/packages/playwright/src/runner/watchMode.ts
@@ -40,7 +40,7 @@ class FSWatcher {
async update(config: FullConfigInternal) {
const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true;
- const projects = filterProjects(config.projects, config.cliProjectFilter, config.cliProjectGrep);
+ const projects = filterProjects(config.projects, config.cliProjectFilter);
const projectClosure = buildProjectsClosure(projects);
const projectFilters = new Map();
for (const [project, type] of projectClosure) {
@@ -263,7 +263,7 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector
// Collect all the affected projects, follow project dependencies.
// Prepare to exclude all the projects that do not depend on this file, as if they did not exist.
- const projects = filterProjects(config.projects, config.cliProjectFilter, config.cliProjectGrep);
+ const projects = filterProjects(config.projects, config.cliProjectFilter);
const projectClosure = buildProjectsClosure(projects);
const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
@@ -388,8 +388,6 @@ function printConfiguration(config: FullConfigInternal, title?: string) {
const tokens: string[] = [];
tokens.push(`${packageManagerCommand} playwright test`);
tokens.push(...(config.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`)));
- if (config.cliProjectGrep)
- tokens.push(colors.blue(`--project-grep ${config.cliProjectGrep}`));
if (config.cliGrep)
tokens.push(colors.red(`--grep ${config.cliGrep}`));
if (config.cliArgs)
diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts
index f6e12f202e..34907428c9 100644
--- a/packages/playwright/src/util.ts
+++ b/packages/playwright/src/util.ts
@@ -15,12 +15,11 @@
*/
import fs from 'fs';
-import { mime } from 'playwright-core/lib/utilsBundle';
import type { StackFrame } from '@protocol/channels';
import util from 'util';
import path from 'path';
import url from 'url';
-import { debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
+import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import { formatCallLog } from 'playwright-core/lib/utils';
import type { TestInfoError } from './../types/test';
import type { Location } from './../types/testReporter';
diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts
index 6a0b5970e6..38fef69915 100644
--- a/tests/playwright-test/config.spec.ts
+++ b/tests/playwright-test/config.spec.ts
@@ -245,7 +245,7 @@ test('should filter by project, case-insensitive', async ({ runInlineTest }) =>
]));
});
-test('should filter by project-grep', async ({ runInlineTest }) => {
+test('should filter by project wildcard', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `
module.exports = {
@@ -260,7 +260,7 @@ test('should filter by project-grep', async ({ runInlineTest }) => {
test('one', async ({}) => {
console.log('%%' + test.info().project.name);
}); `
- }, { '--project-grep': '.*oj.*t-Na.?e' });
+ }, { '--project': '*oj*t-Na*e' });
expect(result.exitCode).toBe(0);
expect(result.output).toContain('Running 1 test using 1 worker');
expect(new Set(result.outputLines)).toEqual(new Set([
@@ -268,7 +268,7 @@ test('should filter by project-grep', async ({ runInlineTest }) => {
]));
});
-test('should print nice error when the project grep does not match anything', async ({ runInlineTest }) => {
+test('should print nice error when the project wildcard does not match anything', async ({ runInlineTest }) => {
const { output, exitCode } = await runInlineTest({
'playwright.config.ts': `
module.exports = { projects: [
@@ -282,38 +282,21 @@ test('should print nice error when the project grep does not match anything', as
console.log(testInfo.project.name);
});
`
- }, { '--project-grep': ['aaa'] });
+ }, { '--project': ['not*found'] });
expect(exitCode).toBe(1);
- expect(output).toContain('Error: Projects matching \"aaa\" not found. Available projects: \"suite1\", \"suite2\"');
+ expect(output).toContain('Error: No projects matched. Available projects: "suite1", "suite2"');
});
-test('should fail if both --project and --project-grep are passed', async ({ runInlineTest }) => {
- const { output, exitCode } = await runInlineTest({
- 'playwright.config.ts': `
- module.exports = { projects: [
- { name: 'suite1' },
- { name: 'suite2' },
- ] };
- `,
- 'a.test.ts': `
- import { test, expect } from '@playwright/test';
- test('pass', async ({}, testInfo) => {
- console.log(testInfo.project.name);
- });
- `
- }, { '--project-grep': 'foo', '--project': 'bar' });
- expect(exitCode).toBe(1);
- expect(output).toContain('Only one of --project and --project-grep can be specified');
-});
-
-test('should filter by project and allow passing RegExp start/end flags', async ({ runInlineTest }) => {
+test('should filter by project wildcard and exact name', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `
module.exports = {
projects: [
- { name: 'prefix-fooBar' },
+ { name: 'first' },
{ name: 'fooBar' },
- { name: 'foobar' },
+ { name: 'foobarBaz' },
+ { name: 'prefix' },
+ { name: 'prefixEnd' },
]
};
`,
@@ -322,9 +305,9 @@ test('should filter by project and allow passing RegExp start/end flags', async
test('one', async ({}) => {
console.log('%%' + test.info().project.name);
}); `
- }, { '--project-grep': '/fooBar$/' });
+ }, { '--project': ['first', '*bar', 'pref*x'] });
expect(result.exitCode).toBe(0);
- expect(new Set(result.outputLines)).toEqual(new Set(['prefix-fooBar', 'fooBar']));
+ expect(new Set(result.outputLines)).toEqual(new Set(['first', 'fooBar', 'prefix']));
});
test('should print nice error when project is unknown', async ({ runInlineTest }) => {
diff --git a/tests/playwright-test/list-files.spec.ts b/tests/playwright-test/list-files.spec.ts
index 0ef2eda304..25cb3e6cee 100644
--- a/tests/playwright-test/list-files.spec.ts
+++ b/tests/playwright-test/list-files.spec.ts
@@ -48,13 +48,13 @@ test('should list files', async ({ runCLICommand }) => {
});
});
-test('should support project-grep list files', async ({ runCLICommand }) => {
+test('should support wildcard list files', async ({ runCLICommand }) => {
const result = await runCLICommand({
'playwright.config.ts': `
module.exports = { projects: [{ name: 'foo' }, { name: 'bar' }] };
`,
'a.test.js': ``
- }, 'list-files', ['--project-grep', 'f.o']);
+ }, 'list-files', ['--project', 'f*o']);
expect(result.exitCode).toBe(0);
const data = JSON.parse(result.stdout);
@@ -62,7 +62,7 @@ test('should support project-grep list files', async ({ runCLICommand }) => {
projects: [
{
name: 'foo',
- testDir: expect.stringContaining('list-files-should-support-project-grep-list-files-playwright-test'),
+ testDir: expect.stringContaining('list-files-should-support-wildcard-list-files-playwright-test'),
use: {},
files: [
expect.stringContaining('a.test.js')