playwright 1.55.1 → 1.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +3 -3
  2. package/ThirdPartyNotices.txt +3 -3
  3. package/lib/agents/generateAgents.js +263 -0
  4. package/lib/agents/generator.md +102 -0
  5. package/lib/agents/healer.md +78 -0
  6. package/lib/agents/planner.md +135 -0
  7. package/lib/common/config.js +1 -1
  8. package/lib/common/expectBundle.js +3 -0
  9. package/lib/common/expectBundleImpl.js +51 -51
  10. package/lib/index.js +7 -8
  11. package/lib/isomorphic/testServerConnection.js +0 -7
  12. package/lib/isomorphic/testTree.js +35 -8
  13. package/lib/matchers/expect.js +8 -21
  14. package/lib/matchers/matcherHint.js +42 -18
  15. package/lib/matchers/matchers.js +12 -6
  16. package/lib/matchers/toBeTruthy.js +16 -14
  17. package/lib/matchers/toEqual.js +18 -13
  18. package/lib/matchers/toHaveURL.js +12 -27
  19. package/lib/matchers/toMatchAriaSnapshot.js +26 -30
  20. package/lib/matchers/toMatchSnapshot.js +15 -12
  21. package/lib/matchers/toMatchText.js +29 -35
  22. package/lib/mcp/browser/actions.d.js +16 -0
  23. package/lib/mcp/browser/browserContextFactory.js +296 -0
  24. package/lib/mcp/browser/browserServerBackend.js +76 -0
  25. package/lib/mcp/browser/codegen.js +66 -0
  26. package/lib/mcp/browser/config.js +383 -0
  27. package/lib/mcp/browser/context.js +284 -0
  28. package/lib/mcp/browser/response.js +228 -0
  29. package/lib/mcp/browser/sessionLog.js +160 -0
  30. package/lib/mcp/browser/tab.js +277 -0
  31. package/lib/mcp/browser/tools/common.js +63 -0
  32. package/lib/mcp/browser/tools/console.js +44 -0
  33. package/lib/mcp/browser/tools/dialogs.js +60 -0
  34. package/lib/mcp/browser/tools/evaluate.js +70 -0
  35. package/lib/mcp/browser/tools/files.js +58 -0
  36. package/lib/mcp/browser/tools/form.js +74 -0
  37. package/lib/mcp/browser/tools/install.js +69 -0
  38. package/lib/mcp/browser/tools/keyboard.js +85 -0
  39. package/lib/mcp/browser/tools/mouse.js +107 -0
  40. package/lib/mcp/browser/tools/navigate.js +62 -0
  41. package/lib/mcp/browser/tools/network.js +54 -0
  42. package/lib/mcp/browser/tools/pdf.js +59 -0
  43. package/lib/mcp/browser/tools/screenshot.js +88 -0
  44. package/lib/mcp/browser/tools/snapshot.js +182 -0
  45. package/lib/mcp/browser/tools/tabs.js +67 -0
  46. package/lib/mcp/browser/tools/tool.js +49 -0
  47. package/lib/mcp/browser/tools/tracing.js +74 -0
  48. package/lib/mcp/browser/tools/utils.js +100 -0
  49. package/lib/mcp/browser/tools/verify.js +154 -0
  50. package/lib/mcp/browser/tools/wait.js +63 -0
  51. package/lib/mcp/browser/tools.js +80 -0
  52. package/lib/mcp/browser/watchdog.js +44 -0
  53. package/lib/mcp/config.d.js +16 -0
  54. package/lib/mcp/extension/cdpRelay.js +351 -0
  55. package/lib/mcp/extension/extensionContextFactory.js +75 -0
  56. package/lib/mcp/extension/protocol.js +28 -0
  57. package/lib/mcp/index.js +61 -0
  58. package/lib/mcp/{tool.js → log.js} +12 -18
  59. package/lib/mcp/program.js +96 -0
  60. package/lib/mcp/{bundle.js → sdk/bundle.js} +24 -2
  61. package/lib/mcp/{exports.js → sdk/exports.js} +12 -10
  62. package/lib/mcp/{transport.js → sdk/http.js} +79 -60
  63. package/lib/mcp/sdk/mdb.js +208 -0
  64. package/lib/mcp/{proxyBackend.js → sdk/proxyBackend.js} +18 -13
  65. package/lib/mcp/sdk/server.js +190 -0
  66. package/lib/mcp/sdk/tool.js +51 -0
  67. package/lib/mcp/test/browserBackend.js +98 -0
  68. package/lib/mcp/test/generatorTools.js +122 -0
  69. package/lib/mcp/test/plannerTools.js +46 -0
  70. package/lib/mcp/test/seed.js +72 -0
  71. package/lib/mcp/test/streams.js +39 -0
  72. package/lib/mcp/test/testBackend.js +97 -0
  73. package/lib/mcp/test/testContext.js +176 -0
  74. package/lib/mcp/test/testTool.js +30 -0
  75. package/lib/mcp/test/testTools.js +115 -0
  76. package/lib/mcpBundleImpl.js +14 -67
  77. package/lib/plugins/webServerPlugin.js +2 -0
  78. package/lib/program.js +68 -0
  79. package/lib/reporters/base.js +15 -17
  80. package/lib/reporters/html.js +39 -26
  81. package/lib/reporters/list.js +8 -4
  82. package/lib/reporters/listModeReporter.js +6 -3
  83. package/lib/reporters/merge.js +3 -1
  84. package/lib/reporters/teleEmitter.js +3 -1
  85. package/lib/runner/dispatcher.js +9 -23
  86. package/lib/runner/failureTracker.js +12 -16
  87. package/lib/runner/loadUtils.js +39 -3
  88. package/lib/runner/projectUtils.js +8 -2
  89. package/lib/runner/tasks.js +18 -7
  90. package/lib/runner/testRunner.js +16 -28
  91. package/lib/runner/testServer.js +17 -23
  92. package/lib/runner/watchMode.js +1 -53
  93. package/lib/runner/workerHost.js +8 -10
  94. package/lib/transform/babelBundleImpl.js +10 -10
  95. package/lib/transform/compilationCache.js +22 -5
  96. package/lib/util.js +12 -16
  97. package/lib/utilsBundleImpl.js +1 -1
  98. package/lib/worker/fixtureRunner.js +15 -7
  99. package/lib/worker/testInfo.js +9 -24
  100. package/lib/worker/workerMain.js +12 -8
  101. package/package.json +7 -3
  102. package/types/test.d.ts +17 -8
  103. package/types/testReporter.d.ts +1 -1
  104. package/lib/mcp/server.js +0 -118
  105. /package/lib/mcp/{inProcessTransport.js → sdk/inProcessTransport.js} +0 -0
