diff --git a/package-lock.json b/package-lock.json index 20e80771cf..ca8f40f91f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2037,7 +2037,6 @@ }, "node_modules/braces": { "version": "3.0.2", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.0.1" @@ -2169,14 +2168,14 @@ }, "node_modules/chokidar": { "version": "3.5.3", - "dev": true, + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "funding": [ { "type": "individual", "url": "https://paulmillr.com/funding/" } ], - "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2195,7 +2194,6 @@ }, "node_modules/chokidar/node_modules/anymatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -2207,7 +2205,6 @@ }, "node_modules/chokidar/node_modules/binary-extensions": { "version": "2.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2215,7 +2212,6 @@ }, "node_modules/chokidar/node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -3521,7 +3517,6 @@ }, "node_modules/fill-range": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3676,7 +3671,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3943,7 +3937,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3959,7 +3952,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -3970,7 +3962,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4419,7 +4410,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4602,7 +4592,6 @@ }, "node_modules/picomatch": { "version": "2.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -4876,7 +4865,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -5397,7 +5385,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6091,6 +6078,7 @@ "license": "Apache-2.0", "dependencies": { "@types/node": "*", + "chokidar": "3.5.3", "playwright-core": "1.31.0-next" }, "bin": { @@ -6935,6 +6923,7 @@ "version": "file:packages/playwright-test", "requires": { "@types/node": "*", + "chokidar": "3.5.3", "playwright-core": "1.31.0-next" } }, @@ -7427,7 +7416,6 @@ }, "braces": { "version": "3.0.2", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -7503,7 +7491,8 @@ }, "chokidar": { "version": "3.5.3", - "dev": true, + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -7517,19 +7506,16 @@ "dependencies": { "anymatch": { "version": "3.1.2", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "binary-extensions": { - "version": "2.2.0", - "dev": true + "version": "2.2.0" }, "is-binary-path": { "version": "2.1.0", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -8328,7 +8314,6 @@ }, "fill-range": { "version": "7.0.1", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -8430,7 +8415,6 @@ }, "glob-parent": { "version": "5.1.2", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -8609,8 +8593,7 @@ } }, "is-extglob": { - "version": "2.1.1", - "dev": true + "version": "2.1.1" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -8618,14 +8601,12 @@ }, "is-glob": { "version": "4.0.3", - "dev": true, "requires": { "is-extglob": "^2.1.1" } }, "is-number": { - "version": "7.0.0", - "dev": true + "version": "7.0.0" }, "is-what": { "version": "4.1.8", @@ -8916,8 +8897,7 @@ } }, "normalize-path": { - "version": "3.0.0", - "dev": true + "version": "3.0.0" }, "normalize-url": { "version": "4.5.1", @@ -9033,8 +9013,7 @@ "version": "1.0.0" }, "picomatch": { - "version": "2.2.3", - "dev": true + "version": "2.2.3" }, "pify": { "version": "4.0.1", @@ -9220,7 +9199,6 @@ }, "readdirp": { "version": "3.6.0", - "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -9548,7 +9526,6 @@ }, "to-regex-range": { "version": "5.0.1", - "dev": true, "requires": { "is-number": "^7.0.0" } diff --git a/packages/playwright-test/ThirdPartyNotices.txt b/packages/playwright-test/ThirdPartyNotices.txt index 0fa7a4a37d..90f46a0a2b 100644 --- a/packages/playwright-test/ThirdPartyNotices.txt +++ b/packages/playwright-test/ThirdPartyNotices.txt @@ -74,6 +74,7 @@ This project incorporates components from the projects listed below. The origina - @types/stack-utils@2.0.1 (https://github.com/DefinitelyTyped/DefinitelyTyped) - @types/yargs-parser@21.0.0 (https://github.com/DefinitelyTyped/DefinitelyTyped) - @types/yargs@16.0.4 (https://github.com/DefinitelyTyped/DefinitelyTyped) +- ansi-colors@4.1.3 (https://github.com/doowb/ansi-colors) - ansi-regex@5.0.1 (https://github.com/chalk/ansi-regex) - ansi-styles@3.2.1 (https://github.com/chalk/ansi-styles) - ansi-styles@4.3.0 (https://github.com/chalk/ansi-styles) @@ -94,6 +95,7 @@ This project incorporates components from the projects listed below. The origina - define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) - diff-sequences@27.5.1 (https://github.com/facebook/jest) - electron-to-chromium@1.4.284 (https://github.com/kilian/electron-to-chromium) +- enquirer@2.3.6 (https://github.com/enquirer/enquirer) - escalade@3.1.1 (https://github.com/lukeed/escalade) - escape-string-regexp@1.0.5 (https://github.com/sindresorhus/escape-string-regexp) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) @@ -2179,6 +2181,32 @@ MIT License ========================================= END OF @types/yargs@16.0.4 AND INFORMATION +%% ansi-colors@4.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015-present, Brian Woodward. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ansi-colors@4.1.3 AND INFORMATION + %% ansi-regex@5.0.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -2944,6 +2972,32 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE ========================================= END OF electron-to-chromium@1.4.284 AND INFORMATION +%% enquirer@2.3.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2016-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF enquirer@2.3.6 AND INFORMATION + %% escalade@3.1.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -3869,6 +3923,6 @@ END OF update-browserslist-db@1.0.10 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 132 +Total Packages: 134 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-test/bundles/utils/package-lock.json b/packages/playwright-test/bundles/utils/package-lock.json index 4e7988f40b..2a45d18e30 100644 --- a/packages/playwright-test/bundles/utils/package-lock.json +++ b/packages/playwright-test/bundles/utils/package-lock.json @@ -8,6 +8,7 @@ "name": "utils-bundle", "version": "0.0.1", "dependencies": { + "enquirer": "^2.3.6", "json5": "2.2.3", "open": "8.4.0", "pirates": "4.0.4", @@ -43,6 +44,14 @@ "@types/node": "*" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -56,6 +65,17 @@ "node": ">=8" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -168,6 +188,11 @@ "@types/node": "*" } }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -178,6 +203,14 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "requires": { + "ansi-colors": "^4.1.1" + } + }, "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", diff --git a/packages/playwright-test/bundles/utils/package.json b/packages/playwright-test/bundles/utils/package.json index 80aa9b4533..d4ab80e3e1 100644 --- a/packages/playwright-test/bundles/utils/package.json +++ b/packages/playwright-test/bundles/utils/package.json @@ -9,6 +9,7 @@ "generate-license": "node ../../../../utils/generate_third_party_notice.js" }, "dependencies": { + "enquirer": "^2.3.6", "json5": "2.2.3", "open": "8.4.0", "pirates": "4.0.4", diff --git a/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts index 52a4b0fd8c..8601827939 100644 --- a/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts @@ -28,3 +28,6 @@ export const sourceMapSupport = sourceMapSupportLibrary; import stoppableLibrary from 'stoppable'; export const stoppable = stoppableLibrary; + +import enquirerLibrary from 'enquirer'; +export const enquirer = enquirerLibrary; \ No newline at end of file diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index f3e599f3ef..1f0c2b86b3 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -34,6 +34,7 @@ "license": "Apache-2.0", "dependencies": { "@types/node": "*", + "chokidar": "3.5.3", "playwright-core": "1.31.0-next" } } diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index 353ff563b9..556e6d78c6 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -21,7 +21,7 @@ import fs from 'fs'; import path from 'path'; import { Runner } from './runner/runner'; import { stopProfiling, startProfiling } from './common/profiler'; -import { experimentalLoaderOption, fileIsModule } from './util'; +import { createFileFilterForArg, experimentalLoaderOption, fileIsModule, forceRegExp } from './util'; import { createTitleMatcher } from './util'; import { showHTMLReport } from './reporters/html'; import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; @@ -61,6 +61,7 @@ function addTestCommand(program: Command) { command.option('--project ', `Only run tests from the specified list of projects (default: run all projects)`); command.option('--timeout ', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})`); command.option('--trace ', `Force tracing mode, can be ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`); + command.option('--watch', `Run watch mode`); command.option('-u, --update-snapshots', `Update snapshots with actual results (default: only create missing snapshots)`); command.option('-x', `Stop after the first failure`); command.action(async (args, opts) => { @@ -159,14 +160,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { configLoader.ignoreProjectDependencies(); const config = configLoader.fullConfig(); - config._internal.cliFileFilters = args.map(arg => { - const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); - return { - re: forceRegExp(match ? match[1] : arg), - line: match ? parseInt(match[2], 10) : null, - column: match?.[3] ? parseInt(match[3], 10) : null, - }; - }); + config._internal.cliFileFilters = args.map(arg => createFileFilterForArg(arg)); const grepMatcher = opts.grep ? createTitleMatcher(forceRegExp(opts.grep)) : () => true; const grepInvertMatcher = opts.grepInvert ? createTitleMatcher(forceRegExp(opts.grepInvert)) : () => false; config._internal.cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); @@ -175,7 +169,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config._internal.passWithNoTests = !!opts.passWithNoTests; const runner = new Runner(config); - const status = await runner.runAllTests(); + if (opts.watch) + process.stdout.write('\x1Bc'); + const status = await runner.runAllTests(!!opts.watch); await stopProfiling(undefined); if (status === 'interrupted') @@ -201,13 +197,6 @@ async function listTestFiles(opts: { [key: string]: any }) { }); } -function forceRegExp(pattern: string): RegExp { - const match = pattern.match(/^\/(.*)\/([gi]*)$/); - if (match) - return new RegExp(match[1], match[2]); - return new RegExp(pattern, 'gi'); -} - function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides { const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; return { diff --git a/packages/playwright-test/src/common/suiteUtils.ts b/packages/playwright-test/src/common/suiteUtils.ts index a46c0b3b46..839e780a04 100644 --- a/packages/playwright-test/src/common/suiteUtils.ts +++ b/packages/playwright-test/src/common/suiteUtils.ts @@ -18,7 +18,7 @@ import path from 'path'; import { calculateSha1 } from 'playwright-core/lib/utils'; import type { Suite, TestCase } from './test'; import type { FullProjectInternal } from './types'; -import type { TestFileFilter } from '../util'; +import type { Matcher, TestFileFilter } from '../util'; import { createFileMatcher } from '../util'; @@ -114,6 +114,12 @@ export function filterByFocusedLine(suite: Suite, focusedTestFileLines: TestFile return filterSuite(suite, suiteFilter, testFilter); } +export function filterByTestIds(suite: Suite, testIdMatcher: Matcher | undefined) { + if (!testIdMatcher) + return; + filterTestsRemoveEmptySuites(suite, test => testIdMatcher(test.id)); +} + function createFileMatcherFromFilter(filter: TestFileFilter) { const fileMatcher = createFileMatcher(filter.re || filter.exact || ''); return (testFileName: string, testLine: number, testColumn: number) => diff --git a/packages/playwright-test/src/common/types.ts b/packages/playwright-test/src/common/types.ts index ae8c712696..087a3e0559 100644 --- a/packages/playwright-test/src/common/types.ts +++ b/packages/playwright-test/src/common/types.ts @@ -53,6 +53,7 @@ type ConfigInternal = { cliFileFilters: TestFileFilter[]; cliTitleMatcher: Matcher; cliProjectFilter?: string[]; + testIdMatcher?: Matcher; passWithNoTests?: boolean; }; diff --git a/packages/playwright-test/src/plugins/index.ts b/packages/playwright-test/src/plugins/index.ts index 4124f0a556..4851668307 100644 --- a/packages/playwright-test/src/plugins/index.ts +++ b/packages/playwright-test/src/plugins/index.ts @@ -21,6 +21,7 @@ export interface TestRunnerPlugin { name: string; setup?(config: FullConfig, configDir: string, reporter: Reporter): Promise; begin?(suite: Suite): Promise; + end?(): Promise; teardown?(): Promise; } diff --git a/packages/playwright-test/src/plugins/vitePlugin.ts b/packages/playwright-test/src/plugins/vitePlugin.ts index cd72c1bd39..ed5e3c6586 100644 --- a/packages/playwright-test/src/plugins/vitePlugin.ts +++ b/packages/playwright-test/src/plugins/vitePlugin.ts @@ -163,7 +163,7 @@ export function createPlugin( } }, - teardown: async () => { + end: async () => { await new Promise(f => stoppableServer.stop(f)); }, }; diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index af6007e436..258e13d958 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -112,7 +112,7 @@ class HtmlReporter implements Reporter { this._buildResult = await builder.build({ ...this.config.metadata, duration }, reports); } - async onExit() { + async _onExit() { if (process.env.CI || !this._buildResult) return; diff --git a/packages/playwright-test/src/reporters/multiplexer.ts b/packages/playwright-test/src/reporters/multiplexer.ts index 8a3a37053d..f5974a4963 100644 --- a/packages/playwright-test/src/reporters/multiplexer.ts +++ b/packages/playwright-test/src/reporters/multiplexer.ts @@ -102,7 +102,7 @@ export class Multiplexer implements Reporter { await Promise.resolve().then(() => reporter.onEnd?.(result)).catch(e => console.error('Error in reporter', e)); for (const reporter of this._reporters) - await Promise.resolve().then(() => (reporter as any).onExit?.()).catch(e => console.error('Error in reporter', e)); + await Promise.resolve().then(() => (reporter as any)._onExit?.()).catch(e => console.error('Error in reporter', e)); } onError(error: TestError) { diff --git a/packages/playwright-test/src/runner/DEPS.list b/packages/playwright-test/src/runner/DEPS.list index e4b32ae307..0ea818521f 100644 --- a/packages/playwright-test/src/runner/DEPS.list +++ b/packages/playwright-test/src/runner/DEPS.list @@ -5,4 +5,5 @@ ../reporters/ ../third_party/ ../plugins/ -../util.ts \ No newline at end of file +../util.ts +../utilsBundle.ts diff --git a/packages/playwright-test/src/runner/loadUtils.ts b/packages/playwright-test/src/runner/loadUtils.ts index 2700476002..990e29c520 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -25,10 +25,10 @@ import { createTitleMatcher, errorWithFile } from '../util'; import type { Matcher, TestFileFilter } from '../util'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils'; import { requireOrImport } from '../common/transform'; -import { buildFileSuiteForProject, filterByFocusedLine, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; +import { buildFileSuiteForProject, filterByFocusedLine, filterByTestIds, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { filterForShard } from './testGroups'; -export async function loadAllTests(config: FullConfigInternal, projectsToIgnore: Set, fileMatcher: Matcher, errors: TestError[]): Promise { +export async function loadAllTests(mode: 'out-of-process' | 'in-process', config: FullConfigInternal, projectsToIgnore: Set, fileMatcher: Matcher, errors: TestError[]): Promise { const projects = filterProjects(config.projects, config._internal.cliProjectFilter); let filesToRunByProject = new Map(); @@ -81,7 +81,7 @@ export async function loadAllTests(config: FullConfigInternal, projectsToIgnore: // Load all test files and create a preprocessed root. Child suites are files there. const fileSuits: Suite[] = []; { - const loaderHost: LoaderHost = process.env.PW_TEST_OOP_LOADER ? new OutOfProcessLoaderHost(config) : new InProcessLoaderHost(config); + const loaderHost: LoaderHost = mode === 'out-of-process' ? new OutOfProcessLoaderHost(config) : new InProcessLoaderHost(config); const allTestFiles = new Set(); for (const files of filesToRunByProject.values()) files.forEach(file => allTestFiles.add(file)); @@ -125,7 +125,7 @@ export async function loadAllTests(config: FullConfigInternal, projectsToIgnore: return rootSuite; } -async function createProjectSuite(fileSuits: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher }, files: string[]): Promise { +async function createProjectSuite(fileSuits: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }, files: string[]): Promise { const fileSuitesMap = new Map(); for (const fileSuite of fileSuits) fileSuitesMap.set(fileSuite._requireFile, fileSuite); @@ -143,8 +143,9 @@ async function createProjectSuite(fileSuits: Suite[], project: FullProjectIntern projectSuite._addSuite(builtSuite); } } - // Filter tests to respect line/column filter. + filterByFocusedLine(projectSuite, options.cliFileFilters); + filterByTestIds(projectSuite, options.testIdMatcher); const grepMatcher = createTitleMatcher(project.grep); const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null; diff --git a/packages/playwright-test/src/runner/loaderHost.ts b/packages/playwright-test/src/runner/loaderHost.ts index 399659a6b3..30390aa7c0 100644 --- a/packages/playwright-test/src/runner/loaderHost.ts +++ b/packages/playwright-test/src/runner/loaderHost.ts @@ -69,6 +69,7 @@ export class OutOfProcessLoaderHost extends LoaderHost { } override async stop() { + await this._startPromise; const result = await this._processHost.sendMessage({ method: 'serializeCompilationCache' }) as any; addToCompilationCache(result); await this._processHost.stop(); diff --git a/packages/playwright-test/src/runner/reporters.ts b/packages/playwright-test/src/runner/reporters.ts index 17c092dea3..d164ba61ed 100644 --- a/packages/playwright-test/src/runner/reporters.ts +++ b/packages/playwright-test/src/runner/reporters.ts @@ -31,11 +31,11 @@ import type { FullConfigInternal } from '../common/types'; import { loadReporter } from './loadUtils'; import type { BuiltInReporter } from '../common/configLoader'; -export async function createReporter(config: FullConfigInternal, list: boolean) { +export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run') { const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { - dot: list ? ListModeReporter : DotReporter, - line: list ? ListModeReporter : LineReporter, - list: list ? ListModeReporter : ListReporter, + dot: mode === 'list' ? ListModeReporter : DotReporter, + line: mode === 'list' ? ListModeReporter : LineReporter, + list: mode === 'list' ? ListModeReporter : ListReporter, github: GitHubReporter, json: JSONReporter, junit: JUnitReporter, @@ -43,18 +43,22 @@ export async function createReporter(config: FullConfigInternal, list: boolean) html: HtmlReporter, }; const reporters: Reporter[] = []; - for (const r of config.reporter) { - const [name, arg] = r; - if (name in defaultReporters) { - reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg)); - } else { - const reporterConstructor = await loadReporter(config, name); - reporters.push(new reporterConstructor(arg)); + if (mode === 'watch') { + reporters.push(new WatchModeReporter()); + } else { + for (const r of config.reporter) { + const [name, arg] = r; + if (name in defaultReporters) { + reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg)); + } else { + const reporterConstructor = await loadReporter(config, name); + reporters.push(new reporterConstructor(arg)); + } + } + if (process.env.PW_TEST_REPORTER) { + const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER); + reporters.push(new reporterConstructor()); } - } - if (process.env.PW_TEST_REPORTER) { - const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER); - reporters.push(new reporterConstructor()); } const someReporterPrintsToStdio = reporters.some(r => { @@ -64,7 +68,7 @@ export async function createReporter(config: FullConfigInternal, list: boolean) if (reporters.length && !someReporterPrintsToStdio) { // Add a line/dot/list-mode reporter for convenience. // Important to put it first, jsut in case some other reporter stalls onEnd. - if (list) + if (mode === 'list') reporters.unshift(new ListModeReporter()); else reporters.unshift(!process.env.CI ? new LineReporter({ omitFailures: true }) : new DotReporter()); @@ -99,3 +103,6 @@ export class ListModeReporter implements Reporter { console.error('\n' + formatError(this.config, error, false).message); } } + +export class WatchModeReporter extends LineReporter { +} diff --git a/packages/playwright-test/src/runner/runner.ts b/packages/playwright-test/src/runner/runner.ts index a25afaaa03..9b5e25e806 100644 --- a/packages/playwright-test/src/runner/runner.ts +++ b/packages/playwright-test/src/runner/runner.ts @@ -24,6 +24,7 @@ import { createTaskRunner, createTaskRunnerForList } from './tasks'; import type { TaskRunnerState } from './tasks'; import type { FullConfigInternal } from '../common/types'; import { colors } from 'playwright-core/lib/utilsBundle'; +import { runWatchModeLoop } from './watchMode'; export class Runner { private _config: FullConfigInternal; @@ -46,7 +47,7 @@ export class Runner { return report; } - async runAllTests(): Promise { + async runAllTests(watchMode: boolean): Promise { const config = this._config; const listOnly = config._internal.listOnly; const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0; @@ -54,9 +55,9 @@ export class Runner { // Legacy webServer support. webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p })); - const reporter = await createReporter(config, listOnly); + const reporter = await createReporter(config, listOnly ? 'list' : watchMode ? 'watch' : 'run'); const taskRunner = listOnly ? createTaskRunnerForList(config, reporter) - : createTaskRunner(config, reporter); + : createTaskRunner(config, reporter, watchMode); const context: TaskRunnerState = { config, @@ -77,12 +78,16 @@ export class Runner { const taskStatus = await taskRunner.run(context, deadline); let status: FullResult['status'] = 'passed'; - if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || context.rootSuite?.allTests().some(test => !test.ok())) + const failedTests = context.rootSuite?.allTests().filter(test => !test.ok()) || []; + if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || failedTests.length) status = 'failed'; if (status === 'passed' && taskStatus !== 'passed') status = taskStatus; await reporter.onExit({ status }); + if (watchMode) + await runWatchModeLoop(config, failedTests); + // Calling process.exit() might truncate large stdout/stderr output. // See https://github.com/nodejs/node/issues/6456. // See https://github.com/nodejs/node/issues/12921 diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index 7a0b20f4d8..fc1ea223f9 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -50,29 +50,42 @@ export type TaskRunnerState = { }[]; }; -export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { +export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer, doNotTeardown: boolean): TaskRunner { const taskRunner = new TaskRunner(reporter, config.globalTimeout); for (const plugin of config._internal.plugins) - taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); - taskRunner.addTask('load tests', createLoadTask()); + taskRunner.addTask('plugin setup', createPluginSetupTask(plugin, doNotTeardown)); + if (config.globalSetup || config.globalTeardown) + taskRunner.addTask('global setup', createGlobalSetupTask(doNotTeardown)); + taskRunner.addTask('load tests', createLoadTask('in-process')); taskRunner.addTask('clear output', createRemoveOutputDirsTask()); - taskRunner.addTask('prepare workers', createTestGroupsTask()); - for (const plugin of config._internal.plugins) - taskRunner.addTask('plugin begin', async ({ rootSuite }) => plugin.instance?.begin?.(rootSuite!)); + addCommonTasks(taskRunner, config); + return taskRunner; +} + +export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, projectsToIgnore?: Set, additionalFileMatcher?: Matcher): TaskRunner { + const taskRunner = new TaskRunner(reporter, config.globalTimeout); + taskRunner.addTask('load tests', createLoadTask('out-of-process', projectsToIgnore, additionalFileMatcher)); + addCommonTasks(taskRunner, config); + return taskRunner; +} + +function addCommonTasks(taskRunner: TaskRunner, config: FullConfigInternal) { + taskRunner.addTask('create tasks', createTestGroupsTask()); taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { reporter.onBegin?.(config, rootSuite!); return () => reporter.onEnd(); }); - if (config.globalSetup || config.globalTeardown) - taskRunner.addTask('global setup', createGlobalSetupTask()); + for (const plugin of config._internal.plugins) + taskRunner.addTask('plugin begin', createPluginBeginTask(plugin)); + taskRunner.addTask('start workers', createWorkersTask()); taskRunner.addTask('test suite', createRunTestsTask()); return taskRunner; } export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { const taskRunner = new TaskRunner(reporter, config.globalTimeout); - taskRunner.addTask('load tests', createLoadTask()); + taskRunner.addTask('load tests', createLoadTask('in-process')); taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { reporter.onBegin?.(config, rootSuite!); return () => reporter.onEnd(); @@ -80,23 +93,30 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Mu return taskRunner; } -function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task { +function createPluginSetupTask(plugin: TestRunnerPluginRegistration, doNotTeardown: boolean): Task { return async ({ config, reporter }) => { if (typeof plugin.factory === 'function') plugin.instance = await plugin.factory(); else plugin.instance = plugin.factory; await plugin.instance?.setup?.(config, config._internal.configDir, reporter); - return () => plugin.instance?.teardown?.(); + return doNotTeardown ? undefined : () => plugin.instance?.teardown?.(); }; } -function createGlobalSetupTask(): Task { +function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task { + return async ({ rootSuite }) => { + await plugin.instance?.begin?.(rootSuite!); + return () => plugin.instance?.end?.(); + }; +} + +function createGlobalSetupTask(doNotTeardown: boolean): Task { return async ({ config }) => { const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined; const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined; const globalSetupResult = setupHook ? await setupHook(config) : undefined; - return async () => { + return doNotTeardown ? undefined : async () => { if (typeof globalSetupResult === 'function') await globalSetupResult(); await teardownHook?.(config); @@ -126,12 +146,12 @@ function createRemoveOutputDirsTask(): Task { }; } -function createLoadTask(projectsToIgnore = new Set(), additionalFileMatcher?: Matcher): Task { +function createLoadTask(mode: 'out-of-process' | 'in-process', projectsToIgnore = new Set(), additionalFileMatcher?: Matcher): Task { return async (context, errors) => { const { config } = context; const cliMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true; const fileMatcher = (value: string) => cliMatcher(value) && (additionalFileMatcher ? additionalFileMatcher(value) : true); - context.rootSuite = await loadAllTests(config, projectsToIgnore, fileMatcher, errors); + context.rootSuite = await loadAllTests(mode, config, projectsToIgnore, fileMatcher, errors); // Fail when no tests. if (!context.rootSuite.allTests().length && !config._internal.passWithNoTests && !config.shard) throw new Error(`No tests found`); @@ -158,9 +178,13 @@ function createTestGroupsTask(): Task { context.phases.push({ dispatcher: new Dispatcher(config, reporter), projects }); context.config._internal.maxConcurrentTestGroups = Math.max(context.config._internal.maxConcurrentTestGroups, testGroupsInPhase); } + }; +} +function createWorkersTask(): Task { + return async ({ phases }) => { return async () => { - for (const { dispatcher } of context.phases.reverse()) + for (const { dispatcher } of phases.reverse()) await dispatcher.stop(); }; }; diff --git a/packages/playwright-test/src/runner/watchMode.ts b/packages/playwright-test/src/runner/watchMode.ts new file mode 100644 index 0000000000..8d5b81bfbb --- /dev/null +++ b/packages/playwright-test/src/runner/watchMode.ts @@ -0,0 +1,275 @@ +/** + * 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 readline from 'readline'; +import { ManualPromise } from 'playwright-core/lib/utils'; +import type { FullConfigInternal, FullProjectInternal } from '../common/types'; +import { Multiplexer } from '../reporters/multiplexer'; +import { createFileFilterForArg, createFileMatcherFromFilters, createTitleMatcher, forceRegExp } from '../util'; +import type { Matcher } from '../util'; +import { createTaskRunnerForWatch } from './tasks'; +import type { TaskRunnerState } from './tasks'; +import { buildProjectsClosure, filterProjects } from './projectUtils'; +import { clearCompilationCache } from '../common/compilationCache'; +import type { FullResult, TestCase } from 'packages/playwright-test/reporter'; +import chokidar from 'chokidar'; +import { WatchModeReporter } from './reporters'; +import { colors } from 'playwright-core/lib/utilsBundle'; +import { enquirer } from '../utilsBundle'; + +class FSWatcher { + private _dirtyFiles = new Set(); + private _notifyDirtyFiles: (() => void) | undefined; + + constructor(dirs: string[]) { + let timer: NodeJS.Timer; + chokidar.watch(dirs, { ignoreInitial: true }).on('all', async (event, file) => { + if (event !== 'add' && event !== 'change') + return; + this._dirtyFiles.add(file); + if (timer) + clearTimeout(timer); + timer = setTimeout(() => { + this._notifyDirtyFiles?.(); + }, 250); + }); + } + + async onDirtyFiles(): Promise { + if (this._dirtyFiles.size) + return; + await new Promise(f => this._notifyDirtyFiles = f); + } + + takeDirtyFiles(): Set { + const result = this._dirtyFiles; + this._dirtyFiles = new Set(); + return result; + } +} + +export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]) { + const projects = filterProjects(config.projects, config._internal.cliProjectFilter); + const projectClosure = buildProjectsClosure(projects); + config._internal.passWithNoTests = true; + const failedTestIdCollector = new Set(failedTests.map(t => t.id)); + + const originalTitleMatcher = config._internal.cliTitleMatcher; + const originalFileFilters = config._internal.cliFileFilters; + + const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir)); + let lastFilePattern: string | undefined; + let lastTestPattern: string | undefined; + while (true) { + process.stdout.write(` +Waiting for file changes... +${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} ${colors.bold('q')} ${colors.dim('to quit')} +`); + const readCommandPromise = readCommand(); + await Promise.race([ + fsWatcher.onDirtyFiles(), + readCommandPromise, + ]); + if (!readCommandPromise.isDone()) + readCommandPromise.resolve('changed'); + + const command = await readCommandPromise; + if (command === 'changed') { + process.stdout.write('\x1Bc'); + await runChangedTests(config, failedTestIdCollector, projectClosure, fsWatcher.takeDirtyFiles()); + continue; + } + if (command === 'all') { + process.stdout.write('\x1Bc'); + // All means reset filters. + config._internal.cliTitleMatcher = originalTitleMatcher; + config._internal.cliFileFilters = originalFileFilters; + lastFilePattern = undefined; + lastTestPattern = undefined; + await runTests(config, failedTestIdCollector); + continue; + } + if (command === 'file') { + const { filePattern } = await enquirer.prompt<{ filePattern: string }>({ + type: 'text', + name: 'filePattern', + message: 'Input filename pattern (regex)', + initial: lastFilePattern, + }); + if (filePattern.trim()) { + lastFilePattern = filePattern; + config._internal.cliFileFilters = [createFileFilterForArg(filePattern)]; + } else { + lastFilePattern = undefined; + config._internal.cliFileFilters = originalFileFilters; + } + await runTests(config, failedTestIdCollector); + continue; + } + if (command === 'grep') { + const { testPattern } = await enquirer.prompt<{ testPattern: string }>({ + type: 'text', + name: 'testPattern', + message: 'Input test name pattern (regex)', + initial: lastTestPattern, + }); + if (testPattern.trim()) { + lastTestPattern = testPattern; + config._internal.cliTitleMatcher = createTitleMatcher(forceRegExp(testPattern)); + } else { + lastTestPattern = undefined; + config._internal.cliTitleMatcher = originalTitleMatcher; + } + await runTests(config, failedTestIdCollector); + continue; + } + if (command === 'failed') { + process.stdout.write('\x1Bc'); + config._internal.testIdMatcher = id => failedTestIdCollector.has(id); + try { + await runTests(config, failedTestIdCollector); + } finally { + config._internal.testIdMatcher = undefined; + } + continue; + } + } +} + +async function runChangedTests(config: FullConfigInternal, failedTestIds: Set, projectClosure: FullProjectInternal[], files: Set) { + const commandLineFileMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true; + + // Collect projects with changes. + const filesByProject = new Map(); + for (const project of projectClosure) { + const projectFiles: string[] = []; + for (const file of files) { + if (!file.startsWith(project.testDir)) + continue; + if (project._internal.type === 'dependency' || commandLineFileMatcher(file)) + projectFiles.push(file); + } + if (projectFiles.length) + filesByProject.set(project, projectFiles); + } + + // 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 affectedProjects = affectedProjectsClosure(projectClosure, [...filesByProject.keys()]); + const affectsAnyDependency = [...affectedProjects].some(p => p._internal.type === 'dependency'); + const projectsToIgnore = new Set(projectClosure.filter(p => !affectedProjects.has(p))); + + // If there are affected dependency projects, do the full run, respect the original CLI. + // if there are no affected dependency projects, intersect CLI with dirty files + const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => files.has(file); + return await runTests(config, failedTestIds, projectsToIgnore, additionalFileMatcher); +} + +async function runTests(config: FullConfigInternal, failedTestIds: Set, projectsToIgnore?: Set, additionalFileMatcher?: Matcher) { + const reporter = new Multiplexer([new WatchModeReporter()]); + const taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher); + const context: TaskRunnerState = { + config, + reporter, + phases: [], + }; + clearCompilationCache(); + reporter.onConfigure(config); + const taskStatus = await taskRunner.run(context, 0); + let status: FullResult['status'] = 'passed'; + + let hasFailedTests = false; + for (const test of context.rootSuite?.allTests() || []) { + if (test.outcome() === 'expected') { + failedTestIds.delete(test.id); + } else { + failedTestIds.add(test.id); + hasFailedTests = true; + } + } + + if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || hasFailedTests) + status = 'failed'; + if (status === 'passed' && taskStatus !== 'passed') + status = taskStatus; + await reporter.onExit({ status }); +} + +function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected: FullProjectInternal[]): Set { + const result = new Set(affected); + for (let i = 0; i < projectClosure.length; ++i) { + for (const p of projectClosure) { + for (const dep of p._internal.deps) { + if (result.has(dep)) + result.add(p); + } + } + } + return result; +} + +function readCommand(): ManualPromise { + const result = new ManualPromise(); + const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }); + readline.emitKeypressEvents(process.stdin, rl); + if (process.stdin.isTTY) + process.stdin.setRawMode(true); + + const handler = (text: string, key: any) => { + if (text === '\x03' || text === '\x1B' || (key && key.name === 'escape') || (key && key.ctrl && key.name === 'c')) + return process.exit(130); + if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') { + process.kill(process.ppid, 'SIGTSTP'); + process.kill(process.pid, 'SIGTSTP'); + } + const name = key?.name; + if (name === 'q') + process.exit(0); + if (name === 'h') { + process.stdout.write(` +Watch Usage +${commands.map(i => colors.dim(' press ') + colors.reset(colors.bold(i[0])) + colors.dim(` to ${i[1]}`)).join('\n')} +`); + return; + } + + switch (name) { + case 'a': result.resolve('all'); break; + case 'p': result.resolve('file'); break; + case 't': result.resolve('grep'); break; + case 'f': result.resolve('failed'); break; + } + }; + + process.stdin.on('keypress', handler); + result.finally(() => { + process.stdin.off('keypress', handler); + rl.close(); + if (process.stdin.isTTY) + process.stdin.setRawMode(false); + }); + return result; +} + +type Command = 'all' | 'failed' | 'changed' | 'file' | 'grep'; + +const commands = [ + ['a', 'rerun all tests'], + ['f', 'rerun only failed tests'], + ['p', 'filter by a filename'], + ['t', 'filter by a test name regex pattern'], + ['q', 'quit'], +]; diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 2bf07fa623..fae89f2ae7 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -106,6 +106,15 @@ export type TestFileFilter = { column: number | null; }; +export function createFileFilterForArg(arg: string): TestFileFilter { + const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); + return { + re: forceRegExp(match ? match[1] : arg), + line: match ? parseInt(match[2], 10) : null, + column: match?.[3] ? parseInt(match[3], 10) : null, + }; +} + export function createFileMatcherFromFilters(filters: TestFileFilter[]): Matcher { return createFileMatcher(filters.map(filter => filter.re || filter.exact || '')); } @@ -174,7 +183,7 @@ export function forceRegExp(pattern: string): RegExp { const match = pattern.match(/^\/(.*)\/([gi]*)$/); if (match) return new RegExp(match[1], match[2]); - return new RegExp(pattern, 'g'); + return new RegExp(pattern, 'gi'); } export function relativeFilePath(file: string): string { diff --git a/packages/playwright-test/src/utilsBundle.ts b/packages/playwright-test/src/utilsBundle.ts index d67e070a62..7f43650b82 100644 --- a/packages/playwright-test/src/utilsBundle.ts +++ b/packages/playwright-test/src/utilsBundle.ts @@ -19,3 +19,4 @@ export const open: typeof import('../bundles/utils/node_modules/open') = require export const pirates: typeof import('../bundles/utils/node_modules/pirates') = require('./utilsBundleImpl').pirates; export const sourceMapSupport: typeof import('../bundles/utils/node_modules/@types/source-map-support') = require('./utilsBundleImpl').sourceMapSupport; export const stoppable: typeof import('../bundles/utils/node_modules/@types/stoppable') = require('./utilsBundleImpl').stoppable; +export const enquirer: typeof import('../bundles/utils/node_modules/enquirer') = require('./utilsBundleImpl').enquirer;