playwright 1.58.0-alpha-2025-12-09 → 1.58.0-alpha-2025-12-11
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/lib/agents/agent.js +2 -1
- package/lib/agents/playwright-test-planner.agent.md +1 -0
- package/lib/index.js +42 -11
- package/lib/isomorphic/teleReceiver.js +14 -2
- package/lib/mcp/browser/config.js +1 -0
- package/lib/mcp/browser/context.js +0 -2
- package/lib/mcp/browser/response.js +43 -9
- package/lib/mcp/browser/tools/pdf.js +1 -2
- package/lib/mcp/browser/tools/runCode.js +8 -6
- package/lib/mcp/browser/tools/screenshot.js +1 -2
- package/lib/mcp/browser/tools/snapshot.js +10 -1
- package/lib/mcp/browser/tools/utils.js +28 -44
- package/lib/reporters/blob.js +3 -0
- package/lib/reporters/internalReporter.js +8 -0
- package/lib/reporters/multiplexer.js +8 -0
- package/lib/reporters/teleEmitter.js +23 -4
- package/lib/runner/dispatcher.js +18 -12
- package/lib/runner/storage.js +105 -0
- package/lib/runner/testRunner.js +2 -1
- package/lib/runner/workerHost.js +6 -0
- package/lib/transform/babelHighlightUtils.js +63 -0
- package/lib/worker/testInfo.js +36 -13
- package/lib/worker/workerMain.js +10 -3
- package/package.json +2 -2
- package/types/test.d.ts +1 -1
package/lib/agents/agent.js
CHANGED
|
@@ -33,7 +33,7 @@ class Agent {
|
|
|
33
33
|
const { clients, tools, callTool } = await this._initClients();
|
|
34
34
|
const prompt = this.spec.description;
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
const { result } = await this.loop.run(`${prompt}
|
|
37
37
|
|
|
38
38
|
Task:
|
|
39
39
|
${task}
|
|
@@ -45,6 +45,7 @@ ${JSON.stringify(params, null, 2)}`, {
|
|
|
45
45
|
callTool,
|
|
46
46
|
resultSchema: this.resultSchema
|
|
47
47
|
});
|
|
48
|
+
return result;
|
|
48
49
|
} finally {
|
|
49
50
|
await this._disconnectFromServers(clients);
|
|
50
51
|
}
|
|
@@ -17,6 +17,7 @@ tools:
|
|
|
17
17
|
- playwright-test/browser_navigate_back
|
|
18
18
|
- playwright-test/browser_network_requests
|
|
19
19
|
- playwright-test/browser_press_key
|
|
20
|
+
- playwright-test/browser_run_code
|
|
20
21
|
- playwright-test/browser_select_option
|
|
21
22
|
- playwright-test/browser_snapshot
|
|
22
23
|
- playwright-test/browser_take_screenshot
|
package/lib/index.js
CHANGED
|
@@ -164,7 +164,7 @@ const playwrightFixtures = {
|
|
|
164
164
|
baseURL,
|
|
165
165
|
contextOptions,
|
|
166
166
|
serviceWorkers
|
|
167
|
-
}, use) => {
|
|
167
|
+
}, use, testInfo) => {
|
|
168
168
|
const options = {};
|
|
169
169
|
if (acceptDownloads !== void 0)
|
|
170
170
|
options.acceptDownloads = acceptDownloads;
|
|
@@ -217,23 +217,21 @@ const playwrightFixtures = {
|
|
|
217
217
|
...options
|
|
218
218
|
});
|
|
219
219
|
}, { box: true }],
|
|
220
|
-
_setupContextOptions: [async ({ playwright,
|
|
220
|
+
_setupContextOptions: [async ({ playwright, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
|
|
221
221
|
if (testIdAttribute)
|
|
222
222
|
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
|
|
223
223
|
testInfo.snapshotSuffix = process.platform;
|
|
224
224
|
if ((0, import_utils.debugMode)() === "inspector")
|
|
225
225
|
testInfo._setDebugMode();
|
|
226
|
-
playwright._defaultContextOptions = _combinedContextOptions;
|
|
227
226
|
playwright._defaultContextTimeout = actionTimeout || 0;
|
|
228
227
|
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
|
|
229
228
|
await use();
|
|
230
|
-
playwright._defaultContextOptions = void 0;
|
|
231
229
|
playwright._defaultContextTimeout = void 0;
|
|
232
230
|
playwright._defaultContextNavigationTimeout = void 0;
|
|
233
231
|
}, { auto: "all-hooks-included", title: "context configuration", box: true }],
|
|
234
|
-
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
|
|
232
|
+
_setupArtifacts: [async ({ playwright, screenshot, _combinedContextOptions, agent }, use, testInfo) => {
|
|
235
233
|
testInfo.setTimeout(testInfo.project.timeout);
|
|
236
|
-
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
|
234
|
+
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, agent);
|
|
237
235
|
await artifactsRecorder.willStartTest(testInfo);
|
|
238
236
|
const tracingGroupSteps = [];
|
|
239
237
|
const csiListener = {
|
|
@@ -279,20 +277,33 @@ const playwrightFixtures = {
|
|
|
279
277
|
if (!keepTestTimeout)
|
|
280
278
|
(0, import_globals.currentTestInfo)()?._setDebugMode();
|
|
281
279
|
},
|
|
280
|
+
runBeforeCreateBrowserContext: async (options) => {
|
|
281
|
+
for (const [key, value] of Object.entries(_combinedContextOptions)) {
|
|
282
|
+
if (!(key in options))
|
|
283
|
+
options[key] = value;
|
|
284
|
+
}
|
|
285
|
+
await artifactsRecorder.willCreateBrowserContext(options);
|
|
286
|
+
},
|
|
287
|
+
runBeforeCreateRequestContext: async (options) => {
|
|
288
|
+
for (const [key, value] of Object.entries(_combinedContextOptions)) {
|
|
289
|
+
if (!(key in options))
|
|
290
|
+
options[key] = value;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
282
293
|
runAfterCreateBrowserContext: async (context) => {
|
|
283
|
-
await artifactsRecorder
|
|
294
|
+
await artifactsRecorder.didCreateBrowserContext(context);
|
|
284
295
|
const testInfo2 = (0, import_globals.currentTestInfo)();
|
|
285
296
|
if (testInfo2)
|
|
286
297
|
attachConnectedHeaderIfNeeded(testInfo2, context.browser());
|
|
287
298
|
},
|
|
288
299
|
runAfterCreateRequestContext: async (context) => {
|
|
289
|
-
await artifactsRecorder
|
|
300
|
+
await artifactsRecorder.didCreateRequestContext(context);
|
|
290
301
|
},
|
|
291
302
|
runBeforeCloseBrowserContext: async (context) => {
|
|
292
|
-
await artifactsRecorder
|
|
303
|
+
await artifactsRecorder.willCloseBrowserContext(context);
|
|
293
304
|
},
|
|
294
305
|
runBeforeCloseRequestContext: async (context) => {
|
|
295
|
-
await artifactsRecorder
|
|
306
|
+
await artifactsRecorder.willCloseRequestContext(context);
|
|
296
307
|
}
|
|
297
308
|
};
|
|
298
309
|
const clientInstrumentation = playwright._instrumentation;
|
|
@@ -551,9 +562,10 @@ class SnapshotRecorder {
|
|
|
551
562
|
}
|
|
552
563
|
}
|
|
553
564
|
class ArtifactsRecorder {
|
|
554
|
-
constructor(playwright, artifactsDir, screenshot) {
|
|
565
|
+
constructor(playwright, artifactsDir, screenshot, agent) {
|
|
555
566
|
this._playwright = playwright;
|
|
556
567
|
this._artifactsDir = artifactsDir;
|
|
568
|
+
this._agent = agent;
|
|
557
569
|
const screenshotOptions = typeof screenshot === "string" ? void 0 : screenshot;
|
|
558
570
|
this._startedCollectingArtifacts = Symbol("startedCollectingArtifacts");
|
|
559
571
|
this._screenshotRecorder = new SnapshotRecorder(this, normalizeScreenshotMode(screenshot), "screenshot", "image/png", ".png", async (page, path2) => {
|
|
@@ -573,10 +585,14 @@ class ArtifactsRecorder {
|
|
|
573
585
|
async didCreateBrowserContext(context) {
|
|
574
586
|
await this._startTraceChunkOnContextCreation(context, context.tracing);
|
|
575
587
|
}
|
|
588
|
+
async willCreateBrowserContext(options) {
|
|
589
|
+
await this._cloneAgentCache(options);
|
|
590
|
+
}
|
|
576
591
|
async willCloseBrowserContext(context) {
|
|
577
592
|
await this._stopTracing(context, context.tracing);
|
|
578
593
|
await this._screenshotRecorder.captureTemporary(context);
|
|
579
594
|
await this._takePageSnapshot(context);
|
|
595
|
+
await this._upstreamAgentCache(context);
|
|
580
596
|
}
|
|
581
597
|
async _takePageSnapshot(context) {
|
|
582
598
|
if (process.env.PLAYWRIGHT_NO_COPY_PROMPT)
|
|
@@ -595,6 +611,21 @@ class ArtifactsRecorder {
|
|
|
595
611
|
} catch {
|
|
596
612
|
}
|
|
597
613
|
}
|
|
614
|
+
async _cloneAgentCache(options) {
|
|
615
|
+
if (!this._agent || this._agent.cacheMode === "ignore")
|
|
616
|
+
return;
|
|
617
|
+
if (!this._agent.cacheFile && !this._agent.cachePathTemplate)
|
|
618
|
+
return;
|
|
619
|
+
const cacheFile = this._agent.cacheFile ?? this._testInfo._applyPathTemplate(this._agent.cachePathTemplate, "cache", ".json");
|
|
620
|
+
const workerFile = await this._testInfo._cloneStorage(cacheFile);
|
|
621
|
+
if (this._agent && workerFile)
|
|
622
|
+
options.agent = { ...this._agent, cacheFile: workerFile };
|
|
623
|
+
}
|
|
624
|
+
async _upstreamAgentCache(context) {
|
|
625
|
+
const agent = context._options.agent;
|
|
626
|
+
if (this._testInfo.status === "passed" && agent?.cacheFile)
|
|
627
|
+
await this._testInfo._upstreamStorage(agent.cacheFile);
|
|
628
|
+
}
|
|
598
629
|
async didCreateRequestContext(context) {
|
|
599
630
|
await this._startTraceChunkOnContextCreation(context, context._tracing);
|
|
600
631
|
}
|
|
@@ -58,6 +58,10 @@ class TeleReporterReceiver {
|
|
|
58
58
|
this._onTestBegin(params.testId, params.result);
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
+
if (method === "onTestPaused") {
|
|
62
|
+
this._onTestPaused(params.testId, params.resultId, params.stepId, params.errors);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
61
65
|
if (method === "onTestEnd") {
|
|
62
66
|
this._onTestEnd(params.test, params.result);
|
|
63
67
|
return;
|
|
@@ -116,6 +120,14 @@ class TeleReporterReceiver {
|
|
|
116
120
|
testResult.setStartTimeNumber(payload.startTime);
|
|
117
121
|
this._reporter.onTestBegin?.(test, testResult);
|
|
118
122
|
}
|
|
123
|
+
_onTestPaused(testId, resultId, stepId, errors) {
|
|
124
|
+
const test = this._tests.get(testId);
|
|
125
|
+
const result = test.results.find((r) => r._id === resultId);
|
|
126
|
+
const step = result._stepMap.get(stepId);
|
|
127
|
+
result.errors.push(...errors);
|
|
128
|
+
result.error = result.errors[0];
|
|
129
|
+
void this._reporter.onTestPaused?.(test, result, step);
|
|
130
|
+
}
|
|
119
131
|
_onTestEnd(testEndPayload, payload) {
|
|
120
132
|
const test = this._tests.get(testEndPayload.testId);
|
|
121
133
|
test.timeout = testEndPayload.timeout;
|
|
@@ -123,8 +135,8 @@ class TeleReporterReceiver {
|
|
|
123
135
|
const result = test.results.find((r) => r._id === payload.id);
|
|
124
136
|
result.duration = payload.duration;
|
|
125
137
|
result.status = payload.status;
|
|
126
|
-
result.errors
|
|
127
|
-
result.error = result.errors
|
|
138
|
+
result.errors.push(...payload.errors ?? []);
|
|
139
|
+
result.error = result.errors[0];
|
|
128
140
|
if (!!payload.attachments)
|
|
129
141
|
result.attachments = this._parseAttachments(payload.attachments);
|
|
130
142
|
if (payload.annotations) {
|
|
@@ -272,6 +272,7 @@ function outputDir(config, clientInfo) {
|
|
|
272
272
|
}
|
|
273
273
|
async function outputFile(config, clientInfo, fileName, options) {
|
|
274
274
|
const file = await resolveFile(config, clientInfo, fileName, options);
|
|
275
|
+
await import_fs.default.promises.mkdir(import_path.default.dirname(file), { recursive: true });
|
|
275
276
|
(0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
|
|
276
277
|
return file;
|
|
277
278
|
}
|
|
@@ -33,7 +33,6 @@ __export(context_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(context_exports);
|
|
35
35
|
var import_fs = __toESM(require("fs"));
|
|
36
|
-
var import_path = __toESM(require("path"));
|
|
37
36
|
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
|
|
38
37
|
var import_playwright_core = require("playwright-core");
|
|
39
38
|
var import_log = require("../log");
|
|
@@ -143,7 +142,6 @@ class Context {
|
|
|
143
142
|
await close(async () => {
|
|
144
143
|
for (const video of videos) {
|
|
145
144
|
const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
|
|
146
|
-
await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
|
|
147
145
|
const p = await video.path();
|
|
148
146
|
if (import_fs.default.existsSync(p)) {
|
|
149
147
|
try {
|
|
@@ -32,8 +32,10 @@ class Response {
|
|
|
32
32
|
this._result = [];
|
|
33
33
|
this._code = [];
|
|
34
34
|
this._images = [];
|
|
35
|
+
this._files = [];
|
|
35
36
|
this._includeSnapshot = "none";
|
|
36
37
|
this._includeTabs = false;
|
|
38
|
+
this._includeMetaOnly = false;
|
|
37
39
|
this._context = context;
|
|
38
40
|
this.toolName = toolName;
|
|
39
41
|
this.toolArgs = toolArgs;
|
|
@@ -63,6 +65,11 @@ class Response {
|
|
|
63
65
|
images() {
|
|
64
66
|
return this._images;
|
|
65
67
|
}
|
|
68
|
+
async addFile(fileName, options) {
|
|
69
|
+
const resolvedFile = await this._context.outputFile(fileName, options);
|
|
70
|
+
this._files.push({ fileName: resolvedFile, title: options.reason });
|
|
71
|
+
return resolvedFile;
|
|
72
|
+
}
|
|
66
73
|
setIncludeSnapshot() {
|
|
67
74
|
this._includeSnapshot = this._context.config.snapshot.mode;
|
|
68
75
|
}
|
|
@@ -75,6 +82,9 @@ class Response {
|
|
|
75
82
|
setIncludeModalStates(modalStates) {
|
|
76
83
|
this._includeModalStates = modalStates;
|
|
77
84
|
}
|
|
85
|
+
setIncludeMetaOnly() {
|
|
86
|
+
this._includeMetaOnly = true;
|
|
87
|
+
}
|
|
78
88
|
async finish() {
|
|
79
89
|
if (this._includeSnapshot !== "none" && this._context.currentTab())
|
|
80
90
|
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
|
|
@@ -90,9 +100,9 @@ class Response {
|
|
|
90
100
|
}
|
|
91
101
|
logEnd() {
|
|
92
102
|
if (requestDebug.enabled)
|
|
93
|
-
requestDebug(this.serialize(
|
|
103
|
+
requestDebug(this.serialize());
|
|
94
104
|
}
|
|
95
|
-
|
|
105
|
+
render() {
|
|
96
106
|
const renderedResponse = new RenderedResponse();
|
|
97
107
|
if (this._result.length)
|
|
98
108
|
renderedResponse.results.push(...this._result);
|
|
@@ -107,21 +117,34 @@ class Response {
|
|
|
107
117
|
const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._tabSnapshot.modalStates);
|
|
108
118
|
renderedResponse.states.modal = modalStatesMarkdown.join("\n");
|
|
109
119
|
} else if (this._tabSnapshot) {
|
|
110
|
-
|
|
111
|
-
renderTabSnapshot(this._tabSnapshot, includeSnapshot, renderedResponse);
|
|
120
|
+
renderTabSnapshot(this._tabSnapshot, this._includeSnapshot, renderedResponse);
|
|
112
121
|
} else if (this._includeModalStates) {
|
|
113
122
|
const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._includeModalStates);
|
|
114
123
|
renderedResponse.states.modal = modalStatesMarkdown.join("\n");
|
|
115
124
|
}
|
|
116
|
-
|
|
125
|
+
if (this._files.length) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
for (const file of this._files)
|
|
128
|
+
lines.push(`- [${file.title}](${file.fileName})`);
|
|
129
|
+
renderedResponse.updates.push({ category: "files", content: lines.join("\n") });
|
|
130
|
+
}
|
|
131
|
+
return this._context.config.secrets ? renderedResponse.redact(this._context.config.secrets) : renderedResponse;
|
|
132
|
+
}
|
|
133
|
+
serialize(options = {}) {
|
|
134
|
+
const renderedResponse = this.render();
|
|
117
135
|
const includeMeta = options._meta && "dev.lowire/history" in options._meta && "dev.lowire/state" in options._meta;
|
|
118
|
-
const _meta = includeMeta ?
|
|
136
|
+
const _meta = includeMeta ? renderedResponse.asMeta() : void 0;
|
|
119
137
|
const content = [
|
|
120
|
-
{
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: renderedResponse.asText(this._includeMetaOnly ? { categories: ["files"] } : void 0)
|
|
141
|
+
}
|
|
121
142
|
];
|
|
143
|
+
if (this._includeMetaOnly)
|
|
144
|
+
return { _meta, content, isError: this._isError };
|
|
122
145
|
if (this._context.config.imageResponses !== "omit") {
|
|
123
146
|
for (const image of this._images)
|
|
124
|
-
content.push({ type: "image", data:
|
|
147
|
+
content.push({ type: "image", data: image.data.toString("base64"), mimeType: image.contentType });
|
|
125
148
|
}
|
|
126
149
|
return {
|
|
127
150
|
_meta,
|
|
@@ -195,7 +218,7 @@ class RenderedResponse {
|
|
|
195
218
|
this.code = copy.code;
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
|
-
asText() {
|
|
221
|
+
asText(filter) {
|
|
199
222
|
const text = [];
|
|
200
223
|
if (this.results.length)
|
|
201
224
|
text.push(`### Result
|
|
@@ -206,6 +229,8 @@ ${this.results.join("\n")}
|
|
|
206
229
|
${this.code.join("\n")}
|
|
207
230
|
`);
|
|
208
231
|
for (const { category, content } of this.updates) {
|
|
232
|
+
if (filter && !filter.categories.includes(category))
|
|
233
|
+
continue;
|
|
209
234
|
if (!content.trim())
|
|
210
235
|
continue;
|
|
211
236
|
switch (category) {
|
|
@@ -217,11 +242,18 @@ ${content}
|
|
|
217
242
|
case "downloads":
|
|
218
243
|
text.push(`### Downloads
|
|
219
244
|
${content}
|
|
245
|
+
`);
|
|
246
|
+
break;
|
|
247
|
+
case "files":
|
|
248
|
+
text.push(`### Files
|
|
249
|
+
${content}
|
|
220
250
|
`);
|
|
221
251
|
break;
|
|
222
252
|
}
|
|
223
253
|
}
|
|
224
254
|
for (const [category, value] of Object.entries(this.states)) {
|
|
255
|
+
if (filter && !filter.categories.includes(category))
|
|
256
|
+
continue;
|
|
225
257
|
if (!value.trim())
|
|
226
258
|
continue;
|
|
227
259
|
switch (category) {
|
|
@@ -291,6 +323,7 @@ function parseResponse(response) {
|
|
|
291
323
|
const consoleMessages = sections.get("New console messages");
|
|
292
324
|
const modalState = sections.get("Modal state");
|
|
293
325
|
const downloads = sections.get("Downloads");
|
|
326
|
+
const files = sections.get("Files");
|
|
294
327
|
const codeNoFrame = code?.replace(/^```js\n/, "").replace(/\n```$/, "");
|
|
295
328
|
const isError = response.isError;
|
|
296
329
|
const attachments = response.content.slice(1);
|
|
@@ -302,6 +335,7 @@ function parseResponse(response) {
|
|
|
302
335
|
consoleMessages,
|
|
303
336
|
modalState,
|
|
304
337
|
downloads,
|
|
338
|
+
files,
|
|
305
339
|
isError,
|
|
306
340
|
attachments,
|
|
307
341
|
_meta: response._meta
|
|
@@ -48,9 +48,8 @@ const pdf = (0, import_tool.defineTabTool)({
|
|
|
48
48
|
type: "readOnly"
|
|
49
49
|
},
|
|
50
50
|
handle: async (tab, params, response) => {
|
|
51
|
-
const fileName = await
|
|
51
|
+
const fileName = await response.addFile(params.filename ?? (0, import_utils.dateAsFileName)("pdf"), { origin: "llm", reason: "Page saved as PDF" });
|
|
52
52
|
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
|
|
53
|
-
response.addResult(`Saved page as ${fileName}`);
|
|
54
53
|
await tab.page.pdf({ path: fileName });
|
|
55
54
|
}
|
|
56
55
|
});
|
|
@@ -36,7 +36,7 @@ var import_utils = require("playwright-core/lib/utils");
|
|
|
36
36
|
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
37
37
|
var import_tool = require("./tool");
|
|
38
38
|
const codeSchema = import_mcpBundle.z.object({
|
|
39
|
-
code: import_mcpBundle.z.string().describe(`Playwright code
|
|
39
|
+
code: import_mcpBundle.z.string().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``)
|
|
40
40
|
});
|
|
41
41
|
const runCode = (0, import_tool.defineTabTool)({
|
|
42
42
|
capability: "core",
|
|
@@ -49,7 +49,7 @@ const runCode = (0, import_tool.defineTabTool)({
|
|
|
49
49
|
},
|
|
50
50
|
handle: async (tab, params, response) => {
|
|
51
51
|
response.setIncludeSnapshot();
|
|
52
|
-
response.addCode(params.code);
|
|
52
|
+
response.addCode(`await (${params.code})(page);`);
|
|
53
53
|
const __end__ = new import_utils.ManualPromise();
|
|
54
54
|
const context = {
|
|
55
55
|
page: tab.page,
|
|
@@ -59,14 +59,16 @@ const runCode = (0, import_tool.defineTabTool)({
|
|
|
59
59
|
await tab.waitForCompletion(async () => {
|
|
60
60
|
const snippet = `(async () => {
|
|
61
61
|
try {
|
|
62
|
-
${params.code};
|
|
63
|
-
__end__.resolve();
|
|
62
|
+
const result = await (${params.code})(page);
|
|
63
|
+
__end__.resolve(JSON.stringify(result));
|
|
64
64
|
} catch (e) {
|
|
65
65
|
__end__.reject(e);
|
|
66
66
|
}
|
|
67
67
|
})()`;
|
|
68
|
-
import_vm.default.runInContext(snippet, context);
|
|
69
|
-
await __end__;
|
|
68
|
+
await import_vm.default.runInContext(snippet, context);
|
|
69
|
+
const result = await __end__;
|
|
70
|
+
if (typeof result === "string")
|
|
71
|
+
response.addResult(result);
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
});
|
|
@@ -61,7 +61,6 @@ const screenshot = (0, import_tool.defineTabTool)({
|
|
|
61
61
|
if (params.fullPage && params.ref)
|
|
62
62
|
throw new Error("fullPage cannot be used with element screenshots.");
|
|
63
63
|
const fileType = params.type || "png";
|
|
64
|
-
const fileName = await tab.context.outputFile(params.filename || (0, import_utils2.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
|
|
65
64
|
const options = {
|
|
66
65
|
type: fileType,
|
|
67
66
|
quality: fileType === "png" ? void 0 : 90,
|
|
@@ -70,6 +69,7 @@ const screenshot = (0, import_tool.defineTabTool)({
|
|
|
70
69
|
};
|
|
71
70
|
const isElementScreenshot = params.element && params.ref;
|
|
72
71
|
const screenshotTarget = isElementScreenshot ? params.element : params.fullPage ? "full page" : "viewport";
|
|
72
|
+
const fileName = await response.addFile(params.filename || (0, import_utils2.dateAsFileName)(fileType), { origin: "llm", reason: `Screenshot of ${screenshotTarget}` });
|
|
73
73
|
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
|
|
74
74
|
const ref = params.ref ? await tab.refLocator({ element: params.element || "", ref: params.ref }) : null;
|
|
75
75
|
if (ref)
|
|
@@ -79,7 +79,6 @@ const screenshot = (0, import_tool.defineTabTool)({
|
|
|
79
79
|
const buffer = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
|
|
80
80
|
await (0, import_utils.mkdirIfNeeded)(fileName);
|
|
81
81
|
await import_fs.default.promises.writeFile(fileName, buffer);
|
|
82
|
-
response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
|
|
83
82
|
response.addImage({
|
|
84
83
|
contentType: fileType === "png" ? "image/png" : "image/jpeg",
|
|
85
84
|
data: scaleImageToFitMessage(buffer, fileType)
|
|
@@ -32,6 +32,7 @@ __export(snapshot_exports, {
|
|
|
32
32
|
elementSchema: () => elementSchema
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(snapshot_exports);
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
35
36
|
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
36
37
|
var import_tool = require("./tool");
|
|
37
38
|
var javascript = __toESM(require("../codegen"));
|
|
@@ -41,12 +42,20 @@ const snapshot = (0, import_tool.defineTool)({
|
|
|
41
42
|
name: "browser_snapshot",
|
|
42
43
|
title: "Page snapshot",
|
|
43
44
|
description: "Capture accessibility snapshot of the current page, this is better than screenshot",
|
|
44
|
-
inputSchema: import_mcpBundle.z.object({
|
|
45
|
+
inputSchema: import_mcpBundle.z.object({
|
|
46
|
+
filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
|
|
47
|
+
}),
|
|
45
48
|
type: "readOnly"
|
|
46
49
|
},
|
|
47
50
|
handle: async (context, params, response) => {
|
|
48
51
|
await context.ensureTab();
|
|
49
52
|
response.setIncludeFullSnapshot();
|
|
53
|
+
if (params.filename) {
|
|
54
|
+
const renderedResponse = response.render();
|
|
55
|
+
const fileName = await response.addFile(params.filename, { origin: "llm", reason: "Saved snapshot" });
|
|
56
|
+
await import_fs.default.promises.writeFile(fileName, renderedResponse.asText());
|
|
57
|
+
response.setIncludeMetaOnly();
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
});
|
|
52
61
|
const elementSchema = import_mcpBundle.z.object({
|
|
@@ -24,55 +24,39 @@ __export(utils_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(utils_exports);
|
|
26
26
|
async function waitForCompletion(tab, callback) {
|
|
27
|
-
const requests =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
const waitBarrier = new Promise((f) => {
|
|
32
|
-
waitCallback = f;
|
|
33
|
-
});
|
|
34
|
-
const responseListener = (request) => {
|
|
35
|
-
requests.delete(request);
|
|
36
|
-
if (!requests.size)
|
|
37
|
-
waitCallback();
|
|
38
|
-
};
|
|
39
|
-
const requestListener = (request) => {
|
|
40
|
-
requests.add(request);
|
|
41
|
-
void request.response().then(() => responseListener(request)).catch(() => {
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
const frameNavigateListener = (frame) => {
|
|
45
|
-
if (frame.parentFrame())
|
|
46
|
-
return;
|
|
47
|
-
frameNavigated = true;
|
|
48
|
-
dispose();
|
|
49
|
-
clearTimeout(timeout);
|
|
50
|
-
void tab.waitForLoadState("load").then(waitCallback);
|
|
51
|
-
};
|
|
52
|
-
const onTimeout = () => {
|
|
53
|
-
dispose();
|
|
54
|
-
waitCallback();
|
|
55
|
-
};
|
|
56
|
-
tab.page.on("request", requestListener);
|
|
57
|
-
tab.page.on("requestfailed", responseListener);
|
|
58
|
-
tab.page.on("framenavigated", frameNavigateListener);
|
|
59
|
-
const timeout = setTimeout(onTimeout, 1e4);
|
|
60
|
-
const dispose = () => {
|
|
27
|
+
const requests = [];
|
|
28
|
+
const requestListener = (request) => requests.push(request);
|
|
29
|
+
const disposeListeners = () => {
|
|
61
30
|
tab.page.off("request", requestListener);
|
|
62
|
-
tab.page.off("requestfailed", responseListener);
|
|
63
|
-
tab.page.off("framenavigated", frameNavigateListener);
|
|
64
|
-
clearTimeout(timeout);
|
|
65
31
|
};
|
|
32
|
+
tab.page.on("request", requestListener);
|
|
33
|
+
let result;
|
|
66
34
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
waitCallback();
|
|
70
|
-
await waitBarrier;
|
|
71
|
-
await tab.waitForTimeout(1e3);
|
|
72
|
-
return result;
|
|
35
|
+
result = await callback();
|
|
36
|
+
await tab.waitForTimeout(500);
|
|
73
37
|
} finally {
|
|
74
|
-
|
|
38
|
+
disposeListeners();
|
|
39
|
+
}
|
|
40
|
+
const requestedNavigation = requests.some((request) => request.isNavigationRequest());
|
|
41
|
+
if (requestedNavigation) {
|
|
42
|
+
await tab.page.mainFrame().waitForLoadState("load", { timeout: 1e4 }).catch(() => {
|
|
43
|
+
});
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
const promises = [];
|
|
47
|
+
for (const request of requests) {
|
|
48
|
+
if (["document", "stylesheet", "script", "xhr", "fetch"].includes(request.resourceType()))
|
|
49
|
+
promises.push(request.response().then((r) => r?.finished()).catch(() => {
|
|
50
|
+
}));
|
|
51
|
+
else
|
|
52
|
+
promises.push(request.response().catch(() => {
|
|
53
|
+
}));
|
|
75
54
|
}
|
|
55
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
56
|
+
await Promise.race([Promise.all(promises), timeout]);
|
|
57
|
+
if (requests.length)
|
|
58
|
+
await tab.waitForTimeout(500);
|
|
59
|
+
return result;
|
|
76
60
|
}
|
|
77
61
|
async function callOnPageNoTrace(page, callback) {
|
|
78
62
|
return await page._wrapApiCall(() => callback(page), { internal: true });
|
package/lib/reporters/blob.js
CHANGED
|
@@ -68,6 +68,9 @@ class BlobReporter extends import_teleEmitter.TeleReporterEmitter {
|
|
|
68
68
|
this._config = config;
|
|
69
69
|
super.onConfigure(config);
|
|
70
70
|
}
|
|
71
|
+
async onTestPaused(test, result, step) {
|
|
72
|
+
return { action: void 0 };
|
|
73
|
+
}
|
|
71
74
|
async onEnd(result) {
|
|
72
75
|
await super.onEnd(result);
|
|
73
76
|
const zipFileName = await this._prepareOutputFile();
|
|
@@ -66,6 +66,12 @@ class InternalReporter {
|
|
|
66
66
|
onStdErr(chunk, test, result) {
|
|
67
67
|
this._reporter.onStdErr?.(chunk, test, result);
|
|
68
68
|
}
|
|
69
|
+
async onTestPaused(test, result, step) {
|
|
70
|
+
this._addSnippetToTestErrors(test, result);
|
|
71
|
+
if (step)
|
|
72
|
+
this._addSnippetToStepError(test, step);
|
|
73
|
+
return await this._reporter.onTestPaused?.(test, result, step) ?? { action: void 0 };
|
|
74
|
+
}
|
|
69
75
|
onTestEnd(test, result) {
|
|
70
76
|
this._addSnippetToTestErrors(test, result);
|
|
71
77
|
this._reporter.onTestEnd?.(test, result);
|
|
@@ -112,6 +118,8 @@ function addLocationAndSnippetToError(config, error, file) {
|
|
|
112
118
|
const location = error.location;
|
|
113
119
|
if (!location)
|
|
114
120
|
return;
|
|
121
|
+
if (!!error.snippet)
|
|
122
|
+
return;
|
|
115
123
|
try {
|
|
116
124
|
const tokens = [];
|
|
117
125
|
const source = import_fs.default.readFileSync(location.file, "utf8");
|
|
@@ -48,6 +48,14 @@ class Multiplexer {
|
|
|
48
48
|
for (const reporter of this._reporters)
|
|
49
49
|
wrap(() => reporter.onStdErr?.(chunk, test, result));
|
|
50
50
|
}
|
|
51
|
+
async onTestPaused(test, result, step) {
|
|
52
|
+
for (const reporter of this._reporters) {
|
|
53
|
+
const disposition = await wrapAsync(() => reporter.onTestPaused?.(test, result, step));
|
|
54
|
+
if (disposition?.action)
|
|
55
|
+
return disposition;
|
|
56
|
+
}
|
|
57
|
+
return { action: void 0 };
|
|
58
|
+
}
|
|
51
59
|
onTestEnd(test, result) {
|
|
52
60
|
for (const reporter of this._reporters)
|
|
53
61
|
wrap(() => reporter.onTestEnd?.(test, result));
|
|
@@ -37,7 +37,8 @@ var import_teleReceiver = require("../isomorphic/teleReceiver");
|
|
|
37
37
|
class TeleReporterEmitter {
|
|
38
38
|
constructor(messageSink, options = {}) {
|
|
39
39
|
this._resultKnownAttachmentCounts = /* @__PURE__ */ new Map();
|
|
40
|
-
|
|
40
|
+
this._resultKnownErrorCounts = /* @__PURE__ */ new Map();
|
|
41
|
+
// In case there is blob reporter and UI mode, make sure one doesn't override
|
|
41
42
|
// the id assigned by the other.
|
|
42
43
|
this._idSymbol = Symbol("id");
|
|
43
44
|
this._messageSink = messageSink;
|
|
@@ -66,6 +67,21 @@ class TeleReporterEmitter {
|
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
}
|
|
70
|
+
async onTestPaused(test, result, step) {
|
|
71
|
+
const resultId = result[this._idSymbol];
|
|
72
|
+
const stepId = step[this._idSymbol];
|
|
73
|
+
this._resultKnownErrorCounts.set(resultId, result.errors.length);
|
|
74
|
+
this._messageSink({
|
|
75
|
+
method: "onTestPaused",
|
|
76
|
+
params: {
|
|
77
|
+
testId: test.id,
|
|
78
|
+
resultId,
|
|
79
|
+
stepId,
|
|
80
|
+
errors: result.errors
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return { action: void 0 };
|
|
84
|
+
}
|
|
69
85
|
onTestEnd(test, result) {
|
|
70
86
|
const testEnd = {
|
|
71
87
|
testId: test.id,
|
|
@@ -81,7 +97,9 @@ class TeleReporterEmitter {
|
|
|
81
97
|
result: this._serializeResultEnd(result)
|
|
82
98
|
}
|
|
83
99
|
});
|
|
84
|
-
|
|
100
|
+
const resultId = result[this._idSymbol];
|
|
101
|
+
this._resultKnownAttachmentCounts.delete(resultId);
|
|
102
|
+
this._resultKnownErrorCounts.delete(resultId);
|
|
85
103
|
}
|
|
86
104
|
onStepBegin(test, result, step) {
|
|
87
105
|
step[this._idSymbol] = (0, import_utils.createGuid)();
|
|
@@ -221,11 +239,12 @@ class TeleReporterEmitter {
|
|
|
221
239
|
};
|
|
222
240
|
}
|
|
223
241
|
_serializeResultEnd(result) {
|
|
242
|
+
const id = result[this._idSymbol];
|
|
224
243
|
return {
|
|
225
|
-
id
|
|
244
|
+
id,
|
|
226
245
|
duration: result.duration,
|
|
227
246
|
status: result.status,
|
|
228
|
-
errors: result.errors,
|
|
247
|
+
errors: this._resultKnownErrorCounts.has(id) ? result.errors.slice(this._resultKnownAttachmentCounts.get(id)) : result.errors,
|
|
229
248
|
annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : void 0
|
|
230
249
|
};
|
|
231
250
|
}
|
package/lib/runner/dispatcher.js
CHANGED
|
@@ -28,6 +28,7 @@ var import_workerHost = require("./workerHost");
|
|
|
28
28
|
var import_ipc = require("../common/ipc");
|
|
29
29
|
var import_internalReporter = require("../reporters/internalReporter");
|
|
30
30
|
var import_util = require("../util");
|
|
31
|
+
var import_storage = require("./storage");
|
|
31
32
|
class Dispatcher {
|
|
32
33
|
constructor(config, reporter, failureTracker) {
|
|
33
34
|
this._workerSlots = [];
|
|
@@ -197,11 +198,11 @@ class Dispatcher {
|
|
|
197
198
|
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
|
|
198
199
|
this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() });
|
|
199
200
|
});
|
|
200
|
-
worker.onRequest("
|
|
201
|
-
|
|
201
|
+
worker.onRequest("cloneStorage", async (params) => {
|
|
202
|
+
return await import_storage.Storage.clone(params.storageFile, worker.artifactsDir());
|
|
202
203
|
});
|
|
203
|
-
worker.onRequest("
|
|
204
|
-
|
|
204
|
+
worker.onRequest("upstreamStorage", async (params) => {
|
|
205
|
+
await import_storage.Storage.upstream(params.workerFile);
|
|
205
206
|
});
|
|
206
207
|
return worker;
|
|
207
208
|
}
|
|
@@ -215,11 +216,6 @@ class Dispatcher {
|
|
|
215
216
|
await Promise.all(this._workerSlots.map(({ worker }) => worker?.stop()));
|
|
216
217
|
this._checkFinished();
|
|
217
218
|
}
|
|
218
|
-
_setStorageValue(fileName, key, value) {
|
|
219
|
-
}
|
|
220
|
-
_getStorageValue(fileName, key) {
|
|
221
|
-
return {};
|
|
222
|
-
}
|
|
223
219
|
}
|
|
224
220
|
class JobDispatcher {
|
|
225
221
|
constructor(job, config, reporter, failureTracker, stopCallback) {
|
|
@@ -469,11 +465,18 @@ class JobDispatcher {
|
|
|
469
465
|
];
|
|
470
466
|
}
|
|
471
467
|
_onTestPaused(worker, params) {
|
|
468
|
+
const data = this._dataByTestId.get(params.testId);
|
|
469
|
+
if (!data)
|
|
470
|
+
return;
|
|
471
|
+
const { result, test, steps } = data;
|
|
472
|
+
const step = steps.get(params.stepId);
|
|
473
|
+
if (!step)
|
|
474
|
+
return;
|
|
472
475
|
const sendMessage = async (message) => {
|
|
473
476
|
try {
|
|
474
477
|
if (this.jobResult.isDone())
|
|
475
478
|
throw new Error("Test has already stopped");
|
|
476
|
-
const response = await worker.sendCustomMessage({ testId:
|
|
479
|
+
const response = await worker.sendCustomMessage({ testId: test.id, request: message.request });
|
|
477
480
|
if (response.error)
|
|
478
481
|
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
|
|
479
482
|
return response;
|
|
@@ -483,8 +486,11 @@ class JobDispatcher {
|
|
|
483
486
|
return { response: void 0, error };
|
|
484
487
|
}
|
|
485
488
|
};
|
|
486
|
-
|
|
487
|
-
|
|
489
|
+
result.errors = params.errors;
|
|
490
|
+
result.error = result.errors[0];
|
|
491
|
+
void this._reporter.onTestPaused?.(test, result, step).then((params2) => {
|
|
492
|
+
worker.sendResume(params2);
|
|
493
|
+
});
|
|
488
494
|
this._failureTracker.onTestPaused?.({ ...params, sendMessage });
|
|
489
495
|
}
|
|
490
496
|
skipWholeJob() {
|
|
@@ -0,0 +1,105 @@
|
|
|
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 storage_exports = {};
|
|
30
|
+
__export(storage_exports, {
|
|
31
|
+
Storage: () => Storage
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(storage_exports);
|
|
34
|
+
var import_fs = __toESM(require("fs"));
|
|
35
|
+
var import_path = __toESM(require("path"));
|
|
36
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
37
|
+
class Storage {
|
|
38
|
+
constructor(fileName) {
|
|
39
|
+
this._writeChain = Promise.resolve();
|
|
40
|
+
this._fileName = fileName;
|
|
41
|
+
}
|
|
42
|
+
static {
|
|
43
|
+
this._storages = /* @__PURE__ */ new Map();
|
|
44
|
+
}
|
|
45
|
+
static {
|
|
46
|
+
this._workerFiles = /* @__PURE__ */ new Map();
|
|
47
|
+
}
|
|
48
|
+
static async clone(storageFile, artifactsDir) {
|
|
49
|
+
const workerFile = await this._storage(storageFile)._clone(artifactsDir);
|
|
50
|
+
const stat = await import_fs.default.promises.stat(workerFile);
|
|
51
|
+
const lastModified = stat.mtime.getTime();
|
|
52
|
+
Storage._workerFiles.set(workerFile, { storageFile, lastModified });
|
|
53
|
+
return workerFile;
|
|
54
|
+
}
|
|
55
|
+
static async upstream(workerFile) {
|
|
56
|
+
const entry = Storage._workerFiles.get(workerFile);
|
|
57
|
+
if (!entry)
|
|
58
|
+
return;
|
|
59
|
+
const { storageFile, lastModified } = entry;
|
|
60
|
+
const stat = await import_fs.default.promises.stat(workerFile);
|
|
61
|
+
const newLastModified = stat.mtime.getTime();
|
|
62
|
+
if (lastModified !== newLastModified)
|
|
63
|
+
await this._storage(storageFile)._upstream(workerFile);
|
|
64
|
+
Storage._workerFiles.delete(workerFile);
|
|
65
|
+
}
|
|
66
|
+
static _storage(fileName) {
|
|
67
|
+
if (!Storage._storages.has(fileName))
|
|
68
|
+
Storage._storages.set(fileName, new Storage(fileName));
|
|
69
|
+
return Storage._storages.get(fileName);
|
|
70
|
+
}
|
|
71
|
+
async _clone(artifactsDir) {
|
|
72
|
+
const entries = await this._load();
|
|
73
|
+
const workerFile = import_path.default.join(artifactsDir, `pw-storage-${(0, import_utils.createGuid)()}.json`);
|
|
74
|
+
await import_fs.default.promises.writeFile(workerFile, JSON.stringify(entries, null, 2)).catch(() => {
|
|
75
|
+
});
|
|
76
|
+
return workerFile;
|
|
77
|
+
}
|
|
78
|
+
async _upstream(workerFile) {
|
|
79
|
+
const entries = await this._load();
|
|
80
|
+
const newEntries = await import_fs.default.promises.readFile(workerFile, "utf8").then(JSON.parse).catch(() => ({}));
|
|
81
|
+
for (const [key, newValue] of Object.entries(newEntries)) {
|
|
82
|
+
const existing = entries[key];
|
|
83
|
+
if (!existing || existing.timestamp < newValue.timestamp)
|
|
84
|
+
entries[key] = newValue;
|
|
85
|
+
}
|
|
86
|
+
await this._writeFile(entries);
|
|
87
|
+
}
|
|
88
|
+
async _load() {
|
|
89
|
+
if (!this._entriesPromise)
|
|
90
|
+
this._entriesPromise = import_fs.default.promises.readFile(this._fileName, "utf8").then(JSON.parse).catch(() => ({}));
|
|
91
|
+
return this._entriesPromise;
|
|
92
|
+
}
|
|
93
|
+
_writeFile(entries) {
|
|
94
|
+
this._writeChain = this._writeChain.then(() => import_fs.default.promises.writeFile(this._fileName, JSON.stringify(entries, null, 2))).catch(() => {
|
|
95
|
+
});
|
|
96
|
+
return this._writeChain;
|
|
97
|
+
}
|
|
98
|
+
async flush() {
|
|
99
|
+
await this._writeChain;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
103
|
+
0 && (module.exports = {
|
|
104
|
+
Storage
|
|
105
|
+
});
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -383,7 +383,8 @@ 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
|
|
386
|
+
const testRun = new import_tasks.TestRun(config, reporter, { pauseAtEnd: config.configCLIOverrides.debug, pauseOnError: config.configCLIOverrides.debug });
|
|
387
|
+
const status = await (0, import_tasks.runTasks)(testRun, tasks, config.config.globalTimeout);
|
|
387
388
|
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
388
389
|
await new Promise((resolve) => process.stderr.write("", () => resolve()));
|
|
389
390
|
return status;
|
package/lib/runner/workerHost.js
CHANGED
|
@@ -61,6 +61,9 @@ class WorkerHost extends import_processHost.ProcessHost {
|
|
|
61
61
|
pauseAtEnd: options.pauseAtEnd
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
artifactsDir() {
|
|
65
|
+
return this._params.artifactsDir;
|
|
66
|
+
}
|
|
64
67
|
async start() {
|
|
65
68
|
await import_fs.default.promises.mkdir(this._params.artifactsDir, { recursive: true });
|
|
66
69
|
return await this.startRunner(this._params, {
|
|
@@ -82,6 +85,9 @@ class WorkerHost extends import_processHost.ProcessHost {
|
|
|
82
85
|
async sendCustomMessage(payload) {
|
|
83
86
|
return await this.sendMessage({ method: "customMessage", params: payload });
|
|
84
87
|
}
|
|
88
|
+
sendResume(payload) {
|
|
89
|
+
this.sendMessageNoReply({ method: "resume", params: payload });
|
|
90
|
+
}
|
|
85
91
|
hash() {
|
|
86
92
|
return this._hash;
|
|
87
93
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
});
|
package/lib/worker/testInfo.js
CHANGED
|
@@ -42,6 +42,7 @@ 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");
|
|
45
46
|
class TestInfoImpl {
|
|
46
47
|
constructor(configInternal, projectInternal, workerParams, test, retry, callbacks) {
|
|
47
48
|
this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
@@ -335,11 +336,33 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
335
336
|
async _didFinishTestFunction() {
|
|
336
337
|
const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure();
|
|
337
338
|
if (shouldPause) {
|
|
338
|
-
this.
|
|
339
|
-
|
|
339
|
+
const location = (this._isFailure() ? this._errorLocation() : await this._testEndLocation()) ?? { file: this.file, line: this.line, column: this.column };
|
|
340
|
+
const step = this._addStep({ category: "hook", title: "Paused", location });
|
|
341
|
+
const result = await Promise.race([
|
|
342
|
+
this._callbacks.onTestPaused({ testId: this.testId, stepId: step.stepId, errors: this._isFailure() ? this.errors : [] }),
|
|
343
|
+
this._interruptedPromise.then(() => "interrupted")
|
|
344
|
+
]);
|
|
345
|
+
step.complete({});
|
|
346
|
+
if (result !== "interrupted") {
|
|
347
|
+
if (result.action === "abort")
|
|
348
|
+
this._interrupt();
|
|
349
|
+
if (result.action === void 0)
|
|
350
|
+
await this._interruptedPromise;
|
|
351
|
+
}
|
|
340
352
|
}
|
|
341
353
|
await this._onDidFinishTestFunctionCallback?.();
|
|
342
354
|
}
|
|
355
|
+
_errorLocation() {
|
|
356
|
+
if (this.error?.stack)
|
|
357
|
+
return (0, import_util.filteredStackTrace)(this.error.stack.split("\n"))[0];
|
|
358
|
+
}
|
|
359
|
+
async _testEndLocation() {
|
|
360
|
+
try {
|
|
361
|
+
const source = await import_fs.default.promises.readFile(this.file, "utf-8");
|
|
362
|
+
return (0, import_babelHighlightUtils.findTestEndLocation)(source, { file: this.file, line: this.line, column: this.column });
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
343
366
|
// ------------ TestInfo methods ------------
|
|
344
367
|
async attach(name, options = {}) {
|
|
345
368
|
const step = this._addStep({
|
|
@@ -422,10 +445,6 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
422
445
|
if (index > 1)
|
|
423
446
|
relativeOutputPath = (0, import_util.addSuffixToFilePath)(relativeOutputPath, `-${index - 1}`);
|
|
424
447
|
}
|
|
425
|
-
const absoluteSnapshotPath = this._applyPathTemplate(kind, subPath, ext);
|
|
426
|
-
return { absoluteSnapshotPath, relativeOutputPath };
|
|
427
|
-
}
|
|
428
|
-
_applyPathTemplate(kind, relativePath, ext) {
|
|
429
448
|
const legacyTemplate = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
|
|
430
449
|
let template;
|
|
431
450
|
if (kind === "screenshot") {
|
|
@@ -436,12 +455,15 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
436
455
|
} else {
|
|
437
456
|
template = this._projectInternal.snapshotPathTemplate || legacyTemplate;
|
|
438
457
|
}
|
|
439
|
-
const
|
|
440
|
-
const
|
|
458
|
+
const nameArgument = import_path.default.join(import_path.default.dirname(subPath), import_path.default.basename(subPath, ext));
|
|
459
|
+
const absoluteSnapshotPath = this._applyPathTemplate(template, nameArgument, ext);
|
|
460
|
+
return { absoluteSnapshotPath, relativeOutputPath };
|
|
461
|
+
}
|
|
462
|
+
_applyPathTemplate(template, nameArgument, ext) {
|
|
441
463
|
const relativeTestFilePath = import_path.default.relative(this.project.testDir, this._requireFile);
|
|
442
464
|
const parsedRelativeTestFilePath = import_path.default.parse(relativeTestFilePath);
|
|
443
465
|
const projectNamePathSegment = (0, import_utils.sanitizeForFilePath)(this.project.name);
|
|
444
|
-
const snapshotPath = template.replace(/\{(.)?testDir\}/g, "$1" + this.project.testDir).replace(/\{(.)?snapshotDir\}/g, "$1" + this.project.snapshotDir).replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? "$1" + this.snapshotSuffix : "").replace(/\{(.)?testFileDir\}/g, "$1" + parsedRelativeTestFilePath.dir).replace(/\{(.)?platform\}/g, "$1" + process.platform).replace(/\{(.)?projectName\}/g, projectNamePathSegment ? "$1" + projectNamePathSegment : "").replace(/\{(.)?testName\}/g, "$1" + this._fsSanitizedTestName()).replace(/\{(.)?testFileName\}/g, "$1" + parsedRelativeTestFilePath.base).replace(/\{(.)?testFilePath\}/g, "$1" + relativeTestFilePath).replace(/\{(.)?arg\}/g, "$1" +
|
|
466
|
+
const snapshotPath = template.replace(/\{(.)?testDir\}/g, "$1" + this.project.testDir).replace(/\{(.)?snapshotDir\}/g, "$1" + this.project.snapshotDir).replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? "$1" + this.snapshotSuffix : "").replace(/\{(.)?testFileDir\}/g, "$1" + parsedRelativeTestFilePath.dir).replace(/\{(.)?platform\}/g, "$1" + process.platform).replace(/\{(.)?projectName\}/g, projectNamePathSegment ? "$1" + projectNamePathSegment : "").replace(/\{(.)?testName\}/g, "$1" + this._fsSanitizedTestName()).replace(/\{(.)?testFileName\}/g, "$1" + parsedRelativeTestFilePath.base).replace(/\{(.)?testFilePath\}/g, "$1" + relativeTestFilePath).replace(/\{(.)?arg\}/g, "$1" + nameArgument).replace(/\{(.)?ext\}/g, ext ? "$1" + ext : "");
|
|
445
467
|
return import_path.default.normalize(import_path.default.resolve(this._configInternal.configDir, snapshotPath));
|
|
446
468
|
}
|
|
447
469
|
snapshotPath(...args) {
|
|
@@ -459,11 +481,12 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
459
481
|
setTimeout(timeout) {
|
|
460
482
|
this._timeoutManager.setTimeout(timeout);
|
|
461
483
|
}
|
|
462
|
-
async
|
|
463
|
-
|
|
484
|
+
async _cloneStorage(cacheFileTemplate) {
|
|
485
|
+
const storageFile = this._applyPathTemplate(cacheFileTemplate, "cache", ".json");
|
|
486
|
+
return await this._callbacks.onCloneStorage?.({ storageFile });
|
|
464
487
|
}
|
|
465
|
-
|
|
466
|
-
this._callbacks.
|
|
488
|
+
async _upstreamStorage(workerFile) {
|
|
489
|
+
await this._callbacks.onUpstreamStorage?.({ workerFile });
|
|
467
490
|
}
|
|
468
491
|
}
|
|
469
492
|
class TestStepInfoImpl {
|
package/lib/worker/workerMain.js
CHANGED
|
@@ -229,14 +229,21 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
229
229
|
return { response: {}, error: (0, import_util2.testInfoError)(error) };
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
+
resume(payload) {
|
|
233
|
+
this._resumePromise?.resolve(payload);
|
|
234
|
+
}
|
|
232
235
|
async _runTest(test, retry, nextTest) {
|
|
233
236
|
const testInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, test, retry, {
|
|
234
237
|
onStepBegin: (payload) => this.dispatchEvent("stepBegin", payload),
|
|
235
238
|
onStepEnd: (payload) => this.dispatchEvent("stepEnd", payload),
|
|
236
239
|
onAttach: (payload) => this.dispatchEvent("attach", payload),
|
|
237
|
-
onTestPaused: (payload) =>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
onTestPaused: (payload) => {
|
|
241
|
+
this._resumePromise = new import_utils.ManualPromise();
|
|
242
|
+
this.dispatchEvent("testPaused", payload);
|
|
243
|
+
return this._resumePromise;
|
|
244
|
+
},
|
|
245
|
+
onCloneStorage: async (payload) => this.sendRequest("cloneStorage", payload),
|
|
246
|
+
onUpstreamStorage: (payload) => this.sendRequest("upstreamStorage", payload)
|
|
240
247
|
});
|
|
241
248
|
const processAnnotation = (annotation) => {
|
|
242
249
|
testInfo.annotations.push(annotation);
|
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-11",
|
|
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-11"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|
package/types/test.d.ts
CHANGED
|
@@ -6644,7 +6644,7 @@ export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, P
|
|
|
6644
6644
|
[K in Exclude<keyof T, keyof PW | keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }];
|
|
6645
6645
|
};
|
|
6646
6646
|
|
|
6647
|
-
type Agent = Exclude<BrowserContextOptions['agent'], undefined> | undefined;
|
|
6647
|
+
type Agent = Exclude<BrowserContextOptions['agent'], undefined> & { cachePathTemplate?: string } | undefined;
|
|
6648
6648
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
|
6649
6649
|
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
|
|
6650
6650
|
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
|