playwright 1.56.1 → 1.57.0

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 (76) hide show
  1. package/README.md +3 -3
  2. package/ThirdPartyNotices.txt +202 -282
  3. package/lib/agents/copilot-setup-steps.yml +34 -0
  4. package/lib/agents/generateAgents.js +292 -160
  5. package/lib/agents/playwright-test-coverage.prompt.md +31 -0
  6. package/lib/agents/playwright-test-generate.prompt.md +8 -0
  7. package/lib/agents/{generator.md → playwright-test-generator.agent.md} +8 -22
  8. package/lib/agents/playwright-test-heal.prompt.md +6 -0
  9. package/lib/agents/{healer.md → playwright-test-healer.agent.md} +5 -28
  10. package/lib/agents/playwright-test-plan.prompt.md +9 -0
  11. package/lib/agents/{planner.md → playwright-test-planner.agent.md} +5 -68
  12. package/lib/common/config.js +6 -0
  13. package/lib/common/expectBundle.js +0 -9
  14. package/lib/common/expectBundleImpl.js +267 -249
  15. package/lib/common/testLoader.js +3 -2
  16. package/lib/common/testType.js +3 -12
  17. package/lib/common/validators.js +68 -0
  18. package/lib/index.js +9 -9
  19. package/lib/isomorphic/teleReceiver.js +1 -0
  20. package/lib/isomorphic/testServerConnection.js +14 -0
  21. package/lib/loader/loaderMain.js +1 -1
  22. package/lib/matchers/expect.js +12 -13
  23. package/lib/matchers/matchers.js +16 -0
  24. package/lib/mcp/browser/browserServerBackend.js +1 -1
  25. package/lib/mcp/browser/config.js +9 -24
  26. package/lib/mcp/browser/context.js +4 -21
  27. package/lib/mcp/browser/response.js +20 -11
  28. package/lib/mcp/browser/tab.js +25 -10
  29. package/lib/mcp/browser/tools/evaluate.js +2 -3
  30. package/lib/mcp/browser/tools/form.js +2 -3
  31. package/lib/mcp/browser/tools/keyboard.js +4 -5
  32. package/lib/mcp/browser/tools/pdf.js +1 -1
  33. package/lib/mcp/browser/tools/runCode.js +75 -0
  34. package/lib/mcp/browser/tools/screenshot.js +33 -15
  35. package/lib/mcp/browser/tools/snapshot.js +13 -14
  36. package/lib/mcp/browser/tools/tabs.js +2 -2
  37. package/lib/mcp/browser/tools/utils.js +0 -11
  38. package/lib/mcp/browser/tools/verify.js +3 -4
  39. package/lib/mcp/browser/tools.js +2 -0
  40. package/lib/mcp/program.js +21 -1
  41. package/lib/mcp/sdk/exports.js +1 -3
  42. package/lib/mcp/sdk/http.js +9 -2
  43. package/lib/mcp/sdk/proxyBackend.js +1 -1
  44. package/lib/mcp/sdk/server.js +13 -5
  45. package/lib/mcp/sdk/tool.js +2 -6
  46. package/lib/mcp/test/browserBackend.js +43 -33
  47. package/lib/mcp/test/generatorTools.js +3 -3
  48. package/lib/mcp/test/plannerTools.js +103 -5
  49. package/lib/mcp/test/seed.js +25 -15
  50. package/lib/mcp/test/streams.js +9 -4
  51. package/lib/mcp/test/testBackend.js +31 -29
  52. package/lib/mcp/test/testContext.js +143 -40
  53. package/lib/mcp/test/testTools.js +12 -21
  54. package/lib/plugins/webServerPlugin.js +37 -9
  55. package/lib/program.js +11 -20
  56. package/lib/reporters/html.js +2 -23
  57. package/lib/reporters/internalReporter.js +4 -2
  58. package/lib/reporters/junit.js +4 -2
  59. package/lib/reporters/list.js +1 -5
  60. package/lib/reporters/merge.js +12 -6
  61. package/lib/reporters/teleEmitter.js +3 -1
  62. package/lib/runner/dispatcher.js +26 -2
  63. package/lib/runner/failureTracker.js +5 -5
  64. package/lib/runner/loadUtils.js +2 -1
  65. package/lib/runner/loaderHost.js +1 -1
  66. package/lib/runner/reporters.js +5 -4
  67. package/lib/runner/testRunner.js +8 -9
  68. package/lib/runner/testServer.js +8 -3
  69. package/lib/runner/workerHost.js +3 -0
  70. package/lib/worker/testInfo.js +28 -17
  71. package/lib/worker/testTracing.js +1 -0
  72. package/lib/worker/workerMain.js +15 -6
  73. package/package.json +2 -2
  74. package/types/test.d.ts +96 -3
  75. package/types/testReporter.d.ts +5 -0
  76. package/lib/mcp/sdk/mdb.js +0 -208
