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
@@ -42,12 +42,13 @@ var import_transform = require("../transform/transform");
42
42
  var import_util2 = require("../util");
43
43
  const defaultTimeout = 3e4;
44
44
  const cachedFileSuites = /* @__PURE__ */ new Map();
45
- async function loadTestFile(file, rootDir, testErrors) {
45
+ async function loadTestFile(file, config, testErrors) {
46
46
  if (cachedFileSuites.has(file))
47
47
  return cachedFileSuites.get(file);
48
- const suite = new import_test.Suite(import_path.default.relative(rootDir, file) || import_path.default.basename(file), "file");
48
+ const suite = new import_test.Suite(import_path.default.relative(config.config.rootDir, file) || import_path.default.basename(file), "file");
49
49
  suite._requireFile = file;
50
50
  suite.location = { file, line: 0, column: 0 };
51
+ suite._tags = [...config.config.tags];
51
52
  (0, import_globals.setCurrentlyLoadingFileSuite)(suite);
52
53
  if (!(0, import_globals.isWorkerProcess)()) {
53
54
  (0, import_compilationCache.startCollectingFileDeps)();
@@ -29,6 +29,7 @@ var import_globals = require("./globals");
29
29
  var import_test = require("./test");
30
30
  var import_expect = require("../matchers/expect");
31
31
  var import_transform = require("../transform/transform");
32
+ var import_validators = require("./validators");
32
33
  const testTypeSymbol = Symbol("testType");
33
34
  class TestTypeImpl {
34
35
  constructor(fixtures) {
@@ -96,7 +97,7 @@ class TestTypeImpl {
96
97
  body = fn;
97
98
  details = fnOrDetails;
98
99
  }
99
- const validatedDetails = validateTestDetails(details, location);
100
+ const validatedDetails = (0, import_validators.validateTestDetails)(details, location);
100
101
  const test = new import_test.TestCase(title, body, this, location);
101
102
  test._requireFile = suite._requireFile;
102
103
  test.annotations.push(...validatedDetails.annotations);
@@ -130,7 +131,7 @@ class TestTypeImpl {
130
131
  details = fnOrDetails;
131
132
  body = fn;
132
133
  }
133
- const validatedDetails = validateTestDetails(details, location);
134
+ const validatedDetails = (0, import_validators.validateTestDetails)(details, location);
134
135
  const child = new import_test.Suite(title, "describe");
135
136
  child._requireFile = suite._requireFile;
136
137
  child.location = location;
@@ -276,16 +277,6 @@ See https://playwright.dev/docs/intro for more information about Playwright Test
276
277
  );
277
278
  }
278
279
  }
279
- function validateTestDetails(details, location) {
280
- const originalAnnotations = Array.isArray(details.annotation) ? details.annotation : details.annotation ? [details.annotation] : [];
281
- const annotations = originalAnnotations.map((annotation) => ({ ...annotation, location }));
282
- const tags = Array.isArray(details.tag) ? details.tag : details.tag ? [details.tag] : [];
283
- for (const tag of tags) {
284
- if (tag[0] !== "@")
285
- throw new Error(`Tag must start with "@" symbol, got "${tag}" instead.`);
286
- }
287
- return { annotations, tags };
288
- }
289
280
  const rootTestType = new TestTypeImpl([]);
290
281
  function mergeTests(...tests) {
291
282
  let result = rootTestType;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var validators_exports = {};
20
+ __export(validators_exports, {
21
+ validateTestAnnotation: () => validateTestAnnotation,
22
+ validateTestDetails: () => validateTestDetails
23
+ });
24
+ module.exports = __toCommonJS(validators_exports);
25
+ var import_utilsBundle = require("playwright-core/lib/utilsBundle");
26
+ const testAnnotationSchema = import_utilsBundle.zod.object({
27
+ type: import_utilsBundle.zod.string(),
28
+ description: import_utilsBundle.zod.string().optional()
29
+ });
30
+ const testDetailsSchema = import_utilsBundle.zod.object({
31
+ tag: import_utilsBundle.zod.union([
32
+ import_utilsBundle.zod.string().optional(),
33
+ import_utilsBundle.zod.array(import_utilsBundle.zod.string())
34
+ ]).transform((val) => Array.isArray(val) ? val : val !== void 0 ? [val] : []).refine((val) => val.every((v) => v.startsWith("@")), {
35
+ message: "Tag must start with '@'"
36
+ }),
37
+ annotation: import_utilsBundle.zod.union([
38
+ testAnnotationSchema,
39
+ import_utilsBundle.zod.array(testAnnotationSchema).optional()
40
+ ]).transform((val) => Array.isArray(val) ? val : val !== void 0 ? [val] : [])
41
+ });
42
+ function validateTestAnnotation(annotation) {
43
+ try {
44
+ return testAnnotationSchema.parse(annotation);
45
+ } catch (error) {
46
+ throwZodError(error);
47
+ }
48
+ }
49
+ function validateTestDetails(details, location) {
50
+ try {
51
+ const parsedDetails = testDetailsSchema.parse(details);
52
+ return {
53
+ annotations: parsedDetails.annotation.map((a) => ({ ...a, location })),
54
+ tags: parsedDetails.tag,
55
+ location
56
+ };
57
+ } catch (error) {
58
+ throwZodError(error);
59
+ }
60
+ }
61
+ function throwZodError(error) {
62
+ throw new Error(error.issues.map((i) => i.message).join("\n"));
63
+ }
64
+ // Annotate the CommonJS export names for ESM import in node:
65
+ 0 && (module.exports = {
66
+ validateTestAnnotation,
67
+ validateTestDetails
68
+ });
package/lib/index.js CHANGED
@@ -89,8 +89,8 @@ const playwrightFixtures = {
89
89
  await use(options);
90
90
  playwright._defaultLaunchOptions = void 0;
91
91
  }, { scope: "worker", auto: true, box: true }],
92
- browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use, testInfo) => {
93
- if (!["chromium", "firefox", "webkit", "_bidiChromium", "_bidiFirefox"].includes(browserName))
92
+ browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use) => {
93
+ if (!["chromium", "firefox", "webkit"].includes(browserName))
94
94
  throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
95
95
  if (connectOptions) {
96
96
  const browser2 = await playwright[browserName].connect({
@@ -220,7 +220,7 @@ const playwrightFixtures = {
220
220
  if ((0, import_utils.debugMode)() === "inspector")
221
221
  testInfo._setDebugMode();
222
222
  playwright._defaultContextOptions = _combinedContextOptions;
223
- playwright._defaultContextTimeout = testInfo._pauseOnError() ? 5e3 : actionTimeout || 0;
223
+ playwright._defaultContextTimeout = actionTimeout || 0;
224
224
  playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
225
225
  await use();
226
226
  playwright._defaultContextOptions = void 0;
@@ -242,8 +242,8 @@ const playwrightFixtures = {
242
242
  if (zone && zone.category === "expect" && isExpectCall) {
243
243
  if (zone.apiName)
244
244
  data.apiName = zone.apiName;
245
- if (zone.title)
246
- data.title = zone.title;
245
+ if (zone.shortTitle || zone.title)
246
+ data.title = zone.shortTitle ?? zone.title;
247
247
  data.stepId = zone.stepId;
248
248
  return;
249
249
  }
@@ -380,13 +380,13 @@ const playwrightFixtures = {
380
380
  attachConnectedHeaderIfNeeded(testInfo, browserImpl);
381
381
  if (!_reuseContext) {
382
382
  const { context: context2, close } = await _contextFactory();
383
- testInfo._onDidFinishTestFunctions.unshift(() => (0, import_browserBackend.runBrowserBackendAtEnd)(context2, testInfo.errors[0]?.message));
383
+ testInfo._onCustomMessageCallback = (0, import_browserBackend.createCustomMessageHandler)(testInfo, context2);
384
384
  await use(context2);
385
385
  await close();
386
386
  return;
387
387
  }
388
388
  const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
389
- testInfo._onDidFinishTestFunctions.unshift(() => (0, import_browserBackend.runBrowserBackendAtEnd)(context, testInfo.errors[0]?.message));
389
+ testInfo._onCustomMessageCallback = (0, import_browserBackend.createCustomMessageHandler)(testInfo, context);
390
390
  await use(context);
391
391
  const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
392
392
  await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
@@ -560,7 +560,7 @@ class ArtifactsRecorder {
560
560
  }
561
561
  async willStartTest(testInfo) {
562
562
  this._testInfo = testInfo;
563
- testInfo._onDidFinishTestFunctions.push(() => this.didFinishTestFunction());
563
+ testInfo._onDidFinishTestFunctionCallback = () => this.didFinishTestFunction();
564
564
  this._screenshotRecorder.fixOrdinal();
565
565
  await Promise.all(this._playwright._allContexts().map((context) => this.didCreateBrowserContext(context)));
566
566
  const existingApiRequests = Array.from(this._playwright.request._contexts);
@@ -586,7 +586,7 @@ class ArtifactsRecorder {
586
586
  return;
587
587
  try {
588
588
  await page._wrapApiCall(async () => {
589
- this._pageSnapshot = await page._snapshotForAI({ timeout: 5e3 });
589
+ this._pageSnapshot = (await page._snapshotForAI({ timeout: 5e3 })).full;
590
590
  }, { internal: true });
591
591
  } catch {
592
592
  }
@@ -445,6 +445,7 @@ const baseFullConfig = {
445
445
  rootDir: "",
446
446
  quiet: false,
447
447
  shard: null,
448
+ tags: [],
448
449
  updateSnapshots: "missing",
449
450
  updateSourceMethod: "patch",
450
451
  version: "",
@@ -29,10 +29,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var testServerConnection_exports = {};
30
30
  __export(testServerConnection_exports, {
31
31
  TestServerConnection: () => TestServerConnection,
32
+ TestServerConnectionClosedError: () => TestServerConnectionClosedError,
32
33
  WebSocketTestServerTransport: () => WebSocketTestServerTransport
33
34
  });
34
35
  module.exports = __toCommonJS(testServerConnection_exports);
35
36
  var events = __toESM(require("./events"));
37
+ class TestServerConnectionClosedError extends Error {
38
+ constructor() {
39
+ super("Test server connection closed");
40
+ }
41
+ }
36
42
  class WebSocketTestServerTransport {
37
43
  constructor(url) {
38
44
  this._ws = new WebSocket(url);
@@ -63,6 +69,7 @@ class TestServerConnection {
63
69
  this._onStdioEmitter = new events.EventEmitter();
64
70
  this._onTestFilesChangedEmitter = new events.EventEmitter();
65
71
  this._onLoadTraceRequestedEmitter = new events.EventEmitter();
72
+ this._onTestPausedEmitter = new events.EventEmitter();
66
73
  this._lastId = 0;
67
74
  this._callbacks = /* @__PURE__ */ new Map();
68
75
  this._isClosed = false;
@@ -71,6 +78,7 @@ class TestServerConnection {
71
78
  this.onStdio = this._onStdioEmitter.event;
72
79
  this.onTestFilesChanged = this._onTestFilesChangedEmitter.event;
73
80
  this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event;
81
+ this.onTestPaused = this._onTestPausedEmitter.event;
74
82
  this._transport = transport;
75
83
  this._transport.onmessage((data) => {
76
84
  const message = JSON.parse(data);
@@ -98,6 +106,9 @@ class TestServerConnection {
98
106
  this._isClosed = true;
99
107
  this._onCloseEmitter.fire();
100
108
  clearInterval(pingInterval);
109
+ for (const callback of this._callbacks.values())
110
+ callback.reject(new TestServerConnectionClosedError());
111
+ this._callbacks.clear();
101
112
  });
102
113
  }
103
114
  isClosed() {
@@ -127,6 +138,8 @@ class TestServerConnection {
127
138
  this._onTestFilesChangedEmitter.fire(params);
128
139
  else if (method === "loadTraceRequested")
129
140
  this._onLoadTraceRequestedEmitter.fire(params);
141
+ else if (method === "testPaused")
142
+ this._onTestPausedEmitter.fire(params);
130
143
  }
131
144
  async initialize(params) {
132
145
  await this._sendMessage("initialize", params);
@@ -207,5 +220,6 @@ class TestServerConnection {
207
220
  // Annotate the CommonJS export names for ESM import in node:
208
221
  0 && (module.exports = {
209
222
  TestServerConnection,
223
+ TestServerConnectionClosedError,
210
224
  WebSocketTestServerTransport
211
225
  });
@@ -42,7 +42,7 @@ class LoaderMain extends import_process.ProcessRunner {
42
42
  async loadTestFile(params) {
43
43
  const testErrors = [];
44
44
  const config = await this._config();
45
- const fileSuite = await (0, import_testLoader.loadTestFile)(params.file, config.config.rootDir, testErrors);
45
+ const fileSuite = await (0, import_testLoader.loadTestFile)(params.file, config, testErrors);
46
46
  this._poolBuilder.buildPools(fileSuite);
47
47
  return { fileSuite: fileSuite._deepSerialize(), testErrors };
48
48
  }
@@ -43,7 +43,7 @@ const printReceivedStringContainExpectedResult = (received, result) => result ==
43
43
  result[0].length
44
44
  );
45
45
  function createMatchers(actual, info, prefix) {
46
- return new Proxy((0, import_expectBundle.expect)(actual), new ExpectMetaInfoProxyHandler(info, prefix));
46
+ return new Proxy((0, import_expectBundle.expect)(actual), new ExpectMetaInfoProxyHandler(actual, info, prefix));
47
47
  }
48
48
  const userMatchersSymbol = Symbol("userMatchers");
49
49
  function qualifiedMatcherName(qualifier, matcherName) {
@@ -185,11 +185,14 @@ const customMatchers = {
185
185
  toMatchSnapshot: import_toMatchSnapshot.toMatchSnapshot
186
186
  };
187
187
  class ExpectMetaInfoProxyHandler {
188
- constructor(info, prefix) {
188
+ constructor(actual, info, prefix) {
189
+ this._actual = actual;
189
190
  this._info = { ...info };
190
191
  this._prefix = prefix;
191
192
  }
192
193
  get(target, matcherName, receiver) {
194
+ if (matcherName === "toThrowError")
195
+ matcherName = "toThrow";
193
196
  let matcher = Reflect.get(target, matcherName, receiver);
194
197
  if (typeof matcherName !== "string")
195
198
  return matcher;
@@ -220,15 +223,17 @@ class ExpectMetaInfoProxyHandler {
220
223
  if (!testInfo)
221
224
  return matcher.call(target, ...args);
222
225
  const customMessage = this._info.message || "";
223
- const argsSuffix = computeArgsSuffix(matcherName, args);
224
- const defaultTitle = `${this._info.poll ? "poll " : ""}${this._info.isSoft ? "soft " : ""}${this._info.isNot ? "not " : ""}${matcherName}${argsSuffix}`;
225
- const title = customMessage || `Expect ${(0, import_utils.escapeWithQuotes)(defaultTitle, '"')}`;
226
- const apiName = `expect${this._info.poll ? ".poll " : ""}${this._info.isSoft ? ".soft " : ""}${this._info.isNot ? ".not" : ""}.${matcherName}${argsSuffix}`;
226
+ const suffixes = (0, import_matchers.computeMatcherTitleSuffix)(matcherName, this._actual, args);
227
+ const defaultTitle = `${this._info.poll ? "poll " : ""}${this._info.isSoft ? "soft " : ""}${this._info.isNot ? "not " : ""}${matcherName}${suffixes.short || ""}`;
228
+ const shortTitle = customMessage || `Expect ${(0, import_utils.escapeWithQuotes)(defaultTitle, '"')}`;
229
+ const longTitle = shortTitle + (suffixes.long || "");
230
+ const apiName = `expect${this._info.poll ? ".poll " : ""}${this._info.isSoft ? ".soft " : ""}${this._info.isNot ? ".not" : ""}.${matcherName}${suffixes.short || ""}`;
227
231
  const stackFrames = (0, import_util.filteredStackTrace)((0, import_utils.captureRawStack)());
228
232
  const stepInfo = {
229
233
  category: "expect",
230
234
  apiName,
231
- title,
235
+ title: longTitle,
236
+ shortTitle,
232
237
  params: args[0] ? { expected: args[0] } : void 0,
233
238
  infectParentStepsWithError: this._info.isSoft
234
239
  };
@@ -299,12 +304,6 @@ async function pollMatcher(qualifiedMatcherName2, info, prefix, ...args) {
299
304
  throw new Error(message);
300
305
  }
301
306
  }
302
- function computeArgsSuffix(matcherName, args) {
303
- let value = "";
304
- if (matcherName === "toHaveScreenshot")
305
- value = (0, import_toMatchSnapshot.toHaveScreenshotStepTitle)(...args);
306
- return value ? `(${value})` : "";
307
- }
308
307
  const expect = createExpect({}, [], {}).extend(customMatchers);
309
308
  function mergeExpects(...expects) {
310
309
  let merged = expect;
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var matchers_exports = {};
20
20
  __export(matchers_exports, {
21
+ computeMatcherTitleSuffix: () => computeMatcherTitleSuffix,
21
22
  toBeAttached: () => toBeAttached,
22
23
  toBeChecked: () => toBeChecked,
23
24
  toBeDisabled: () => toBeDisabled,
@@ -56,6 +57,7 @@ var import_toBeTruthy = require("./toBeTruthy");
56
57
  var import_toEqual = require("./toEqual");
57
58
  var import_toHaveURL = require("./toHaveURL");
58
59
  var import_toMatchText = require("./toMatchText");
60
+ var import_toMatchSnapshot = require("./toMatchSnapshot");
59
61
  var import_config = require("../common/config");
60
62
  var import_globals = require("../common/globals");
61
63
  var import_testInfo = require("../worker/testInfo");
@@ -332,8 +334,22 @@ async function toPass(callback, options = {}) {
332
334
  }
333
335
  return { pass: !this.isNot, message: () => "" };
334
336
  }
337
+ function computeMatcherTitleSuffix(matcherName, receiver, args) {
338
+ if (matcherName === "toHaveScreenshot") {
339
+ const title = (0, import_toMatchSnapshot.toHaveScreenshotStepTitle)(...args);
340
+ return { short: title ? `(${title})` : "" };
341
+ }
342
+ if (receiver && typeof receiver === "object" && receiver.constructor?.name === "Locator") {
343
+ try {
344
+ return { long: " " + (0, import_utils.asLocatorDescription)("javascript", receiver._selector) };
345
+ } catch {
346
+ }
347
+ }
348
+ return {};
349
+ }
335
350
  // Annotate the CommonJS export names for ESM import in node:
336
351
  0 && (module.exports = {
352
+ computeMatcherTitleSuffix,
337
353
  toBeAttached,
338
354
  toBeChecked,
339
355
  toBeDisabled,
@@ -33,7 +33,7 @@ class BrowserServerBackend {
33
33
  this._browserContextFactory = factory;
34
34
  this._tools = (0, import_tools.filteredTools)(config);
35
35
  }
36
- async initialize(server, clientInfo) {
36
+ async initialize(clientInfo) {
37
37
  this._sessionLog = this._config.saveSession ? await import_sessionLog.SessionLog.create(this._config, clientInfo) : void 0;
38
38
  this._context = new import_context.Context({
39
39
  config: this._config,
@@ -38,8 +38,7 @@ __export(config_exports, {
38
38
  outputFile: () => outputFile,
39
39
  resolutionParser: () => resolutionParser,
40
40
  resolveCLIConfig: () => resolveCLIConfig,
41
- resolveConfig: () => resolveConfig,
42
- semicolonSeparatedList: () => semicolonSeparatedList
41
+ resolveConfig: () => resolveConfig
43
42
  });
44
43
  module.exports = __toCommonJS(config_exports);
45
44
  var import_fs = __toESM(require("fs"));
@@ -61,10 +60,6 @@ const defaultConfig = {
61
60
  viewport: null
62
61
  }
63
62
  },
64
- network: {
65
- allowedOrigins: void 0,
66
- blockedOrigins: void 0
67
- },
68
63
  server: {},
69
64
  saveTrace: false,
70
65
  timeouts: {
@@ -164,6 +159,7 @@ function configFromCLIOptions(cliOptions) {
164
159
  contextOptions,
165
160
  cdpEndpoint: cliOptions.cdpEndpoint,
166
161
  cdpHeaders: cliOptions.cdpHeader,
162
+ initPage: cliOptions.initPage,
167
163
  initScript: cliOptions.initScript
168
164
  },
169
165
  server: {
@@ -172,10 +168,6 @@ function configFromCLIOptions(cliOptions) {
172
168
  allowedHosts: cliOptions.allowedHosts
173
169
  },
174
170
  capabilities: cliOptions.caps,
175
- network: {
176
- allowedOrigins: cliOptions.allowedOrigins,
177
- blockedOrigins: cliOptions.blockedOrigins
178
- },
179
171
  saveSession: cliOptions.saveSession,
180
172
  saveTrace: cliOptions.saveTrace,
181
173
  saveVideo: cliOptions.saveVideo,
@@ -183,6 +175,7 @@ function configFromCLIOptions(cliOptions) {
183
175
  sharedBrowserContext: cliOptions.sharedBrowserContext,
184
176
  outputDir: cliOptions.outputDir,
185
177
  imageResponses: cliOptions.imageResponses,
178
+ testIdAttribute: cliOptions.testIdAttribute,
186
179
  timeouts: {
187
180
  action: cliOptions.timeoutAction,
188
181
  navigation: cliOptions.timeoutNavigation
@@ -193,8 +186,6 @@ function configFromCLIOptions(cliOptions) {
193
186
  function configFromEnv() {
194
187
  const options = {};
195
188
  options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
196
- options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
197
- options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
198
189
  options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
199
190
  options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
200
191
  options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
@@ -207,6 +198,9 @@ function configFromEnv() {
207
198
  options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
208
199
  options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
209
200
  options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
201
+ const initPage = envToString(process.env.PLAYWRIGHT_MCP_INIT_PAGE);
202
+ if (initPage)
203
+ options.initPage = [initPage];
210
204
  const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
211
205
  if (initScript)
212
206
  options.initScript = [initScript];
@@ -222,6 +216,7 @@ function configFromEnv() {
222
216
  options.saveVideo = resolutionParser("--save-video", process.env.PLAYWRIGHT_MCP_SAVE_VIDEO);
223
217
  options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE);
224
218
  options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
219
+ options.testIdAttribute = envToString(process.env.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
225
220
  options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
226
221
  options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
227
222
  options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
@@ -258,7 +253,7 @@ async function resolveFile(config, clientInfo, fileName, options) {
258
253
  fileName = fileName.split("\\").join("/");
259
254
  const resolvedFile = import_path.default.resolve(dir, fileName);
260
255
  if (!resolvedFile.startsWith(import_path.default.resolve(dir) + import_path.default.sep))
261
- throw new Error(`Resolved file path for ${fileName} is outside of the output directory`);
256
+ throw new Error(`Resolved file path ${resolvedFile} is outside of the output directory ${dir}. Use relative file names to stay within the output directory.`);
262
257
  return resolvedFile;
263
258
  }
264
259
  return import_path.default.join(dir, sanitizeForFilePath(fileName));
@@ -290,10 +285,6 @@ function mergeConfig(base, overrides) {
290
285
  ...pickDefined(base),
291
286
  ...pickDefined(overrides),
292
287
  browser,
293
- network: {
294
- ...pickDefined(base.network),
295
- ...pickDefined(overrides.network)
296
- },
297
288
  server: {
298
289
  ...pickDefined(base.server),
299
290
  ...pickDefined(overrides.server)
@@ -304,11 +295,6 @@ function mergeConfig(base, overrides) {
304
295
  }
305
296
  };
306
297
  }
307
- function semicolonSeparatedList(value) {
308
- if (!value)
309
- return void 0;
310
- return value.split(";").map((v) => v.trim());
311
- }
312
298
  function commaSeparatedList(value) {
313
299
  if (!value)
314
300
  return void 0;
@@ -378,6 +364,5 @@ function sanitizeForFilePath(s) {
378
364
  outputFile,
379
365
  resolutionParser,
380
366
  resolveCLIConfig,
381
- resolveConfig,
382
- semicolonSeparatedList
367
+ resolveConfig
383
368
  });
@@ -35,6 +35,7 @@ module.exports = __toCommonJS(context_exports);
35
35
  var import_fs = __toESM(require("fs"));
36
36
  var import_path = __toESM(require("path"));
37
37
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
38
+ var import_playwright_core = require("playwright-core");
38
39
  var import_log = require("../log");
39
40
  var import_tab = require("./tab");
40
41
  var import_config = require("./config");
@@ -74,6 +75,7 @@ class Context {
74
75
  const { browserContext } = await this._ensureBrowserContext();
75
76
  const page = await browserContext.newPage();
76
77
  this._currentTab = this._tabs.find((t) => t.page === page);
78
+ await this._currentTab.initializedPromise;
77
79
  return this._currentTab;
78
80
  }
79
81
  async selectTab(index) {
@@ -167,17 +169,6 @@ class Context {
167
169
  await this.closeBrowserContext();
168
170
  Context._allContexts.delete(this);
169
171
  }
170
- async _setupRequestInterception(context) {
171
- if (this.config.network?.allowedOrigins?.length) {
172
- await context.route("**", (route) => route.abort("blockedbyclient"));
173
- for (const origin of this.config.network.allowedOrigins)
174
- await context.route(originOrHostGlob(origin), (route) => route.continue());
175
- }
176
- if (this.config.network?.blockedOrigins?.length) {
177
- for (const origin of this.config.network.blockedOrigins)
178
- await context.route(originOrHostGlob(origin), (route) => route.abort("blockedbyclient"));
179
- }
180
- }
181
172
  async ensureBrowserContext() {
182
173
  const { browserContext } = await this._ensureBrowserContext();
183
174
  return browserContext;
@@ -194,9 +185,10 @@ class Context {
194
185
  async _setupBrowserContext() {
195
186
  if (this._closeBrowserContextPromise)
196
187
  throw new Error("Another browser context is being closed.");
188
+ if (this.config.testIdAttribute)
189
+ import_playwright_core.selectors.setTestIdAttribute(this.config.testIdAttribute);
197
190
  const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
198
191
  const { browserContext } = result;
199
- await this._setupRequestInterception(browserContext);
200
192
  if (this.sessionLog)
201
193
  await InputRecorder.create(this, browserContext);
202
194
  for (const page of browserContext.pages())
@@ -221,15 +213,6 @@ class Context {
221
213
  };
222
214
  }
223
215
  }
224
- function originOrHostGlob(originOrHost) {
225
- try {
226
- const url = new URL(originOrHost);
227
- if (url.origin !== "null")
228
- return `${url.origin}/**`;
229
- } catch {
230
- }
231
- return `*://${originOrHost}/**`;
232
- }
233
216
  class InputRecorder {
234
217
  constructor(context, browserContext) {
235
218
  this._context = context;
@@ -31,7 +31,7 @@ class Response {
31
31
  this._result = [];
32
32
  this._code = [];
33
33
  this._images = [];
34
- this._includeSnapshot = false;
34
+ this._includeSnapshot = "none";
35
35
  this._includeTabs = false;
36
36
  this._context = context;
37
37
  this.toolName = toolName;
@@ -62,14 +62,14 @@ class Response {
62
62
  images() {
63
63
  return this._images;
64
64
  }
65
- setIncludeSnapshot() {
66
- this._includeSnapshot = true;
65
+ setIncludeSnapshot(full) {
66
+ this._includeSnapshot = full ?? "incremental";
67
67
  }
68
68
  setIncludeTabs() {
69
69
  this._includeTabs = true;
70
70
  }
71
71
  async finish() {
72
- if (this._includeSnapshot && this._context.currentTab())
72
+ if (this._includeSnapshot !== "none" && this._context.currentTab())
73
73
  this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
74
74
  for (const tab of this._context.tabs())
75
75
  await tab.updateTitle();
@@ -99,13 +99,14 @@ ${this._code.join("\n")}
99
99
  \`\`\``);
100
100
  response.push("");
101
101
  }
102
- if (this._includeSnapshot || this._includeTabs)
102
+ if (this._includeSnapshot !== "none" || this._includeTabs)
103
103
  response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
104
104
  if (this._tabSnapshot?.modalStates.length) {
105
105
  response.push(...(0, import_tab.renderModalStates)(this._context, this._tabSnapshot.modalStates));
106
106
  response.push("");
107
107
  } else if (this._tabSnapshot) {
108
- response.push(renderTabSnapshot(this._tabSnapshot, options));
108
+ const includeSnapshot = options.omitSnapshot ? "none" : this._includeSnapshot;
109
+ response.push(renderTabSnapshot(this._tabSnapshot, includeSnapshot));
109
110
  response.push("");
110
111
  }
111
112
  const content = [
@@ -129,7 +130,7 @@ ${this._code.join("\n")}
129
130
  }
130
131
  }
131
132
  }
132
- function renderTabSnapshot(tabSnapshot, options = {}) {
133
+ function renderTabSnapshot(tabSnapshot, includeSnapshot) {
133
134
  const lines = [];
134
135
  if (tabSnapshot.consoleMessages.length) {
135
136
  lines.push(`### New console messages`);
@@ -147,13 +148,21 @@ function renderTabSnapshot(tabSnapshot, options = {}) {
147
148
  }
148
149
  lines.push("");
149
150
  }
151
+ if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff === "") {
152
+ return lines.join("\n");
153
+ }
150
154
  lines.push(`### Page state`);
151
155
  lines.push(`- Page URL: ${tabSnapshot.url}`);
152
156
  lines.push(`- Page Title: ${tabSnapshot.title}`);
153
- lines.push(`- Page Snapshot:`);
154
- lines.push("```yaml");
155
- lines.push(options.omitSnapshot ? "<snapshot>" : tabSnapshot.ariaSnapshot);
156
- lines.push("```");
157
+ if (includeSnapshot !== "none") {
158
+ lines.push(`- Page Snapshot:`);
159
+ lines.push("```yaml");
160
+ if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff !== void 0)
161
+ lines.push(tabSnapshot.ariaSnapshotDiff);
162
+ else
163
+ lines.push(tabSnapshot.ariaSnapshot);
164
+ lines.push("```");
165
+ }
157
166
  return lines.join("\n");
158
167
  }
159
168
  function renderTabsMarkdown(tabs, force = false) {