playwright 1.54.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 (116) hide show
  1. package/README.md +3 -3
  2. package/ThirdPartyNotices.txt +2727 -434
  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 +3 -1
  8. package/lib/common/configLoader.js +2 -1
  9. package/lib/common/expectBundle.js +3 -0
  10. package/lib/common/expectBundleImpl.js +51 -51
  11. package/lib/common/fixtures.js +1 -1
  12. package/lib/common/suiteUtils.js +0 -9
  13. package/lib/index.js +127 -115
  14. package/lib/isomorphic/testTree.js +35 -8
  15. package/lib/matchers/expect.js +6 -7
  16. package/lib/matchers/matcherHint.js +43 -15
  17. package/lib/matchers/matchers.js +10 -4
  18. package/lib/matchers/toBeTruthy.js +16 -14
  19. package/lib/matchers/toEqual.js +18 -13
  20. package/lib/matchers/toHaveURL.js +12 -27
  21. package/lib/matchers/toMatchAriaSnapshot.js +26 -31
  22. package/lib/matchers/toMatchSnapshot.js +15 -12
  23. package/lib/matchers/toMatchText.js +29 -35
  24. package/lib/mcp/browser/actions.d.js +16 -0
  25. package/lib/mcp/browser/browserContextFactory.js +296 -0
  26. package/lib/mcp/browser/browserServerBackend.js +76 -0
  27. package/lib/mcp/browser/codegen.js +66 -0
  28. package/lib/mcp/browser/config.js +383 -0
  29. package/lib/mcp/browser/context.js +284 -0
  30. package/lib/mcp/browser/response.js +228 -0
  31. package/lib/mcp/browser/sessionLog.js +160 -0
  32. package/lib/mcp/browser/tab.js +277 -0
  33. package/lib/mcp/browser/tools/common.js +63 -0
  34. package/lib/mcp/browser/tools/console.js +44 -0
  35. package/lib/mcp/browser/tools/dialogs.js +60 -0
  36. package/lib/mcp/browser/tools/evaluate.js +70 -0
  37. package/lib/mcp/browser/tools/files.js +58 -0
  38. package/lib/mcp/browser/tools/form.js +74 -0
  39. package/lib/mcp/browser/tools/install.js +69 -0
  40. package/lib/mcp/browser/tools/keyboard.js +85 -0
  41. package/lib/mcp/browser/tools/mouse.js +107 -0
  42. package/lib/mcp/browser/tools/navigate.js +62 -0
  43. package/lib/mcp/browser/tools/network.js +54 -0
  44. package/lib/mcp/browser/tools/pdf.js +59 -0
  45. package/lib/mcp/browser/tools/screenshot.js +88 -0
  46. package/lib/mcp/browser/tools/snapshot.js +182 -0
  47. package/lib/mcp/browser/tools/tabs.js +67 -0
  48. package/lib/mcp/browser/tools/tool.js +49 -0
  49. package/lib/mcp/browser/tools/tracing.js +74 -0
  50. package/lib/mcp/browser/tools/utils.js +100 -0
  51. package/lib/mcp/browser/tools/verify.js +154 -0
  52. package/lib/mcp/browser/tools/wait.js +63 -0
  53. package/lib/mcp/browser/tools.js +80 -0
  54. package/lib/mcp/browser/watchdog.js +44 -0
  55. package/lib/mcp/config.d.js +16 -0
  56. package/lib/mcp/extension/cdpRelay.js +351 -0
  57. package/lib/mcp/extension/extensionContextFactory.js +75 -0
  58. package/lib/mcp/extension/protocol.js +28 -0
  59. package/lib/mcp/index.js +61 -0
  60. package/lib/mcp/log.js +35 -0
  61. package/lib/mcp/program.js +96 -0
  62. package/lib/mcp/sdk/bundle.js +81 -0
  63. package/lib/mcp/sdk/exports.js +32 -0
  64. package/lib/mcp/sdk/http.js +180 -0
  65. package/lib/mcp/sdk/inProcessTransport.js +71 -0
  66. package/lib/mcp/sdk/mdb.js +208 -0
  67. package/lib/mcp/sdk/proxyBackend.js +128 -0
  68. package/lib/mcp/sdk/server.js +190 -0
  69. package/lib/mcp/sdk/tool.js +51 -0
  70. package/lib/mcp/test/browserBackend.js +98 -0
  71. package/lib/mcp/test/generatorTools.js +122 -0
  72. package/lib/mcp/test/plannerTools.js +46 -0
  73. package/lib/mcp/test/seed.js +72 -0
  74. package/lib/mcp/test/streams.js +39 -0
  75. package/lib/mcp/test/testBackend.js +97 -0
  76. package/lib/mcp/test/testContext.js +176 -0
  77. package/lib/mcp/test/testTool.js +30 -0
  78. package/lib/mcp/test/testTools.js +115 -0
  79. package/lib/mcpBundleImpl.js +41 -0
  80. package/lib/plugins/webServerPlugin.js +2 -0
  81. package/lib/program.js +77 -57
  82. package/lib/reporters/base.js +34 -29
  83. package/lib/reporters/dot.js +11 -11
  84. package/lib/reporters/github.js +2 -1
  85. package/lib/reporters/html.js +58 -41
  86. package/lib/reporters/internalReporter.js +2 -1
  87. package/lib/reporters/line.js +15 -15
  88. package/lib/reporters/list.js +24 -19
  89. package/lib/reporters/listModeReporter.js +69 -0
  90. package/lib/reporters/markdown.js +3 -3
  91. package/lib/reporters/merge.js +3 -1
  92. package/lib/reporters/teleEmitter.js +3 -1
  93. package/lib/runner/dispatcher.js +9 -2
  94. package/lib/runner/failureTracker.js +12 -2
  95. package/lib/runner/lastRun.js +7 -4
  96. package/lib/runner/loadUtils.js +46 -12
  97. package/lib/runner/projectUtils.js +8 -2
  98. package/lib/runner/reporters.js +7 -32
  99. package/lib/runner/tasks.js +20 -10
  100. package/lib/runner/testRunner.js +390 -0
  101. package/lib/runner/testServer.js +57 -276
  102. package/lib/runner/watchMode.js +5 -1
  103. package/lib/runner/workerHost.js +8 -6
  104. package/lib/transform/babelBundleImpl.js +179 -195
  105. package/lib/transform/compilationCache.js +22 -5
  106. package/lib/transform/transform.js +1 -1
  107. package/lib/util.js +12 -35
  108. package/lib/utilsBundleImpl.js +1 -1
  109. package/lib/worker/fixtureRunner.js +7 -2
  110. package/lib/worker/testInfo.js +76 -45
  111. package/lib/worker/testTracing.js +8 -7
  112. package/lib/worker/workerMain.js +12 -3
  113. package/package.json +10 -2
  114. package/types/test.d.ts +63 -44
  115. package/types/testReporter.d.ts +1 -1
  116. package/lib/runner/runner.js +0 -110
