@argos-ci/playwright 6.5.0 → 6.6.0

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/dist/index.mjs ADDED
@@ -0,0 +1,564 @@
1
+ import { createRequire } from "node:module";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { getGitRepositoryPath, getMetadataPath, getScreenshotName, readVersionFromPackage, validateThreshold, writeMetadata } from "@argos-ci/util";
4
+ import { dirname, relative, resolve } from "node:path";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { getGlobalScript, resolveViewport } from "@argos-ci/browser";
7
+ import { createHash } from "node:crypto";
8
+ //#region src/metadata.ts
9
+ const require$1 = createRequire(import.meta.url);
10
+ /**
11
+ * Try to resolve a package.
12
+ */
13
+ function tryResolve(pkg) {
14
+ try {
15
+ return require$1.resolve(pkg);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ /**
21
+ * Private metadata config storage.
22
+ * Used to inject the metadata from other SDKs like @argos-ci/storybook.
23
+ */
24
+ const metadataConfigStorage = new AsyncLocalStorage();
25
+ /**
26
+ * Set the metadata config.
27
+ */
28
+ function setMetadataConfig(metadata) {
29
+ metadataConfigStorage.enterWith(metadata);
30
+ }
31
+ const DEFAULT_PLAYWRIGHT_LIBRARIES = [
32
+ "@playwright/test",
33
+ "playwright",
34
+ "playwright-core"
35
+ ];
36
+ /**
37
+ * Get the metadata overrides set by the SDK.
38
+ */
39
+ function getMetadataOverrides() {
40
+ return metadataConfigStorage.getStore();
41
+ }
42
+ /**
43
+ * Get the name and version of the automation library.
44
+ */
45
+ async function getAutomationLibraryMetadata() {
46
+ const libraries = metadataConfigStorage.getStore()?.playwrightLibraries ?? DEFAULT_PLAYWRIGHT_LIBRARIES;
47
+ for (const name of libraries) {
48
+ const pkgPath = tryResolve(`${name}/package.json`);
49
+ if (pkgPath) return {
50
+ version: await readVersionFromPackage(pkgPath),
51
+ name
52
+ };
53
+ }
54
+ throw new Error(`Unable to find any of the following packages: ${libraries.join(", ")}`);
55
+ }
56
+ /**
57
+ * Get the version of the Argos Playwright SDK.
58
+ */
59
+ async function getArgosPlaywrightVersion() {
60
+ return readVersionFromPackage(require$1.resolve("@argos-ci/playwright/package.json"));
61
+ }
62
+ /**
63
+ * Get the name and version of the SDK.
64
+ */
65
+ async function getSdkMetadata() {
66
+ const metadataConfig = metadataConfigStorage.getStore();
67
+ if (metadataConfig) return metadataConfig.sdk;
68
+ return {
69
+ name: "@argos-ci/playwright",
70
+ version: await getArgosPlaywrightVersion()
71
+ };
72
+ }
73
+ /**
74
+ * Get the metadata of the automation library and the SDK.
75
+ */
76
+ async function getLibraryMetadata() {
77
+ const [automationLibrary, sdk] = await Promise.all([getAutomationLibraryMetadata(), getSdkMetadata()]);
78
+ return {
79
+ automationLibrary,
80
+ sdk
81
+ };
82
+ }
83
+ /**
84
+ * Resolve the test file path relative to the repository path.
85
+ * If the repository path is not set, it returns the absolute path.
86
+ */
87
+ function resolveTestFilePath(filepath, repositoryPath) {
88
+ if (!repositoryPath) return filepath;
89
+ return relative(repositoryPath, filepath);
90
+ }
91
+ /**
92
+ * Get the metadata of the test.
93
+ */
94
+ async function getTestMetadata(testInfo) {
95
+ const repositoryPath = await getGitRepositoryPath();
96
+ const metadataConfig = metadataConfigStorage.getStore();
97
+ if (metadataConfig?.test) return {
98
+ ...metadataConfig.test,
99
+ location: metadataConfig.test?.location ? {
100
+ file: resolveTestFilePath(metadataConfig.test.location.file, repositoryPath),
101
+ line: metadataConfig.test.location.line,
102
+ column: metadataConfig.test.location.column
103
+ } : void 0
104
+ };
105
+ if (!testInfo) return null;
106
+ return {
107
+ id: testInfo.testId,
108
+ title: testInfo.title,
109
+ titlePath: testInfo.titlePath,
110
+ tags: testInfo.tags.length > 0 ? testInfo.tags : void 0,
111
+ retry: testInfo.retry,
112
+ retries: testInfo.project.retries,
113
+ repeat: testInfo.repeatEachIndex,
114
+ location: {
115
+ file: resolveTestFilePath(testInfo.file, repositoryPath),
116
+ line: testInfo.line,
117
+ column: testInfo.column
118
+ },
119
+ annotations: testInfo.annotations
120
+ };
121
+ }
122
+ //#endregion
123
+ //#region src/util.ts
124
+ const require = createRequire(import.meta.url);
125
+ /**
126
+ * Check if the project is using the Argos reporter.
127
+ */
128
+ function checkIsUsingArgosReporter(testInfo) {
129
+ if (!testInfo) return false;
130
+ const reporterPath = require.resolve("@argos-ci/playwright/reporter");
131
+ return testInfo.config.reporter.some((reporter) => reporter[0].includes("@argos-ci/playwright/reporter") || reporter[0] === reporterPath);
132
+ }
133
+ const PNG_EXTENSION = `.png`;
134
+ const ARIA_EXTENSION = `.aria.yml`;
135
+ 255 - PNG_EXTENSION.length - `.argos.json`.length;
136
+ /**
137
+ * Get test info from the Playwright test.
138
+ */
139
+ async function getTestInfo() {
140
+ try {
141
+ const { test } = await import("@playwright/test");
142
+ return test.info();
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ /**
148
+ * Check if the value is a Page.
149
+ */
150
+ function checkIsPage(value) {
151
+ return Boolean(value && typeof value === "object" && "bringToFront" in value && typeof value.bringToFront === "function");
152
+ }
153
+ /**
154
+ * Check if the value is an element handle.
155
+ */
156
+ function checkIsElementHandle(value) {
157
+ return Boolean(value && typeof value === "object" && "asElement" in value && typeof value.asElement === "function");
158
+ }
159
+ /**
160
+ * Check if the handler is a Frame.
161
+ */
162
+ function checkIsFrame(handler) {
163
+ return "page" in handler && typeof handler.page === "function";
164
+ }
165
+ /**
166
+ * Get the Playwright `Page` from the handler.
167
+ * If the handler is a Frame, it returns the parent page.
168
+ * Otherwise, it returns the handler itself.
169
+ */
170
+ function getPage(handler) {
171
+ if (checkIsFrame(handler)) return handler.page();
172
+ return handler;
173
+ }
174
+ /**
175
+ * Get the viewport size.
176
+ */
177
+ function getViewportSize(page) {
178
+ const viewportSize = page.viewportSize();
179
+ if (!viewportSize) throw new Error("Snapshots can't be taken without a viewport.");
180
+ return viewportSize;
181
+ }
182
+ /**
183
+ * Sets the viewport size and waits for the visual viewport to match the specified dimensions.
184
+ * @returns A promise that resolves when the viewport size has been successfully set and matched.
185
+ */
186
+ async function setViewportSize(page, viewportSize) {
187
+ await page.setViewportSize(viewportSize);
188
+ await page.waitForFunction(({ width, height }) => window.innerWidth === width && window.innerHeight === height, {
189
+ width: viewportSize.width,
190
+ height: viewportSize.height
191
+ });
192
+ }
193
+ /**
194
+ * Get the snapshot names based on the test info.
195
+ */
196
+ function getSnapshotNames(name, testInfo) {
197
+ if (testInfo) {
198
+ const projectName = `${testInfo.project.name}/${name}`;
199
+ if (testInfo.repeatEachIndex > 0) return {
200
+ name: `${projectName} repeat-${testInfo.repeatEachIndex}`,
201
+ baseName: projectName
202
+ };
203
+ return {
204
+ name: projectName,
205
+ baseName: null
206
+ };
207
+ }
208
+ return {
209
+ name,
210
+ baseName: null
211
+ };
212
+ }
213
+ /**
214
+ * Inject Argos script into the document.
215
+ */
216
+ async function injectArgos(handler) {
217
+ if (!await handler.evaluate(() => typeof window.__ARGOS__ !== "undefined")) await handler.addScriptTag({ content: getGlobalScript() });
218
+ }
219
+ /**
220
+ * Prepare Argos screenshot by injecting the SDK and creating the root directory.
221
+ */
222
+ async function prepare(args) {
223
+ const { handler, useArgosReporter, root } = args;
224
+ await Promise.all([useArgosReporter ? null : mkdir(root, { recursive: true }), injectArgos(handler)]);
225
+ }
226
+ /**
227
+ * Get metadata and path.
228
+ */
229
+ async function getPathAndMetadata(args) {
230
+ const { handler, testInfo, names, extension, root, useArgosReporter } = args;
231
+ const overrides = getMetadataOverrides();
232
+ const path = useArgosReporter && testInfo ? testInfo.outputPath("argos", `${names.name}${extension}`) : resolve(root, `${names.name}${extension}`);
233
+ const dir = dirname(path);
234
+ const [colorScheme, mediaType, libMetadata, testMetadata] = await Promise.all([
235
+ handler.evaluate(() => window.__ARGOS__.getColorScheme()),
236
+ handler.evaluate(() => window.__ARGOS__.getMediaType()),
237
+ getLibraryMetadata(),
238
+ getTestMetadata(testInfo),
239
+ dir !== root ? mkdir(dir, { recursive: true }) : null
240
+ ]);
241
+ const viewportSize = checkIsFrame(handler) ? null : getViewportSize(handler);
242
+ const browser = getPage(handler).context().browser();
243
+ if (!browser) throw new Error("Can't take screenshots without a browser.");
244
+ const browserName = browser.browserType().name();
245
+ const browserVersion = browser.version();
246
+ const metadata = {
247
+ url: overrides?.url ?? handler.url(),
248
+ colorScheme,
249
+ mediaType,
250
+ test: testMetadata,
251
+ story: overrides?.story,
252
+ browser: {
253
+ name: browserName,
254
+ version: browserVersion
255
+ },
256
+ ...libMetadata
257
+ };
258
+ const viewport = viewportSize ?? getMetadataOverrides()?.viewport;
259
+ if (viewport) metadata.viewport = viewport;
260
+ metadata.transient = {};
261
+ if (names.baseName) metadata.transient.baseName = `${names.baseName}${extension}`;
262
+ return {
263
+ metadata,
264
+ path
265
+ };
266
+ }
267
+ /**
268
+ * Convert a screenshot to a snapshot path
269
+ */
270
+ function screenshotToSnapshotPath(value) {
271
+ return value.replace(/\.png$/, ARIA_EXTENSION);
272
+ }
273
+ /**
274
+ * Run before taking all screenshots.
275
+ */
276
+ async function beforeAll(handler, context, options) {
277
+ await handler.evaluate((context) => window.__ARGOS__.beforeAll(context), context);
278
+ if (options?.disableHover) await getPage(handler).mouse.move(0, 0);
279
+ return async () => {
280
+ await handler.evaluate(() => window.__ARGOS__.afterAll());
281
+ };
282
+ }
283
+ /**
284
+ * Run before taking each screenshot.
285
+ */
286
+ async function beforeEach(handler, context) {
287
+ await handler.evaluate((context) => window.__ARGOS__.beforeEach(context), context);
288
+ return async () => {
289
+ await handler.evaluate(() => window.__ARGOS__.afterEach());
290
+ };
291
+ }
292
+ /**
293
+ * Increase the timeout for the test x3.
294
+ * Returns a function to reset the timeout.
295
+ */
296
+ async function increaseTimeout() {
297
+ const testInfo = await getTestInfo();
298
+ if (testInfo) {
299
+ const { timeout } = testInfo;
300
+ testInfo.setTimeout(timeout * 3);
301
+ return {
302
+ value: timeout,
303
+ reset: () => {
304
+ testInfo.setTimeout(timeout);
305
+ }
306
+ };
307
+ }
308
+ return null;
309
+ }
310
+ /**
311
+ * Wait for the UI to be ready before taking the screenshot.
312
+ */
313
+ async function waitForReadiness(handler, context) {
314
+ const timeout = await increaseTimeout();
315
+ try {
316
+ await handler.waitForFunction((context) => {
317
+ return window.__ARGOS__.waitFor(context);
318
+ }, context, timeout ? { timeout: timeout.value } : void 0);
319
+ timeout?.reset();
320
+ } catch (error) {
321
+ const reasons = await handler.evaluate((context) => window.__ARGOS__.getWaitFailureExplanations(context), context);
322
+ throw new Error(`
323
+ Failed to stabilize screenshot, found the following issues:
324
+ ${reasons.map((reason) => `- ${reason}`).join("\n")}
325
+ `.trim(), { cause: error });
326
+ }
327
+ }
328
+ /**
329
+ * Attach attachments to test info if necessary.
330
+ */
331
+ async function attachAttachments(args) {
332
+ const { attachments, useArgosReporter, testInfo } = args;
333
+ if (useArgosReporter && testInfo) await Promise.all(attachments.map((attachment) => testInfo.attach(attachment.name, {
334
+ path: attachment.path,
335
+ contentType: attachment.contentType
336
+ })));
337
+ }
338
+ //#endregion
339
+ //#region src/attachment.ts
340
+ function getAttachmentName(name, type) {
341
+ return `argos/${type}___${name}`;
342
+ }
343
+ //#endregion
344
+ //#region src/aria-snapshot.ts
345
+ const DEFAULT_SNAPSHOTS_ROOT = "./screenshots";
346
+ /**
347
+ * Stabilize the UI and takes a snapshot of the application under test.
348
+ *
349
+ * @example
350
+ * argosAriaSnapshot(page, "my-screenshot")
351
+ * @see https://playwright.dev/docs/aria-snapshots
352
+ */
353
+ async function argosAriaSnapshot(handler, name, options = {}) {
354
+ const { element, has, hasText, hasNot, hasNotText, timeout, root = DEFAULT_SNAPSHOTS_ROOT } = options;
355
+ if (!handler) throw new Error("A Playwright `handler` object is required.");
356
+ if (!name) throw new Error("The `name` argument is required.");
357
+ const snapshotTarget = typeof element === "string" ? handler.locator(element, {
358
+ has,
359
+ hasText,
360
+ hasNot,
361
+ hasNotText
362
+ }) : element ?? handler.locator("body");
363
+ const testInfo = await getTestInfo();
364
+ const useArgosReporter = checkIsUsingArgosReporter(testInfo);
365
+ await prepare({
366
+ handler,
367
+ useArgosReporter,
368
+ root
369
+ });
370
+ const context = getStabilizationContext$1(options);
371
+ const afterAll = await beforeAll(handler, context);
372
+ const names = getSnapshotNames(name, testInfo);
373
+ const { path: snapshotPath, metadata } = await getPathAndMetadata({
374
+ handler,
375
+ extension: ARIA_EXTENSION,
376
+ names,
377
+ root,
378
+ testInfo,
379
+ useArgosReporter
380
+ });
381
+ await waitForReadiness(handler, context);
382
+ const afterEach = await beforeEach(handler, context);
383
+ await waitForReadiness(handler, context);
384
+ await Promise.all([snapshotTarget.ariaSnapshot({ timeout }).then((snapshot) => {
385
+ return writeFile(snapshotPath, snapshot, "utf-8");
386
+ }), writeMetadata(snapshotPath, metadata)]);
387
+ const attachments = [{
388
+ name: getAttachmentName(names.name, "aria"),
389
+ contentType: "application/yaml",
390
+ path: snapshotPath
391
+ }, {
392
+ name: getAttachmentName(names.name, "aria/metadata"),
393
+ contentType: "application/json",
394
+ path: getMetadataPath(snapshotPath)
395
+ }];
396
+ await attachAttachments({
397
+ attachments,
398
+ testInfo,
399
+ useArgosReporter
400
+ });
401
+ await afterEach();
402
+ await afterAll();
403
+ return attachments;
404
+ }
405
+ /**
406
+ * Get the stabilization context from the options.
407
+ */
408
+ function getStabilizationContext$1(options) {
409
+ const { stabilize } = options;
410
+ return {
411
+ fullPage: false,
412
+ argosCSS: void 0,
413
+ viewports: void 0,
414
+ options: stabilize
415
+ };
416
+ }
417
+ //#endregion
418
+ //#region src/screenshot.ts
419
+ const DEFAULT_SCREENSHOT_ROOT = "./screenshots";
420
+ /**
421
+ * Stabilize the UI and takes a screenshot of the application under test.
422
+ *
423
+ * @example
424
+ * argosScreenshot(page, "my-screenshot")
425
+ * @see https://argos-ci.com/docs/playwright#api-overview
426
+ */
427
+ async function argosScreenshot(handler, name, options = {}) {
428
+ const { element, has, hasText, hasNot, hasNotText, viewports, argosCSS: _argosCSS, root = DEFAULT_SCREENSHOT_ROOT, ariaSnapshot, disableHover = true, ...playwrightOptions } = options;
429
+ if (!handler) throw new Error("A Playwright `handler` object is required.");
430
+ if (!name) throw new Error("The `name` argument is required.");
431
+ const screenshotTarget = typeof element === "string" ? handler.locator(element, {
432
+ has,
433
+ hasText,
434
+ hasNot,
435
+ hasNotText
436
+ }) : element ?? (checkIsFrame(handler) ? handler.locator("body") : handler);
437
+ const testInfo = await getTestInfo();
438
+ const useArgosReporter = checkIsUsingArgosReporter(testInfo);
439
+ await prepare({
440
+ handler,
441
+ useArgosReporter,
442
+ root
443
+ });
444
+ const originalViewportSize = checkIsFrame(handler) ? null : getViewportSize(handler);
445
+ const fullPage = options.fullPage !== void 0 ? options.fullPage : screenshotTarget === handler;
446
+ const context = getStabilizationContext(options);
447
+ const afterAll = await beforeAll(handler, context, { disableHover });
448
+ const stabilizeAndScreenshot = async (name) => {
449
+ const names = getSnapshotNames(name, testInfo);
450
+ const { path: screenshotPath, metadata } = await getPathAndMetadata({
451
+ handler,
452
+ extension: PNG_EXTENSION,
453
+ root,
454
+ names,
455
+ testInfo,
456
+ useArgosReporter
457
+ });
458
+ if (options.tag) metadata.tags = Array.isArray(options.tag) ? options.tag : [options.tag];
459
+ if (options.threshold !== void 0) {
460
+ validateThreshold(options.threshold);
461
+ metadata.transient.threshold = options.threshold;
462
+ }
463
+ await options.beforeScreenshot?.({ runStabilization: (stabilizationOptions) => waitForReadiness(handler, getStabilizationContext({
464
+ ...options,
465
+ stabilize: stabilizationOptions ?? options.stabilize
466
+ })) });
467
+ await waitForReadiness(handler, context);
468
+ const afterEach = await beforeEach(handler, context);
469
+ await waitForReadiness(handler, context);
470
+ const [snapshotPath] = await Promise.all([
471
+ (async () => {
472
+ if (!ariaSnapshot) return null;
473
+ const snapshotTarget = checkIsPage(screenshotTarget) ? screenshotTarget.locator("body") : screenshotTarget;
474
+ if (checkIsElementHandle(snapshotTarget)) throw new Error(`Element handle is not supported with "ariaSnapshot" option. Use a Locator instead.`);
475
+ const snapshotPath = screenshotToSnapshotPath(screenshotPath);
476
+ const snapshotMetadata = {
477
+ ...metadata,
478
+ transient: {
479
+ parentName: `${names.name}${PNG_EXTENSION}`,
480
+ ...metadata.transient.baseName ? { baseName: screenshotToSnapshotPath(metadata.transient.baseName) } : {}
481
+ }
482
+ };
483
+ await Promise.all([snapshotTarget.ariaSnapshot().then((snapshot) => {
484
+ return writeFile(snapshotPath, snapshot, "utf-8");
485
+ }), writeMetadata(snapshotPath, snapshotMetadata)]);
486
+ return snapshotPath;
487
+ })(),
488
+ screenshotTarget.screenshot({
489
+ path: screenshotPath,
490
+ type: "png",
491
+ fullPage,
492
+ mask: [handler.locator("[data-visual-test=\"blackout\"]")],
493
+ animations: "disabled",
494
+ ...playwrightOptions
495
+ }),
496
+ writeMetadata(screenshotPath, metadata)
497
+ ]);
498
+ const attachments = [{
499
+ name: getAttachmentName(names.name, "screenshot"),
500
+ contentType: "image/png",
501
+ path: screenshotPath
502
+ }, {
503
+ name: getAttachmentName(names.name, "screenshot/metadata"),
504
+ contentType: "application/json",
505
+ path: getMetadataPath(screenshotPath)
506
+ }];
507
+ if (snapshotPath) attachments.push({
508
+ name: getAttachmentName(names.name, "aria"),
509
+ contentType: "application/yaml",
510
+ path: snapshotPath
511
+ }, {
512
+ name: getAttachmentName(names.name, "aria/metadata"),
513
+ contentType: "application/json",
514
+ path: getMetadataPath(snapshotPath)
515
+ });
516
+ await attachAttachments({
517
+ attachments,
518
+ testInfo,
519
+ useArgosReporter
520
+ });
521
+ await afterEach();
522
+ await options.afterScreenshot?.();
523
+ return attachments;
524
+ };
525
+ const allAttachments = [];
526
+ if (viewports) {
527
+ if (checkIsFrame(handler)) throw new Error(`viewports option is not supported with an iframe`);
528
+ for (const viewport of viewports) {
529
+ const viewportSize = resolveViewport(viewport);
530
+ await setViewportSize(handler, viewportSize);
531
+ const attachments = await stabilizeAndScreenshot(getScreenshotName(name, { viewportWidth: viewportSize.width }));
532
+ allAttachments.push(...attachments);
533
+ }
534
+ if (!originalViewportSize) throw new Error(`Invariant: viewport size must be saved`);
535
+ await setViewportSize(handler, originalViewportSize);
536
+ } else {
537
+ const attachments = await stabilizeAndScreenshot(name);
538
+ allAttachments.push(...attachments);
539
+ }
540
+ await afterAll();
541
+ return allAttachments;
542
+ }
543
+ /**
544
+ * Get the stabilization context from the options.
545
+ */
546
+ function getStabilizationContext(options) {
547
+ const { fullPage, argosCSS, stabilize, viewports } = options;
548
+ return {
549
+ fullPage,
550
+ argosCSS,
551
+ viewports,
552
+ options: stabilize
553
+ };
554
+ }
555
+ //#endregion
556
+ //#region src/csp.ts
557
+ /**
558
+ * Get the CSP script hash.
559
+ */
560
+ function getCSPScriptHash() {
561
+ return `'sha256-${createHash("sha256").update(getGlobalScript()).digest("base64")}'`;
562
+ }
563
+ //#endregion
564
+ export { setMetadataConfig as DO_NOT_USE_setMetadataConfig, argosAriaSnapshot, argosScreenshot, getCSPScriptHash };