123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- "use strict";
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
- exports.runWatchModeLoop = runWatchModeLoop;
- var _readline = _interopRequireDefault(require("readline"));
- var _utils = require("playwright-core/lib/utils");
- var _internalReporter = require("../reporters/internalReporter");
- var _util = require("../util");
- var _tasks = require("./tasks");
- var _projectUtils = require("./projectUtils");
- var _compilationCache = require("../transform/compilationCache");
- var _utilsBundle = require("../utilsBundle");
- var _utilsBundle2 = require("playwright-core/lib/utilsBundle");
- var _base = require("../reporters/base");
- var _playwrightServer = require("playwright-core/lib/remote/playwrightServer");
- var _list = _interopRequireDefault(require("../reporters/list"));
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
- /**
- * 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.
- */
- class FSWatcher {
- constructor() {
- this._dirtyTestFiles = new Map();
- this._notifyDirtyFiles = void 0;
- this._watcher = void 0;
- this._timer = void 0;
- }
- async update(config) {
- const commandLineFileMatcher = config.cliArgs.length ? (0, _util.createFileMatcherFromArguments)(config.cliArgs) : () => true;
- const projects = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
- const projectClosure = (0, _projectUtils.buildProjectsClosure)(projects);
- const projectFilters = new Map();
- for (const [project, type] of projectClosure) {
- const testMatch = (0, _util.createFileMatcher)(project.project.testMatch);
- const testIgnore = (0, _util.createFileMatcher)(project.project.testIgnore);
- projectFilters.set(project, file => {
- if (!file.startsWith(project.project.testDir) || !testMatch(file) || testIgnore(file)) return false;
- return type === 'dependency' || commandLineFileMatcher(file);
- });
- }
- if (this._timer) clearTimeout(this._timer);
- if (this._watcher) await this._watcher.close();
- this._watcher = _utilsBundle.chokidar.watch([...projectClosure.keys()].map(p => p.project.testDir), {
- ignoreInitial: true
- }).on('all', async (event, file) => {
- if (event !== 'add' && event !== 'change') return;
- const testFiles = new Set();
- (0, _compilationCache.collectAffectedTestFiles)(file, testFiles);
- const testFileArray = [...testFiles];
- let hasMatches = false;
- for (const [project, filter] of projectFilters) {
- const filteredFiles = testFileArray.filter(filter);
- if (!filteredFiles.length) continue;
- let set = this._dirtyTestFiles.get(project);
- if (!set) {
- set = new Set();
- this._dirtyTestFiles.set(project, set);
- }
- filteredFiles.map(f => set.add(f));
- hasMatches = true;
- }
- if (!hasMatches) return;
- if (this._timer) clearTimeout(this._timer);
- this._timer = setTimeout(() => {
- var _this$_notifyDirtyFil;
- (_this$_notifyDirtyFil = this._notifyDirtyFiles) === null || _this$_notifyDirtyFil === void 0 ? void 0 : _this$_notifyDirtyFil.call(this);
- }, 250);
- });
- }
- async onDirtyTestFiles() {
- if (this._dirtyTestFiles.size) return;
- await new Promise(f => this._notifyDirtyFiles = f);
- }
- takeDirtyTestFiles() {
- const result = this._dirtyTestFiles;
- this._dirtyTestFiles = new Map();
- return result;
- }
- }
- async function runWatchModeLoop(config) {
- // Reset the settings that don't apply to watch.
- config.cliPassWithNoTests = true;
- for (const p of config.projects) p.project.retries = 0;
- // Perform global setup.
- const reporter = new _internalReporter.InternalReporter(new _list.default());
- const testRun = new _tasks.TestRun(config, reporter);
- const taskRunner = (0, _tasks.createTaskRunnerForWatchSetup)(config, reporter);
- reporter.onConfigure(config.config);
- const {
- status,
- cleanup: globalCleanup
- } = await taskRunner.runDeferCleanup(testRun, 0);
- if (status !== 'passed') await globalCleanup();
- await reporter.onEnd({
- status
- });
- await reporter.onExit();
- if (status !== 'passed') return status;
- // Prepare projects that will be watched, set up watcher.
- const failedTestIdCollector = new Set();
- const originalWorkers = config.config.workers;
- const fsWatcher = new FSWatcher();
- await fsWatcher.update(config);
- let lastRun = {
- type: 'regular'
- };
- let result = 'passed';
- // Enter the watch loop.
- await runTests(config, failedTestIdCollector);
- while (true) {
- printPrompt();
- const readCommandPromise = readCommand();
- await Promise.race([fsWatcher.onDirtyTestFiles(), readCommandPromise]);
- if (!readCommandPromise.isDone()) readCommandPromise.resolve('changed');
- const command = await readCommandPromise;
- if (command === 'changed') {
- const dirtyTestFiles = fsWatcher.takeDirtyTestFiles();
- // Resolve files that depend on the changed files.
- await runChangedTests(config, failedTestIdCollector, dirtyTestFiles);
- lastRun = {
- type: 'changed',
- dirtyTestFiles
- };
- continue;
- }
- if (command === 'run') {
- // All means reset filters.
- await runTests(config, failedTestIdCollector);
- lastRun = {
- type: 'regular'
- };
- continue;
- }
- if (command === 'project') {
- const {
- projectNames
- } = await _utilsBundle.enquirer.prompt({
- type: 'multiselect',
- name: 'projectNames',
- message: 'Select projects',
- choices: config.projects.map(p => ({
- name: p.project.name
- }))
- }).catch(() => ({
- projectNames: null
- }));
- if (!projectNames) continue;
- config.cliProjectFilter = projectNames.length ? projectNames : undefined;
- await fsWatcher.update(config);
- await runTests(config, failedTestIdCollector);
- lastRun = {
- type: 'regular'
- };
- continue;
- }
- if (command === 'file') {
- const {
- filePattern
- } = await _utilsBundle.enquirer.prompt({
- type: 'text',
- name: 'filePattern',
- message: 'Input filename pattern (regex)'
- }).catch(() => ({
- filePattern: null
- }));
- if (filePattern === null) continue;
- if (filePattern.trim()) config.cliArgs = filePattern.split(' ');else config.cliArgs = [];
- await fsWatcher.update(config);
- await runTests(config, failedTestIdCollector);
- lastRun = {
- type: 'regular'
- };
- continue;
- }
- if (command === 'grep') {
- const {
- testPattern
- } = await _utilsBundle.enquirer.prompt({
- type: 'text',
- name: 'testPattern',
- message: 'Input test name pattern (regex)'
- }).catch(() => ({
- testPattern: null
- }));
- if (testPattern === null) continue;
- if (testPattern.trim()) config.cliGrep = testPattern;else config.cliGrep = undefined;
- await fsWatcher.update(config);
- await runTests(config, failedTestIdCollector);
- lastRun = {
- type: 'regular'
- };
- continue;
- }
- if (command === 'failed') {
- config.testIdMatcher = id => failedTestIdCollector.has(id);
- const failedTestIds = new Set(failedTestIdCollector);
- await runTests(config, failedTestIdCollector, {
- title: 'running failed tests'
- });
- config.testIdMatcher = undefined;
- lastRun = {
- type: 'failed',
- failedTestIds
- };
- continue;
- }
- if (command === 'repeat') {
- if (lastRun.type === 'regular') {
- await runTests(config, failedTestIdCollector, {
- title: 're-running tests'
- });
- continue;
- } else if (lastRun.type === 'changed') {
- await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles, 're-running tests');
- } else if (lastRun.type === 'failed') {
- config.testIdMatcher = id => lastRun.failedTestIds.has(id);
- await runTests(config, failedTestIdCollector, {
- title: 're-running tests'
- });
- config.testIdMatcher = undefined;
- }
- continue;
- }
- if (command === 'toggle-show-browser') {
- await toggleShowBrowser(config, originalWorkers);
- continue;
- }
- if (command === 'exit') break;
- if (command === 'interrupted') {
- result = 'interrupted';
- break;
- }
- }
- const cleanupStatus = await globalCleanup();
- return result === 'passed' ? cleanupStatus : result;
- }
- async function runChangedTests(config, failedTestIdCollector, filesByProject, title) {
- const testFiles = new Set();
- for (const files of filesByProject.values()) files.forEach(f => testFiles.add(f));
- // Collect all the affected projects, follow project dependencies.
- // Prepare to exclude all the projects that do not depend on this file, as if they did not exist.
- const projects = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
- const projectClosure = (0, _projectUtils.buildProjectsClosure)(projects);
- const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
- const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
- // 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 => testFiles.has(file);
- await runTests(config, failedTestIdCollector, {
- additionalFileMatcher,
- title: title || 'files changed'
- });
- }
- async function runTests(config, failedTestIdCollector, options) {
- printConfiguration(config, options === null || options === void 0 ? void 0 : options.title);
- const reporter = new _internalReporter.InternalReporter(new _list.default());
- const taskRunner = (0, _tasks.createTaskRunnerForWatch)(config, reporter, options === null || options === void 0 ? void 0 : options.additionalFileMatcher);
- const testRun = new _tasks.TestRun(config, reporter);
- (0, _compilationCache.clearCompilationCache)();
- reporter.onConfigure(config.config);
- const taskStatus = await taskRunner.run(testRun, 0);
- let status = 'passed';
- let hasFailedTests = false;
- for (const test of ((_testRun$rootSuite = testRun.rootSuite) === null || _testRun$rootSuite === void 0 ? void 0 : _testRun$rootSuite.allTests()) || []) {
- var _testRun$rootSuite;
- if (test.outcome() === 'unexpected') {
- failedTestIdCollector.add(test.id);
- hasFailedTests = true;
- } else {
- failedTestIdCollector.delete(test.id);
- }
- }
- if (testRun.failureTracker.hasWorkerErrors() || hasFailedTests) status = 'failed';
- if (status === 'passed' && taskStatus !== 'passed') status = taskStatus;
- await reporter.onEnd({
- status
- });
- await reporter.onExit();
- }
- function affectedProjectsClosure(projectClosure, affected) {
- const result = new Set(affected);
- for (let i = 0; i < projectClosure.length; ++i) {
- for (const p of projectClosure) {
- for (const dep of p.deps) {
- if (result.has(dep)) result.add(p);
- }
- if (p.teardown && result.has(p.teardown)) result.add(p);
- }
- }
- return result;
- }
- function readCommand() {
- const result = new _utils.ManualPromise();
- const rl = _readline.default.createInterface({
- input: process.stdin,
- escapeCodeTimeout: 50
- });
- _readline.default.emitKeypressEvents(process.stdin, rl);
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
- const handler = (text, key) => {
- if (text === '\x03' || text === '\x1B' || key && key.name === 'escape' || key && key.ctrl && key.name === 'c') {
- result.resolve('interrupted');
- return;
- }
- if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') {
- process.kill(process.ppid, 'SIGTSTP');
- process.kill(process.pid, 'SIGTSTP');
- }
- const name = key === null || key === void 0 ? void 0 : key.name;
- if (name === 'q') {
- result.resolve('exit');
- return;
- }
- if (name === 'h') {
- process.stdout.write(`${(0, _base.separator)()}
- Run tests
- ${_utilsBundle2.colors.bold('enter')} ${_utilsBundle2.colors.dim('run tests')}
- ${_utilsBundle2.colors.bold('f')} ${_utilsBundle2.colors.dim('run failed tests')}
- ${_utilsBundle2.colors.bold('r')} ${_utilsBundle2.colors.dim('repeat last run')}
- ${_utilsBundle2.colors.bold('q')} ${_utilsBundle2.colors.dim('quit')}
- Change settings
- ${_utilsBundle2.colors.bold('c')} ${_utilsBundle2.colors.dim('set project')}
- ${_utilsBundle2.colors.bold('p')} ${_utilsBundle2.colors.dim('set file filter')}
- ${_utilsBundle2.colors.bold('t')} ${_utilsBundle2.colors.dim('set title filter')}
- ${_utilsBundle2.colors.bold('s')} ${_utilsBundle2.colors.dim('toggle show & reuse the browser')}
- `);
- return;
- }
- switch (name) {
- case 'return':
- result.resolve('run');
- break;
- case 'r':
- result.resolve('repeat');
- break;
- case 'c':
- result.resolve('project');
- break;
- case 'p':
- result.resolve('file');
- break;
- case 't':
- result.resolve('grep');
- break;
- case 'f':
- result.resolve('failed');
- break;
- case 's':
- result.resolve('toggle-show-browser');
- break;
- }
- };
- process.stdin.on('keypress', handler);
- void result.finally(() => {
- process.stdin.off('keypress', handler);
- rl.close();
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
- });
- return result;
- }
- let showBrowserServer;
- let seq = 0;
- function printConfiguration(config, title) {
- var _ref;
- const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
- const tokens = [];
- tokens.push(`${packageManagerCommand} playwright test`);
- tokens.push(...((_ref = config.cliProjectFilter || []) === null || _ref === void 0 ? void 0 : _ref.map(p => _utilsBundle2.colors.blue(`--project ${p}`))));
- if (config.cliGrep) tokens.push(_utilsBundle2.colors.red(`--grep ${config.cliGrep}`));
- if (config.cliArgs) tokens.push(...config.cliArgs.map(a => _utilsBundle2.colors.bold(a)));
- if (title) tokens.push(_utilsBundle2.colors.dim(`(${title})`));
- if (seq) tokens.push(_utilsBundle2.colors.dim(`#${seq}`));
- ++seq;
- const lines = [];
- const sep = (0, _base.separator)();
- lines.push('\x1Bc' + sep);
- lines.push(`${tokens.join(' ')}`);
- lines.push(`${_utilsBundle2.colors.dim('Show & reuse browser:')} ${_utilsBundle2.colors.bold(showBrowserServer ? 'on' : 'off')}`);
- process.stdout.write(lines.join('\n'));
- }
- function printPrompt() {
- const sep = (0, _base.separator)();
- process.stdout.write(`
- ${sep}
- ${_utilsBundle2.colors.dim('Waiting for file changes. Press')} ${_utilsBundle2.colors.bold('enter')} ${_utilsBundle2.colors.dim('to run tests')}, ${_utilsBundle2.colors.bold('q')} ${_utilsBundle2.colors.dim('to quit or')} ${_utilsBundle2.colors.bold('h')} ${_utilsBundle2.colors.dim('for more options.')}
- `);
- }
- async function toggleShowBrowser(config, originalWorkers) {
- if (!showBrowserServer) {
- config.config.workers = 1;
- showBrowserServer = new _playwrightServer.PlaywrightServer({
- mode: 'extension',
- path: '/' + (0, _utils.createGuid)(),
- maxConnections: 1
- });
- const wsEndpoint = await showBrowserServer.listen();
- process.env.PW_TEST_REUSE_CONTEXT = '1';
- process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint;
- process.stdout.write(`${_utilsBundle2.colors.dim('Show & reuse browser:')} ${_utilsBundle2.colors.bold('on')}\n`);
- } else {
- var _showBrowserServer;
- config.config.workers = originalWorkers;
- await ((_showBrowserServer = showBrowserServer) === null || _showBrowserServer === void 0 ? void 0 : _showBrowserServer.close());
- showBrowserServer = undefined;
- delete process.env.PW_TEST_REUSE_CONTEXT;
- delete process.env.PW_TEST_CONNECT_WS_ENDPOINT;
- process.stdout.write(`${_utilsBundle2.colors.dim('Show & reuse browser:')} ${_utilsBundle2.colors.bold('off')}\n`);
- }
- }
|