From f3fde992eb5ce8a35afd99158928e06d8a47f322 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 16 Feb 2024 19:18:00 -0800 Subject: [PATCH] chore: remove --project-grep, add wildcard support (#29537) Reference #15128 --- docs/src/test-cli-js.md | 3 +- .../src/server/injected/selectorGenerator.ts | 7 +--- packages/playwright-core/src/utils/index.ts | 1 + .../src/utils/isomorphic/stringUtils.ts | 7 +++- packages/playwright-core/src/utils/rtti.ts | 4 +- packages/playwright/src/common/config.ts | 1 - packages/playwright/src/program.ts | 14 ++----- packages/playwright/src/runner/loadUtils.ts | 2 +- .../playwright/src/runner/projectUtils.ts | 42 ++++++++++++------- packages/playwright/src/runner/runner.ts | 4 +- packages/playwright/src/runner/tasks.ts | 2 +- packages/playwright/src/runner/watchMode.ts | 6 +-- packages/playwright/src/util.ts | 3 +- tests/playwright-test/config.spec.ts | 41 ++++++------------ tests/playwright-test/list-files.spec.ts | 6 +-- 15 files changed, 61 insertions(+), 82 deletions(-) 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')