playwright 1.58.0-alpha-2025-12-08 → 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/README.md +2 -2
- package/lib/common/process.js +28 -0
- 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 +20 -3
- package/lib/runner/processHost.js +16 -0
- 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 -10
- package/lib/worker/workerMain.js +16 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🎭 Playwright
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
|
4
4
|
|
|
5
5
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|
|
10
10
|
| :--- | :---: | :---: | :---: |
|
|
11
11
|
| Chromium <!-- GEN:chromium-version -->143.0.7499.25<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
12
12
|
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
13
|
-
| Firefox <!-- GEN:firefox-version -->145.0.
|
|
13
|
+
| Firefox <!-- GEN:firefox-version -->145.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
14
14
|
|
|
15
15
|
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
|
16
16
|
|
package/lib/common/process.js
CHANGED
|
@@ -30,6 +30,13 @@ class ProcessRunner {
|
|
|
30
30
|
const response = { method, params };
|
|
31
31
|
sendMessageToParent({ method: "__dispatch__", params: response });
|
|
32
32
|
}
|
|
33
|
+
async sendRequest(method, params) {
|
|
34
|
+
return await sendRequestToParent(method, params);
|
|
35
|
+
}
|
|
36
|
+
async sendMessageNoReply(method, params) {
|
|
37
|
+
void sendRequestToParent(method, params).catch(() => {
|
|
38
|
+
});
|
|
39
|
+
}
|
|
33
40
|
}
|
|
34
41
|
let gracefullyCloseCalled = false;
|
|
35
42
|
let forceExitInitiated = false;
|
|
@@ -70,6 +77,8 @@ process.on("message", async (message) => {
|
|
|
70
77
|
sendMessageToParent({ method: "__dispatch__", params: response });
|
|
71
78
|
}
|
|
72
79
|
}
|
|
80
|
+
if (message.method === "__response__")
|
|
81
|
+
handleResponseFromParent(message.params);
|
|
73
82
|
});
|
|
74
83
|
const kForceExitTimeout = +(process.env.PWTEST_FORCE_EXIT_TIMEOUT || 3e4);
|
|
75
84
|
async function gracefullyCloseAndExit(forceExit) {
|
|
@@ -98,6 +107,25 @@ function sendMessageToParent(message) {
|
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
}
|
|
110
|
+
let lastId = 0;
|
|
111
|
+
const requestCallbacks = /* @__PURE__ */ new Map();
|
|
112
|
+
async function sendRequestToParent(method, params) {
|
|
113
|
+
const id = ++lastId;
|
|
114
|
+
sendMessageToParent({ method: "__request__", params: { id, method, params } });
|
|
115
|
+
const promise = new import_utils.ManualPromise();
|
|
116
|
+
requestCallbacks.set(id, promise);
|
|
117
|
+
return promise;
|
|
118
|
+
}
|
|
119
|
+
function handleResponseFromParent(response) {
|
|
120
|
+
const promise = requestCallbacks.get(response.id);
|
|
121
|
+
if (!promise)
|
|
122
|
+
return;
|
|
123
|
+
requestCallbacks.delete(response.id);
|
|
124
|
+
if (response.error)
|
|
125
|
+
promise.reject(new Error(response.error.message));
|
|
126
|
+
else
|
|
127
|
+
promise.resolve(response.result);
|
|
128
|
+
}
|
|
101
129
|
// Annotate the CommonJS export names for ESM import in node:
|
|
102
130
|
0 && (module.exports = {
|
|
103
131
|
ProcessRunner
|
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,6 +198,12 @@ class Dispatcher {
|
|
|
197
198
|
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
|
|
198
199
|
this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() });
|
|
199
200
|
});
|
|
201
|
+
worker.onRequest("cloneStorage", async (params) => {
|
|
202
|
+
return await import_storage.Storage.clone(params.storageFile, worker.artifactsDir());
|
|
203
|
+
});
|
|
204
|
+
worker.onRequest("upstreamStorage", async (params) => {
|
|
205
|
+
await import_storage.Storage.upstream(params.workerFile);
|
|
206
|
+
});
|
|
200
207
|
return worker;
|
|
201
208
|
}
|
|
202
209
|
producedEnvByProjectId() {
|
|
@@ -458,11 +465,18 @@ class JobDispatcher {
|
|
|
458
465
|
];
|
|
459
466
|
}
|
|
460
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;
|
|
461
475
|
const sendMessage = async (message) => {
|
|
462
476
|
try {
|
|
463
477
|
if (this.jobResult.isDone())
|
|
464
478
|
throw new Error("Test has already stopped");
|
|
465
|
-
const response = await worker.sendCustomMessage({ testId:
|
|
479
|
+
const response = await worker.sendCustomMessage({ testId: test.id, request: message.request });
|
|
466
480
|
if (response.error)
|
|
467
481
|
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
|
|
468
482
|
return response;
|
|
@@ -472,8 +486,11 @@ class JobDispatcher {
|
|
|
472
486
|
return { response: void 0, error };
|
|
473
487
|
}
|
|
474
488
|
};
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
});
|
|
477
494
|
this._failureTracker.onTestPaused?.({ ...params, sendMessage });
|
|
478
495
|
}
|
|
479
496
|
skipWholeJob() {
|
|
@@ -44,6 +44,7 @@ class ProcessHost extends import_events.EventEmitter {
|
|
|
44
44
|
this._lastMessageId = 0;
|
|
45
45
|
this._callbacks = /* @__PURE__ */ new Map();
|
|
46
46
|
this._producedEnv = {};
|
|
47
|
+
this._requestHandlers = /* @__PURE__ */ new Map();
|
|
47
48
|
this._runnerScript = runnerScript;
|
|
48
49
|
this._processName = processName;
|
|
49
50
|
this._extraEnv = env;
|
|
@@ -95,6 +96,18 @@ class ProcessHost extends import_events.EventEmitter {
|
|
|
95
96
|
} else {
|
|
96
97
|
this.emit(method, params);
|
|
97
98
|
}
|
|
99
|
+
} else if (message.method === "__request__") {
|
|
100
|
+
const { id, method, params } = message.params;
|
|
101
|
+
const handler = this._requestHandlers.get(method);
|
|
102
|
+
if (!handler) {
|
|
103
|
+
this.send({ method: "__response__", params: { id, error: { message: "Unknown method" } } });
|
|
104
|
+
} else {
|
|
105
|
+
handler(params).then((result) => {
|
|
106
|
+
this.send({ method: "__response__", params: { id, result } });
|
|
107
|
+
}).catch((error2) => {
|
|
108
|
+
this.send({ method: "__response__", params: { id, error: { message: error2.message } } });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
98
111
|
} else {
|
|
99
112
|
this.emit(message.method, message.params);
|
|
100
113
|
}
|
|
@@ -138,6 +151,9 @@ class ProcessHost extends import_events.EventEmitter {
|
|
|
138
151
|
}
|
|
139
152
|
async onExit() {
|
|
140
153
|
}
|
|
154
|
+
onRequest(method, handler) {
|
|
155
|
+
this._requestHandlers.set(method, handler);
|
|
156
|
+
}
|
|
141
157
|
async stop() {
|
|
142
158
|
if (!this._processDidExit && !this._didSendStop) {
|
|
143
159
|
this.send({ method: "__stop__" });
|
|
@@ -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,8 +42,9 @@ 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
|
-
constructor(configInternal, projectInternal, workerParams, test, retry,
|
|
47
|
+
constructor(configInternal, projectInternal, workerParams, test, retry, callbacks) {
|
|
47
48
|
this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
48
49
|
this._ariaSnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
49
50
|
this._interruptedPromise = new import_utils.ManualPromise();
|
|
@@ -60,10 +61,7 @@ class TestInfoImpl {
|
|
|
60
61
|
this.snapshotSuffix = "";
|
|
61
62
|
this.errors = [];
|
|
62
63
|
this.testId = test?.id ?? "";
|
|
63
|
-
this.
|
|
64
|
-
this._onStepEnd = onStepEnd;
|
|
65
|
-
this._onAttach = onAttach;
|
|
66
|
-
this._onTestPaused = onTestPaused;
|
|
64
|
+
this._callbacks = callbacks;
|
|
67
65
|
this._startTime = (0, import_utils.monotonicTime)();
|
|
68
66
|
this._startWallTime = Date.now();
|
|
69
67
|
this._requireFile = test?._requireFile ?? "";
|
|
@@ -240,7 +238,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
240
238
|
suggestedRebaseline: result.suggestedRebaseline,
|
|
241
239
|
annotations: step.info.annotations
|
|
242
240
|
};
|
|
243
|
-
this.
|
|
241
|
+
this._callbacks.onStepEnd?.(payload);
|
|
244
242
|
}
|
|
245
243
|
if (step.group !== "internal") {
|
|
246
244
|
const errorForTrace = step.error ? { name: "", message: step.error.message || "", stack: step.error.stack } : void 0;
|
|
@@ -262,7 +260,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
262
260
|
wallTime: Date.now(),
|
|
263
261
|
location: step.location
|
|
264
262
|
};
|
|
265
|
-
this.
|
|
263
|
+
this._callbacks.onStepBegin?.(payload);
|
|
266
264
|
}
|
|
267
265
|
if (step.group !== "internal") {
|
|
268
266
|
this._tracing.appendBeforeActionForStep({
|
|
@@ -338,11 +336,33 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
338
336
|
async _didFinishTestFunction() {
|
|
339
337
|
const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure();
|
|
340
338
|
if (shouldPause) {
|
|
341
|
-
this.
|
|
342
|
-
|
|
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
|
+
}
|
|
343
352
|
}
|
|
344
353
|
await this._onDidFinishTestFunctionCallback?.();
|
|
345
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
|
+
}
|
|
346
366
|
// ------------ TestInfo methods ------------
|
|
347
367
|
async attach(name, options = {}) {
|
|
348
368
|
const step = this._addStep({
|
|
@@ -367,7 +387,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
367
387
|
this._tracing.appendBeforeActionForStep({ stepId: stepId2, title: `Attach ${(0, import_utils.escapeWithQuotes)(attachment.name, '"')}`, category: "test.attach", stack: [] });
|
|
368
388
|
this._tracing.appendAfterActionForStep(stepId2, void 0, [attachment]);
|
|
369
389
|
}
|
|
370
|
-
this.
|
|
390
|
+
this._callbacks.onAttach?.({
|
|
371
391
|
testId: this.testId,
|
|
372
392
|
name: attachment.name,
|
|
373
393
|
contentType: attachment.contentType,
|
|
@@ -462,6 +482,12 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
462
482
|
setTimeout(timeout) {
|
|
463
483
|
this._timeoutManager.setTimeout(timeout);
|
|
464
484
|
}
|
|
485
|
+
async _cloneStorage(storageFile) {
|
|
486
|
+
return await this._callbacks.onCloneStorage?.({ storageFile });
|
|
487
|
+
}
|
|
488
|
+
async _upstreamStorage(workerFile) {
|
|
489
|
+
await this._callbacks.onUpstreamStorage?.({ workerFile });
|
|
490
|
+
}
|
|
465
491
|
}
|
|
466
492
|
class TestStepInfoImpl {
|
|
467
493
|
constructor(testInfo, stepId, title, parentStep) {
|
package/lib/worker/workerMain.js
CHANGED
|
@@ -95,11 +95,7 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
95
95
|
if (!this._config) {
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
-
const fakeTestInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, void 0, 0,
|
|
99
|
-
}, () => {
|
|
100
|
-
}, () => {
|
|
101
|
-
}, () => {
|
|
102
|
-
});
|
|
98
|
+
const fakeTestInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, void 0, 0, {});
|
|
103
99
|
const runnable = { type: "teardown" };
|
|
104
100
|
await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {
|
|
105
101
|
});
|
|
@@ -233,18 +229,22 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
233
229
|
return { response: {}, error: (0, import_util2.testInfoError)(error) };
|
|
234
230
|
}
|
|
235
231
|
}
|
|
232
|
+
resume(payload) {
|
|
233
|
+
this._resumePromise?.resolve(payload);
|
|
234
|
+
}
|
|
236
235
|
async _runTest(test, retry, nextTest) {
|
|
237
|
-
const testInfo = new import_testInfo.TestInfoImpl(
|
|
238
|
-
this.
|
|
239
|
-
this.
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
(
|
|
247
|
-
|
|
236
|
+
const testInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, test, retry, {
|
|
237
|
+
onStepBegin: (payload) => this.dispatchEvent("stepBegin", payload),
|
|
238
|
+
onStepEnd: (payload) => this.dispatchEvent("stepEnd", payload),
|
|
239
|
+
onAttach: (payload) => this.dispatchEvent("attach", payload),
|
|
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)
|
|
247
|
+
});
|
|
248
248
|
const processAnnotation = (annotation) => {
|
|
249
249
|
testInfo.annotations.push(annotation);
|
|
250
250
|
switch (annotation.type) {
|
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"
|