@@ -26,6 +26,8 @@ var import_utils2 = require("playwright-core/lib/utils");
26
26
  var import_rebase = require("./rebase");
27
27
  var import_workerHost = require("./workerHost");
28
28
  var import_ipc = require("../common/ipc");
29
+ var import_internalReporter = require("../reporters/internalReporter");
30
+ var import_util = require("../util");
29
31
  class Dispatcher {
30
32
  constructor(config, reporter, failureTracker) {
31
33
  this._workerSlots = [];
@@ -70,7 +72,7 @@ class Dispatcher {
70
72
  return;
71
73
  }
72
74
  this._queue.splice(jobIndex, 1);
73
- const jobDispatcher = new JobDispatcher(job, this._reporter, this._failureTracker, () => this.stop().catch(() => {
75
+ const jobDispatcher = new JobDispatcher(job, this._config, this._reporter, this._failureTracker, () => this.stop().catch(() => {
74
76
  }));
75
77
  this._workerSlots[workerIndex].busy = true;
76
78
  this._workerSlots[workerIndex].jobDispatcher = jobDispatcher;
@@ -209,7 +211,7 @@ class Dispatcher {
209
211
  }
210
212
  }
211
213
  class JobDispatcher {
212
- constructor(job, reporter, failureTracker, stopCallback) {
214
+ constructor(job, config, reporter, failureTracker, stopCallback) {
213
215
  this.jobResult = new import_utils.ManualPromise();
214
216
  this._listeners = [];
215
217
  this._failedTests = /* @__PURE__ */ new Set();
@@ -219,6 +221,7 @@ class JobDispatcher {
219
221
  this._parallelIndex = 0;
220
222
  this._workerIndex = 0;
221
223
  this.job = job;
224
+ this._config = config;
222
225
  this._reporter = reporter;
223
226
  this._failureTracker = failureTracker;
224
227
  this._stopCallback = stopCallback;
@@ -449,10 +452,30 @@ class JobDispatcher {
449
452
  import_utils.eventsHelper.addEventListener(worker, "stepBegin", this._onStepBegin.bind(this)),
450
453
  import_utils.eventsHelper.addEventListener(worker, "stepEnd", this._onStepEnd.bind(this)),
451
454
  import_utils.eventsHelper.addEventListener(worker, "attach", this._onAttach.bind(this)),
455
+ import_utils.eventsHelper.addEventListener(worker, "testPaused", this._onTestPaused.bind(this, worker)),
452
456
  import_utils.eventsHelper.addEventListener(worker, "done", this._onDone.bind(this)),
453
457
  import_utils.eventsHelper.addEventListener(worker, "exit", this.onExit.bind(this))
454
458
  ];
455
459
  }
460
+ _onTestPaused(worker, params) {
461
+ const sendMessage = async (message) => {
462
+ try {
463
+ if (this.jobResult.isDone())
464
+ throw new Error("Test has already stopped");
465
+ const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request });
466
+ if (response.error)
467
+ (0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
468
+ return response;
469
+ } catch (e) {
470
+ const error = (0, import_util.serializeError)(e);
471
+ (0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
472
+ return { response: void 0, error };
473
+ }
474
+ };
475
+ for (const error of params.errors)
476
+ (0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
477
+ this._failureTracker.onTestPaused?.({ ...params, sendMessage });
478
+ }
456
479
  skipWholeJob() {
457
480
  const allTestsSkipped = this.job.tests.every((test) => test.expectedStatus === "skipped");
458
481
  if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) {
@@ -460,6 +483,7 @@ class JobDispatcher {
460
483
  const result = test._appendTestResult();
461
484
  this._reporter.onTestBegin?.(test, result);
462
485
  result.status = "skipped";
486
+ result.annotations = [...test.annotations];
463
487
  this._reportTestEnd(test, result);
464
488
  }
465
489
  return true;
@@ -22,13 +22,13 @@ __export(failureTracker_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(failureTracker_exports);
24
24
  class FailureTracker {
25
- constructor(_config, options) {
26
- this._config = _config;
25
+ constructor(config, options) {
27
26
  this._failureCount = 0;
28
27
  this._hasWorkerErrors = false;
29
28
  this._topLevelProjects = [];
30
- this._pauseOnError = options?.pauseOnError ?? false;
31
- this._pauseAtEnd = options?.pauseAtEnd ?? false;
29
+ this._config = config;
30
+ this._pauseOnError = !!options?.pauseOnError;
31
+ this._pauseAtEnd = !!options?.pauseAtEnd;
32
32
  }
33
33
  onRootSuite(rootSuite, topLevelProjects) {
34
34
  this._rootSuite = rootSuite;
@@ -45,7 +45,7 @@ class FailureTracker {
45
45
  return this._pauseOnError;
46
46
  }
47
47
  pauseAtEnd(inProject) {
48
- return this._pauseAtEnd && this._topLevelProjects.includes(inProject);
48
+ return this._topLevelProjects.includes(inProject) && this._pauseAtEnd;
49
49
  }
50
50
  hasReachedMaxFailures() {
51
51
  return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
@@ -159,7 +159,8 @@ async function createRootSuite(testRun, errors, shouldFilterOnly) {
159
159
  if (config.config.shard) {
160
160
  const testGroups = [];
161
161
  for (const projectSuite of rootSuite.suites) {
162
- testGroups.push(...(0, import_testGroups.createTestGroups)(projectSuite, config.config.shard.total));
162
+ for (const group of (0, import_testGroups.createTestGroups)(projectSuite, config.config.shard.total))
163
+ testGroups.push(group);
163
164
  }
164
165
  const testGroupsInThisShard = (0, import_testGroups.filterForShard)(config.config.shard, testGroups);
165
166
  const testsInThisShard = /* @__PURE__ */ new Set();
@@ -48,7 +48,7 @@ class InProcessLoaderHost {
48
48
  return true;
49
49
  }
50
50
  async loadTestFile(file, testErrors) {
51
- const result = await (0, import_testLoader.loadTestFile)(file, this._config.config.rootDir, testErrors);
51
+ const result = await (0, import_testLoader.loadTestFile)(file, this._config, testErrors);
52
52
  this._poolBuilder.buildPools(result, testErrors);
53
53
  return result;
54
54
  }
@@ -47,7 +47,7 @@ var import_line = __toESM(require("../reporters/line"));
47
47
  var import_list = __toESM(require("../reporters/list"));
48
48
  var import_listModeReporter = __toESM(require("../reporters/listModeReporter"));
49
49
  var import_reporterV2 = require("../reporters/reporterV2");
50
- async function createReporters(config, mode, isTestServer, descriptions) {
50
+ async function createReporters(config, mode, descriptions) {
51
51
  const defaultReporters = {
52
52
  blob: import_blob.BlobReporter,
53
53
  dot: mode === "list" ? import_listModeReporter.default : import_dot.default,
@@ -63,7 +63,7 @@ async function createReporters(config, mode, isTestServer, descriptions) {
63
63
  descriptions ??= config.config.reporter;
64
64
  if (config.configCLIOverrides.additionalReporters)
65
65
  descriptions = [...descriptions, ...config.configCLIOverrides.additionalReporters];
66
- const runOptions = reporterOptions(config, mode, isTestServer);
66
+ const runOptions = reporterOptions(config, mode);
67
67
  for (const r of descriptions) {
68
68
  const [name, arg] = r;
69
69
  const options = { ...runOptions, ...arg };
@@ -104,11 +104,10 @@ function createErrorCollectingReporter(screen) {
104
104
  errors: () => errors
105
105
  };
106
106
  }
107
- function reporterOptions(config, mode, isTestServer) {
107
+ function reporterOptions(config, mode) {
108
108
  return {
109
109
  configDir: config.configDir,
110
110
  _mode: mode,
111
- _isTestServer: isTestServer,
112
111
  _commandHash: computeCommandHash(config)
113
112
  };
114
113
  }
@@ -125,6 +124,8 @@ function computeCommandHash(config) {
125
124
  command.cliGrepInvert = config.cliGrepInvert;
126
125
  if (config.cliOnlyChanged)
127
126
  command.cliOnlyChanged = config.cliOnlyChanged;
127
+ if (config.config.tags.length)
128
+ command.tags = config.config.tags.join(" ");
128
129
  if (Object.keys(command).length)
129
130
  parts.push((0, import_utils.calculateSha1)(JSON.stringify(command)).substring(0, 7));
130
131
  return parts.join("-");
@@ -51,7 +51,8 @@ var import_reporters = require("./reporters");
51
51
  var import_tasks = require("./tasks");
52
52
  var import_lastRun = require("./lastRun");
53
53
  const TestRunnerEvent = {
54
- TestFilesChanged: "testFilesChanged"
54
+ TestFilesChanged: "testFilesChanged",
55
+ TestPaused: "testPaused"
55
56
  };
56
57
  class TestRunner extends import_events.default {
57
58
  constructor(configLocation, configCLIOverrides) {
@@ -93,7 +94,7 @@ class TestRunner extends import_events.default {
93
94
  }
94
95
  async installBrowsers() {
95
96
  const executables = import_server.registry.defaultExecutables();
96
- await import_server.registry.install(executables, false);
97
+ await import_server.registry.install(executables);
97
98
  }
98
99
  async loadConfig() {
99
100
  const { config, error } = await this._loadConfig(this._configCLIOverrides);
@@ -245,16 +246,13 @@ class TestRunner extends import_events.default {
245
246
  ...params.video === "on" || params.video === "off" ? { video: params.video } : {},
246
247
  ...params.headed !== void 0 ? { headless: !params.headed } : {},
247
248
  _optionContextReuseMode: params.reuseContext ? "when-possible" : void 0,
248
- _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : void 0
249
+ _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : void 0,
250
+ actionTimeout: params.actionTimeout
249
251
  },
250
252
  ...params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {},
251
253
  ...params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {},
252
254
  ...params.workers ? { workers: params.workers } : {}
253
255
  };
254
- if (params.trace === "on")
255
- process.env.PW_LIVE_TRACE_STACKS = "1";
256
- else
257
- process.env.PW_LIVE_TRACE_STACKS = void 0;
258
256
  const config = await this._loadConfigOrReportError(new import_internalReporter.InternalReporter([userReporter]), overrides);
259
257
  if (!config)
260
258
  return { status: "failed" };
@@ -269,7 +267,7 @@ class TestRunner extends import_events.default {
269
267
  const testIdSet = new Set(params.testIds);
270
268
  config.preOnlyTestFilters.push((test) => testIdSet.has(test.id));
271
269
  }
272
- const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test", true);
270
+ const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test");
273
271
  const reporter = new import_internalReporter.InternalReporter([...configReporters, userReporter]);
274
272
  const stop = new import_utils.ManualPromise();
275
273
  const tasks = [
@@ -278,6 +276,7 @@ class TestRunner extends import_events.default {
278
276
  ...(0, import_tasks.createRunTestsTasks)(config)
279
277
  ];
280
278
  const testRun = new import_tasks.TestRun(config, reporter, { pauseOnError: params.pauseOnError, pauseAtEnd: params.pauseAtEnd });
279
+ testRun.failureTracker.onTestPaused = (params2) => this.emit(TestRunnerEvent.TestPaused, params2);
281
280
  const run = (0, import_tasks.runTasks)(testRun, tasks, 0, stop).then(async (status) => {
282
281
  this._testRun = void 0;
283
282
  return status;
@@ -363,7 +362,7 @@ async function runAllTestsWithConfig(config) {
363
362
  const listOnly = config.cliListOnly;
364
363
  (0, import_gitCommitInfoPlugin.addGitCommitInfoPlugin)(config);
365
364
  (0, import_webServerPlugin.webServerPluginsForConfig)(config).forEach((p) => config.plugins.push({ factory: p }));
366
- const reporters = await (0, import_reporters.createReporters)(config, listOnly ? "list" : "test", false);
365
+ const reporters = await (0, import_reporters.createReporters)(config, listOnly ? "list" : "test");
367
366
  const lastRun = new import_lastRun.LastRunReporter(config);
368
367
  if (config.cliLastFailed)
369
368
  await lastRun.filterLastFailed();
@@ -45,6 +45,7 @@ var import_testRunner = require("./testRunner");
45
45
  const originalDebugLog = import_utilsBundle.debug.log;
46
46
  const originalStdoutWrite = process.stdout.write;
47
47
  const originalStderrWrite = process.stderr.write;
48
+ const originalStdinIsTTY = process.stdin.isTTY;
48
49
  class TestServer {
49
50
  constructor(configLocation, configCLIOverrides) {
50
51
  this._configLocation = configLocation;
@@ -74,6 +75,7 @@ class TestServerDispatcher {
74
75
  };
75
76
  this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
76
77
  this._testRunner.on(import_testRunner.TestRunnerEvent.TestFilesChanged, (testFiles) => this._dispatchEvent("testFilesChanged", { testFiles }));
78
+ this._testRunner.on(import_testRunner.TestRunnerEvent.TestPaused, (params) => this._dispatchEvent("testPaused", { errors: params.errors }));
77
79
  }
78
80
  async _wireReporter(messageSink) {
79
81
  return await (0, import_reporters.createReporterForTestServer)(this._serializer, messageSink);
@@ -110,7 +112,6 @@ class TestServerDispatcher {
110
112
  await this._testRunner.installBrowsers();
111
113
  }
112
114
  async runGlobalSetup(params) {
113
- await this.runGlobalTeardown();
114
115
  const { reporter, report } = await this._collectingReporter();
115
116
  this._globalSetupReport = report;
116
117
  const { status } = await this._testRunner.runGlobalSetup([reporter, new import_list.default()]);
@@ -151,7 +152,9 @@ class TestServerDispatcher {
151
152
  const wireReporter = await this._wireReporter((e) => this._dispatchEvent("report", e));
152
153
  const { status } = await this._testRunner.runTests(wireReporter, {
153
154
  ...params,
154
- doNotRunDepsOutsideProjectFilter: true
155
+ doNotRunDepsOutsideProjectFilter: true,
156
+ pauseAtEnd: params.pauseAtEnd,
157
+ pauseOnError: params.pauseOnError
155
158
  });
156
159
  return { status };
157
160
  }
@@ -191,17 +194,19 @@ class TestServerDispatcher {
191
194
  };
192
195
  process.stdout.write = stdoutWrite;
193
196
  process.stderr.write = stderrWrite;
197
+ process.stdin.isTTY = void 0;
194
198
  } else {
195
199
  import_utilsBundle.debug.log = originalDebugLog;
196
200
  process.stdout.write = originalStdoutWrite;
197
201
  process.stderr.write = originalStderrWrite;
202
+ process.stdin.isTTY = originalStdinIsTTY;
198
203
  }
199
204
  }
200
205
  }
201
206
  async function runUIMode(configFile, configCLIOverrides, options) {
202
207
  const configLocation = (0, import_configLoader.resolveConfigLocation)(configFile);
203
208
  return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server, cancelPromise) => {
204
- await (0, import_server.installRootRedirect)(server, [], { ...options, webApp: "uiMode.html" });
209
+ await (0, import_server.installRootRedirect)(server, void 0, { ...options, webApp: "uiMode.html" });
205
210
  if (options.host !== void 0 || options.port !== void 0) {
206
211
  await (0, import_server.openTraceInBrowser)(server.urlPrefix("human-readable"));
207
212
  } else {
@@ -79,6 +79,9 @@ class WorkerHost extends import_processHost.ProcessHost {
79
79
  runTestGroup(runPayload) {
80
80
  this.sendMessageNoReply({ method: "runTestGroup", params: runPayload });
81
81
  }
82
+ async sendCustomMessage(payload) {
83
+ return await this.sendMessage({ method: "customMessage", params: payload });
84
+ }
82
85
  hash() {
83
86
  return this._hash;
84
87
  }
@@ -43,14 +43,13 @@ var import_testTracing = require("./testTracing");
43
43
  var import_util2 = require("./util");
44
44
  var import_transform = require("../transform/transform");
45
45
  class TestInfoImpl {
46
- constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach) {
46
+ constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach, onTestPaused) {
47
47
  this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
48
48
  this._ariaSnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
49
- this._wasInterrupted = false;
49
+ this._interruptedPromise = new import_utils.ManualPromise();
50
50
  this._lastStepId = 0;
51
51
  this._steps = [];
52
52
  this._stepMap = /* @__PURE__ */ new Map();
53
- this._onDidFinishTestFunctions = [];
54
53
  this._hasNonRetriableError = false;
55
54
  this._hasUnhandledError = false;
56
55
  this._allowSkips = false;
@@ -64,6 +63,7 @@ class TestInfoImpl {
64
63
  this._onStepBegin = onStepBegin;
65
64
  this._onStepEnd = onStepEnd;
66
65
  this._onAttach = onAttach;
66
+ this._onTestPaused = onTestPaused;
67
67
  this._startTime = (0, import_utils.monotonicTime)();
68
68
  this._startWallTime = Date.now();
69
69
  this._requireFile = test?._requireFile ?? "";
@@ -107,11 +107,17 @@ class TestInfoImpl {
107
107
  return import_path.default.join(this.project.snapshotDir, relativeTestFilePath + "-snapshots");
108
108
  })();
109
109
  this._attachmentsPush = this.attachments.push.bind(this.attachments);
110
- this.attachments.push = (...attachments) => {
110
+ const attachmentsPush = (...attachments) => {
111
111
  for (const a of attachments)
112
112
  this._attach(a, this._parentStep()?.stepId);
113
113
  return this.attachments.length;
114
114
  };
115
+ Object.defineProperty(this.attachments, "push", {
116
+ value: attachmentsPush,
117
+ writable: true,
118
+ enumerable: false,
119
+ configurable: true
120
+ });
115
121
  this._tracing = new import_testTracing.TestTracing(this, workerParams.artifactsDir);
116
122
  this.skip = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("skip", location, args));
117
123
  this.fixme = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("fixme", location, args));
@@ -262,7 +268,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
262
268
  this._tracing.appendBeforeActionForStep({
263
269
  stepId,
264
270
  parentId: parentStep?.stepId,
265
- title: step.title,
271
+ title: step.shortTitle ?? step.title,
266
272
  category: step.category,
267
273
  params: step.params,
268
274
  stack: step.location ? [step.location] : [],
@@ -272,7 +278,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
272
278
  return step;
273
279
  }
274
280
  _interrupt() {
275
- this._wasInterrupted = true;
281
+ this._interruptedPromise.resolve();
276
282
  this._timeoutManager.interrupt();
277
283
  if (this.status === "passed")
278
284
  this.status = "interrupted";
@@ -314,7 +320,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
314
320
  }
315
321
  });
316
322
  } catch (error) {
317
- if (!this._wasInterrupted && error instanceof import_timeoutManager.TimeoutManagerError)
323
+ if (!this._interruptedPromise.isDone() && error instanceof import_timeoutManager.TimeoutManagerError)
318
324
  this._failWithError(error);
319
325
  throw error;
320
326
  }
@@ -329,6 +335,14 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
329
335
  _setDebugMode() {
330
336
  this._timeoutManager.setIgnoreTimeouts();
331
337
  }
338
+ async _didFinishTestFunction() {
339
+ const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure();
340
+ if (shouldPause) {
341
+ this._onTestPaused({ testId: this.testId, errors: this._isFailure() ? this.errors : [] });
342
+ await this._interruptedPromise;
343
+ }
344
+ await this._onDidFinishTestFunctionCallback?.();
345
+ }
332
346
  // ------------ TestInfo methods ------------
333
347
  async attach(name, options = {}) {
334
348
  const step = this._addStep({
@@ -337,14 +351,17 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
337
351
  });
338
352
  this._attach(
339
353
  await (0, import_util.normalizeAndSaveAttachment)(this.outputPath(), name, options),
340
- step.group ? void 0 : step.stepId
354
+ step.stepId
341
355
  );
342
356
  step.complete({});
343
357
  }
344
358
  _attach(attachment, stepId) {
345
359
  const index = this._attachmentsPush(attachment) - 1;
346
- if (stepId) {
347
- this._stepMap.get(stepId).attachmentIndices.push(index);
360
+ let step = stepId ? this._stepMap.get(stepId) : void 0;
361
+ if (!!step?.group)
362
+ step = void 0;
363
+ if (step) {
364
+ step.attachmentIndices.push(index);
348
365
  } else {
349
366
  const stepId2 = `attach@${(0, import_utils.createGuid)()}`;
350
367
  this._tracing.appendBeforeActionForStep({ stepId: stepId2, title: `Attach ${(0, import_utils.escapeWithQuotes)(attachment.name, '"')}`, category: "test.attach", stack: [] });
@@ -356,7 +373,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
356
373
  contentType: attachment.contentType,
357
374
  path: attachment.path,
358
375
  body: attachment.body?.toString("base64"),
359
- stepId
376
+ stepId: step?.stepId
360
377
  });
361
378
  }
362
379
  outputPath(...pathSegments) {
@@ -445,12 +462,6 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
445
462
  setTimeout(timeout) {
446
463
  this._timeoutManager.setTimeout(timeout);
447
464
  }
448
- _pauseOnError() {
449
- return this._workerParams.pauseOnError;
450
- }
451
- _pauseAtEnd() {
452
- return this._workerParams.pauseAtEnd;
453
- }
454
465
  }
455
466
  class TestStepInfoImpl {
456
467
  constructor(testInfo, stepId, title, parentStep) {
@@ -53,6 +53,7 @@ class TestTracing {
53
53
  type: "context-options",
54
54
  origin: "testRunner",
55
55
  browserName: "",
56
+ playwrightVersion: (0, import_utils.getPlaywrightVersion)(),
56
57
  options: {},
57
58
  platform: process.platform,
58
59
  wallTime: Date.now(),
@@ -98,6 +98,7 @@ class WorkerMain extends import_process.ProcessRunner {
98
98
  const fakeTestInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, void 0, 0, () => {
99
99
  }, () => {
100
100
  }, () => {
101
+ }, () => {
101
102
  });
102
103
  const runnable = { type: "teardown" };
103
104
  await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {
@@ -179,7 +180,7 @@ class WorkerMain extends import_process.ProcessRunner {
179
180
  let fatalUnknownTestIds;
180
181
  try {
181
182
  await this._loadIfNeeded();
182
- const fileSuite = await (0, import_testLoader.loadTestFile)(runPayload.file, this._config.config.rootDir);
183
+ const fileSuite = await (0, import_testLoader.loadTestFile)(runPayload.file, this._config);
183
184
  const suite = (0, import_suiteUtils.bindFileSuiteToProject)(this._project, fileSuite);
184
185
  if (this._params.repeatEachIndex)
185
186
  (0, import_suiteUtils.applyRepeatEachIndex)(this._project, suite, this._params.repeatEachIndex);
@@ -222,6 +223,16 @@ class WorkerMain extends import_process.ProcessRunner {
222
223
  this._runFinished.resolve();
223
224
  }
224
225
  }
226
+ async customMessage(payload) {
227
+ try {
228
+ if (this._currentTest?.testId !== payload.testId)
229
+ throw new Error("Test has already stopped");
230
+ const response = await this._currentTest._onCustomMessageCallback?.(payload.request);
231
+ return { response };
232
+ } catch (error) {
233
+ return { response: {}, error: (0, import_util2.testInfoError)(error) };
234
+ }
235
+ }
225
236
  async _runTest(test, retry, nextTest) {
226
237
  const testInfo = new import_testInfo.TestInfoImpl(
227
238
  this._config,
@@ -231,7 +242,8 @@ class WorkerMain extends import_process.ProcessRunner {
231
242
  retry,
232
243
  (stepBeginPayload) => this.dispatchEvent("stepBegin", stepBeginPayload),
233
244
  (stepEndPayload) => this.dispatchEvent("stepEnd", stepEndPayload),
234
- (attachment) => this.dispatchEvent("attach", attachment)
245
+ (attachment) => this.dispatchEvent("attach", attachment),
246
+ (testPausedPayload) => this.dispatchEvent("testPaused", testPausedPayload)
235
247
  );
236
248
  const processAnnotation = (annotation) => {
237
249
  testInfo.annotations.push(annotation);
@@ -318,10 +330,7 @@ class WorkerMain extends import_process.ProcessRunner {
318
330
  await testInfo._runAsStep({ title: "After Hooks", category: "hook" }, async () => {
319
331
  let firstAfterHooksError;
320
332
  try {
321
- await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, async () => {
322
- for (const fn of testInfo._onDidFinishTestFunctions)
323
- await fn();
324
- });
333
+ await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, () => testInfo._didFinishTestFunction());
325
334
  } catch (error) {
326
335
  firstAfterHooksError = firstAfterHooksError ?? error;
327
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright",
3
- "version": "1.56.1",
3
+ "version": "1.57.0",
4
4
  "description": "A high-level API to automate web browsers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "license": "Apache-2.0",
66
66
  "dependencies": {
67
- "playwright-core": "1.56.1"
67
+ "playwright-core": "1.57.0"
68
68
  },
69
69
  "optionalDependencies": {
70
70
  "fsevents": "2.3.2"
package/types/test.d.ts CHANGED
@@ -873,7 +873,9 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
873
873
  * - A module name like `'my-awesome-reporter'`.
874
874
  * - A relative path to the reporter like `'./reporters/my-awesome-reporter.js'`.
875
875
  *
876
- * You can pass options to the reporter in a tuple like `['json', { outputFile: './report.json' }]`.
876
+ * You can pass options to the reporter in a tuple like `['json', { outputFile: './report.json' }]`. If the property
877
+ * is not specified, Playwright uses the `'dot'` reporter when the CI environment variable is set, and the `'list'`
878
+ * reporter otherwise.
877
879
  *
878
880
  * Learn more in the [reporters guide](https://playwright.dev/docs/test-reporters).
879
881
  *
@@ -990,6 +992,31 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
990
992
  * });
991
993
  * ```
992
994
  *
995
+ * If your webserver runs on varying ports, use `wait` to capture the port:
996
+ *
997
+ * ```js
998
+ * import { defineConfig } from '@playwright/test';
999
+ *
1000
+ * export default defineConfig({
1001
+ * webServer: {
1002
+ * command: 'npm run start',
1003
+ * wait: {
1004
+ * stdout: '/Listening on port (?<my_server_port>\\d+)/'
1005
+ * },
1006
+ * },
1007
+ * });
1008
+ * ```
1009
+ *
1010
+ * ```js
1011
+ * import { test, expect } from '@playwright/test';
1012
+ *
1013
+ * test.use({ baseUrl: `http://localhost:${process.env.MY_SERVER_PORT ?? 3000}` });
1014
+ *
1015
+ * test('homepage', async ({ page }) => {
1016
+ * await page.goto('/');
1017
+ * });
1018
+ * ```
1019
+ *
993
1020
  */
994
1021
  webServer?: TestConfigWebServer | TestConfigWebServer[];
995
1022
  /**
@@ -1758,6 +1785,26 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
1758
1785
  */
1759
1786
  snapshotPathTemplate?: string;
1760
1787
 
1788
+ /**
1789
+ * Tag or tags prepended to each test in the report. Useful for tagging your test run to differentiate between
1790
+ * [CI environments](https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-environments).
1791
+ *
1792
+ * Note that each tag must start with `@` symbol. Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests).
1793
+ *
1794
+ * **Usage**
1795
+ *
1796
+ * ```js
1797
+ * // playwright.config.ts
1798
+ * import { defineConfig } from '@playwright/test';
1799
+ *
1800
+ * export default defineConfig({
1801
+ * tag: process.env.CI_ENVIRONMENT_NAME, // for example "@APIv2"
1802
+ * });
1803
+ * ```
1804
+ *
1805
+ */
1806
+ tag?: string|Array<string>;
1807
+
1761
1808
  /**
1762
1809
  * Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
1763
1810
  *
@@ -2035,6 +2082,11 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
2035
2082
  current: number;
2036
2083
  };
2037
2084
 
2085
+ /**
2086
+ * Resolved global tags. See [testConfig.tag](https://playwright.dev/docs/api/class-testconfig#test-config-tag).
2087
+ */
2088
+ tags: Array<string>;
2089
+
2038
2090
  /**
2039
2091
  * See [testConfig.updateSnapshots](https://playwright.dev/docs/api/class-testconfig#test-config-update-snapshots).
2040
2092
  */
@@ -6180,7 +6232,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
6180
6232
  * const name = this.constructor.name + '.' + (context.name as string);
6181
6233
  * return test.step(name, async () => {
6182
6234
  * return await target.call(this, ...args);
6183
- * });
6235
+ * }, { box: true });
6184
6236
  * };
6185
6237
  * }
6186
6238
  *
@@ -6339,7 +6391,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
6339
6391
  * const name = this.constructor.name + '.' + (context.name as string);
6340
6392
  * return test.step(name, async () => {
6341
6393
  * return await target.call(this, ...args);
6342
- * });
6394
+ * }, { box: true });
6343
6395
  * };
6344
6396
  * }
6345
6397
  *
@@ -7703,6 +7755,27 @@ interface AsymmetricMatchers {
7703
7755
  * @param expected Expected array that is a subset of the received value.
7704
7756
  */
7705
7757
  arrayContaining(sample: Array<unknown>): AsymmetricMatcher;
7758
+ /**
7759
+ * `expect.arrayOf()` matches array of objects created from the
7760
+ * [`constructor`](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-of-option-constructor)
7761
+ * or a corresponding primitive type. Use it inside
7762
+ * [expect(value).toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal)
7763
+ * to perform pattern matching.
7764
+ *
7765
+ * **Usage**
7766
+ *
7767
+ * ```js
7768
+ * // Match instance of a class.
7769
+ * class Example {}
7770
+ * expect([new Example(), new Example()]).toEqual(expect.arrayOf(Example));
7771
+ *
7772
+ * // Match any string.
7773
+ * expect(['a', 'b', 'c']).toEqual(expect.arrayOf(String));
7774
+ * ```
7775
+ *
7776
+ * @param constructor Constructor of the expected object like `ExampleClass`, or a primitive boxed type like `Number`.
7777
+ */
7778
+ arrayOf(sample: unknown): AsymmetricMatcher;
7706
7779
  /**
7707
7780
  * Compares floating point numbers for approximate equality. Use this method inside
7708
7781
  * [expect(value).toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal)
@@ -8086,6 +8159,7 @@ interface GenericAssertions<R> {
8086
8159
  * - [expect(value).any(constructor)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-any)
8087
8160
  * - [expect(value).anything()](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-anything)
8088
8161
  * - [expect(value).arrayContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-containing)
8162
+ * - [expect(value).arrayOf(constructor)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-of)
8089
8163
  * - [expect(value).closeTo(expected[, numDigits])](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-close-to)
8090
8164
  * - [expect(value).objectContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-object-containing)
8091
8165
  * - [expect(value).stringContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-string-containing)
@@ -10145,6 +10219,25 @@ interface TestConfigWebServer {
10145
10219
  */
10146
10220
  stdout?: "pipe"|"ignore";
10147
10221
 
10222
+ /**
10223
+ * Consider command started only when given output has been produced.
10224
+ */
10225
+ wait?: {
10226
+ /**
10227
+ * Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the
10228
+ * environment, for example `/Listening on port (?<my_server_port>\\d+)/` will store the port number in
10229
+ * `process.env['MY_SERVER_PORT']`.
10230
+ */
10231
+ stdout?: RegExp;
10232
+
10233
+ /**
10234
+ * Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the
10235
+ * environment, for example `/Listening on port (?<my_server_port>\\d+)/` will store the port number in
10236
+ * `process.env['MY_SERVER_PORT']`.
10237
+ */
10238
+ stderr?: RegExp;
10239
+ };
10240
+
10148
10241
  /**
10149
10242
  * How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
10150
10243
  */