@@ -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
@@ -28,8 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var program_exports = {};
30
30
  __export(program_exports, {
31
- program: () => import_program2.program,
32
- withRunnerAndMutedWrite: () => withRunnerAndMutedWrite
31
+ program: () => import_program2.program
33
32
  });
34
33
  module.exports = __toCommonJS(program_exports);
35
34
  var import_fs = __toESM(require("fs"));
@@ -43,10 +42,17 @@ var import_base = require("./reporters/base");
43
42
  var import_html = require("./reporters/html");
44
43
  var import_merge = require("./reporters/merge");
45
44
  var import_projectUtils = require("./runner/projectUtils");
46
- var import_runner = require("./runner/runner");
47
45
  var testServer = __toESM(require("./runner/testServer"));
48
46
  var import_watchMode = require("./runner/watchMode");
49
- var import_util = require("./util");
47
+ var import_testRunner = require("./runner/testRunner");
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");
50
56
  function addTestCommand(program3) {
51
57
  const command = program3.command("test [test-filter...]");
52
58
  command.description("run tests with Playwright Test");
@@ -78,44 +84,24 @@ Examples:
78
84
  $ npx playwright test --headed
79
85
  $ npx playwright test --project=webkit`);
80
86
  }
81
- function addListFilesCommand(program3) {
82
- const command = program3.command("list-files [file-filter...]", { hidden: true });
83
- command.description("List files with Playwright Test tests");
84
- command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
85
- command.option("--project <project-name...>", `Only run tests from the specified list of projects, supports '*' wildcard (default: list all projects)`);
86
- command.action(async (args, opts) => listTestFiles(opts));
87
- }
88
87
  function addClearCacheCommand(program3) {
89
88
  const command = program3.command("clear-cache");
90
89
  command.description("clears build and test caches");
91
90
  command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
92
91
  command.action(async (opts) => {
93
- const config = await (0, import_configLoader.loadConfigFromFile)(opts.config);
94
- const runner = new import_runner.Runner(config);
95
- const { status } = await runner.clearCache();
92
+ const runner = new import_testRunner.TestRunner((0, import_configLoader.resolveConfigLocation)(opts.config), {});
93
+ const { status } = await runner.clearCache((0, import_reporters.createErrorCollectingReporter)(import_base.terminalScreen));
96
94
  const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
97
95
  (0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
98
96
  });
99
97
  }
100
- function addFindRelatedTestFilesCommand(program3) {
101
- const command = program3.command("find-related-test-files [source-files...]", { hidden: true });
102
- command.description("Returns the list of related tests to the given files");
103
- command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
104
- command.action(async (files, options) => {
105
- const resolvedFiles = files.map((file) => import_path.default.resolve(process.cwd(), file));
106
- await withRunnerAndMutedWrite(options.config, (runner) => runner.findRelatedTestFiles(resolvedFiles));
107
- });
108
- }
109
98
  function addDevServerCommand(program3) {
110
99
  const command = program3.command("dev-server", { hidden: true });
111
100
  command.description("start dev server");
112
101
  command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
113
102
  command.action(async (options) => {
114
- const config = await (0, import_configLoader.loadConfigFromFile)(options.config);
115
- const runner = new import_runner.Runner(config);
116
- const { status } = await runner.runDevServer();
117
- const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
118
- (0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
103
+ const runner = new import_testRunner.TestRunner((0, import_configLoader.resolveConfigLocation)(options.config), {});
104
+ await runner.startDevServer((0, import_reporters.createErrorCollectingReporter)(import_base.terminalScreen), "in-process");
119
105
  });
120
106
  }
121
107
  function addTestServerCommand(program3) {
@@ -160,6 +146,60 @@ Arguments [dir]:
160
146
  Examples:
161
147
  $ npx playwright merge-reports playwright-report`);
