watchMode.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.runWatchModeLoop = runWatchModeLoop;
  6. var _readline = _interopRequireDefault(require("readline"));
  7. var _utils = require("playwright-core/lib/utils");
  8. var _internalReporter = require("../reporters/internalReporter");
  9. var _util = require("../util");
  10. var _tasks = require("./tasks");
  11. var _projectUtils = require("./projectUtils");
  12. var _compilationCache = require("../transform/compilationCache");
  13. var _utilsBundle = require("../utilsBundle");
  14. var _utilsBundle2 = require("playwright-core/lib/utilsBundle");
  15. var _base = require("../reporters/base");
  16. var _playwrightServer = require("playwright-core/lib/remote/playwrightServer");
  17. var _list = _interopRequireDefault(require("../reporters/list"));
  18. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  19. /**
  20. * Copyright Microsoft Corporation. All rights reserved.
  21. *
  22. * Licensed under the Apache License, Version 2.0 (the "License");
  23. * you may not use this file except in compliance with the License.
  24. * You may obtain a copy of the License at
  25. *
  26. * http://www.apache.org/licenses/LICENSE-2.0
  27. *
  28. * Unless required by applicable law or agreed to in writing, software
  29. * distributed under the License is distributed on an "AS IS" BASIS,
  30. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31. * See the License for the specific language governing permissions and
  32. * limitations under the License.
  33. */
  34. class FSWatcher {
  35. constructor() {
  36. this._dirtyTestFiles = new Map();
  37. this._notifyDirtyFiles = void 0;
  38. this._watcher = void 0;
  39. this._timer = void 0;
  40. }
  41. async update(config) {
  42. const commandLineFileMatcher = config.cliArgs.length ? (0, _util.createFileMatcherFromArguments)(config.cliArgs) : () => true;
  43. const projects = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
  44. const projectClosure = (0, _projectUtils.buildProjectsClosure)(projects);
  45. const projectFilters = new Map();
  46. for (const [project, type] of projectClosure) {
  47. const testMatch = (0, _util.createFileMatcher)(project.project.testMatch);
  48. const testIgnore = (0, _util.createFileMatcher)(project.project.testIgnore);
  49. projectFilters.set(project, file => {
  50. if (!file.startsWith(project.project.testDir) || !testMatch(file) || testIgnore(file)) return false;
  51. return type === 'dependency' || commandLineFileMatcher(file);
  52. });
  53. }
  54. if (this._timer) clearTimeout(this._timer);
  55. if (this._watcher) await this._watcher.close();
  56. this._watcher = _utilsBundle.chokidar.watch([...projectClosure.keys()].map(p => p.project.testDir), {
  57. ignoreInitial: true
  58. }).on('all', async (event, file) => {
  59. if (event !== 'add' && event !== 'change') return;
  60. const testFiles = new Set();
  61. (0, _compilationCache.collectAffectedTestFiles)(file, testFiles);
  62. const testFileArray = [...testFiles];
  63. let hasMatches = false;
  64. for (const [project, filter] of projectFilters) {
  65. const filteredFiles = testFileArray.filter(filter);
  66. if (!filteredFiles.length) continue;
  67. let set = this._dirtyTestFiles.get(project);
  68. if (!set) {
  69. set = new Set();
  70. this._dirtyTestFiles.set(project, set);
  71. }
  72. filteredFiles.map(f => set.add(f));
  73. hasMatches = true;
  74. }
  75. if (!hasMatches) return;
  76. if (this._timer) clearTimeout(this._timer);
  77. this._timer = setTimeout(() => {
  78. var _this$_notifyDirtyFil;
  79. (_this$_notifyDirtyFil = this._notifyDirtyFiles) === null || _this$_notifyDirtyFil === void 0 ? void 0 : _this$_notifyDirtyFil.call(this);
  80. }, 250);
  81. });
  82. }
  83. async onDirtyTestFiles() {
  84. if (this._dirtyTestFiles.size) return;
  85. await new Promise(f => this._notifyDirtyFiles = f);
  86. }
  87. takeDirtyTestFiles() {
  88. const result = this._dirtyTestFiles;
  89. this._dirtyTestFiles = new Map();
  90. return result;
  91. }
  92. }
  93. async function runWatchModeLoop(config) {
  94. // Reset the settings that don't apply to watch.
  95. config.cliPassWithNoTests = true;
  96. for (const p of config.projects) p.project.retries = 0;
  97. // Perform global setup.
  98. const reporter = new _internalReporter.InternalReporter(new _list.default());
  99. const testRun = new _tasks.TestRun(config, reporter);
  100. const taskRunner = (0, _tasks.createTaskRunnerForWatchSetup)(config, reporter);
  101. reporter.onConfigure(config.config);
  102. const {
  103. status,
  104. cleanup: globalCleanup
  105. } = await taskRunner.runDeferCleanup(testRun, 0);
  106. if (status !== 'passed') await globalCleanup();
  107. await reporter.onEnd({
  108. status
  109. });
  110. await reporter.onExit();
  111. if (status !== 'passed') return status;
  112. // Prepare projects that will be watched, set up watcher.
  113. const failedTestIdCollector = new Set();
  114. const originalWorkers = config.config.workers;
  115. const fsWatcher = new FSWatcher();
  116. await fsWatcher.update(config);
  117. let lastRun = {
  118. type: 'regular'
  119. };
  120. let result = 'passed';
  121. // Enter the watch loop.
  122. await runTests(config, failedTestIdCollector);
  123. while (true) {
  124. printPrompt();
  125. const readCommandPromise = readCommand();
  126. await Promise.race([fsWatcher.onDirtyTestFiles(), readCommandPromise]);
  127. if (!readCommandPromise.isDone()) readCommandPromise.resolve('changed');
  128. const command = await readCommandPromise;
  129. if (command === 'changed') {
  130. const dirtyTestFiles = fsWatcher.takeDirtyTestFiles();
  131. // Resolve files that depend on the changed files.
  132. await runChangedTests(config, failedTestIdCollector, dirtyTestFiles);
  133. lastRun = {
  134. type: 'changed',
  135. dirtyTestFiles
  136. };
  137. continue;
  138. }
  139. if (command === 'run') {
  140. // All means reset filters.
  141. await runTests(config, failedTestIdCollector);
  142. lastRun = {
  143. type: 'regular'
  144. };
  145. continue;
  146. }
  147. if (command === 'project') {
  148. const {
  149. projectNames
  150. } = await _utilsBundle.enquirer.prompt({
  151. type: 'multiselect',
  152. name: 'projectNames',
  153. message: 'Select projects',
  154. choices: config.projects.map(p => ({
  155. name: p.project.name
  156. }))
  157. }).catch(() => ({
  158. projectNames: null
  159. }));
  160. if (!projectNames) continue;
  161. config.cliProjectFilter = projectNames.length ? projectNames : undefined;
  162. await fsWatcher.update(config);
  163. await runTests(config, failedTestIdCollector);
  164. lastRun = {
  165. type: 'regular'
  166. };
  167. continue;
  168. }
  169. if (command === 'file') {
  170. const {
  171. filePattern
  172. } = await _utilsBundle.enquirer.prompt({
  173. type: 'text',
  174. name: 'filePattern',
  175. message: 'Input filename pattern (regex)'
  176. }).catch(() => ({
  177. filePattern: null
  178. }));
  179. if (filePattern === null) continue;
  180. if (filePattern.trim()) config.cliArgs = filePattern.split(' ');else config.cliArgs = [];
  181. await fsWatcher.update(config);
  182. await runTests(config, failedTestIdCollector);
  183. lastRun = {
  184. type: 'regular'
  185. };
  186. continue;
  187. }
  188. if (command === 'grep') {
  189. const {
  190. testPattern
  191. } = await _utilsBundle.enquirer.prompt({
  192. type: 'text',
  193. name: 'testPattern',
  194. message: 'Input test name pattern (regex)'
  195. }).catch(() => ({
  196. testPattern: null
  197. }));
  198. if (testPattern === null) continue;
  199. if (testPattern.trim()) config.cliGrep = testPattern;else config.cliGrep = undefined;
  200. await fsWatcher.update(config);
  201. await runTests(config, failedTestIdCollector);
  202. lastRun = {
  203. type: 'regular'
  204. };
  205. continue;
  206. }
  207. if (command === 'failed') {
  208. config.testIdMatcher = id => failedTestIdCollector.has(id);
  209. const failedTestIds = new Set(failedTestIdCollector);
  210. await runTests(config, failedTestIdCollector, {
  211. title: 'running failed tests'
  212. });
  213. config.testIdMatcher = undefined;
  214. lastRun = {
  215. type: 'failed',
  216. failedTestIds
  217. };
  218. continue;
  219. }
  220. if (command === 'repeat') {
  221. if (lastRun.type === 'regular') {
  222. await runTests(config, failedTestIdCollector, {
  223. title: 're-running tests'
  224. });
  225. continue;
  226. } else if (lastRun.type === 'changed') {
  227. await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles, 're-running tests');
  228. } else if (lastRun.type === 'failed') {
  229. config.testIdMatcher = id => lastRun.failedTestIds.has(id);
  230. await runTests(config, failedTestIdCollector, {
  231. title: 're-running tests'
  232. });
  233. config.testIdMatcher = undefined;
  234. }
  235. continue;
  236. }
  237. if (command === 'toggle-show-browser') {
  238. await toggleShowBrowser(config, originalWorkers);
  239. continue;
  240. }
  241. if (command === 'exit') break;
  242. if (command === 'interrupted') {
  243. result = 'interrupted';
  244. break;
  245. }
  246. }
  247. const cleanupStatus = await globalCleanup();
  248. return result === 'passed' ? cleanupStatus : result;
  249. }
  250. async function runChangedTests(config, failedTestIdCollector, filesByProject, title) {
  251. const testFiles = new Set();
  252. for (const files of filesByProject.values()) files.forEach(f => testFiles.add(f));
  253. // Collect all the affected projects, follow project dependencies.
  254. // Prepare to exclude all the projects that do not depend on this file, as if they did not exist.
  255. const projects = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
  256. const projectClosure = (0, _projectUtils.buildProjectsClosure)(projects);
  257. const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
  258. const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
  259. // If there are affected dependency projects, do the full run, respect the original CLI.
  260. // if there are no affected dependency projects, intersect CLI with dirty files
  261. const additionalFileMatcher = affectsAnyDependency ? () => true : file => testFiles.has(file);
  262. await runTests(config, failedTestIdCollector, {
  263. additionalFileMatcher,
  264. title: title || 'files changed'
  265. });
  266. }
  267. async function runTests(config, failedTestIdCollector, options) {
  268. printConfiguration(config, options === null || options === void 0 ? void 0 : options.title);
  269. const reporter = new _internalReporter.InternalReporter(new _list.default());
  270. const taskRunner = (0, _tasks.createTaskRunnerForWatch)(config, reporter, options === null || options === void 0 ? void 0 : options.additionalFileMatcher);
  271. const testRun = new _tasks.TestRun(config, reporter);
  272. (0, _compilationCache.clearCompilationCache)();
  273. reporter.onConfigure(config.config);
  274. const taskStatus = await taskRunner.run(testRun, 0);
  275. let status = 'passed';
  276. let hasFailedTests = false;
  277. for (const test of ((_testRun$rootSuite = testRun.rootSuite) === null || _testRun$rootSuite === void 0 ? void 0 : _testRun$rootSuite.allTests()) || []) {
  278. var _testRun$rootSuite;
  279. if (test.outcome() === 'unexpected') {
  280. failedTestIdCollector.add(test.id);
  281. hasFailedTests = true;
  282. } else {
  283. failedTestIdCollector.delete(test.id);
  284. }
  285. }
  286. if (testRun.failureTracker.hasWorkerErrors() || hasFailedTests) status = 'failed';
  287. if (status === 'passed' && taskStatus !== 'passed') status = taskStatus;
  288. await reporter.onEnd({
  289. status
  290. });
  291. await reporter.onExit();
  292. }
  293. function affectedProjectsClosure(projectClosure, affected) {
  294. const result = new Set(affected);
  295. for (let i = 0; i < projectClosure.length; ++i) {
  296. for (const p of projectClosure) {
  297. for (const dep of p.deps) {
  298. if (result.has(dep)) result.add(p);
  299. }
  300. if (p.teardown && result.has(p.teardown)) result.add(p);
  301. }
  302. }
  303. return result;
  304. }
  305. function readCommand() {
  306. const result = new _utils.ManualPromise();
  307. const rl = _readline.default.createInterface({
  308. input: process.stdin,
  309. escapeCodeTimeout: 50
  310. });
  311. _readline.default.emitKeypressEvents(process.stdin, rl);
  312. if (process.stdin.isTTY) process.stdin.setRawMode(true);
  313. const handler = (text, key) => {
  314. if (text === '\x03' || text === '\x1B' || key && key.name === 'escape' || key && key.ctrl && key.name === 'c') {
  315. result.resolve('interrupted');
  316. return;
  317. }
  318. if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') {
  319. process.kill(process.ppid, 'SIGTSTP');
  320. process.kill(process.pid, 'SIGTSTP');
  321. }
  322. const name = key === null || key === void 0 ? void 0 : key.name;
  323. if (name === 'q') {
  324. result.resolve('exit');
  325. return;
  326. }
  327. if (name === 'h') {
  328. process.stdout.write(`${(0, _base.separator)()}
  329. Run tests
  330. ${_utilsBundle2.colors.bold('enter')} ${_utilsBundle2.colors.dim('run tests')}
  331. ${_utilsBundle2.colors.bold('f')} ${_utilsBundle2.colors.dim('run failed tests')}
  332. ${_utilsBundle2.colors.bold('r')} ${_utilsBundle2.colors.dim('repeat last run')}
  333. ${_utilsBundle2.colors.bold('q')} ${_utilsBundle2.colors.dim('quit')}
  334. Change settings
  335. ${_utilsBundle2.colors.bold('c')} ${_utilsBundle2.colors.dim('set project')}
  336. ${_utilsBundle2.colors.bold('p')} ${_utilsBundle2.colors.dim('set file filter')}
  337. ${_utilsBundle2.colors.bold('t')} ${_utilsBundle2.colors.dim('set title filter')}
  338. ${_utilsBundle2.colors.bold('s')} ${_utilsBundle2.colors.dim('toggle show & reuse the browser')}
  339. `);
  340. return;
  341. }
  342. switch (name) {
  343. case 'return':
  344. result.resolve('run');
  345. break;
  346. case 'r':
  347. result.resolve('repeat');
  348. break;
  349. case 'c':
  350. result.resolve('project');
  351. break;
  352. case 'p':
  353. result.resolve('file');
  354. break;
  355. case 't':
  356. result.resolve('grep');
  357. break;
  358. case 'f':
  359. result.resolve('failed');
  360. break;
  361. case 's':
  362. result.resolve('toggle-show-browser');
  363. break;
  364. }
  365. };
  366. process.stdin.on('keypress', handler);
  367. void result.finally(() => {
  368. process.stdin.off('keypress', handler);
  369. rl.close();
  370. if (process.stdin.isTTY) process.stdin.setRawMode(false);
  371. });
  372. return result;
  373. }
  374. let showBrowserServer;
  375. let seq = 0;
  376. function printConfiguration(config, title) {
  377. var _ref;
  378. const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
  379. const tokens = [];
  380. tokens.push(`${packageManagerCommand} playwright test`);
  381. tokens.push(...((_ref = config.cliProjectFilter || []) === null || _ref === void 0 ? void 0 : _ref.map(p => _utilsBundle2.colors.blue(`--project ${p}`))));
  382. if (config.cliGrep) tokens.push(_utilsBundle2.colors.red(`--grep ${config.cliGrep}`));
  383. if (config.cliArgs) tokens.push(...config.cliArgs.map(a => _utilsBundle2.colors.bold(a)));
  384. if (title) tokens.push(_utilsBundle2.colors.dim(`(${title})`));
  385. if (seq) tokens.push(_utilsBundle2.colors.dim(`#${seq}`));
  386. ++seq;
  387. const lines = [];
  388. const sep = (0, _base.separator)();
  389. lines.push('\x1Bc' + sep);
  390. lines.push(`${tokens.join(' ')}`);
  391. lines.push(`${_utilsBundle2.colors.dim('Show & reuse browser:')} ${_utilsBundle2.colors.bold(showBrowserServer ? 'on' : 'off')}`);
  392. process.stdout.write(lines.join('\n'));
  393. }
  394. function printPrompt() {
  395. const sep = (0, _base.separator)();
  396. process.stdout.write(`
  397. ${sep}
  398. ${_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.')}
  399. `);
  400. }
  401. async function toggleShowBrowser(config, originalWorkers) {
  402. if (!showBrowserServer) {
  403. config.config.workers = 1;
  404. showBrowserServer = new _playwrightServer.PlaywrightServer({
  405. mode: 'extension',
  406. path: '/' + (0, _utils.createGuid)(),
  407. maxConnections: 1
  408. });
  409. const wsEndpoint = await showBrowserServer.listen();
  410. process.env.PW_TEST_REUSE_CONTEXT = '1';
  411. process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint;
  412. process.stdout.write(`${_utilsBundle2.colors.dim('Show & reuse browser:')} ${_utilsBundle2.colors.bold('on')}\n`);
  413. } else {
  414. var _showBrowserServer;
  415. config.config.workers = originalWorkers;
  416. await ((_showBrowserServer = showBrowserServer) === null || _showBrowserServer === void 0 ? void 0 : _showBrowserServer.close());
  417. showBrowserServer = undefined;
  418. delete process.env.PW_TEST_REUSE_CONTEXT;
  419. delete process.env.PW_TEST_CONNECT_WS_ENDPOINT;
  420. process.stdout.write(`${_utilsBundle2.colors.dim('Show & reuse browser:')} ${_utilsBundle2.colors.bold('off')}\n`);
  421. }
  422. }