playwright 1.58.0-alpha-2025-12-04 → 1.58.0-alpha-2025-12-06
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 +2 -2
- package/lib/agents/performTask.js +110 -2
- package/lib/index.js +5 -2
- package/lib/isomorphic/teleReceiver.js +2 -14
- package/lib/mcp/browser/browserContextFactory.js +2 -0
- package/lib/mcp/browser/config.js +21 -2
- package/lib/mcp/browser/tab.js +37 -3
- package/lib/mcp/browser/tools/console.js +2 -2
- package/lib/mcp/browser/tools/network.js +16 -10
- package/lib/mcp/program.js +1 -1
- package/lib/mcp/test/browserBackend.js +1 -1
- package/lib/reporters/internalReporter.js +11 -7
- package/lib/reporters/merge.js +1 -6
- package/lib/reporters/multiplexer.js +0 -4
- package/lib/reporters/teleEmitter.js +1 -10
- package/lib/runner/dispatcher.js +10 -27
- package/lib/runner/testRunner.js +1 -2
- package/lib/worker/testInfo.js +3 -27
- package/lib/worker/workerMain.js +1 -4
- package/package.json +2 -2
- package/lib/transform/babelHighlightUtils.js +0 -63
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🎭 Playwright
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
|
4
4
|
|
|
5
5
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|
|
10
10
|
| :--- | :---: | :---: | :---: |
|
|
11
11
|
| Chromium <!-- GEN:chromium-version -->143.0.7499.25<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
12
12
|
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
13
|
-
| Firefox <!-- GEN:firefox-version -->
|
|
13
|
+
| Firefox <!-- GEN:firefox-version -->145.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
14
14
|
|
|
15
15
|
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
|
16
16
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,19 +17,44 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var performTask_exports = {};
|
|
20
30
|
__export(performTask_exports, {
|
|
31
|
+
performCache: () => performCache,
|
|
21
32
|
performTask: () => performTask
|
|
22
33
|
});
|
|
23
34
|
module.exports = __toCommonJS(performTask_exports);
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
36
|
+
var import_path = __toESM(require("path"));
|
|
24
37
|
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
|
|
25
38
|
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
26
39
|
var import_browserContextFactory = require("../mcp/browser/browserContextFactory");
|
|
27
40
|
var import_browserServerBackend = require("../mcp/browser/browserServerBackend");
|
|
28
41
|
var import_config = require("../mcp/browser/config");
|
|
29
42
|
var import_server = require("../mcp/sdk/server");
|
|
30
|
-
|
|
43
|
+
const resultSchema = import_mcpBundle.z.object({
|
|
44
|
+
code: import_mcpBundle.z.string().optional().describe(`
|
|
45
|
+
Generated code to perform the task using Playwright API.
|
|
46
|
+
Check out the <code> blocks and combine them. Should be presented in the following form:
|
|
47
|
+
|
|
48
|
+
perform(async ({ page }) => {
|
|
49
|
+
// generated code here.
|
|
50
|
+
});
|
|
51
|
+
`),
|
|
52
|
+
error: import_mcpBundle.z.string().optional().describe("The error that occurred if execution failed.").optional()
|
|
53
|
+
});
|
|
54
|
+
async function performTask(testInfo, context, userTask, options) {
|
|
55
|
+
const cacheStatus = await performTaskFromCache(testInfo, context, userTask);
|
|
56
|
+
if (cacheStatus === "success")
|
|
57
|
+
return;
|
|
31
58
|
const backend = new import_browserServerBackend.BrowserServerBackend(import_config.defaultConfig, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
|
|
32
59
|
const client = await (0, import_server.wrapInClient)(backend, { name: "Internal", version: "0.0.0" });
|
|
33
60
|
const callTool = async (params) => {
|
|
@@ -44,12 +71,93 @@ async function performTask(context, task, options) {
|
|
|
44
71
|
tools: await backend.listTools()
|
|
45
72
|
});
|
|
46
73
|
try {
|
|
47
|
-
|
|
74
|
+
const result = await loop.run(userTask, { resultSchema: (0, import_mcpBundle.zodToJsonSchema)(resultSchema) });
|
|
75
|
+
if (result.code)
|
|
76
|
+
await updatePerformFile(testInfo, userTask, result.code, options);
|
|
48
77
|
} finally {
|
|
49
78
|
await client.close();
|
|
50
79
|
}
|
|
51
80
|
}
|
|
81
|
+
async function updatePerformFile(testInfo, userTask, taskCode, options) {
|
|
82
|
+
const relativeFile = import_path.default.relative(testInfo.project.testDir, testInfo.file);
|
|
83
|
+
const promptCacheFile = testInfo.file.replace(".spec.ts", ".cache.ts");
|
|
84
|
+
const testTitle = testInfo.title;
|
|
85
|
+
const loop = new import_mcpBundle.Loop(options.provider ?? "github", {
|
|
86
|
+
model: options.model ?? "claude-sonnet-4.5",
|
|
87
|
+
reasoning: options.reasoning,
|
|
88
|
+
temperature: options.temperature,
|
|
89
|
+
maxTokens: options.maxTokens,
|
|
90
|
+
summarize: true,
|
|
91
|
+
debug: import_utilsBundle.debug,
|
|
92
|
+
callTool: async () => ({ content: [] }),
|
|
93
|
+
tools: []
|
|
94
|
+
});
|
|
95
|
+
const resultSchema2 = import_mcpBundle.z.object({
|
|
96
|
+
code: import_mcpBundle.z.string().optional().describe(`
|
|
97
|
+
Generated code with all the perofrm routines combined or updated into the following format:
|
|
98
|
+
|
|
99
|
+
import { performCache } from '@playwright/test';
|
|
100
|
+
|
|
101
|
+
performCache({
|
|
102
|
+
file: 'tests/page/perform-task.spec.ts',
|
|
103
|
+
test: 'perform task',
|
|
104
|
+
task: 'Click the learn more button',
|
|
105
|
+
code: async ({ page }) => {
|
|
106
|
+
await page.getByRole('link', { name: 'Learn more' }).click();
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
`)
|
|
110
|
+
});
|
|
111
|
+
const existingCode = await import_fs.default.promises.readFile(promptCacheFile, "utf8").catch(() => "");
|
|
112
|
+
const task = `
|
|
113
|
+
- Create or update a perform file to include performCache block for the given task and code.
|
|
114
|
+
- Dedupe items with the same file, test, and task.
|
|
115
|
+
- Should produce code in the following format
|
|
116
|
+
|
|
117
|
+
import { performCache } from '@playwright/test';
|
|
118
|
+
|
|
119
|
+
performCache({
|
|
120
|
+
file: '<file>',
|
|
121
|
+
test: '<test>',
|
|
122
|
+
task: '<task>',
|
|
123
|
+
code: async ({ page }) => {
|
|
124
|
+
<code>
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
performCache({
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
## Params for the new or updated performCache block
|
|
132
|
+
<file-content>${existingCode}</file-content>
|
|
133
|
+
<file>${relativeFile}</file>
|
|
134
|
+
<test>${testTitle}</test>
|
|
135
|
+
<task>${userTask}</task>
|
|
136
|
+
<code>${taskCode}</code>
|
|
137
|
+
`;
|
|
138
|
+
const result = await loop.run(task, { resultSchema: (0, import_mcpBundle.zodToJsonSchema)(resultSchema2) });
|
|
139
|
+
if (result.code)
|
|
140
|
+
await import_fs.default.promises.writeFile(promptCacheFile, result.code);
|
|
141
|
+
}
|
|
142
|
+
const performCacheMap = /* @__PURE__ */ new Map();
|
|
143
|
+
function performCache(entry) {
|
|
144
|
+
performCacheMap.set(JSON.stringify({ ...entry, code: void 0 }), entry);
|
|
145
|
+
}
|
|
146
|
+
async function performTaskFromCache(testInfo, context, userTask) {
|
|
147
|
+
const relativeFile = import_path.default.relative(testInfo.project.testDir, testInfo.file);
|
|
148
|
+
const key = JSON.stringify({ file: relativeFile, test: testInfo.title, task: userTask });
|
|
149
|
+
const entry = performCacheMap.get(key);
|
|
150
|
+
if (!entry)
|
|
151
|
+
return "cache-miss";
|
|
152
|
+
try {
|
|
153
|
+
await entry.code({ page: context.pages()[0] });
|
|
154
|
+
return "success";
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
52
159
|
// Annotate the CommonJS export names for ESM import in node:
|
|
53
160
|
0 && (module.exports = {
|
|
161
|
+
performCache,
|
|
54
162
|
performTask
|
|
55
163
|
});
|
package/lib/index.js
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
expect: () => import_expect.expect,
|
|
34
34
|
mergeExpects: () => import_expect2.mergeExpects,
|
|
35
35
|
mergeTests: () => import_testType2.mergeTests,
|
|
36
|
+
performCache: () => import_performTask2.performCache,
|
|
36
37
|
test: () => test
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -48,6 +49,7 @@ var import_expect = require("./matchers/expect");
|
|
|
48
49
|
var import_configLoader = require("./common/configLoader");
|
|
49
50
|
var import_testType2 = require("./common/testType");
|
|
50
51
|
var import_expect2 = require("./matchers/expect");
|
|
52
|
+
var import_performTask2 = require("./agents/performTask");
|
|
51
53
|
const _baseTest = import_testType.rootTestType.test;
|
|
52
54
|
(0, import_utils.setBoxedStackPrefixes)([import_path.default.dirname(require.resolve("../package.json"))]);
|
|
53
55
|
if (process["__pw_initiator__"]) {
|
|
@@ -417,9 +419,9 @@ const playwrightFixtures = {
|
|
|
417
419
|
await request.dispose();
|
|
418
420
|
}
|
|
419
421
|
},
|
|
420
|
-
_perform: async ({ context }, use) => {
|
|
422
|
+
_perform: async ({ context }, use, testInfo) => {
|
|
421
423
|
await use(async (task, options) => {
|
|
422
|
-
await (0, import_performTask.performTask)(context, task, options ?? {});
|
|
424
|
+
await (0, import_performTask.performTask)(testInfo, context, task, options ?? {});
|
|
423
425
|
});
|
|
424
426
|
}
|
|
425
427
|
};
|
|
@@ -684,5 +686,6 @@ const test = _baseTest.extend(playwrightFixtures);
|
|
|
684
686
|
expect,
|
|
685
687
|
mergeExpects,
|
|
686
688
|
mergeTests,
|
|
689
|
+
performCache,
|
|
687
690
|
test
|
|
688
691
|
});
|
|
@@ -70,10 +70,6 @@ class TeleReporterReceiver {
|
|
|
70
70
|
this._onAttach(params.testId, params.resultId, params.attachments);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
if (method === "onTestError") {
|
|
74
|
-
this._onTestError(params.testId, params.resultId, params.error);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
73
|
if (method === "onStepEnd") {
|
|
78
74
|
this._onStepEnd(params.testId, params.resultId, params.step);
|
|
79
75
|
return;
|
|
@@ -127,10 +123,8 @@ class TeleReporterReceiver {
|
|
|
127
123
|
const result = test.results.find((r) => r._id === payload.id);
|
|
128
124
|
result.duration = payload.duration;
|
|
129
125
|
result.status = payload.status;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
result.error = result.errors[0];
|
|
133
|
-
}
|
|
126
|
+
result.errors = payload.errors;
|
|
127
|
+
result.error = result.errors?.[0];
|
|
134
128
|
if (!!payload.attachments)
|
|
135
129
|
result.attachments = this._parseAttachments(payload.attachments);
|
|
136
130
|
if (payload.annotations) {
|
|
@@ -173,12 +167,6 @@ class TeleReporterReceiver {
|
|
|
173
167
|
body: a.base64 && globalThis.Buffer ? Buffer.from(a.base64, "base64") : void 0
|
|
174
168
|
})));
|
|
175
169
|
}
|
|
176
|
-
_onTestError(testId, resultId, error) {
|
|
177
|
-
const test = this._tests.get(testId);
|
|
178
|
-
const result = test.results.find((r) => r._id === resultId);
|
|
179
|
-
result.errors.push(error);
|
|
180
|
-
result.error = result.errors[0];
|
|
181
|
-
}
|
|
182
170
|
_onError(error) {
|
|
183
171
|
this._reporter.onError?.(error);
|
|
184
172
|
}
|
|
@@ -216,6 +216,8 @@ class PersistentContextFactory {
|
|
|
216
216
|
});
|
|
217
217
|
await afterClose();
|
|
218
218
|
this._userDataDirs.delete(userDataDir);
|
|
219
|
+
if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
|
|
220
|
+
await import_fs.default.promises.rm(userDataDir, { recursive: true }).catch(import_log.logUnhandledError);
|
|
219
221
|
(0, import_log.testDebug)("close browser context complete (persistent)");
|
|
220
222
|
}
|
|
221
223
|
async _createUserDataDir(clientInfo) {
|
|
@@ -32,6 +32,7 @@ __export(config_exports, {
|
|
|
32
32
|
configFromCLIOptions: () => configFromCLIOptions,
|
|
33
33
|
defaultConfig: () => defaultConfig,
|
|
34
34
|
dotenvFileLoader: () => dotenvFileLoader,
|
|
35
|
+
enumParser: () => enumParser,
|
|
35
36
|
headerParser: () => headerParser,
|
|
36
37
|
numberParser: () => numberParser,
|
|
37
38
|
outputDir: () => outputDir,
|
|
@@ -61,6 +62,9 @@ const defaultConfig = {
|
|
|
61
62
|
viewport: null
|
|
62
63
|
}
|
|
63
64
|
},
|
|
65
|
+
console: {
|
|
66
|
+
level: "info"
|
|
67
|
+
},
|
|
64
68
|
network: {
|
|
65
69
|
allowedOrigins: void 0,
|
|
66
70
|
blockedOrigins: void 0
|
|
@@ -182,6 +186,9 @@ function configFromCLIOptions(cliOptions) {
|
|
|
182
186
|
allowedHosts: cliOptions.allowedHosts
|
|
183
187
|
},
|
|
184
188
|
capabilities: cliOptions.caps,
|
|
189
|
+
console: {
|
|
190
|
+
level: cliOptions.consoleLevel
|
|
191
|
+
},
|
|
185
192
|
network: {
|
|
186
193
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
187
194
|
blockedOrigins: cliOptions.blockedOrigins
|
|
@@ -213,6 +220,8 @@ function configFromEnv() {
|
|
|
213
220
|
options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
|
|
214
221
|
options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {});
|
|
215
222
|
options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
|
|
223
|
+
if (process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
|
|
224
|
+
options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
|
|
216
225
|
options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
|
|
217
226
|
options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
|
|
218
227
|
options.grantPermissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
|
|
@@ -226,8 +235,8 @@ function configFromEnv() {
|
|
|
226
235
|
if (initScript)
|
|
227
236
|
options.initScript = [initScript];
|
|
228
237
|
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
|
|
229
|
-
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES
|
|
230
|
-
options.imageResponses = "omit";
|
|
238
|
+
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
|
|
239
|
+
options.imageResponses = enumParser("--image-responses", ["allow", "omit"], process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
|
|
231
240
|
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
|
|
232
241
|
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
|
|
233
242
|
options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT);
|
|
@@ -306,6 +315,10 @@ function mergeConfig(base, overrides) {
|
|
|
306
315
|
...pickDefined(base),
|
|
307
316
|
...pickDefined(overrides),
|
|
308
317
|
browser,
|
|
318
|
+
console: {
|
|
319
|
+
...pickDefined(base.console),
|
|
320
|
+
...pickDefined(overrides.console)
|
|
321
|
+
},
|
|
309
322
|
network: {
|
|
310
323
|
...pickDefined(base.network),
|
|
311
324
|
...pickDefined(overrides.network)
|
|
@@ -369,6 +382,11 @@ function headerParser(arg, previous) {
|
|
|
369
382
|
result[name] = value;
|
|
370
383
|
return result;
|
|
371
384
|
}
|
|
385
|
+
function enumParser(name, options, value) {
|
|
386
|
+
if (!options.includes(value))
|
|
387
|
+
throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
372
390
|
function envToBoolean(value) {
|
|
373
391
|
if (value === "true" || value === "1")
|
|
374
392
|
return true;
|
|
@@ -392,6 +410,7 @@ function sanitizeForFilePath(s) {
|
|
|
392
410
|
configFromCLIOptions,
|
|
393
411
|
defaultConfig,
|
|
394
412
|
dotenvFileLoader,
|
|
413
|
+
enumParser,
|
|
395
414
|
headerParser,
|
|
396
415
|
numberParser,
|
|
397
416
|
outputDir,
|
package/lib/mcp/browser/tab.js
CHANGED
|
@@ -173,9 +173,9 @@ class Tab extends import_events.EventEmitter {
|
|
|
173
173
|
}
|
|
174
174
|
await this.waitForLoadState("load", { timeout: 5e3 });
|
|
175
175
|
}
|
|
176
|
-
async consoleMessages(
|
|
176
|
+
async consoleMessages(level) {
|
|
177
177
|
await this._initializedPromise;
|
|
178
|
-
return this._consoleMessages.filter((message) =>
|
|
178
|
+
return this._consoleMessages.filter((message) => shouldIncludeMessage(level, message.type));
|
|
179
179
|
}
|
|
180
180
|
async requests() {
|
|
181
181
|
await this._initializedPromise;
|
|
@@ -197,7 +197,7 @@ class Tab extends import_events.EventEmitter {
|
|
|
197
197
|
};
|
|
198
198
|
});
|
|
199
199
|
if (tabSnapshot) {
|
|
200
|
-
tabSnapshot.consoleMessages = this._recentConsoleMessages;
|
|
200
|
+
tabSnapshot.consoleMessages = this._recentConsoleMessages.filter((message) => shouldIncludeMessage(this.context.config.console.level, message.type));
|
|
201
201
|
this._recentConsoleMessages = [];
|
|
202
202
|
}
|
|
203
203
|
this._needsFullSnapshot = !tabSnapshot;
|
|
@@ -287,6 +287,40 @@ function renderModalStates(modalStates) {
|
|
|
287
287
|
result.push(`- [${state.description}]: can be handled by the "${state.clearedBy}" tool`);
|
|
288
288
|
return result;
|
|
289
289
|
}
|
|
290
|
+
const consoleMessageLevels = ["error", "warning", "info", "debug"];
|
|
291
|
+
function shouldIncludeMessage(thresholdLevel, type) {
|
|
292
|
+
const messageLevel = consoleLevelForMessageType(type);
|
|
293
|
+
return consoleMessageLevels.indexOf(messageLevel) <= consoleMessageLevels.indexOf(thresholdLevel);
|
|
294
|
+
}
|
|
295
|
+
function consoleLevelForMessageType(type) {
|
|
296
|
+
switch (type) {
|
|
297
|
+
case "assert":
|
|
298
|
+
case "error":
|
|
299
|
+
return "error";
|
|
300
|
+
case "warning":
|
|
301
|
+
return "warning";
|
|
302
|
+
case "count":
|
|
303
|
+
case "dir":
|
|
304
|
+
case "dirxml":
|
|
305
|
+
case "info":
|
|
306
|
+
case "log":
|
|
307
|
+
case "table":
|
|
308
|
+
case "time":
|
|
309
|
+
case "timeEnd":
|
|
310
|
+
return "info";
|
|
311
|
+
case "clear":
|
|
312
|
+
case "debug":
|
|
313
|
+
case "endGroup":
|
|
314
|
+
case "profile":
|
|
315
|
+
case "profileEnd":
|
|
316
|
+
case "startGroup":
|
|
317
|
+
case "startGroupCollapsed":
|
|
318
|
+
case "trace":
|
|
319
|
+
return "debug";
|
|
320
|
+
default:
|
|
321
|
+
return "info";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
290
324
|
const tabSymbol = Symbol("tabSymbol");
|
|
291
325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
292
326
|
0 && (module.exports = {
|
|
@@ -30,12 +30,12 @@ const console = (0, import_tool.defineTabTool)({
|
|
|
30
30
|
title: "Get console messages",
|
|
31
31
|
description: "Returns all console messages",
|
|
32
32
|
inputSchema: import_mcpBundle.z.object({
|
|
33
|
-
|
|
33
|
+
level: import_mcpBundle.z.enum(["error", "warning", "info", "debug"]).default("info").describe('Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".')
|
|
34
34
|
}),
|
|
35
35
|
type: "readOnly"
|
|
36
36
|
},
|
|
37
37
|
handle: async (tab, params, response) => {
|
|
38
|
-
const messages = await tab.consoleMessages(params.
|
|
38
|
+
const messages = await tab.consoleMessages(params.level);
|
|
39
39
|
messages.map((message) => response.addResult(message.toString()));
|
|
40
40
|
}
|
|
41
41
|
});
|
|
@@ -29,24 +29,30 @@ const requests = (0, import_tool.defineTabTool)({
|
|
|
29
29
|
name: "browser_network_requests",
|
|
30
30
|
title: "List network requests",
|
|
31
31
|
description: "Returns all network requests since loading the page",
|
|
32
|
-
inputSchema: import_mcpBundle.z.object({
|
|
32
|
+
inputSchema: import_mcpBundle.z.object({
|
|
33
|
+
includeStatic: import_mcpBundle.z.boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.")
|
|
34
|
+
}),
|
|
33
35
|
type: "readOnly"
|
|
34
36
|
},
|
|
35
37
|
handle: async (tab, params, response) => {
|
|
36
38
|
const requests2 = await tab.requests();
|
|
37
|
-
for (const request of requests2)
|
|
38
|
-
|
|
39
|
+
for (const request of requests2) {
|
|
40
|
+
const rendered = await renderRequest(request, params.includeStatic);
|
|
41
|
+
if (rendered)
|
|
42
|
+
response.addResult(rendered);
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
});
|
|
41
|
-
async function renderRequest(request) {
|
|
46
|
+
async function renderRequest(request, includeStatic) {
|
|
47
|
+
const response = request._hasResponse ? await request.response() : void 0;
|
|
48
|
+
const isStaticRequest = ["document", "stylesheet", "image", "media", "font", "script", "manifest"].includes(request.resourceType());
|
|
49
|
+
const isSuccessfulRequest = !response || response.status() < 400;
|
|
50
|
+
if (isStaticRequest && isSuccessfulRequest && !includeStatic)
|
|
51
|
+
return void 0;
|
|
42
52
|
const result = [];
|
|
43
53
|
result.push(`[${request.method().toUpperCase()}] ${request.url()}`);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const response = await request.response();
|
|
47
|
-
if (response)
|
|
48
|
-
result.push(`=> [${response.status()}] ${response.statusText()}`);
|
|
49
|
-
}
|
|
54
|
+
if (response)
|
|
55
|
+
result.push(`=> [${response.status()}] ${response.statusText()}`);
|
|
50
56
|
return result.join(" ");
|
|
51
57
|
}
|
|
52
58
|
var network_default = [
|
package/lib/mcp/program.js
CHANGED
|
@@ -42,7 +42,7 @@ var import_proxyBackend = require("./sdk/proxyBackend");
|
|
|
42
42
|
var import_browserServerBackend = require("./browser/browserServerBackend");
|
|
43
43
|
var import_extensionContextFactory = require("./extension/extensionContextFactory");
|
|
44
44
|
function decorateCommand(command, version) {
|
|
45
|
-
command.option("--allowed-hosts <hosts...>", "comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.", import_config.commaSeparatedList).option("--allowed-origins <origins>", "semicolon-separated list of TRUSTED origins to allow the browser to request. Default is to allow all.\nImportant: *does not* serve as a security boundary and *does not* affect redirects. ", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.\nImportant: *does not* serve as a security boundary and *does not* affect redirects.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-page <path...>", "path to TypeScript file to evaluate on Playwright page object").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--save-video <size>", 'Whether to save the video of the session into the output directory. For example "--save-video=800x600"', import_config.resolutionParser.bind(null, "--save-video")).option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--snapshot-mode <mode>", 'when taking snapshots for responses, specifies the mode to use. Can be "incremental", "full", or "none". Default is incremental.').option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--test-id-attribute <attribute>", 'specify the attribute to use for test ids, defaults to "data-testid"').option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280x720"', import_config.resolutionParser.bind(null, "--viewport-size")).addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
|
|
45
|
+
command.option("--allowed-hosts <hosts...>", "comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.", import_config.commaSeparatedList).option("--allowed-origins <origins>", "semicolon-separated list of TRUSTED origins to allow the browser to request. Default is to allow all.\nImportant: *does not* serve as a security boundary and *does not* affect redirects. ", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.\nImportant: *does not* serve as a security boundary and *does not* affect redirects.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--console-level <level>", 'level of console messages to return: "error", "warning", "info", "debug". Each level includes the messages of more severe levels.', import_config.enumParser.bind(null, "--console-level", ["error", "warning", "info", "debug"])).option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-page <path...>", "path to TypeScript file to evaluate on Playwright page object").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".', import_config.enumParser.bind(null, "--image-responses", ["allow", "omit"])).option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--save-video <size>", 'Whether to save the video of the session into the output directory. For example "--save-video=800x600"', import_config.resolutionParser.bind(null, "--save-video")).option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--snapshot-mode <mode>", 'when taking snapshots for responses, specifies the mode to use. Can be "incremental", "full", or "none". Default is incremental.').option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--test-id-attribute <attribute>", 'specify the attribute to use for test ids, defaults to "data-testid"').option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280x720"', import_config.resolutionParser.bind(null, "--viewport-size")).addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
|
|
46
46
|
(0, import_watchdog.setupExitWatchdog)();
|
|
47
47
|
if (options.vision) {
|
|
48
48
|
console.error("The --vision option is deprecated, use --caps=vision instead");
|
|
@@ -74,7 +74,7 @@ async function generatePausedMessage(testInfo, context) {
|
|
|
74
74
|
`- Page Title: ${await page.title()}`.trim()
|
|
75
75
|
);
|
|
76
76
|
let console = testInfo.errors.length ? await import_tab.Tab.collectConsoleMessages(page) : [];
|
|
77
|
-
console = console.filter((msg) =>
|
|
77
|
+
console = console.filter((msg) => msg.type === "error");
|
|
78
78
|
if (console.length) {
|
|
79
79
|
lines.push("- Console Messages:");
|
|
80
80
|
for (const message of console)
|
|
@@ -66,11 +66,8 @@ class InternalReporter {
|
|
|
66
66
|
onStdErr(chunk, test, result) {
|
|
67
67
|
this._reporter.onStdErr?.(chunk, test, result);
|
|
68
68
|
}
|
|
69
|
-
onTestError(test, result, error) {
|
|
70
|
-
addLocationAndSnippetToError(this._config, error, test.location.file);
|
|
71
|
-
this._reporter.onTestError?.(test, result, error);
|
|
72
|
-
}
|
|
73
69
|
onTestEnd(test, result) {
|
|
70
|
+
this._addSnippetToTestErrors(test, result);
|
|
74
71
|
this._reporter.onTestEnd?.(test, result);
|
|
75
72
|
}
|
|
76
73
|
async onEnd(result) {
|
|
@@ -87,20 +84,27 @@ class InternalReporter {
|
|
|
87
84
|
await this._reporter.onExit?.();
|
|
88
85
|
}
|
|
89
86
|
onError(error) {
|
|
90
|
-
addLocationAndSnippetToError(this._config, error
|
|
87
|
+
addLocationAndSnippetToError(this._config, error);
|
|
91
88
|
this._reporter.onError?.(error);
|
|
92
89
|
}
|
|
93
90
|
onStepBegin(test, result, step) {
|
|
94
91
|
this._reporter.onStepBegin?.(test, result, step);
|
|
95
92
|
}
|
|
96
93
|
onStepEnd(test, result, step) {
|
|
97
|
-
|
|
98
|
-
addLocationAndSnippetToError(this._config, step.error, test.location.file);
|
|
94
|
+
this._addSnippetToStepError(test, step);
|
|
99
95
|
this._reporter.onStepEnd?.(test, result, step);
|
|
100
96
|
}
|
|
101
97
|
printsToStdio() {
|
|
102
98
|
return this._reporter.printsToStdio ? this._reporter.printsToStdio() : true;
|
|
103
99
|
}
|
|
100
|
+
_addSnippetToTestErrors(test, result) {
|
|
101
|
+
for (const error of result.errors)
|
|
102
|
+
addLocationAndSnippetToError(this._config, error, test.location.file);
|
|
103
|
+
}
|
|
104
|
+
_addSnippetToStepError(test, step) {
|
|
105
|
+
if (step.error)
|
|
106
|
+
addLocationAndSnippetToError(this._config, step.error, test.location.file);
|
|
107
|
+
}
|
|
104
108
|
}
|
|
105
109
|
function addLocationAndSnippetToError(config, error, file) {
|
|
106
110
|
if (error.stack && !error.location)
|
package/lib/reporters/merge.js
CHANGED
|
@@ -342,7 +342,6 @@ class IdsPatcher {
|
|
|
342
342
|
case "onProject":
|
|
343
343
|
this._onProject(params.project);
|
|
344
344
|
return;
|
|
345
|
-
case "onTestError":
|
|
346
345
|
case "onAttach":
|
|
347
346
|
case "onTestBegin":
|
|
348
347
|
case "onStepBegin":
|
|
@@ -424,7 +423,7 @@ class PathSeparatorPatcher {
|
|
|
424
423
|
test.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
|
|
425
424
|
const testResult = jsonEvent.params.result;
|
|
426
425
|
testResult.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
|
|
427
|
-
testResult.errors
|
|
426
|
+
testResult.errors.forEach((error) => this._updateErrorLocations(error));
|
|
428
427
|
(testResult.attachments ?? []).forEach((attachment) => {
|
|
429
428
|
if (attachment.path)
|
|
430
429
|
attachment.path = this._updatePath(attachment.path);
|
|
@@ -442,10 +441,6 @@ class PathSeparatorPatcher {
|
|
|
442
441
|
step.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
|
|
443
442
|
return;
|
|
444
443
|
}
|
|
445
|
-
if (jsonEvent.method === "onTestError") {
|
|
446
|
-
this._updateErrorLocations(jsonEvent.params.error);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
444
|
if (jsonEvent.method === "onAttach") {
|
|
450
445
|
const attach = jsonEvent.params;
|
|
451
446
|
attach.attachments.forEach((attachment) => {
|
|
@@ -48,10 +48,6 @@ class Multiplexer {
|
|
|
48
48
|
for (const reporter of this._reporters)
|
|
49
49
|
wrap(() => reporter.onStdErr?.(chunk, test, result));
|
|
50
50
|
}
|
|
51
|
-
onTestError(test, result, error) {
|
|
52
|
-
for (const reporter of this._reporters)
|
|
53
|
-
wrap(() => reporter.onTestError?.(test, result, error));
|
|
54
|
-
}
|
|
55
51
|
onTestEnd(test, result) {
|
|
56
52
|
for (const reporter of this._reporters)
|
|
57
53
|
wrap(() => reporter.onTestEnd?.(test, result));
|
|
@@ -66,16 +66,6 @@ class TeleReporterEmitter {
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
onTestError(test, result, error) {
|
|
70
|
-
this._messageSink({
|
|
71
|
-
method: "onTestError",
|
|
72
|
-
params: {
|
|
73
|
-
testId: test.id,
|
|
74
|
-
resultId: result[this._idSymbol],
|
|
75
|
-
error
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
69
|
onTestEnd(test, result) {
|
|
80
70
|
const testEnd = {
|
|
81
71
|
testId: test.id,
|
|
@@ -235,6 +225,7 @@ class TeleReporterEmitter {
|
|
|
235
225
|
id: result[this._idSymbol],
|
|
236
226
|
duration: result.duration,
|
|
237
227
|
status: result.status,
|
|
228
|
+
errors: result.errors,
|
|
238
229
|
annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : void 0
|
|
239
230
|
};
|
|
240
231
|
}
|
package/lib/runner/dispatcher.js
CHANGED
|
@@ -243,6 +243,7 @@ class JobDispatcher {
|
|
|
243
243
|
_onTestEnd(params) {
|
|
244
244
|
if (this._failureTracker.hasReachedMaxFailures()) {
|
|
245
245
|
params.status = "interrupted";
|
|
246
|
+
params.errors = [];
|
|
246
247
|
}
|
|
247
248
|
const data = this._dataByTestId.get(params.testId);
|
|
248
249
|
if (!data) {
|
|
@@ -252,6 +253,8 @@ class JobDispatcher {
|
|
|
252
253
|
this._remainingByTestId.delete(params.testId);
|
|
253
254
|
const { result, test } = data;
|
|
254
255
|
result.duration = params.duration;
|
|
256
|
+
result.errors = params.errors;
|
|
257
|
+
result.error = result.errors[0];
|
|
255
258
|
result.status = params.status;
|
|
256
259
|
result.annotations = params.annotations;
|
|
257
260
|
test.annotations = [...params.annotations];
|
|
@@ -338,20 +341,6 @@ class JobDispatcher {
|
|
|
338
341
|
this._reporter.onStdErr?.("Internal error: step id not found: " + params.stepId);
|
|
339
342
|
}
|
|
340
343
|
}
|
|
341
|
-
_onTestErrors(params) {
|
|
342
|
-
if (this._failureTracker.hasReachedMaxFailures()) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const data = this._dataByTestId.get(params.testId);
|
|
346
|
-
if (!data)
|
|
347
|
-
return;
|
|
348
|
-
const { test, result } = data;
|
|
349
|
-
for (const error of params.errors) {
|
|
350
|
-
result.errors.push(error);
|
|
351
|
-
result.error = result.errors[0];
|
|
352
|
-
this._reporter.onTestError?.(test, result, error);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
344
|
_failTestWithErrors(test, errors) {
|
|
356
345
|
const runData = this._dataByTestId.get(test.id);
|
|
357
346
|
let result;
|
|
@@ -361,11 +350,8 @@ class JobDispatcher {
|
|
|
361
350
|
result = test._appendTestResult();
|
|
362
351
|
this._reporter.onTestBegin?.(test, result);
|
|
363
352
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
result.error = result.errors[0];
|
|
367
|
-
this._reporter.onTestError?.(test, result, error);
|
|
368
|
-
}
|
|
353
|
+
result.errors = [...errors];
|
|
354
|
+
result.error = result.errors[0];
|
|
369
355
|
result.status = errors.length ? "failed" : "skipped";
|
|
370
356
|
this._reportTestEnd(test, result);
|
|
371
357
|
this._failedTests.add(test);
|
|
@@ -466,32 +452,29 @@ class JobDispatcher {
|
|
|
466
452
|
import_utils.eventsHelper.addEventListener(worker, "stepBegin", this._onStepBegin.bind(this)),
|
|
467
453
|
import_utils.eventsHelper.addEventListener(worker, "stepEnd", this._onStepEnd.bind(this)),
|
|
468
454
|
import_utils.eventsHelper.addEventListener(worker, "attach", this._onAttach.bind(this)),
|
|
469
|
-
import_utils.eventsHelper.addEventListener(worker, "testErrors", this._onTestErrors.bind(this)),
|
|
470
455
|
import_utils.eventsHelper.addEventListener(worker, "testPaused", this._onTestPaused.bind(this, worker)),
|
|
471
456
|
import_utils.eventsHelper.addEventListener(worker, "done", this._onDone.bind(this)),
|
|
472
457
|
import_utils.eventsHelper.addEventListener(worker, "exit", this.onExit.bind(this))
|
|
473
458
|
];
|
|
474
459
|
}
|
|
475
460
|
_onTestPaused(worker, params) {
|
|
476
|
-
const data = this._dataByTestId.get(params.testId);
|
|
477
|
-
if (!data)
|
|
478
|
-
return;
|
|
479
|
-
const { test, result } = data;
|
|
480
461
|
const sendMessage = async (message) => {
|
|
481
462
|
try {
|
|
482
463
|
if (this.jobResult.isDone())
|
|
483
464
|
throw new Error("Test has already stopped");
|
|
484
465
|
const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request });
|
|
485
466
|
if (response.error)
|
|
486
|
-
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error
|
|
467
|
+
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
|
|
487
468
|
return response;
|
|
488
469
|
} catch (e) {
|
|
489
470
|
const error = (0, import_util.serializeError)(e);
|
|
490
|
-
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error
|
|
471
|
+
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
|
|
491
472
|
return { response: void 0, error };
|
|
492
473
|
}
|
|
493
474
|
};
|
|
494
|
-
|
|
475
|
+
for (const error of params.errors)
|
|
476
|
+
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
|
|
477
|
+
this._failureTracker.onTestPaused?.({ ...params, sendMessage });
|
|
495
478
|
}
|
|
496
479
|
skipWholeJob() {
|
|
497
480
|
const allTestsSkipped = this.job.tests.every((test) => test.expectedStatus === "skipped");
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -383,8 +383,7 @@ async function runAllTestsWithConfig(config) {
|
|
|
383
383
|
(0, import_tasks.createLoadTask)("in-process", { filterOnly: true, failOnLoadErrors: true }),
|
|
384
384
|
...(0, import_tasks.createRunTestsTasks)(config)
|
|
385
385
|
];
|
|
386
|
-
const
|
|
387
|
-
const status = await (0, import_tasks.runTasks)(testRun, tasks, config.config.globalTimeout);
|
|
386
|
+
const status = await (0, import_tasks.runTasks)(new import_tasks.TestRun(config, reporter), tasks, config.config.globalTimeout);
|
|
388
387
|
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
389
388
|
await new Promise((resolve) => process.stderr.write("", () => resolve()));
|
|
390
389
|
return status;
|
package/lib/worker/testInfo.js
CHANGED
|
@@ -42,9 +42,8 @@ var import_util = require("../util");
|
|
|
42
42
|
var import_testTracing = require("./testTracing");
|
|
43
43
|
var import_util2 = require("./util");
|
|
44
44
|
var import_transform = require("../transform/transform");
|
|
45
|
-
var import_babelHighlightUtils = require("../transform/babelHighlightUtils");
|
|
46
45
|
class TestInfoImpl {
|
|
47
|
-
constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach,
|
|
46
|
+
constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach, onTestPaused) {
|
|
48
47
|
this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
49
48
|
this._ariaSnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
50
49
|
this._interruptedPromise = new import_utils.ManualPromise();
|
|
@@ -60,12 +59,10 @@ class TestInfoImpl {
|
|
|
60
59
|
this.status = "passed";
|
|
61
60
|
this.snapshotSuffix = "";
|
|
62
61
|
this.errors = [];
|
|
63
|
-
this._reportedErrorCount = 0;
|
|
64
62
|
this.testId = test?.id ?? "";
|
|
65
63
|
this._onStepBegin = onStepBegin;
|
|
66
64
|
this._onStepEnd = onStepEnd;
|
|
67
65
|
this._onAttach = onAttach;
|
|
68
|
-
this._onErrors = onErrors;
|
|
69
66
|
this._onTestPaused = onTestPaused;
|
|
70
67
|
this._startTime = (0, import_utils.monotonicTime)();
|
|
71
68
|
this._startWallTime = Date.now();
|
|
@@ -341,32 +338,11 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
341
338
|
async _didFinishTestFunction() {
|
|
342
339
|
const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure();
|
|
343
340
|
if (shouldPause) {
|
|
344
|
-
|
|
345
|
-
this.
|
|
346
|
-
this._onTestPaused({ testId: this.testId });
|
|
347
|
-
await this._runAsStep({ title: this._isFailure() ? "Paused on Error" : "Paused at End", category: "test.step", location }, async () => {
|
|
348
|
-
await this._interruptedPromise;
|
|
349
|
-
});
|
|
341
|
+
this._onTestPaused({ testId: this.testId, errors: this._isFailure() ? this.errors : [] });
|
|
342
|
+
await this._interruptedPromise;
|
|
350
343
|
}
|
|
351
344
|
await this._onDidFinishTestFunctionCallback?.();
|
|
352
345
|
}
|
|
353
|
-
_emitErrors() {
|
|
354
|
-
const errors = this.errors.slice(this._reportedErrorCount);
|
|
355
|
-
this._reportedErrorCount = Math.max(this._reportedErrorCount, this.errors.length);
|
|
356
|
-
if (errors.length)
|
|
357
|
-
this._onErrors({ testId: this.testId, errors });
|
|
358
|
-
}
|
|
359
|
-
_errorLocation() {
|
|
360
|
-
if (this.error?.stack)
|
|
361
|
-
return (0, import_util.filteredStackTrace)(this.error.stack.split("\n"))[0];
|
|
362
|
-
}
|
|
363
|
-
async _testEndLocation() {
|
|
364
|
-
try {
|
|
365
|
-
const source = await import_fs.default.promises.readFile(this.file, "utf-8");
|
|
366
|
-
return (0, import_babelHighlightUtils.findTestEndLocation)(source, { file: this.file, line: this.line, column: this.column });
|
|
367
|
-
} catch {
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
346
|
// ------------ TestInfo methods ------------
|
|
371
347
|
async attach(name, options = {}) {
|
|
372
348
|
const step = this._addStep({
|
package/lib/worker/workerMain.js
CHANGED
|
@@ -99,7 +99,6 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
99
99
|
}, () => {
|
|
100
100
|
}, () => {
|
|
101
101
|
}, () => {
|
|
102
|
-
}, () => {
|
|
103
102
|
});
|
|
104
103
|
const runnable = { type: "teardown" };
|
|
105
104
|
await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {
|
|
@@ -244,7 +243,6 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
244
243
|
(stepBeginPayload) => this.dispatchEvent("stepBegin", stepBeginPayload),
|
|
245
244
|
(stepEndPayload) => this.dispatchEvent("stepEnd", stepEndPayload),
|
|
246
245
|
(attachment) => this.dispatchEvent("attach", attachment),
|
|
247
|
-
(errors) => this.dispatchEvent("testErrors", errors),
|
|
248
246
|
(testPausedPayload) => this.dispatchEvent("testPaused", testPausedPayload)
|
|
249
247
|
);
|
|
250
248
|
const processAnnotation = (annotation) => {
|
|
@@ -285,7 +283,6 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
285
283
|
});
|
|
286
284
|
if (isSkipped && nextTest && !hasAfterAllToRunBeforeNextTest) {
|
|
287
285
|
testInfo.status = "skipped";
|
|
288
|
-
testInfo._emitErrors();
|
|
289
286
|
this.dispatchEvent("testEnd", buildTestEndPayload(testInfo));
|
|
290
287
|
return;
|
|
291
288
|
}
|
|
@@ -399,7 +396,6 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
399
396
|
testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed + afterHooksSlot.elapsed | 0;
|
|
400
397
|
this._currentTest = null;
|
|
401
398
|
(0, import_globals.setCurrentTestInfo)(null);
|
|
402
|
-
testInfo._emitErrors();
|
|
403
399
|
this.dispatchEvent("testEnd", buildTestEndPayload(testInfo));
|
|
404
400
|
const preserveOutput = this._config.config.preserveOutput === "always" || this._config.config.preserveOutput === "failures-only" && testInfo._isFailure();
|
|
405
401
|
if (!preserveOutput)
|
|
@@ -502,6 +498,7 @@ function buildTestEndPayload(testInfo) {
|
|
|
502
498
|
testId: testInfo.testId,
|
|
503
499
|
duration: testInfo.duration,
|
|
504
500
|
status: testInfo.status,
|
|
501
|
+
errors: testInfo.errors,
|
|
505
502
|
hasNonRetriableError: testInfo._hasNonRetriableError,
|
|
506
503
|
expectedStatus: testInfo.expectedStatus,
|
|
507
504
|
annotations: testInfo.annotations,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.58.0-alpha-2025-12-
|
|
3
|
+
"version": "1.58.0-alpha-2025-12-06",
|
|
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.58.0-alpha-2025-12-
|
|
67
|
+
"playwright-core": "1.58.0-alpha-2025-12-06"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
var babelHighlightUtils_exports = {};
|
|
30
|
-
__export(babelHighlightUtils_exports, {
|
|
31
|
-
findTestEndLocation: () => findTestEndLocation
|
|
32
|
-
});
|
|
33
|
-
module.exports = __toCommonJS(babelHighlightUtils_exports);
|
|
34
|
-
var import_path = __toESM(require("path"));
|
|
35
|
-
var import_babelBundle = require("./babelBundle");
|
|
36
|
-
function containsLocation(range, location) {
|
|
37
|
-
if (location.line < range.start.line || location.line > range.end.line)
|
|
38
|
-
return false;
|
|
39
|
-
if (location.line === range.start.line && location.column < range.start.column)
|
|
40
|
-
return false;
|
|
41
|
-
if (location.line === range.end.line && location.column > range.end.column)
|
|
42
|
-
return false;
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
function findTestEndLocation(text, testStartLocation) {
|
|
46
|
-
const ast = (0, import_babelBundle.babelParse)(text, import_path.default.basename(testStartLocation.file), false);
|
|
47
|
-
let result;
|
|
48
|
-
(0, import_babelBundle.traverse)(ast, {
|
|
49
|
-
enter(path2) {
|
|
50
|
-
if (import_babelBundle.types.isCallExpression(path2.node) && path2.node.loc && containsLocation(path2.node.loc, testStartLocation)) {
|
|
51
|
-
const callNode = path2.node;
|
|
52
|
-
const funcNode = callNode.arguments[callNode.arguments.length - 1];
|
|
53
|
-
if (callNode.arguments.length >= 2 && import_babelBundle.types.isFunction(funcNode) && funcNode.body.loc)
|
|
54
|
-
result = { file: testStartLocation.file, ...funcNode.body.loc.end };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
61
|
-
0 && (module.exports = {
|
|
62
|
-
findTestEndLocation
|
|
63
|
-
});
|