162
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
+ }
163
203
  async function runTests(args, opts) {
164
204
  await (0, import_utils.startProfiling)();
165
205
  const cliOverrides = overridesFromOptions(opts);
@@ -172,6 +212,8 @@ async function runTests(args, opts) {
172
212
  config.cliProjectFilter = opts.project || void 0;
173
213
  config.cliPassWithNoTests = !!opts.passWithNoTests;
174
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;
175
217
  (0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
176
218
  if (opts.ui || opts.uiHost || opts.uiPort) {
177
219
  if (opts.onlyChanged)
@@ -206,8 +248,7 @@ async function runTests(args, opts) {
206
248
  (0, import_utils.gracefullyProcessExitDoNotHang)(exitCode2);
207
249
  return;
208
250
  }
209
- const runner = new import_runner.Runner(config);
210
- const status = await runner.runAllTests();
251
+ const status = await (0, import_testRunner.runAllTestsWithConfig)(config);
211
252
  await (0, import_utils.stopProfiling)("runner");
212
253
  const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
213
254
  (0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
@@ -219,29 +260,6 @@ async function runTestServer(opts) {
219
260
  const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
220
261
  (0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
221
262
  }
222
- async function withRunnerAndMutedWrite(configFile, callback) {
223
- const stdoutWrite = process.stdout.write.bind(process.stdout);
224
- process.stdout.write = (a, b, c) => process.stderr.write(a, b, c);
225
- try {
226
- const config = await (0, import_configLoader.loadConfigFromFile)(configFile);
227
- const runner = new import_runner.Runner(config);
228
- const result = await callback(runner);
229
- stdoutWrite(JSON.stringify(result, void 0, 2), () => {
230
- (0, import_utils.gracefullyProcessExitDoNotHang)(0);
231
- });
232
- } catch (e) {
233
- const error = (0, import_util.serializeError)(e);
234
- error.location = (0, import_base.prepareErrorStack)(e.stack).location;
235
- stdoutWrite(JSON.stringify({ error }, void 0, 2), () => {
236
- (0, import_utils.gracefullyProcessExitDoNotHang)(0);
237
- });
238
- }
239
- }
240
- async function listTestFiles(opts) {
241
- await withRunnerAndMutedWrite(opts.config, async (runner) => {
242
- return await runner.listTestFiles();
243
- });
244
- }
245
263
  async function mergeReports(reportDir, opts) {
246
264
  const configFile = opts.config;
247
265
  const config = configFile ? await (0, import_configLoader.loadConfigFromFile)(configFile) : await (0, import_configLoader.loadEmptyConfigForMergeReports)();
@@ -366,6 +384,8 @@ const testOptions = [
366
384
  ["--reporter <reporter>", { description: `Reporter to use, comma-separated, can be ${import_config.builtInReporters.map((name) => `"${name}"`).join(", ")} (default: "${import_config.defaultReporter}")` }],
367
385
  ["--retries <retries>", { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
368
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.` }],
369
389
  ["--timeout <timeout>", { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${import_config.defaultTimeout})` }],
370
390
  ["--trace <mode>", { description: `Force tracing mode`, choices: kTraceModes }],
371
391
  ["--tsconfig <path>", { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }],
@@ -379,14 +399,14 @@ const testOptions = [
379
399
  ];
380
400
  addTestCommand(import_program.program);
381
401
  addShowReportCommand(import_program.program);
382
- addListFilesCommand(import_program.program);
383
402
  addMergeReportsCommand(import_program.program);
384
403
  addClearCacheCommand(import_program.program);
385
- addFindRelatedTestFilesCommand(import_program.program);
404
+ addBrowserMCPServerCommand(import_program.program);
405
+ addTestMCPServerCommand(import_program.program);
386
406
  addDevServerCommand(import_program.program);
387
407
  addTestServerCommand(import_program.program);
408
+ addInitAgentsCommand(import_program.program);
388
409
  // Annotate the CommonJS export names for ESM import in node:
389
410
  0 && (module.exports = {
390
- program,
391
- withRunnerAndMutedWrite
411
+ program
392
412
  });
@@ -54,18 +54,20 @@ var import_utilsBundle2 = require("../utilsBundle");
54
54
  const kOutputSymbol = Symbol("output");
55
55
  const DEFAULT_TTY_WIDTH = 100;
56
56
  const DEFAULT_TTY_HEIGHT = 40;
57
+ const originalProcessStdout = process.stdout;
58
+ const originalProcessStderr = process.stderr;
57
59
  const terminalScreen = (() => {
58
- let isTTY = !!process.stdout.isTTY;
59
- let ttyWidth = process.stdout.columns || 0;
60
- let ttyHeight = process.stdout.rows || 0;
60
+ let isTTY = !!originalProcessStdout.isTTY;
61
+ let ttyWidth = originalProcessStdout.columns || 0;
62
+ let ttyHeight = originalProcessStdout.rows || 0;
61
63
  if (process.env.PLAYWRIGHT_FORCE_TTY === "false" || process.env.PLAYWRIGHT_FORCE_TTY === "0") {
62
64
  isTTY = false;
63
65
  ttyWidth = 0;
64
66
  ttyHeight = 0;
65
67
  } else if (process.env.PLAYWRIGHT_FORCE_TTY === "true" || process.env.PLAYWRIGHT_FORCE_TTY === "1") {
66
68
  isTTY = true;
67
- ttyWidth = process.stdout.columns || DEFAULT_TTY_WIDTH;
68
- ttyHeight = process.stdout.rows || DEFAULT_TTY_HEIGHT;
69
+ ttyWidth = originalProcessStdout.columns || DEFAULT_TTY_WIDTH;
70
+ ttyHeight = originalProcessStdout.rows || DEFAULT_TTY_HEIGHT;
69
71
  } else if (process.env.PLAYWRIGHT_FORCE_TTY) {
70
72
  isTTY = true;
71
73
  const sizeMatch = process.env.PLAYWRIGHT_FORCE_TTY.match(/^(\d+)x(\d+)$/);
@@ -92,7 +94,9 @@ const terminalScreen = (() => {
92
94
  isTTY,
93
95
  ttyWidth,
94
96
  ttyHeight,
95
- colors
97
+ colors,
98
+ stdout: originalProcessStdout,
99
+ stderr: originalProcessStderr
96
100
  };
97
101
  })();
98
102
  const nonTerminalScreen = {
@@ -111,12 +115,12 @@ const internalScreen = {
111
115
  };
112
116
  class TerminalReporter {
113
117
  constructor(options = {}) {
114
- this.screen = terminalScreen;
115
118
  this.totalTestCount = 0;
116
119
  this.fileDurations = /* @__PURE__ */ new Map();
117
120
  this._fatalErrors = [];
118
121
  this._failureCount = 0;
119
- this._omitFailures = options.omitFailures || false;
122
+ this.screen = options.screen ?? terminalScreen;
123
+ this._options = options;
120
124
  }
121
125
  version() {
122
126
  return "v2";
@@ -254,48 +258,51 @@ class TerminalReporter {
254
258
  epilogue(full) {
255
259
  const summary = this.generateSummary();
256
260
  const summaryMessage = this.generateSummaryMessage(summary);
257
- if (full && summary.failuresToPrint.length && !this._omitFailures)
261
+ if (full && summary.failuresToPrint.length && !this._options.omitFailures)
258
262
  this._printFailures(summary.failuresToPrint);
259
263
  this._printSlowTests();
260
264
  this._printSummary(summaryMessage);
261
265
  }
262
266
  _printFailures(failures) {
263
- console.log("");
267
+ this.writeLine("");
264
268
  failures.forEach((test, index) => {
265
- console.log(this.formatFailure(test, index + 1));
269
+ this.writeLine(this.formatFailure(test, index + 1));
266
270
  });
267
271
  }
268
272
  _printSlowTests() {
269
273
  const slowTests = this.getSlowTests();
270
274
  slowTests.forEach(([file, duration]) => {
271
- console.log(this.screen.colors.yellow(" Slow test file: ") + file + this.screen.colors.yellow(` (${(0, import_utilsBundle.ms)(duration)})`));
275
+ this.writeLine(this.screen.colors.yellow(" Slow test file: ") + file + this.screen.colors.yellow(` (${(0, import_utilsBundle.ms)(duration)})`));
272
276
  });
273
277
  if (slowTests.length)
274
- console.log(this.screen.colors.yellow(" Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel"));
278
+ this.writeLine(this.screen.colors.yellow(" Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel"));
275
279
  }
276
280
  _printSummary(summary) {
277
281
  if (summary.trim())
278
- console.log(summary);
282
+ this.writeLine(summary);
279
283
  }
280
284
  willRetry(test) {
281
285
  return test.outcome() === "unexpected" && test.results.length <= test.retries;
282
286
  }
283
- formatTestTitle(test, step, omitLocation = false) {
284
- return formatTestTitle(this.screen, this.config, test, step, omitLocation);
287
+ formatTestTitle(test, step) {
288
+ return formatTestTitle(this.screen, this.config, test, step, this._options);
285
289
  }
286
290
  formatTestHeader(test, options = {}) {
287
- return formatTestHeader(this.screen, this.config, test, options);
291
+ return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId });
288
292
  }
289
293
  formatFailure(test, index) {
290
- return formatFailure(this.screen, this.config, test, index);
294
+ return formatFailure(this.screen, this.config, test, index, this._options);
291
295
  }
292
296
  formatError(error) {
293
297
  return formatError(this.screen, error);
294
298
  }
299
+ writeLine(line) {
300
+ this.screen.stdout?.write(line ? line + "\n" : "\n");
301
+ }
295
302
  }
296
- function formatFailure(screen, config, test, index) {
303
+ function formatFailure(screen, config, test, index, options) {
297
304
  const lines = [];
298
- const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error" });
305
+ const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
299
306
  lines.push(screen.colors.red(header));
300
307
  for (const result of test.results) {
301
308
  const resultLines = [];
@@ -409,20 +416,18 @@ function stepSuffix(step) {
409
416
  const stepTitles = step ? step.titlePath() : [];
410
417
  return stepTitles.map((t) => t.split("\n")[0]).map((t) => " \u203A " + t).join("");
411
418
  }
412
- function formatTestTitle(screen, config, test, step, omitLocation = false) {
419
+ function formatTestTitle(screen, config, test, step, options = {}) {
413
420
  const [, projectName, , ...titles] = test.titlePath();
414
- let location;
415
- if (omitLocation)
416
- location = `${relativeTestPath(screen, config, test)}`;
417
- else
418
- location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
419
- const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
420
- 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 ")}`;
421
426
  const extraTags = test.tags.filter((t) => !testTitle.includes(t));
422
427
  return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
423
428
  }
424
429
  function formatTestHeader(screen, config, test, options = {}) {
425
- const title = formatTestTitle(screen, config, test);
430
+ const title = formatTestTitle(screen, config, test, void 0, options);
426
431
  const header = `${options.indent || ""}${options.index ? options.index + ") " : ""}${title}`;
427
432
  let fullHeader = header;
428
433
  if (options.mode === "error") {
@@ -29,53 +29,53 @@ class DotReporter extends import_base.TerminalReporter {
29
29
  }
30
30
  onBegin(suite) {
31
31
  super.onBegin(suite);
32
- console.log(this.generateStartingMessage());
32
+ this.writeLine(this.generateStartingMessage());
33
33
  }
34
34
  onStdOut(chunk, test, result) {
35
35
  super.onStdOut(chunk, test, result);
36
36
  if (!this.config.quiet)
37
- process.stdout.write(chunk);
37
+ this.screen.stdout.write(chunk);
38
38
  }
39
39
  onStdErr(chunk, test, result) {
40
40
  super.onStdErr(chunk, test, result);
41
41
  if (!this.config.quiet)
42
- process.stderr.write(chunk);
42
+ this.screen.stderr.write(chunk);
43
43
  }
44
44
  onTestEnd(test, result) {
45
45
  super.onTestEnd(test, result);
46
46
  if (this._counter === 80) {
47
- process.stdout.write("\n");
47
+ this.screen.stdout.write("\n");
48
48
  this._counter = 0;
49
49
  }
50
50
  ++this._counter;
51
51
  if (result.status === "skipped") {
52
- process.stdout.write(this.screen.colors.yellow("\xB0"));
52
+ this.screen.stdout.write(this.screen.colors.yellow("\xB0"));
53
53
  return;
54
54
  }
55
55
  if (this.willRetry(test)) {
56
- process.stdout.write(this.screen.colors.gray("\xD7"));
56
+ this.screen.stdout.write(this.screen.colors.gray("\xD7"));
57
57
  return;
58
58
  }
59
59
  switch (test.outcome()) {
60
60
  case "expected":
61
- process.stdout.write(this.screen.colors.green("\xB7"));
61
+ this.screen.stdout.write(this.screen.colors.green("\xB7"));
62
62
  break;
63
63
  case "unexpected":
64
- process.stdout.write(this.screen.colors.red(result.status === "timedOut" ? "T" : "F"));
64
+ this.screen.stdout.write(this.screen.colors.red(result.status === "timedOut" ? "T" : "F"));
65
65
  break;
66
66
  case "flaky":
67
- process.stdout.write(this.screen.colors.yellow("\xB1"));
67
+ this.screen.stdout.write(this.screen.colors.yellow("\xB1"));
68
68
  break;
69
69
  }
70
70
  }
71
71
  onError(error) {
72
72
  super.onError(error);
73
- console.log("\n" + this.formatError(error).message);
73
+ this.writeLine("\n" + this.formatError(error).message);
74
74
  this._counter = 0;
75
75
  }
76
76
  async onEnd(result) {
77
77
  await super.onEnd(result);
78
- process.stdout.write("\n");
78
+ this.screen.stdout.write("\n");
79
79
  this.epilogue(true);
80
80
  }
81
81
  }
@@ -41,7 +41,8 @@ class GitHubLogger {
41
41
  _log(message, type = "notice", options = {}) {
42
42
  message = message.replace(/\n/g, "%0A");
43
43
  const configs = Object.entries(options).map(([key, option]) => `${key}=${option}`).join(",");
44
- console.log((0, import_util.stripAnsiEscapes)(`::${type} ${configs}::${message}`));
44
+ process.stdout.write((0, import_util.stripAnsiEscapes)(`::${type} ${configs}::${message}
45
+ `));
45
46
  }
46
47
  debug(message, options) {
47
48
  this._log(message, "debug", options);
@@ -76,12 +76,12 @@ class HtmlReporter {
76
76
  if (reportedWarnings.has(key))
77
77
  continue;
78
78
  reportedWarnings.add(key);
79
- console.log(import_utils2.colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
80
- console.log(`
79
+ writeLine(import_utils2.colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
80
+ writeLine(`
81
81
  html reporter folder: ${import_utils2.colors.bold(outputFolder)}
82
82
  test results folder: ${import_utils2.colors.bold(project.outputDir)}`);
83
- console.log("");
84
- console.log(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss.
83
+ writeLine("");
84
+ writeLine(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss.
85
85
  `);
86
86
  }
87
87
  }
@@ -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() {
@@ -128,9 +138,9 @@ class HtmlReporter {
128
138
  const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? "" : " " + import_path.default.relative(process.cwd(), this._outputFolder);
129
139
  const hostArg = this._host ? ` --host ${this._host}` : "";
130
140
  const portArg = this._port ? ` --port ${this._port}` : "";
131
- console.log("");
132
- console.log("To open last HTML report run:");
133
- console.log(import_utils2.colors.cyan(`
141
+ writeLine("");
142
+ writeLine("To open last HTML report run:");
143
+ writeLine(import_utils2.colors.cyan(`
134
144
  ${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg}
135
145
  `));
136
146
  }
@@ -145,7 +155,7 @@ function getHtmlReportOptionProcessEnv() {
145
155
  if (!htmlOpenEnv)
146
156
  return void 0;
147
157
  if (!isHtmlReportOption(htmlOpenEnv)) {
148
- console.log(import_utils2.colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(", ")}`));
158
+ writeLine(import_utils2.colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(", ")}`));
149
159
  return void 0;
150
160
  }
151
161
  return htmlOpenEnv;
@@ -158,15 +168,15 @@ async function showHTMLReport(reportFolder, host = "localhost", port, testId) {
158
168
  try {
159
169
  (0, import_utils.assert)(import_fs.default.statSync(folder).isDirectory());
160
170
  } catch (e) {
161
- console.log(import_utils2.colors.red(`No report found at "${folder}"`));
171
+ writeLine(import_utils2.colors.red(`No report found at "${folder}"`));
162
172
  (0, import_utils.gracefullyProcessExitDoNotHang)(1);
163
173
  return;
164
174
  }
165
175
  const server = startHtmlReportServer(folder);
166
176
  await server.start({ port, host, preferredPort: port ? void 0 : 9323 });
167
177
  let url = server.urlPrefix("human-readable");
168
- console.log("");
169
- console.log(import_utils2.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
178
+ writeLine("");
179
+ writeLine(import_utils2.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
170
180
  if (testId)
171
181
  url += `#?testId=${testId}`;
172
182
  url = url.replace("0.0.0.0", "localhost");
@@ -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;
@@ -287,7 +282,8 @@ class HtmlBuilder {
287
282
  const popup = window.open(hmrURL);
288
283
  const listener = (evt) => {
289
284
  if (evt.source === popup && evt.data === "ready") {
290
- popup.postMessage(window.playwrightReportBase64, hmrURL.origin);
285
+ const element = document.getElementById("playwrightReportBase64");
286
+ popup.postMessage(element?.textContent ?? "", hmrURL.origin);
291
287
  window.removeEventListener("message", listener);
292
288
  window.close();
293
289
  }
@@ -319,24 +315,42 @@ class HtmlBuilder {
319
315
  return { ok, singleTestId };
320
316
  }
321
317
  async _writeReportData(filePath) {
322
- import_fs.default.appendFileSync(filePath, '<script>\nwindow.playwrightReportBase64 = "data:application/zip;base64,');
318
+ import_fs.default.appendFileSync(filePath, '<script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,');
323
319
  await new Promise((f) => {
324
320
  this._dataZipFile.end(void 0, () => {
325
321
  this._dataZipFile.outputStream.pipe(new Base64Encoder()).pipe(import_fs.default.createWriteStream(filePath, { flags: "a" })).on("close", f);
326
322
  });
327
323
  });
328
- import_fs.default.appendFileSync(filePath, '";</script>');
324
+ import_fs.default.appendFileSync(filePath, "</script>");
329
325
  }
330
326
  _addDataFile(fileName, data) {
331
327
  this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
332
328
  }
333
- _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) {
334
348
  const newPath = [...path2, suite.title];
335
349
  suite.entries().forEach((e) => {
336
350
  if (e.type === "test")
337
351
  outTests.push(this._createTestEntry(e, projectName, newPath));
338
- else
339
- this._processSuite(e, projectName, newPath, outTests);
352
+ else if (deep)
353
+ this._processSuite(e, projectName, newPath, deep, outTests);
340
354
  });
341
355
  }
342
356
  _createTestEntry(test, projectName, path2) {
@@ -471,7 +485,7 @@ class HtmlBuilder {
471
485
  _createTestStep(dedupedStep, result) {
472
486
  const { step, duration, count } = dedupedStep;
473
487
  const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
474
- let title = (0, import_util.stepTitle)(step.category, step.title);
488
+ let title = step.title;
475
489
  if (skipped)
476
490
  title = `${title} (skipped${skipped.description ? ": " + skipped.description : ""})`;
477
491
  const testStep = {
@@ -619,6 +633,9 @@ function createErrorCodeframe(message, location) {
619
633
  }
620
634
  );
621
635
  }
636
+ function writeLine(line) {
637
+ process.stdout.write(line + "\n");
638
+ }
622
639
  var html_default = HtmlReporter;
623
640
  // Annotate the CommonJS export names for ESM import in node:
624
641
  0 && (module.exports = {