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.
- package/README.md +3 -3
- package/ThirdPartyNotices.txt +202 -282
- package/lib/agents/copilot-setup-steps.yml +34 -0
- package/lib/agents/generateAgents.js +292 -160
- package/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/lib/agents/{generator.md → playwright-test-generator.agent.md} +8 -22
- package/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/lib/agents/{healer.md → playwright-test-healer.agent.md} +5 -28
- package/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/lib/agents/{planner.md → playwright-test-planner.agent.md} +5 -68
- package/lib/common/config.js +6 -0
- package/lib/common/expectBundle.js +0 -9
- package/lib/common/expectBundleImpl.js +267 -249
- package/lib/common/testLoader.js +3 -2
- package/lib/common/testType.js +3 -12
- package/lib/common/validators.js +68 -0
- package/lib/index.js +9 -9
- package/lib/isomorphic/teleReceiver.js +1 -0
- package/lib/isomorphic/testServerConnection.js +14 -0
- package/lib/loader/loaderMain.js +1 -1
- package/lib/matchers/expect.js +12 -13
- package/lib/matchers/matchers.js +16 -0
- package/lib/mcp/browser/browserServerBackend.js +1 -1
- package/lib/mcp/browser/config.js +9 -24
- package/lib/mcp/browser/context.js +4 -21
- package/lib/mcp/browser/response.js +20 -11
- package/lib/mcp/browser/tab.js +25 -10
- package/lib/mcp/browser/tools/evaluate.js +2 -3
- package/lib/mcp/browser/tools/form.js +2 -3
- package/lib/mcp/browser/tools/keyboard.js +4 -5
- package/lib/mcp/browser/tools/pdf.js +1 -1
- package/lib/mcp/browser/tools/runCode.js +75 -0
- package/lib/mcp/browser/tools/screenshot.js +33 -15
- package/lib/mcp/browser/tools/snapshot.js +13 -14
- package/lib/mcp/browser/tools/tabs.js +2 -2
- package/lib/mcp/browser/tools/utils.js +0 -11
- package/lib/mcp/browser/tools/verify.js +3 -4
- package/lib/mcp/browser/tools.js +2 -0
- package/lib/mcp/program.js +21 -1
- package/lib/mcp/sdk/exports.js +1 -3
- package/lib/mcp/sdk/http.js +9 -2
- package/lib/mcp/sdk/proxyBackend.js +1 -1
- package/lib/mcp/sdk/server.js +13 -5
- package/lib/mcp/sdk/tool.js +2 -6
- package/lib/mcp/test/browserBackend.js +43 -33
- package/lib/mcp/test/generatorTools.js +3 -3
- package/lib/mcp/test/plannerTools.js +103 -5
- package/lib/mcp/test/seed.js +25 -15
- package/lib/mcp/test/streams.js +9 -4
- package/lib/mcp/test/testBackend.js +31 -29
- package/lib/mcp/test/testContext.js +143 -40
- package/lib/mcp/test/testTools.js +12 -21
- package/lib/plugins/webServerPlugin.js +37 -9
- package/lib/program.js +11 -20
- package/lib/reporters/html.js +2 -23
- package/lib/reporters/internalReporter.js +4 -2
- package/lib/reporters/junit.js +4 -2
- package/lib/reporters/list.js +1 -5
- package/lib/reporters/merge.js +12 -6
- package/lib/reporters/teleEmitter.js +3 -1
- package/lib/runner/dispatcher.js +26 -2
- package/lib/runner/failureTracker.js +5 -5
- package/lib/runner/loadUtils.js +2 -1
- package/lib/runner/loaderHost.js +1 -1
- package/lib/runner/reporters.js +5 -4
- package/lib/runner/testRunner.js +8 -9
- package/lib/runner/testServer.js +8 -3
- package/lib/runner/workerHost.js +3 -0
- package/lib/worker/testInfo.js +28 -17
- package/lib/worker/testTracing.js +1 -0
- package/lib/worker/workerMain.js +15 -6
- package/package.json +2 -2
- package/types/test.d.ts +96 -3
- package/types/testReporter.d.ts +5 -0
- package/lib/mcp/sdk/mdb.js +0 -208
package/lib/common/testLoader.js
CHANGED
|
@@ -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,
|
|
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)();
|
package/lib/common/testType.js
CHANGED
|
@@ -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
|
|
93
|
-
if (!["chromium", "firefox", "webkit"
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|
|
@@ -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
|
});
|
package/lib/loader/loaderMain.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/matchers/expect.js
CHANGED
|
@@ -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
|
|
224
|
-
const defaultTitle = `${this._info.poll ? "poll " : ""}${this._info.isSoft ? "soft " : ""}${this._info.isNot ? "not " : ""}${matcherName}${
|
|
225
|
-
const
|
|
226
|
-
const
|
|
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;
|
package/lib/matchers/matchers.js
CHANGED
|
@@ -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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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) {
|