mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: watch mode first cut (#20647)
This commit is contained in:
parent
b6df48758d
commit
430d08f4fb
45
package-lock.json
generated
45
package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
@ -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
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -28,3 +28,6 @@ export const sourceMapSupport = sourceMapSupportLibrary;
|
||||
|
||||
import stoppableLibrary from 'stoppable';
|
||||
export const stoppable = stoppableLibrary;
|
||||
|
||||
import enquirerLibrary from 'enquirer';
|
||||
export const enquirer = enquirerLibrary;
|
@ -34,6 +34,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"chokidar": "3.5.3",
|
||||
"playwright-core": "1.31.0-next"
|
||||
}
|
||||
}
|
||||
|
@ -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 <project-name...>', `Only run tests from the specified list of projects (default: run all projects)`);
|
||||
command.option('--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})`);
|
||||
command.option('--trace <mode>', `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 {
|
||||
|
@ -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) =>
|
||||
|
@ -53,6 +53,7 @@ type ConfigInternal = {
|
||||
cliFileFilters: TestFileFilter[];
|
||||
cliTitleMatcher: Matcher;
|
||||
cliProjectFilter?: string[];
|
||||
testIdMatcher?: Matcher;
|
||||
passWithNoTests?: boolean;
|
||||
};
|
||||
|
||||
|
@ -21,6 +21,7 @@ export interface TestRunnerPlugin {
|
||||
name: string;
|
||||
setup?(config: FullConfig, configDir: string, reporter: Reporter): Promise<void>;
|
||||
begin?(suite: Suite): Promise<void>;
|
||||
end?(): Promise<void>;
|
||||
teardown?(): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ export function createPlugin(
|
||||
}
|
||||
},
|
||||
|
||||
teardown: async () => {
|
||||
end: async () => {
|
||||
await new Promise(f => stoppableServer.stop(f));
|
||||
},
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -6,3 +6,4 @@
|
||||
../third_party/
|
||||
../plugins/
|
||||
../util.ts
|
||||
../utilsBundle.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<FullProjectInternal>, fileMatcher: Matcher, errors: TestError[]): Promise<Suite> {
|
||||
export async function loadAllTests(mode: 'out-of-process' | 'in-process', config: FullConfigInternal, projectsToIgnore: Set<FullProjectInternal>, fileMatcher: Matcher, errors: TestError[]): Promise<Suite> {
|
||||
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
||||
|
||||
let filesToRunByProject = new Map<FullProjectInternal, string[]>();
|
||||
@ -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<string>();
|
||||
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<Suite | null> {
|
||||
async function createProjectSuite(fileSuits: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }, files: string[]): Promise<Suite | null> {
|
||||
const fileSuitesMap = new Map<string, Suite>();
|
||||
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;
|
||||
|
@ -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();
|
||||
|
@ -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,6 +43,9 @@ export async function createReporter(config: FullConfigInternal, list: boolean)
|
||||
html: HtmlReporter,
|
||||
};
|
||||
const reporters: Reporter[] = [];
|
||||
if (mode === 'watch') {
|
||||
reporters.push(new WatchModeReporter());
|
||||
} else {
|
||||
for (const r of config.reporter) {
|
||||
const [name, arg] = r;
|
||||
if (name in defaultReporters) {
|
||||
@ -56,6 +59,7 @@ export async function createReporter(config: FullConfigInternal, list: boolean)
|
||||
const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER);
|
||||
reporters.push(new reporterConstructor());
|
||||
}
|
||||
}
|
||||
|
||||
const someReporterPrintsToStdio = reporters.some(r => {
|
||||
const prints = r.printsToStdio ? r.printsToStdio() : true;
|
||||
@ -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 {
|
||||
}
|
||||
|
@ -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<FullResult['status']> {
|
||||
async runAllTests(watchMode: boolean): Promise<FullResult['status']> {
|
||||
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
|
||||
|
@ -50,29 +50,42 @@ export type TaskRunnerState = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
|
||||
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer, doNotTeardown: boolean): TaskRunner<TaskRunnerState> {
|
||||
const taskRunner = new TaskRunner<TaskRunnerState>(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<FullProjectInternal>, additionalFileMatcher?: Matcher): TaskRunner<TaskRunnerState> {
|
||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', projectsToIgnore, additionalFileMatcher));
|
||||
addCommonTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
function addCommonTasks(taskRunner: TaskRunner<TaskRunnerState>, 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<TaskRunnerState> {
|
||||
const taskRunner = new TaskRunner<TaskRunnerState>(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<TaskRunnerState> {
|
||||
function createPluginSetupTask(plugin: TestRunnerPluginRegistration, doNotTeardown: boolean): Task<TaskRunnerState> {
|
||||
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<TaskRunnerState> {
|
||||
function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TaskRunnerState> {
|
||||
return async ({ rootSuite }) => {
|
||||
await plugin.instance?.begin?.(rootSuite!);
|
||||
return () => plugin.instance?.end?.();
|
||||
};
|
||||
}
|
||||
|
||||
function createGlobalSetupTask(doNotTeardown: boolean): Task<TaskRunnerState> {
|
||||
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<TaskRunnerState> {
|
||||
};
|
||||
}
|
||||
|
||||
function createLoadTask(projectsToIgnore = new Set<FullProjectInternal>(), additionalFileMatcher?: Matcher): Task<TaskRunnerState> {
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', projectsToIgnore = new Set<FullProjectInternal>(), additionalFileMatcher?: Matcher): Task<TaskRunnerState> {
|
||||
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<TaskRunnerState> {
|
||||
context.phases.push({ dispatcher: new Dispatcher(config, reporter), projects });
|
||||
context.config._internal.maxConcurrentTestGroups = Math.max(context.config._internal.maxConcurrentTestGroups, testGroupsInPhase);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkersTask(): Task<TaskRunnerState> {
|
||||
return async ({ phases }) => {
|
||||
return async () => {
|
||||
for (const { dispatcher } of context.phases.reverse())
|
||||
for (const { dispatcher } of phases.reverse())
|
||||
await dispatcher.stop();
|
||||
};
|
||||
};
|
||||
|
275
packages/playwright-test/src/runner/watchMode.ts
Normal file
275
packages/playwright-test/src/runner/watchMode.ts
Normal file
@ -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<string>();
|
||||
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<void> {
|
||||
if (this._dirtyFiles.size)
|
||||
return;
|
||||
await new Promise<void>(f => this._notifyDirtyFiles = f);
|
||||
}
|
||||
|
||||
takeDirtyFiles(): Set<string> {
|
||||
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<string>, projectClosure: FullProjectInternal[], files: Set<string>) {
|
||||
const commandLineFileMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true;
|
||||
|
||||
// Collect projects with changes.
|
||||
const filesByProject = new Map<FullProjectInternal, string[]>();
|
||||
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<string>, projectsToIgnore?: Set<FullProjectInternal>, 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<FullProjectInternal> {
|
||||
const result = new Set<FullProjectInternal>(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<Command> {
|
||||
const result = new ManualPromise<Command>();
|
||||
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'],
|
||||
];
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user