@@ -80,6 +80,8 @@ class WebServerPlugin {
80
80
  const port = new URL(this._options.url).port;
81
81
  throw new Error(`${this._options.url ?? `http://localhost${port ? ":" + port : ""}`} is already used, make sure that nothing is running on the port/url or set reuseExistingServer:true in config.webServer.`);
82
82
  }
83
+ if (!this._options.command)
84
+ throw new Error("config.webServer.command cannot be empty");
83
85
  debugWebServer(`Starting WebServer process ${this._options.command}...`);
84
86
  const { launchedProcess, gracefullyClose } = await (0, import_utils.launchProcess)({
85
87
  command: this._options.command,
package/lib/program.js CHANGED
@@ -46,6 +46,13 @@ var testServer = __toESM(require("./runner/testServer"));
46
46
  var import_watchMode = require("./runner/watchMode");
47
47
  var import_testRunner = require("./runner/testRunner");
48
48
  var import_reporters = require("./runner/reporters");
49
+ var import_exports = require("./mcp/sdk/exports");
50
+ var import_testBackend = require("./mcp/test/testBackend");
51
+ var import_seed = require("./mcp/test/seed");
52
+ var import_program3 = require("./mcp/program");
53
+ var import_watchdog = require("./mcp/browser/watchdog");
54
+ var import_generateAgents = require("./agents/generateAgents");
55
+ const packageJSON = require("../package.json");
49
56
  function addTestCommand(program3) {
50
57
  const command = program3.command("test [test-filter...]");
51
58
  command.description("run tests with Playwright Test");
@@ -139,6 +146,60 @@ Arguments [dir]:
139
146
  Examples:
140
147
  $ npx playwright merge-reports playwright-report`);
141
148
  }
149
+ function addBrowserMCPServerCommand(program3) {
150
+ const command = program3.command("run-mcp-server", { hidden: true });
151
+ command.description("Interact with the browser over MCP");
152
+ (0, import_program3.decorateCommand)(command, packageJSON.version);
153
+ }
154
+ function addTestMCPServerCommand(program3) {
155
+ const command = program3.command("run-test-mcp-server", { hidden: true });
156
+ command.description("Interact with the test runner over MCP");
157
+ command.option("--headless", "run browser in headless mode, headed by default");
158
+ command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
159
+ command.option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.");
160
+ command.option("--port <port>", "port to listen on for SSE transport.");
161
+ command.action(async (options) => {
162
+ (0, import_watchdog.setupExitWatchdog)();
163
+ const backendFactory = {
164
+ name: "Playwright Test Runner",
165
+ nameInConfig: "playwright-test-runner",
166
+ version: packageJSON.version,
167
+ create: () => new import_testBackend.TestServerBackend(options.config, { muteConsole: options.port === void 0, headless: options.headless })
168
+ };
169
+ const mdbUrl = await (0, import_exports.runMainBackend)(
170
+ backendFactory,
171
+ {
172
+ port: options.port === void 0 ? void 0 : +options.port
173
+ }
174
+ );
175
+ if (mdbUrl)
176
+ console.error("MCP Listening on: ", mdbUrl);
177
+ });
178
+ }
179
+ function addInitAgentsCommand(program3) {
180
+ const command = program3.command("init-agents");
181
+ command.description("Initialize repository agents");
182
+ const option = command.createOption("--loop <loop>", "Agentic loop provider");
183
+ option.choices(["vscode", "claude", "opencode"]);
184
+ command.addOption(option);
185
+ command.option("-c, --config <file>", `Configuration file to find a project to use for seed test`);
186
+ command.option("--project <project>", "Project to use for seed test");
187
+ command.action(async (opts) => {
188
+ if (opts.loop === "opencode") {
189
+ await (0, import_generateAgents.initOpencodeRepo)();
190
+ } else if (opts.loop === "vscode") {
191
+ await (0, import_generateAgents.initVSCodeRepo)();
192
+ } else if (opts.loop === "claude") {
193
+ await (0, import_generateAgents.initClaudeCodeRepo)();
194
+ } else {
195
+ command.help();
196
+ return;
197
+ }
198
+ const config = await (0, import_configLoader.loadConfigFromFile)(opts.config);
199
+ const project = (0, import_seed.seedProject)(config, opts.project);
200
+ await (0, import_seed.ensureSeedTest)(project, true);
201
+ });
202
+ }
142
203
  async function runTests(args, opts) {
143
204
  await (0, import_utils.startProfiling)();
144
205
  const cliOverrides = overridesFromOptions(opts);
@@ -151,6 +212,8 @@ async function runTests(args, opts) {
151
212
  config.cliProjectFilter = opts.project || void 0;
152
213
  config.cliPassWithNoTests = !!opts.passWithNoTests;
153
214
  config.cliLastFailed = !!opts.lastFailed;
215
+ config.cliTestList = opts.testList ? import_path.default.resolve(process.cwd(), opts.testList) : void 0;
216
+ config.cliTestListInvert = opts.testListInvert ? import_path.default.resolve(process.cwd(), opts.testListInvert) : void 0;
154
217
  (0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
155
218
  if (opts.ui || opts.uiHost || opts.uiPort) {
156
219
  if (opts.onlyChanged)
@@ -321,6 +384,8 @@ const testOptions = [
321
384
  ["--reporter <reporter>", { description: `Reporter to use, comma-separated, can be ${import_config.builtInReporters.map((name) => `"${name}"`).join(", ")} (default: "${import_config.defaultReporter}")` }],
322
385
  ["--retries <retries>", { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
323
386
  ["--shard <shard>", { description: `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"` }],
387
+ ["--test-list <file>", { description: `Path to a file containing a list of tests to run. See https://playwright.dev/docs/test-cli for more details.` }],
388
+ ["--test-list-invert <file>", { description: `Path to a file containing a list of tests to skip. See https://playwright.dev/docs/test-cli for more details.` }],
324
389
  ["--timeout <timeout>", { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${import_config.defaultTimeout})` }],
325
390
  ["--trace <mode>", { description: `Force tracing mode`, choices: kTraceModes }],
326
391
  ["--tsconfig <path>", { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }],
@@ -336,8 +401,11 @@ addTestCommand(import_program.program);
336
401
  addShowReportCommand(import_program.program);
337
402
  addMergeReportsCommand(import_program.program);
338
403
  addClearCacheCommand(import_program.program);
404
+ addBrowserMCPServerCommand(import_program.program);
405
+ addTestMCPServerCommand(import_program.program);
339
406
  addDevServerCommand(import_program.program);
340
407
  addTestServerCommand(import_program.program);
408
+ addInitAgentsCommand(import_program.program);
341
409
  // Annotate the CommonJS export names for ESM import in node:
342
410
  0 && (module.exports = {
343
411
  program
@@ -120,7 +120,7 @@ class TerminalReporter {
120
120
  this._fatalErrors = [];
121
121
  this._failureCount = 0;
122
122
  this.screen = options.screen ?? terminalScreen;
123
- this._omitFailures = options.omitFailures || false;
123
+ this._options = options;
124
124
  }
125
125
  version() {
126
126
  return "v2";
@@ -258,7 +258,7 @@ class TerminalReporter {
258
258
  epilogue(full) {
259
259
  const summary = this.generateSummary();
260
260
  const summaryMessage = this.generateSummaryMessage(summary);
261
- if (full && summary.failuresToPrint.length && !this._omitFailures)
261
+ if (full && summary.failuresToPrint.length && !this._options.omitFailures)
262
262
  this._printFailures(summary.failuresToPrint);
263
263
  this._printSlowTests();
264
264
  this._printSummary(summaryMessage);
@@ -284,14 +284,14 @@ class TerminalReporter {
284
284
  willRetry(test) {
285
285
  return test.outcome() === "unexpected" && test.results.length <= test.retries;
286
286
  }
287
- formatTestTitle(test, step, omitLocation = false) {
288
- return formatTestTitle(this.screen, this.config, test, step, omitLocation);
287
+ formatTestTitle(test, step) {
288
+ return formatTestTitle(this.screen, this.config, test, step, this._options);
289
289
  }
290
290
  formatTestHeader(test, options = {}) {
291
- return formatTestHeader(this.screen, this.config, test, options);
291
+ return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId });
292
292
  }
293
293
  formatFailure(test, index) {
294
- return formatFailure(this.screen, this.config, test, index);
294
+ return formatFailure(this.screen, this.config, test, index, this._options);
295
295
  }
296
296
  formatError(error) {
297
297
  return formatError(this.screen, error);
@@ -300,9 +300,9 @@ class TerminalReporter {
300
300
  this.screen.stdout?.write(line ? line + "\n" : "\n");
301
301
  }
302
302
  }
303
- function formatFailure(screen, config, test, index) {
303
+ function formatFailure(screen, config, test, index, options) {
304
304
  const lines = [];
305
- const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error" });
305
+ const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
306
306
  lines.push(screen.colors.red(header));
307
307
  for (const result of test.results) {
308
308
  const resultLines = [];
@@ -416,20 +416,18 @@ function stepSuffix(step) {
416
416
  const stepTitles = step ? step.titlePath() : [];
417
417
  return stepTitles.map((t) => t.split("\n")[0]).map((t) => " \u203A " + t).join("");
418
418
  }
419
- function formatTestTitle(screen, config, test, step, omitLocation = false) {
419
+ function formatTestTitle(screen, config, test, step, options = {}) {
420
420
  const [, projectName, , ...titles] = test.titlePath();
421
- let location;
422
- if (omitLocation)
423
- location = `${relativeTestPath(screen, config, test)}`;
424
- else
425
- location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
426
- const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
427
- const testTitle = `${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
421
+ const location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
422
+ const testId = options.includeTestId ? `[id=${test.id}] ` : "";
423
+ const projectLabel = options.includeTestId ? `project=` : "";
424
+ const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : "";
425
+ const testTitle = `${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
428
426
  const extraTags = test.tags.filter((t) => !testTitle.includes(t));
429
427
  return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
430
428
  }
431
429
  function formatTestHeader(screen, config, test, options = {}) {
432
- const title = formatTestTitle(screen, config, test);
430
+ const title = formatTestTitle(screen, config, test, void 0, options);
433
431
  const header = `${options.indent || ""}${options.index ? options.index + ") " : ""}${title}`;
434
432
  let fullHeader = header;
435
433
  if (options.mode === "error") {
@@ -113,7 +113,17 @@ class HtmlReporter {
113
113
  else if (process.env.PLAYWRIGHT_HTML_NO_SNIPPETS)
114
114
  noSnippets = true;
115
115
  noSnippets = noSnippets || this._options.noSnippets;
116
- const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, noSnippets);
116
+ let noCopyPrompt;
117
+ if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === "false" || process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === "0")
118
+ noCopyPrompt = false;
119
+ else if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT)
120
+ noCopyPrompt = true;
121
+ noCopyPrompt = noCopyPrompt || this._options.noCopyPrompt;
122
+ const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, {
123
+ title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title,
124
+ noSnippets,
125
+ noCopyPrompt
126
+ });
117
127
  this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
118
128
  }
119
129
  async onExit() {
@@ -197,41 +207,26 @@ function startHtmlReportServer(folder) {
197
207
  return server;
198
208
  }
199
209
  class HtmlBuilder {
200
- constructor(config, outputDir, attachmentsBaseURL, title, noSnippets = false) {
210
+ constructor(config, outputDir, attachmentsBaseURL, options) {
201
211
  this._stepsInFile = new import_utils.MultiMap();
202
212
  this._hasTraces = false;
203
213
  this._config = config;
204
214
  this._reportFolder = outputDir;
205
- this._noSnippets = noSnippets;
215
+ this._options = options;
206
216
  import_fs.default.mkdirSync(this._reportFolder, { recursive: true });
207
217
  this._dataZipFile = new import_zipBundle.yazl.ZipFile();
208
218
  this._attachmentsBaseURL = attachmentsBaseURL;
209
- this._title = title;
210
219
  }
211
220
  async build(metadata, projectSuites, result, topLevelErrors) {
212
221
  const data = /* @__PURE__ */ new Map();
213
222
  for (const projectSuite of projectSuites) {
223
+ const projectName = projectSuite.project().name;
214
224
  for (const fileSuite of projectSuite.suites) {
215
225
  const fileName = this._relativeLocation(fileSuite.location).file;
216
- const fileId = (0, import_utils.calculateSha1)((0, import_utils.toPosixPath)(fileName)).slice(0, 20);
217
- let fileEntry = data.get(fileId);
218
- if (!fileEntry) {
219
- fileEntry = {
220
- testFile: { fileId, fileName, tests: [] },
221
- testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }
222
- };
223
- data.set(fileId, fileEntry);
224
- }
225
- const { testFile, testFileSummary } = fileEntry;
226
- const testEntries = [];
227
- this._processSuite(fileSuite, projectSuite.project().name, [], testEntries);
228
- for (const test of testEntries) {
229
- testFile.tests.push(test.testCase);
230
- testFileSummary.tests.push(test.testCaseSummary);
231
- }
226
+ this._createEntryForSuite(data, projectName, fileSuite, fileName, true);
232
227
  }
233
228
  }
234
- if (!this._noSnippets)
229
+ if (!this._options.noSnippets)
235
230
  createSnippets(this._stepsInFile);
236
231
  let ok = true;
237
232
  for (const [fileId, { testFile, testFileSummary }] of data) {
@@ -260,13 +255,13 @@ class HtmlBuilder {
260
255
  }
261
256
  const htmlReport = {
262
257
  metadata,
263
- title: this._title,
264
258
  startTime: result.startTime.getTime(),
265
259
  duration: result.duration,
266
260
  files: [...data.values()].map((e) => e.testFileSummary),
267
261
  projectNames: projectSuites.map((r) => r.project().name),
268
262
  stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) },
269
- errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message)
263
+ errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message),
264
+ options: this._options
270
265
  };
271
266
  htmlReport.files.sort((f1, f2) => {
272
267
  const w1 = f1.stats.unexpected * 1e3 + f1.stats.flaky;
@@ -331,13 +326,31 @@ class HtmlBuilder {
331
326
  _addDataFile(fileName, data) {
332
327
  this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
333
328
  }
334
- _processSuite(suite, projectName, path2, outTests) {
329
+ _createEntryForSuite(data, projectName, suite, fileName, deep) {
330
+ const fileId = (0, import_utils.calculateSha1)(fileName).slice(0, 20);
331
+ let fileEntry = data.get(fileId);
332
+ if (!fileEntry) {
333
+ fileEntry = {
334
+ testFile: { fileId, fileName, tests: [] },
335
+ testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }
336
+ };
337
+ data.set(fileId, fileEntry);
338
+ }
339
+ const { testFile, testFileSummary } = fileEntry;
340
+ const testEntries = [];
341
+ this._processSuite(suite, projectName, [], deep, testEntries);
342
+ for (const test of testEntries) {
343
+ testFile.tests.push(test.testCase);
344
+ testFileSummary.tests.push(test.testCaseSummary);
345
+ }
346
+ }
347
+ _processSuite(suite, projectName, path2, deep, outTests) {
335
348
  const newPath = [...path2, suite.title];
336
349
  suite.entries().forEach((e) => {
337
350
  if (e.type === "test")
338
351
  outTests.push(this._createTestEntry(e, projectName, newPath));
339
- else
340
- this._processSuite(e, projectName, newPath, outTests);
352
+ else if (deep)
353
+ this._processSuite(e, projectName, newPath, deep, outTests);
341
354
  });
342
355
  }
343
356
  _createTestEntry(test, projectName, path2) {
@@ -39,6 +39,7 @@ class ListReporter extends import_base.TerminalReporter {
39
39
  this._stepIndex = /* @__PURE__ */ new Map();
40
40
  this._needNewLine = false;
41
41
  this._printSteps = (0, import_utils.getAsBooleanFromENV)("PLAYWRIGHT_LIST_PRINT_STEPS", options?.printSteps);
42
+ this._prefixStdio = options?.prefixStdio;
42
43
  }
43
44
  onBegin(suite) {
44
45
  super.onBegin(suite);
@@ -61,11 +62,11 @@ class ListReporter extends import_base.TerminalReporter {
61
62
  }
62
63
  onStdOut(chunk, test, result) {
63
64
  super.onStdOut(chunk, test, result);
64
- this._dumpToStdio(test, chunk, this.screen.stdout);
65
+ this._dumpToStdio(test, chunk, this.screen.stdout, "out");
65
66
  }
66
67
  onStdErr(chunk, test, result) {
67
68
  super.onStdErr(chunk, test, result);
68
- this._dumpToStdio(test, chunk, this.screen.stderr);
69
+ this._dumpToStdio(test, chunk, this.screen.stderr, "err");
69
70
  }
70
71
  getStepIndex(testIndex, result, step) {
71
72
  if (this._stepIndex.has(step))
@@ -137,12 +138,15 @@ class ListReporter extends import_base.TerminalReporter {
137
138
  }
138
139
  }
139
140
  }
140
- _dumpToStdio(test, chunk, stream) {
141
+ _dumpToStdio(test, chunk, stream, stdio) {
141
142
  if (this.config.quiet)
142
143
  return;
143
144
  const text = chunk.toString("utf-8");
144
145
  this._updateLineCountAndNewLineFlagForOutput(text);
145
- stream.write(chunk);
146
+ if (this._prefixStdio)
147
+ stream.write(`[${stdio}] ${chunk}`);
148
+ else
149
+ stream.write(chunk);
146
150
  }
147
151
  onTestEnd(test, result) {
148
152
  super.onTestEnd(test, result);
@@ -34,7 +34,8 @@ module.exports = __toCommonJS(listModeReporter_exports);
34
34
  var import_path = __toESM(require("path"));
35
35
  var import_base = require("./base");
36
36
  class ListModeReporter {
37
- constructor(options) {
37
+ constructor(options = {}) {
38
+ this._options = options;
38
39
  this.screen = options?.screen ?? import_base.terminalScreen;
39
40
  }
40
41
  version() {
@@ -50,8 +51,10 @@ class ListModeReporter {
50
51
  for (const test of tests) {
51
52
  const [, projectName, , ...titles] = test.titlePath();
52
53
  const location = `${import_path.default.relative(this.config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`;
53
- const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
54
- this._writeLine(` ${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`);
54
+ const testId = this._options.includeTestId ? `[id=${test.id}] ` : "";
55
+ const projectLabel = this._options.includeTestId ? `project=` : "";
56
+ const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : "";
57
+ this._writeLine(` ${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`);
55
58
  files.add(test.location.file);
56
59
  }
57
60
  this._writeLine(`Total: ${tests.length} ${tests.length === 1 ? "test" : "tests"} in ${files.size} ${files.size === 1 ? "file" : "files"}`);
@@ -216,7 +216,9 @@ function mergeConfigureEvents(configureEvents, rootDirOverride) {
216
216
  metadata: {},
217
217
  rootDir: "",
218
218
  version: "",
219
- workers: 0
219
+ workers: 0,
220
+ globalSetup: null,
221
+ globalTeardown: null
220
222
  };
221
223
  for (const event of configureEvents)
222
224
  config = mergeConfigs(config, event.params.config);
@@ -152,7 +152,9 @@ class TeleReporterEmitter {
152
152
  metadata: config.metadata,
153
153
  rootDir: config.rootDir,
154
154
  version: config.version,
155
- workers: config.workers
155
+ workers: config.workers,
156
+ globalSetup: config.globalSetup,
157
+ globalTeardown: config.globalTeardown
156
158
  };
157
159
  }
158
160
  _serializeProject(suite) {
@@ -158,8 +158,14 @@ class Dispatcher {
158
158
  _createWorker(testGroup, parallelIndex, loaderData) {
159
159
  const projectConfig = this._config.projects.find((p) => p.id === testGroup.projectId);
160
160
  const outputDir = projectConfig.project.outputDir;
161
- const recoverFromStepErrors = this._failureTracker.canRecoverFromStepError();
162
- const worker = new import_workerHost.WorkerHost(testGroup, parallelIndex, loaderData, recoverFromStepErrors, this._extraEnvByProjectId.get(testGroup.projectId) || {}, outputDir);
161
+ const worker = new import_workerHost.WorkerHost(testGroup, {
162
+ parallelIndex,
163
+ config: loaderData,
164
+ extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {},
165
+ outputDir,
166
+ pauseOnError: this._failureTracker.pauseOnError(),
167
+ pauseAtEnd: this._failureTracker.pauseAtEnd(projectConfig)
168
+ });
163
169
  const handleOutput = (params) => {
164
170
  const chunk = chunkFromParams(params);
165
171
  if (worker.didFail()) {
@@ -312,24 +318,6 @@ class JobDispatcher {
312
318
  steps.delete(params.stepId);
313
319
  this._reporter.onStepEnd?.(test, result, step);
314
320
  }
315
- _onStepRecoverFromError(resumeAfterStepError, params) {
316
- const data = this._dataByTestId.get(params.testId);
317
- if (!data) {
318
- resumeAfterStepError({ stepId: params.stepId, status: "failed" });
319
- return;
320
- }
321
- const { steps } = data;
322
- const step = steps.get(params.stepId);
323
- if (!step) {
324
- resumeAfterStepError({ stepId: params.stepId, status: "failed" });
325
- return;
326
- }
327
- const testError = {
328
- ...params.error,
329
- location: step.location
330
- };
331
- this._failureTracker.recoverFromStepError(params.stepId, testError, resumeAfterStepError);
332
- }
333
321
  _onAttach(params) {
334
322
  const data = this._dataByTestId.get(params.testId);
335
323
  if (!data) {
@@ -382,7 +370,7 @@ class JobDispatcher {
382
370
  }
383
371
  }
384
372
  _onDone(params) {
385
- if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError) {
373
+ if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError && !params.stoppedDueToUnhandledErrorInTestFail) {
386
374
  this._finished({ didFail: false });
387
375
  return;
388
376
  }
@@ -455,13 +443,11 @@ class JobDispatcher {
455
443
  })
456
444
  };
457
445
  worker.runTestGroup(runPayload);
458
- const resumeAfterStepError = worker.resumeAfterStepError.bind(worker);
459
446
  this._listeners = [
460
447
  import_utils.eventsHelper.addEventListener(worker, "testBegin", this._onTestBegin.bind(this)),
461
448
  import_utils.eventsHelper.addEventListener(worker, "testEnd", this._onTestEnd.bind(this)),
462
449
  import_utils.eventsHelper.addEventListener(worker, "stepBegin", this._onStepBegin.bind(this)),
463
450
  import_utils.eventsHelper.addEventListener(worker, "stepEnd", this._onStepEnd.bind(this)),
464
- import_utils.eventsHelper.addEventListener(worker, "stepRecoverFromError", this._onStepRecoverFromError.bind(this, resumeAfterStepError)),
465
451
  import_utils.eventsHelper.addEventListener(worker, "attach", this._onAttach.bind(this)),
466
452
  import_utils.eventsHelper.addEventListener(worker, "done", this._onDone.bind(this)),
467
453
  import_utils.eventsHelper.addEventListener(worker, "exit", this.onExit.bind(this))
@@ -22,35 +22,31 @@ __export(failureTracker_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(failureTracker_exports);
24
24
  class FailureTracker {
25
- constructor(_config) {
25
+ constructor(_config, options) {
26
26
  this._config = _config;
27
27
  this._failureCount = 0;
28
28
  this._hasWorkerErrors = false;
29
+ this._topLevelProjects = [];
30
+ this._pauseOnError = options?.pauseOnError ?? false;
31
+ this._pauseAtEnd = options?.pauseAtEnd ?? false;
29
32
  }
30
- canRecoverFromStepError() {
31
- return !!this._recoverFromStepErrorHandler;
32
- }
33
- setRecoverFromStepErrorHandler(recoverFromStepErrorHandler) {
34
- this._recoverFromStepErrorHandler = recoverFromStepErrorHandler;
35
- }
36
- onRootSuite(rootSuite) {
33
+ onRootSuite(rootSuite, topLevelProjects) {
37
34
  this._rootSuite = rootSuite;
35
+ this._topLevelProjects = topLevelProjects;
38
36
  }
39
37
  onTestEnd(test, result) {
40
38
  if (test.outcome() === "unexpected" && test.results.length > test.retries)
41
39
  ++this._failureCount;
42
40
  }
43
- recoverFromStepError(stepId, error, resumeAfterStepError) {
44
- if (!this._recoverFromStepErrorHandler) {
45
- resumeAfterStepError({ stepId, status: "failed" });
46
- return;
47
- }
48
- void this._recoverFromStepErrorHandler(stepId, error).then(resumeAfterStepError).catch(() => {
49
- });
50
- }
51
41
  onWorkerError() {
52
42
  this._hasWorkerErrors = true;
53
43
  }
44
+ pauseOnError() {
45
+ return this._pauseOnError;
46
+ }
47
+ pauseAtEnd(inProject) {
48
+ return this._pauseAtEnd && this._topLevelProjects.includes(inProject);
49
+ }
54
50
  hasReachedMaxFailures() {
55
51
  return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
56
52
  }
@@ -32,10 +32,13 @@ __export(loadUtils_exports, {
32
32
  createRootSuite: () => createRootSuite,
33
33
  loadFileSuites: () => loadFileSuites,
34
34
  loadGlobalHook: () => loadGlobalHook,
35
- loadReporter: () => loadReporter
35
+ loadReporter: () => loadReporter,
36
+ loadTestList: () => loadTestList
36
37
  });
37
38
  module.exports = __toCommonJS(loadUtils_exports);
38
39
  var import_path = __toESM(require("path"));
40
+ var import_fs = __toESM(require("fs"));
41
+ var import_utils = require("playwright-core/lib/utils");
39
42
  var import_loaderHost = require("./loaderHost");
40
43
  var import_util = require("../util");
41
44
  var import_projectUtils = require("./projectUtils");
@@ -168,14 +171,17 @@ async function createRootSuite(testRun, errors, shouldFilterOnly) {
168
171
  }
169
172
  if (config.postShardTestFilters.length)
170
173
  (0, import_suiteUtils.filterTestsRemoveEmptySuites)(rootSuite, (test) => config.postShardTestFilters.every((filter) => filter(test)));
174
+ const topLevelProjects = [];
171
175
  {
172
176
  const projectClosure2 = new Map((0, import_projectUtils.buildProjectsClosure)(rootSuite.suites.map((suite) => suite._fullProject)));
173
177
  for (const [project, level] of projectClosure2.entries()) {
174
178
  if (level === "dependency")
175
179
  rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project)));
180
+ else
181
+ topLevelProjects.push(project);
176
182
  }
177
183
  }
178
- return rootSuite;
184
+ return { rootSuite, topLevelProjects };
179
185
  }
180
186
  function createProjectSuite(project, fileSuites) {
181
187
  const projectSuite = new import_test.Suite(project.project.name, "project");
@@ -287,11 +293,41 @@ function sourceMapSources(file, cache) {
287
293
  return sources;
288
294
  }
289
295
  }
296
+ async function loadTestList(config, filePath) {
297
+ try {
298
+ const content = await import_fs.default.promises.readFile(filePath, "utf-8");
299
+ const lines = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
300
+ const descriptions = lines.map((line) => {
301
+ const delimiter = line.includes("\u203A") ? "\u203A" : ">";
302
+ const tokens = line.split(delimiter).map((token) => token.trim());
303
+ let project;
304
+ if (tokens[0].startsWith("[")) {
305
+ if (!tokens[0].endsWith("]"))
306
+ throw new Error(`Malformed test description: ${line}`);
307
+ project = tokens[0].substring(1, tokens[0].length - 1);
308
+ tokens.shift();
309
+ }
310
+ return { project, file: (0, import_utils.toPosixPath)((0, import_util.parseLocationArg)(tokens[0]).file), titlePath: tokens.slice(1) };
311
+ });
312
+ return (test) => descriptions.some((d) => {
313
+ const [projectName, , ...titles] = test.titlePath();
314
+ if (d.project !== void 0 && d.project !== projectName)
315
+ return false;
316
+ const relativeFile = (0, import_utils.toPosixPath)(import_path.default.relative(config.config.rootDir, test.location.file));
317
+ if (relativeFile !== d.file)
318
+ return false;
319
+ return d.titlePath.length === titles.length && d.titlePath.every((_, index) => titles[index] === d.titlePath[index]);
320
+ });
321
+ } catch (e) {
322
+ throw (0, import_util.errorWithFile)(filePath, "Cannot read test list file: " + e.message);
323
+ }
324
+ }
290
325
  // Annotate the CommonJS export names for ESM import in node:
291
326
  0 && (module.exports = {
292
327
  collectProjectsAndTestFiles,
293
328
  createRootSuite,
294
329
  loadFileSuites,
295
330
  loadGlobalHook,
296
- loadReporter
331
+ loadReporter,
332
+ loadTestList
297
333
  });
@@ -32,7 +32,8 @@ __export(projectUtils_exports, {
32
32
  buildProjectsClosure: () => buildProjectsClosure,
33
33
  buildTeardownToSetupsMap: () => buildTeardownToSetupsMap,
34
34
  collectFilesForProject: () => collectFilesForProject,
35
- filterProjects: () => filterProjects
35
+ filterProjects: () => filterProjects,
36
+ findTopLevelProjects: () => findTopLevelProjects
36
37
  });
37
38
  module.exports = __toCommonJS(projectUtils_exports);
38
39
  var import_fs = __toESM(require("fs"));
@@ -116,6 +117,10 @@ function buildProjectsClosure(projects, hasTests) {
116
117
  visit(0, p);
117
118
  return result;
118
119
  }
120
+ function findTopLevelProjects(config) {
121
+ const closure = buildProjectsClosure(config.projects);
122
+ return [...closure].filter((entry) => entry[1] === "top-level").map((entry) => entry[0]);
123
+ }
119
124
  function buildDependentProjects(forProjects, projects) {
120
125
  const reverseDeps = new Map(projects.map((p) => [p, []]));
121
126
  for (const project of projects) {
@@ -231,5 +236,6 @@ async function collectFiles(testDir, respectGitIgnore) {
231
236
  buildProjectsClosure,
232
237
  buildTeardownToSetupsMap,
233
238
  collectFilesForProject,
234
- filterProjects
239
+ filterProjects,
240
+ findTopLevelProjects
235
241
  });