playwright 1.58.0-alpha-2025-12-09 → 1.58.0-alpha-2025-12-10
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/index.js +10 -1
- 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/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 +29 -6
- package/lib/worker/workerMain.js +10 -3
- package/package.json +2 -2
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;
|
|
@@ -212,10 +212,19 @@ const playwrightFixtures = {
|
|
|
212
212
|
options.baseURL = baseURL;
|
|
213
213
|
if (serviceWorkers !== void 0)
|
|
214
214
|
options.serviceWorkers = serviceWorkers;
|
|
215
|
+
const workerFile = agent?.cacheFile && agent.cacheMode !== "ignore" ? await testInfo._cloneStorage(agent.cacheFile) : void 0;
|
|
216
|
+
if (agent && workerFile) {
|
|
217
|
+
options.agent = {
|
|
218
|
+
...agent,
|
|
219
|
+
cacheFile: workerFile
|
|
220
|
+
};
|
|
221
|
+
}
|
|
215
222
|
await use({
|
|
216
223
|
...contextOptions,
|
|
217
224
|
...options
|
|
218
225
|
});
|
|
226
|
+
if (workerFile)
|
|
227
|
+
await testInfo._upstreamStorage(workerFile);
|
|
219
228
|
}, { box: true }],
|
|
220
229
|
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
|
|
221
230
|
if (testIdAttribute)
|
|
@@ -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
|
});
|
|
@@ -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({
|
|
@@ -459,11 +482,11 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
459
482
|
setTimeout(timeout) {
|
|
460
483
|
this._timeoutManager.setTimeout(timeout);
|
|
461
484
|
}
|
|
462
|
-
async
|
|
463
|
-
return await this._callbacks.
|
|
485
|
+
async _cloneStorage(storageFile) {
|
|
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-10",
|
|
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-10"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|