ScuttleBot

Blame History Raw 727 lines
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 index_exports = {};
30
__export(index_exports, {
31
_baseTest: () => _baseTest,
32
defineConfig: () => import_configLoader.defineConfig,
33
expect: () => import_expect.expect,
34
mergeExpects: () => import_expect2.mergeExpects,
35
mergeTests: () => import_testType2.mergeTests,
36
test: () => test
37
});
38
module.exports = __toCommonJS(index_exports);
39
var import_fs = __toESM(require("fs"));
40
var import_path = __toESM(require("path"));
41
var playwrightLibrary = __toESM(require("playwright-core"));
42
var import_utils = require("playwright-core/lib/utils");
43
var import_globals = require("./common/globals");
44
var import_testType = require("./common/testType");
45
var import_browserBackend = require("./mcp/test/browserBackend");
46
var import_expect = require("./matchers/expect");
47
var import_configLoader = require("./common/configLoader");
48
var import_testType2 = require("./common/testType");
49
var import_expect2 = require("./matchers/expect");
50
const _baseTest = import_testType.rootTestType.test;
51
(0, import_utils.setBoxedStackPrefixes)([import_path.default.dirname(require.resolve("../package.json"))]);
52
if (process["__pw_initiator__"]) {
53
const originalStackTraceLimit = Error.stackTraceLimit;
54
Error.stackTraceLimit = 200;
55
try {
56
throw new Error("Requiring @playwright/test second time, \nFirst:\n" + process["__pw_initiator__"] + "\n\nSecond: ");
57
} finally {
58
Error.stackTraceLimit = originalStackTraceLimit;
59
}
60
} else {
61
process["__pw_initiator__"] = new Error().stack;
62
}
63
const playwrightFixtures = {
64
defaultBrowserType: ["chromium", { scope: "worker", option: true, box: true }],
65
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: "worker", option: true, box: true }],
66
playwright: [async ({}, use) => {
67
await use(require("playwright-core"));
68
}, { scope: "worker", box: true }],
69
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: "worker", option: true, box: true }],
70
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: "worker", option: true, box: true }],
71
launchOptions: [{}, { scope: "worker", option: true, box: true }],
72
connectOptions: [async ({ _optionConnectOptions }, use) => {
73
await use(connectOptionsFromEnv() || _optionConnectOptions);
74
}, { scope: "worker", option: true, box: true }],
75
screenshot: ["off", { scope: "worker", option: true, box: true }],
76
video: ["off", { scope: "worker", option: true, box: true }],
77
trace: ["off", { scope: "worker", option: true, box: true }],
78
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
79
const options = {
80
handleSIGINT: false,
81
...launchOptions,
82
tracesDir: tracing().tracesDir()
83
};
84
if (headless !== void 0)
85
options.headless = headless;
86
if (channel !== void 0)
87
options.channel = channel;
88
playwright._defaultLaunchOptions = options;
89
await use(options);
90
playwright._defaultLaunchOptions = void 0;
91
}, { scope: "worker", auto: true, box: true }],
92
browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use) => {
93
if (!["chromium", "firefox", "webkit"].includes(browserName))
94
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
95
if (connectOptions) {
96
const browser2 = await playwright[browserName].connect({
97
...connectOptions,
98
exposeNetwork: connectOptions.exposeNetwork ?? connectOptions._exposeNetwork,
99
headers: {
100
// HTTP headers are ASCII only (not UTF-8).
101
"x-playwright-launch-options": (0, import_utils.jsonStringifyForceASCII)(_browserOptions),
102
...connectOptions.headers
103
}
104
});
105
await use(browser2);
106
await browser2.close({ reason: "Test ended." });
107
return;
108
}
109
const browser = await playwright[browserName].launch();
110
await use(browser);
111
await browser.close({ reason: "Test ended." });
112
}, { scope: "worker", timeout: 0 }],
113
acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true, box: true }],
114
bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true, box: true }],
115
colorScheme: [({ contextOptions }, use) => use(contextOptions.colorScheme === void 0 ? "light" : contextOptions.colorScheme), { option: true, box: true }],
116
deviceScaleFactor: [({ contextOptions }, use) => use(contextOptions.deviceScaleFactor), { option: true, box: true }],
117
extraHTTPHeaders: [({ contextOptions }, use) => use(contextOptions.extraHTTPHeaders), { option: true, box: true }],
118
geolocation: [({ contextOptions }, use) => use(contextOptions.geolocation), { option: true, box: true }],
119
hasTouch: [({ contextOptions }, use) => use(contextOptions.hasTouch ?? false), { option: true, box: true }],
120
httpCredentials: [({ contextOptions }, use) => use(contextOptions.httpCredentials), { option: true, box: true }],
121
ignoreHTTPSErrors: [({ contextOptions }, use) => use(contextOptions.ignoreHTTPSErrors ?? false), { option: true, box: true }],
122
isMobile: [({ contextOptions }, use) => use(contextOptions.isMobile ?? false), { option: true, box: true }],
123
javaScriptEnabled: [({ contextOptions }, use) => use(contextOptions.javaScriptEnabled ?? true), { option: true, box: true }],
124
locale: [({ contextOptions }, use) => use(contextOptions.locale ?? "en-US"), { option: true, box: true }],
125
offline: [({ contextOptions }, use) => use(contextOptions.offline ?? false), { option: true, box: true }],
126
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true, box: true }],
127
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true, box: true }],
128
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true, box: true }],
129
clientCertificates: [({ contextOptions }, use) => use(contextOptions.clientCertificates), { option: true, box: true }],
130
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true, box: true }],
131
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true, box: true }],
132
viewport: [({ contextOptions }, use) => use(contextOptions.viewport === void 0 ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true, box: true }],
133
actionTimeout: [0, { option: true, box: true }],
134
testIdAttribute: ["data-testid", { option: true, box: true }],
135
navigationTimeout: [0, { option: true, box: true }],
136
baseURL: [async ({}, use) => {
137
await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
138
}, { option: true, box: true }],
139
serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? "allow"), { option: true, box: true }],
140
contextOptions: [{}, { option: true, box: true }],
141
agentOptions: [void 0, { option: true, box: true }],
142
_combinedContextOptions: [async ({
143
acceptDownloads,
144
bypassCSP,
145
clientCertificates,
146
colorScheme,
147
deviceScaleFactor,
148
extraHTTPHeaders,
149
hasTouch,
150
geolocation,
151
httpCredentials,
152
ignoreHTTPSErrors,
153
isMobile,
154
javaScriptEnabled,
155
locale,
156
offline,
157
permissions,
158
proxy,
159
storageState,
160
viewport,
161
timezoneId,
162
userAgent,
163
baseURL,
164
contextOptions,
165
serviceWorkers
166
}, use, testInfo) => {
167
const options = {};
168
if (acceptDownloads !== void 0)
169
options.acceptDownloads = acceptDownloads;
170
if (bypassCSP !== void 0)
171
options.bypassCSP = bypassCSP;
172
if (colorScheme !== void 0)
173
options.colorScheme = colorScheme;
174
if (deviceScaleFactor !== void 0)
175
options.deviceScaleFactor = deviceScaleFactor;
176
if (extraHTTPHeaders !== void 0)
177
options.extraHTTPHeaders = extraHTTPHeaders;
178
if (geolocation !== void 0)
179
options.geolocation = geolocation;
180
if (hasTouch !== void 0)
181
options.hasTouch = hasTouch;
182
if (httpCredentials !== void 0)
183
options.httpCredentials = httpCredentials;
184
if (ignoreHTTPSErrors !== void 0)
185
options.ignoreHTTPSErrors = ignoreHTTPSErrors;
186
if (isMobile !== void 0)
187
options.isMobile = isMobile;
188
if (javaScriptEnabled !== void 0)
189
options.javaScriptEnabled = javaScriptEnabled;
190
if (locale !== void 0)
191
options.locale = locale;
192
if (offline !== void 0)
193
options.offline = offline;
194
if (permissions !== void 0)
195
options.permissions = permissions;
196
if (proxy !== void 0)
197
options.proxy = proxy;
198
if (storageState !== void 0)
199
options.storageState = storageState;
200
if (clientCertificates?.length)
201
options.clientCertificates = resolveClientCerticates(clientCertificates);
202
if (timezoneId !== void 0)
203
options.timezoneId = timezoneId;
204
if (userAgent !== void 0)
205
options.userAgent = userAgent;
206
if (viewport !== void 0)
207
options.viewport = viewport;
208
if (baseURL !== void 0)
209
options.baseURL = baseURL;
210
if (serviceWorkers !== void 0)
211
options.serviceWorkers = serviceWorkers;
212
await use({
213
...contextOptions,
214
...options
215
});
216
}, { box: true }],
217
_setupContextOptions: [async ({ playwright, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
218
if (testIdAttribute)
219
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
220
testInfo.snapshotSuffix = process.platform;
221
if ((0, import_utils.debugMode)() === "inspector")
222
testInfo._setDebugMode();
223
playwright._defaultContextTimeout = actionTimeout || 0;
224
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
225
await use();
226
playwright._defaultContextTimeout = void 0;
227
playwright._defaultContextNavigationTimeout = void 0;
228
}, { auto: "all-hooks-included", title: "context configuration", box: true }],
229
_setupArtifacts: [async ({ playwright, screenshot, _combinedContextOptions }, use, testInfo) => {
230
testInfo.setTimeout(testInfo.project.timeout);
231
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
232
await artifactsRecorder.willStartTest(testInfo);
233
const tracingGroupSteps = [];
234
const csiListener = {
235
onApiCallBegin: (data, channel) => {
236
const testInfo2 = (0, import_globals.currentTestInfo)();
237
if (!testInfo2 || data.apiName.includes("setTestIdAttribute") || data.apiName === "tracing.groupEnd")
238
return;
239
const zone = (0, import_utils.currentZone)().data("stepZone");
240
const isExpectCall = data.apiName === "locator._expect" || data.apiName === "frame._expect" || data.apiName === "page._expectScreenshot";
241
if (zone && zone.category === "expect" && isExpectCall) {
242
if (zone.apiName)
243
data.apiName = zone.apiName;
244
if (zone.shortTitle || zone.title)
245
data.title = zone.shortTitle ?? zone.title;
246
data.stepId = zone.stepId;
247
return;
248
}
249
const step = testInfo2._addStep({
250
location: data.frames[0],
251
category: "pw:api",
252
title: renderTitle(channel.type, channel.method, channel.params, data.title),
253
apiName: data.apiName,
254
params: channel.params,
255
group: (0, import_utils.getActionGroup)({ type: channel.type, method: channel.method })
256
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
257
data.userData = step;
258
data.stepId = step.stepId;
259
if (data.apiName === "tracing.group")
260
tracingGroupSteps.push(step);
261
},
262
onApiCallEnd: (data) => {
263
if (data.apiName === "tracing.group")
264
return;
265
if (data.apiName === "tracing.groupEnd") {
266
const step2 = tracingGroupSteps.pop();
267
step2?.complete({ error: data.error });
268
return;
269
}
270
const step = data.userData;
271
step?.complete({ error: data.error });
272
},
273
onWillPause: ({ keepTestTimeout }) => {
274
if (!keepTestTimeout)
275
(0, import_globals.currentTestInfo)()?._setDebugMode();
276
},
277
runBeforeCreateBrowserContext: async (options) => {
278
for (const [key, value] of Object.entries(_combinedContextOptions)) {
279
if (!(key in options))
280
options[key] = value;
281
}
282
},
283
runBeforeCreateRequestContext: async (options) => {
284
for (const [key, value] of Object.entries(_combinedContextOptions)) {
285
if (!(key in options))
286
options[key] = value;
287
}
288
},
289
runAfterCreateBrowserContext: async (context) => {
290
await artifactsRecorder.didCreateBrowserContext(context);
291
const testInfo2 = (0, import_globals.currentTestInfo)();
292
if (testInfo2)
293
attachConnectedHeaderIfNeeded(testInfo2, context.browser());
294
},
295
runAfterCreateRequestContext: async (context) => {
296
await artifactsRecorder.didCreateRequestContext(context);
297
},
298
runBeforeCloseBrowserContext: async (context) => {
299
await artifactsRecorder.willCloseBrowserContext(context);
300
},
301
runBeforeCloseRequestContext: async (context) => {
302
await artifactsRecorder.willCloseRequestContext(context);
303
}
304
};
305
const clientInstrumentation = playwright._instrumentation;
306
clientInstrumentation.addListener(csiListener);
307
await use();
308
clientInstrumentation.removeListener(csiListener);
309
await artifactsRecorder.didFinishTest();
310
}, { auto: "all-hooks-included", title: "trace recording", box: true, timeout: 0 }],
311
_contextFactory: [async ({
312
browser,
313
video,
314
_reuseContext,
315
_combinedContextOptions
316
/** mitigate dep-via-auto lack of traceability */
317
}, use, testInfo) => {
318
const testInfoImpl = testInfo;
319
const videoMode = normalizeVideoMode(video);
320
const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext;
321
const contexts = /* @__PURE__ */ new Map();
322
let counter = 0;
323
await use(async (options) => {
324
const hook = testInfoImpl._currentHookType();
325
if (hook === "beforeAll" || hook === "afterAll") {
326
throw new Error([
327
`"context" and "page" fixtures are not supported in "${hook}" since they are created on a per-test basis.`,
328
`If you would like to reuse a single page between tests, create context manually with browser.newContext(). See https://aka.ms/playwright/reuse-page for details.`,
329
`If you would like to configure your page before each test, do that in beforeEach hook instead.`
330
].join("\n"));
331
}
332
const videoOptions = captureVideo ? {
333
recordVideo: {
334
dir: tracing().artifactsDir(),
335
size: typeof video === "string" ? void 0 : video.size
336
}
337
} : {};
338
const context = await browser.newContext({ ...videoOptions, ...options });
339
if (process.env.PW_CLOCK === "frozen") {
340
await context._wrapApiCall(async () => {
341
await context.clock.install({ time: 0 });
342
await context.clock.pauseAt(1e3);
343
}, { internal: true });
344
} else if (process.env.PW_CLOCK === "realtime") {
345
await context._wrapApiCall(async () => {
346
await context.clock.install({ time: 0 });
347
}, { internal: true });
348
}
349
let closed = false;
350
const close = async () => {
351
if (closed)
352
return;
353
closed = true;
354
const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
355
await context.close({ reason: closeReason });
356
const testFailed = testInfo.status !== testInfo.expectedStatus;
357
const preserveVideo = captureVideo && (videoMode === "on" || testFailed && videoMode === "retain-on-failure" || videoMode === "on-first-retry" && testInfo.retry === 1);
358
if (preserveVideo) {
359
const { pagesWithVideo: pagesForVideo } = contexts.get(context);
360
const videos = pagesForVideo.map((p) => p.video()).filter((video2) => !!video2);
361
await Promise.all(videos.map(async (v) => {
362
try {
363
const savedPath = testInfo.outputPath(`video${counter ? "-" + counter : ""}.webm`);
364
++counter;
365
await v.saveAs(savedPath);
366
testInfo.attachments.push({ name: "video", path: savedPath, contentType: "video/webm" });
367
} catch (e) {
368
}
369
}));
370
}
371
};
372
const contextData = { close, pagesWithVideo: [] };
373
if (captureVideo)
374
context.on("page", (page) => contextData.pagesWithVideo.push(page));
375
contexts.set(context, contextData);
376
return { context, close };
377
});
378
await Promise.all([...contexts.values()].map((data) => data.close()));
379
}, { scope: "test", title: "context", box: true }],
380
_optionContextReuseMode: ["none", { scope: "worker", option: true, box: true }],
381
_optionConnectOptions: [void 0, { scope: "worker", option: true, box: true }],
382
_reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
383
let mode = _optionContextReuseMode;
384
if (process.env.PW_TEST_REUSE_CONTEXT)
385
mode = "when-possible";
386
const reuse = mode === "when-possible" && normalizeVideoMode(video) === "off";
387
await use(reuse);
388
}, { scope: "worker", title: "context", box: true }],
389
context: async ({ browser, _reuseContext, _contextFactory }, use, testInfo) => {
390
const browserImpl = browser;
391
attachConnectedHeaderIfNeeded(testInfo, browserImpl);
392
if (!_reuseContext) {
393
const { context: context2, close } = await _contextFactory();
394
testInfo._onCustomMessageCallback = (0, import_browserBackend.createCustomMessageHandler)(testInfo, context2);
395
await use(context2);
396
await close();
397
return;
398
}
399
const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
400
testInfo._onCustomMessageCallback = (0, import_browserBackend.createCustomMessageHandler)(testInfo, context);
401
await use(context);
402
const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
403
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
404
},
405
page: async ({ context, _reuseContext }, use) => {
406
if (!_reuseContext) {
407
await use(await context.newPage());
408
return;
409
}
410
let [page] = context.pages();
411
if (!page)
412
page = await context.newPage();
413
await use(page);
414
},
415
agent: async ({ page, agentOptions }, use, testInfo) => {
416
const testInfoImpl = testInfo;
417
const cachePathTemplate = agentOptions?.cachePathTemplate ?? "{testDir}/{testFilePath}-cache.json";
418
const resolvedCacheFile = testInfoImpl._applyPathTemplate(cachePathTemplate, "", ".json");
419
const cacheFile = testInfoImpl.config.runAgents === "all" ? void 0 : await testInfoImpl._cloneStorage(resolvedCacheFile);
420
const cacheOutFile = import_path.default.join(testInfoImpl.artifactsDir(), "agent-cache-" + (0, import_utils.createGuid)() + ".json");
421
const provider = agentOptions?.provider && testInfo.config.runAgents !== "none" ? agentOptions.provider : void 0;
422
if (provider)
423
testInfo.setTimeout(0);
424
const cache = {
425
cacheFile,
426
cacheOutFile
427
};
428
const agent = await page.agent({
429
provider,
430
cache,
431
limits: agentOptions?.limits,
432
secrets: agentOptions?.secrets,
433
systemPrompt: agentOptions?.systemPrompt,
434
expect: {
435
timeout: testInfoImpl._projectInternal.expect?.timeout
436
}
437
});
438
await use(agent);
439
const usage = await agent.usage();
440
if (usage.turns > 0)
441
await testInfoImpl.attach("agent-usage", { contentType: "application/json", body: Buffer.from(JSON.stringify(usage, null, 2)) });
442
if (!resolvedCacheFile || !cacheOutFile)
443
return;
444
if (testInfo.status !== "passed")
445
return;
446
await testInfoImpl._upstreamStorage(resolvedCacheFile, cacheOutFile);
447
},
448
request: async ({ playwright }, use) => {
449
const request = await playwright.request.newContext();
450
await use(request);
451
const hook = test.info()._currentHookType();
452
if (hook === "beforeAll") {
453
await request.dispose({ reason: [
454
`Fixture { request } from beforeAll cannot be reused in a test.`,
455
` - Recommended fix: use a separate { request } in the test.`,
456
` - Alternatively, manually create APIRequestContext in beforeAll and dispose it in afterAll.`,
457
`See https://playwright.dev/docs/api-testing#sending-api-requests-from-ui-tests for more details.`
458
].join("\n") });
459
} else {
460
await request.dispose();
461
}
462
}
463
};
464
function normalizeVideoMode(video) {
465
if (!video)
466
return "off";
467
let videoMode = typeof video === "string" ? video : video.mode;
468
if (videoMode === "retry-with-video")
469
videoMode = "on-first-retry";
470
return videoMode;
471
}
472
function shouldCaptureVideo(videoMode, testInfo) {
473
return videoMode === "on" || videoMode === "retain-on-failure" || videoMode === "on-first-retry" && testInfo.retry === 1;
474
}
475
function normalizeScreenshotMode(screenshot) {
476
if (!screenshot)
477
return "off";
478
return typeof screenshot === "string" ? screenshot : screenshot.mode;
479
}
480
function attachConnectedHeaderIfNeeded(testInfo, browser) {
481
const connectHeaders = browser?._connection.headers;
482
if (!connectHeaders)
483
return;
484
for (const header of connectHeaders) {
485
if (header.name !== "x-playwright-attachment")
486
continue;
487
const [name, value] = header.value.split("=");
488
if (!name || !value)
489
continue;
490
if (testInfo.attachments.some((attachment) => attachment.name === name))
491
continue;
492
testInfo.attachments.push({ name, contentType: "text/plain", body: Buffer.from(value) });
493
}
494
}
495
function resolveFileToConfig(file) {
496
const config = test.info().config.configFile;
497
if (!config || !file)
498
return file;
499
if (import_path.default.isAbsolute(file))
500
return file;
501
return import_path.default.resolve(import_path.default.dirname(config), file);
502
}
503
function resolveClientCerticates(clientCertificates) {
504
for (const cert of clientCertificates) {
505
cert.certPath = resolveFileToConfig(cert.certPath);
506
cert.keyPath = resolveFileToConfig(cert.keyPath);
507
cert.pfxPath = resolveFileToConfig(cert.pfxPath);
508
}
509
return clientCertificates;
510
}
511
const kTracingStarted = Symbol("kTracingStarted");
512
function connectOptionsFromEnv() {
513
const wsEndpoint = process.env.PW_TEST_CONNECT_WS_ENDPOINT;
514
if (!wsEndpoint)
515
return void 0;
516
const headers = process.env.PW_TEST_CONNECT_HEADERS ? JSON.parse(process.env.PW_TEST_CONNECT_HEADERS) : void 0;
517
return {
518
wsEndpoint,
519
headers,
520
exposeNetwork: process.env.PW_TEST_CONNECT_EXPOSE_NETWORK
521
};
522
}
523
class SnapshotRecorder {
524
constructor(_artifactsRecorder, _mode, _name, _contentType, _extension, _doSnapshot) {
525
this._artifactsRecorder = _artifactsRecorder;
526
this._mode = _mode;
527
this._name = _name;
528
this._contentType = _contentType;
529
this._extension = _extension;
530
this._doSnapshot = _doSnapshot;
531
this._ordinal = 0;
532
this._temporary = [];
533
}
534
fixOrdinal() {
535
this._ordinal = this.testInfo.attachments.filter((a) => a.name === this._name).length;
536
}
537
shouldCaptureUponFinish() {
538
return this._mode === "on" || this._mode === "only-on-failure" && this.testInfo._isFailure() || this._mode === "on-first-failure" && this.testInfo._isFailure() && this.testInfo.retry === 0;
539
}
540
async maybeCapture() {
541
if (!this.shouldCaptureUponFinish())
542
return;
543
await Promise.all(this._artifactsRecorder._playwright._allPages().map((page) => this._snapshotPage(page, false)));
544
}
545
async persistTemporary() {
546
if (this.shouldCaptureUponFinish()) {
547
await Promise.all(this._temporary.map(async (file) => {
548
try {
549
const path2 = this._createAttachmentPath();
550
await import_fs.default.promises.rename(file, path2);
551
this._attach(path2);
552
} catch {
553
}
554
}));
555
}
556
}
557
async captureTemporary(context) {
558
if (this._mode === "on" || this._mode === "only-on-failure" || this._mode === "on-first-failure" && this.testInfo.retry === 0)
559
await Promise.all(context.pages().map((page) => this._snapshotPage(page, true)));
560
}
561
_attach(screenshotPath) {
562
this.testInfo.attachments.push({ name: this._name, path: screenshotPath, contentType: this._contentType });
563
}
564
_createAttachmentPath() {
565
const testFailed = this.testInfo._isFailure();
566
const index = this._ordinal + 1;
567
++this._ordinal;
568
const path2 = this.testInfo.outputPath(`test-${testFailed ? "failed" : "finished"}-${index}${this._extension}`);
569
return path2;
570
}
571
_createTemporaryArtifact(...name) {
572
const file = import_path.default.join(this._artifactsRecorder._artifactsDir, ...name);
573
return file;
574
}
575
async _snapshotPage(page, temporary) {
576
if (page[this.testInfo._uniqueSymbol])
577
return;
578
page[this.testInfo._uniqueSymbol] = true;
579
try {
580
const path2 = temporary ? this._createTemporaryArtifact((0, import_utils.createGuid)() + this._extension) : this._createAttachmentPath();
581
await this._doSnapshot(page, path2);
582
if (temporary)
583
this._temporary.push(path2);
584
else
585
this._attach(path2);
586
} catch {
587
}
588
}
589
get testInfo() {
590
return this._artifactsRecorder._testInfo;
591
}
592
}
593
class ArtifactsRecorder {
594
constructor(playwright, artifactsDir, screenshot) {
595
this._playwright = playwright;
596
this._artifactsDir = artifactsDir;
597
const screenshotOptions = typeof screenshot === "string" ? void 0 : screenshot;
598
this._startedCollectingArtifacts = Symbol("startedCollectingArtifacts");
599
this._screenshotRecorder = new SnapshotRecorder(this, normalizeScreenshotMode(screenshot), "screenshot", "image/png", ".png", async (page, path2) => {
600
await page._wrapApiCall(async () => {
601
await page.screenshot({ ...screenshotOptions, timeout: 5e3, path: path2, caret: "initial" });
602
}, { internal: true });
603
});
604
}
605
async willStartTest(testInfo) {
606
this._testInfo = testInfo;
607
testInfo._onDidFinishTestFunctionCallback = () => this.didFinishTestFunction();
608
this._screenshotRecorder.fixOrdinal();
609
await Promise.all(this._playwright._allContexts().map((context) => this.didCreateBrowserContext(context)));
610
const existingApiRequests = Array.from(this._playwright.request._contexts);
611
await Promise.all(existingApiRequests.map((c) => this.didCreateRequestContext(c)));
612
}
613
async didCreateBrowserContext(context) {
614
await this._startTraceChunkOnContextCreation(context, context.tracing);
615
}
616
async willCloseBrowserContext(context) {
617
await this._stopTracing(context, context.tracing);
618
await this._screenshotRecorder.captureTemporary(context);
619
await this._takePageSnapshot(context);
620
}
621
async _takePageSnapshot(context) {
622
if (process.env.PLAYWRIGHT_NO_COPY_PROMPT)
623
return;
624
if (this._testInfo.errors.length === 0)
625
return;
626
if (this._pageSnapshot)
627
return;
628
const page = context.pages()[0];
629
if (!page)
630
return;
631
try {
632
await page._wrapApiCall(async () => {
633
this._pageSnapshot = (await page._snapshotForAI({ timeout: 5e3 })).full;
634
}, { internal: true });
635
} catch {
636
}
637
}
638
async didCreateRequestContext(context) {
639
await this._startTraceChunkOnContextCreation(context, context._tracing);
640
}
641
async willCloseRequestContext(context) {
642
await this._stopTracing(context, context._tracing);
643
}
644
async didFinishTestFunction() {
645
await this._screenshotRecorder.maybeCapture();
646
}
647
async didFinishTest() {
648
await this.didFinishTestFunction();
649
const leftoverContexts = this._playwright._allContexts();
650
const leftoverApiRequests = Array.from(this._playwright.request._contexts);
651
await Promise.all(leftoverContexts.map(async (context2) => {
652
await this._stopTracing(context2, context2.tracing);
653
}).concat(leftoverApiRequests.map(async (context2) => {
654
await this._stopTracing(context2, context2._tracing);
655
})));
656
await this._screenshotRecorder.persistTemporary();
657
const context = leftoverContexts[0];
658
if (context)
659
await this._takePageSnapshot(context);
660
if (this._pageSnapshot && this._testInfo.errors.length > 0 && !this._testInfo.attachments.some((a) => a.name === "error-context")) {
661
const lines = [
662
"# Page snapshot",
663
"",
664
"```yaml",
665
this._pageSnapshot,
666
"```"
667
];
668
const filePath = this._testInfo.outputPath("error-context.md");
669
await import_fs.default.promises.writeFile(filePath, lines.join("\n"), "utf8");
670
this._testInfo._attach({
671
name: "error-context",
672
contentType: "text/markdown",
673
path: filePath
674
}, void 0);
675
}
676
}
677
async _startTraceChunkOnContextCreation(channelOwner, tracing2) {
678
await channelOwner._wrapApiCall(async () => {
679
const options = this._testInfo._tracing.traceOptions();
680
if (options) {
681
const title = this._testInfo._tracing.traceTitle();
682
const name = this._testInfo._tracing.generateNextTraceRecordingName();
683
if (!tracing2[kTracingStarted]) {
684
await tracing2.start({ ...options, title, name });
685
tracing2[kTracingStarted] = true;
686
} else {
687
await tracing2.startChunk({ title, name });
688
}
689
} else {
690
if (tracing2[kTracingStarted]) {
691
tracing2[kTracingStarted] = false;
692
await tracing2.stop();
693
}
694
}
695
}, { internal: true });
696
}
697
async _stopTracing(channelOwner, tracing2) {
698
await channelOwner._wrapApiCall(async () => {
699
if (tracing2[this._startedCollectingArtifacts])
700
return;
701
tracing2[this._startedCollectingArtifacts] = true;
702
if (this._testInfo._tracing.traceOptions() && tracing2[kTracingStarted])
703
await tracing2.stopChunk({ path: this._testInfo._tracing.maybeGenerateNextTraceRecordingPath() });
704
}, { internal: true });
705
}
706
}
707
function renderTitle(type, method, params, title) {
708
const prefix = (0, import_utils.renderTitleForCall)({ title, type, method, params });
709
let selector;
710
if (params?.["selector"] && typeof params.selector === "string")
711
selector = (0, import_utils.asLocatorDescription)("javascript", params.selector);
712
return prefix + (selector ? ` ${selector}` : "");
713
}
714
function tracing() {
715
return test.info()._tracing;
716
}
717
const test = _baseTest.extend(playwrightFixtures);
718
// Annotate the CommonJS export names for ESM import in node:
719
0 && (module.exports = {
720
_baseTest,
721
defineConfig,
722
expect,
723
mergeExpects,
724
mergeTests,
725
test
726
});
727

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button