@argos-ci/playwright 3.2.0 → 3.4.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.d.ts CHANGED
@@ -20,6 +20,31 @@ type ArgosScreenshotOptions = {
20
20
  * @default true
21
21
  */
22
22
  disableHover?: boolean;
23
+ /**
24
+ * Sensitivity threshold between 0 and 1.
25
+ * The higher the threshold, the less sensitive the diff will be.
26
+ * @default 0.5
27
+ */
28
+ threshold?: number;
23
29
  } & LocatorOptions & ScreenshotOptions<LocatorScreenshotOptions> & ScreenshotOptions<PageScreenshotOptions>;
24
- declare function argosScreenshot(page: Page, name: string, options?: ArgosScreenshotOptions): Promise<void>;
30
+ /**
31
+ * Stabilize the UI and takes a screenshot of the application under test.
32
+ *
33
+ * @example
34
+ * argosScreenshot(page, "my-screenshot")
35
+ * @see https://argos-ci.com/docs/playwright#api-overview
36
+ */
37
+ declare function argosScreenshot(
38
+ /**
39
+ * Playwright `page` object.
40
+ */
41
+ page: Page,
42
+ /**
43
+ * Name of the screenshot. Must be unique.
44
+ */
45
+ name: string,
46
+ /**
47
+ * Options for the screenshot.
48
+ */
49
+ options?: ArgosScreenshotOptions): Promise<void>;
25
50
  export { ArgosScreenshotOptions, argosScreenshot };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdir } from 'node:fs/promises';
2
2
  import { relative, resolve, dirname } from 'node:path';
3
3
  import { resolveViewport, getGlobalScript } from '@argos-ci/browser';
4
- import { getGitRepositoryPath, readVersionFromPackage, getScreenshotName, writeMetadata, getMetadataPath } from '@argos-ci/util';
4
+ import { getGitRepositoryPath, readVersionFromPackage, getScreenshotName, validateThreshold, writeMetadata, getMetadataPath } from '@argos-ci/util';
5
5
  import { createRequire } from 'node:module';
6
6
 
7
7
  function getAttachmentName(name, type) {
@@ -60,6 +60,7 @@ async function getTestMetadataFromTestInfo(testInfo) {
60
60
  titlePath: testInfo.titlePath,
61
61
  retry: testInfo.retry,
62
62
  retries: testInfo.project.retries,
63
+ repeat: testInfo.repeatEachIndex,
63
64
  location: {
64
65
  file: repositoryPath ? relative(repositoryPath, testInfo.file) : testInfo.file,
65
66
  line: testInfo.line,
@@ -125,7 +126,40 @@ function getViewportSize(page) {
125
126
  });
126
127
  };
127
128
  }
128
- async function argosScreenshot(page, name, options = {}) {
129
+ /**
130
+ * Get the screenshot names based on the test info.
131
+ */ function getScreenshotNames(name, testInfo) {
132
+ if (testInfo) {
133
+ const projectName = `${testInfo.project.name}/${name}`;
134
+ if (testInfo.repeatEachIndex > 0) {
135
+ return {
136
+ name: `${projectName} repeat-${testInfo.repeatEachIndex}`,
137
+ baseName: projectName
138
+ };
139
+ }
140
+ return {
141
+ name: projectName,
142
+ baseName: null
143
+ };
144
+ }
145
+ return {
146
+ name,
147
+ baseName: null
148
+ };
149
+ }
150
+ /**
151
+ * Stabilize the UI and takes a screenshot of the application under test.
152
+ *
153
+ * @example
154
+ * argosScreenshot(page, "my-screenshot")
155
+ * @see https://argos-ci.com/docs/playwright#api-overview
156
+ */ async function argosScreenshot(/**
157
+ * Playwright `page` object.
158
+ */ page, /**
159
+ * Name of the screenshot. Must be unique.
160
+ */ name, /**
161
+ * Options for the screenshot.
162
+ */ options = {}) {
129
163
  const { element, has, hasText, viewports, argosCSS, ...playwrightOptions } = options;
130
164
  if (!page) {
131
165
  throw new Error("A Playwright `page` object is required.");
@@ -180,9 +214,17 @@ async function argosScreenshot(page, name, options = {}) {
180
214
  };
181
215
  const stabilizeAndScreenshot = async (name)=>{
182
216
  await page.waitForFunction(()=>window.__ARGOS__.waitForStability());
217
+ const names = getScreenshotNames(name, testInfo);
183
218
  const metadata = await collectMetadata(testInfo);
184
- const nameInProject = testInfo?.project.name ? `${testInfo.project.name}/${name}` : name;
185
- const screenshotPath = useArgosReporter && testInfo ? testInfo.outputPath("argos", `${nameInProject}.png`) : resolve(screenshotFolder, `${nameInProject}.png`);
219
+ metadata.transient = {};
220
+ if (options.threshold !== undefined) {
221
+ validateThreshold(options.threshold);
222
+ metadata.transient.threshold = options.threshold;
223
+ }
224
+ if (names.baseName) {
225
+ metadata.transient.baseName = `${names.baseName}.png`;
226
+ }
227
+ const screenshotPath = useArgosReporter && testInfo ? testInfo.outputPath("argos", `${names.name}.png`) : resolve(screenshotFolder, `${names.name}.png`);
186
228
  const dir = dirname(screenshotPath);
187
229
  if (dir !== screenshotFolder) {
188
230
  await mkdir(dirname(screenshotPath), {
@@ -204,11 +246,11 @@ async function argosScreenshot(page, name, options = {}) {
204
246
  ]);
205
247
  if (useArgosReporter && testInfo) {
206
248
  await Promise.all([
207
- testInfo.attach(getAttachmentName(nameInProject, "metadata"), {
249
+ testInfo.attach(getAttachmentName(names.name, "metadata"), {
208
250
  path: getMetadataPath(screenshotPath),
209
251
  contentType: "application/json"
210
252
  }),
211
- testInfo.attach(getAttachmentName(nameInProject, "screenshot"), {
253
+ testInfo.attach(getAttachmentName(names.name, "screenshot"), {
212
254
  path: screenshotPath,
213
255
  contentType: "image/png"
214
256
  })
@@ -1,15 +1,39 @@
1
1
  /// <reference types="node" />
2
2
  import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from "@playwright/test/reporter";
3
3
  import { UploadParameters } from "@argos-ci/core";
4
- type ArgosReporterOptions = Omit<UploadParameters, "files" | "root"> & {
4
+ /**
5
+ * Dynamic build name.
6
+ * We require all values in order to ensure it works correctly in parallel mode.
7
+ */
8
+ type DynamicBuildName<T extends readonly string[]> = {
9
+ /**
10
+ * The values that the build name can take.
11
+ * It is required to ensure Argos will always upload
12
+ * for each build name in order to work in sharding mode.
13
+ */
14
+ values: readonly [...T];
15
+ /**
16
+ * Get the build name for a test case.
17
+ * Returns any of the values in `values`.
18
+ */
19
+ get: (test: TestCase) => T[number];
20
+ };
21
+ type ArgosReporterOptions<T extends string[] = string[]> = Omit<UploadParameters, "files" | "root" | "buildName"> & {
5
22
  /**
6
23
  * Upload the report to Argos.
7
24
  * @default true
8
25
  */
9
26
  uploadToArgos?: boolean;
27
+ /**
28
+ * The name of the build in Argos.
29
+ * Can be a string or a function that receives the test case and returns the build name.
30
+ */
31
+ buildName?: string | DynamicBuildName<T> | null;
10
32
  };
33
+ declare function createArgosReporterOptions<T extends string[]>(options: ArgosReporterOptions<T>): ArgosReporterOptions<T>;
11
34
  declare class ArgosReporter implements Reporter {
12
- createUploadDirPromise: null | Promise<string>;
35
+ rootUploadDirectoryPromise: null | Promise<string>;
36
+ uploadDirectoryPromises: Map<string, Promise<string>>;
13
37
  config: ArgosReporterOptions;
14
38
  playwrightConfig: FullConfig;
15
39
  uploadToArgos: boolean;
@@ -35,12 +59,17 @@ declare class ArgosReporter implements Reporter {
35
59
  * Copy the trace file if found in the result.
36
60
  */
37
61
  copyTraceIfFound(result: TestResult, path: string): Promise<void>;
38
- getAutomaticScreenshotName(test: TestCase, result: TestResult): string;
39
- getUploadDir(): Promise<string>;
62
+ /**
63
+ * Get the root upload directory (cached).
64
+ */
65
+ /**
66
+ * Get the root upload directory (cached).
67
+ */
68
+ getRootUploadDirectory(): Promise<string>;
40
69
  onBegin(config: FullConfig, _suite: Suite): void;
41
70
  onTestEnd(test: TestCase, result: TestResult): Promise<void>;
42
71
  onEnd(_result: FullResult): Promise<{
43
72
  status: "failed";
44
73
  } | undefined>;
45
74
  }
46
- export { ArgosReporter as default, ArgosReporterOptions };
75
+ export { ArgosReporter as default, ArgosReporterOptions, createArgosReporterOptions };
package/dist/reporter.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { upload, readConfig } from '@argos-ci/core';
3
3
  import { randomBytes } from 'node:crypto';
4
- import { mkdir, writeFile, copyFile } from 'node:fs/promises';
4
+ import { writeFile, copyFile, readdir, mkdir } from 'node:fs/promises';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { relative, dirname, join } from 'node:path';
7
7
  import { getGitRepositoryPath, readVersionFromPackage } from '@argos-ci/util';
@@ -84,6 +84,7 @@ async function getTestMetadataFromTestCase(testCase, testResult) {
84
84
  titlePath: testCase.titlePath(),
85
85
  retry: testResult.retry,
86
86
  retries: testCase.retries,
87
+ repeat: testCase.repeatEachIndex,
87
88
  location: {
88
89
  file: repositoryPath ? relative(repositoryPath, testCase.location.file) : testCase.location.file,
89
90
  line: testCase.location.line,
@@ -107,16 +108,38 @@ async function getMetadataFromTestCase(testCase, testResult) {
107
108
  const KEY = "@argos-ci/playwright";
108
109
  const debug = createDebug(KEY);
109
110
 
110
- async function createUploadDirectory() {
111
- debug("Creating temporary directory for screenshots");
111
+ const createDirectoryPromises = new Map();
112
+ /**
113
+ * Create a directory if it doesn't exist.
114
+ */ async function createDirectory(pathname) {
115
+ let promise = createDirectoryPromises.get(pathname);
116
+ if (promise) {
117
+ return promise;
118
+ }
119
+ promise = mkdir(pathname, {
120
+ recursive: true
121
+ }).then(()=>{});
122
+ createDirectoryPromises.set(pathname, promise);
123
+ return promise;
124
+ }
125
+ /**
126
+ * Create temporary directory.
127
+ */ async function createTemporaryDirectory() {
128
+ debug("Creating temporary directory");
112
129
  const osTmpDirectory = tmpdir();
113
130
  const path = join(osTmpDirectory, "argos." + randomBytes(16).toString("hex"));
114
- await mkdir(path, {
115
- recursive: true
116
- });
131
+ await createDirectory(path);
117
132
  debug(`Temporary directory created: ${path}`);
118
133
  return path;
119
134
  }
135
+ /**
136
+ * Check if the build name is dynamic.
137
+ */ function checkIsDynamicBuildName(buildName) {
138
+ return Boolean(typeof buildName === "object" && buildName);
139
+ }
140
+ function createArgosReporterOptions(options) {
141
+ return options;
142
+ }
120
143
  async function getParallelFromConfig(config) {
121
144
  if (!config.shard) return null;
122
145
  if (config.shard.total === 1) return null;
@@ -130,26 +153,30 @@ async function getParallelFromConfig(config) {
130
153
  index: config.shard.current
131
154
  };
132
155
  }
156
+ /**
157
+ * Get the automatic screenshot name.
158
+ */ function getAutomaticScreenshotName(test, result) {
159
+ let name = test.titlePath().join(" ");
160
+ name += result.retry > 0 ? ` #${result.retry + 1}` : "";
161
+ name += result.status === "failed" || result.status === "timedOut" ? " (failed)" : "";
162
+ return name;
163
+ }
133
164
  class ArgosReporter {
134
- createUploadDirPromise;
165
+ rootUploadDirectoryPromise;
166
+ uploadDirectoryPromises;
135
167
  config;
136
168
  playwrightConfig;
137
169
  uploadToArgos;
138
170
  constructor(config){
139
171
  this.config = config;
140
172
  this.uploadToArgos = config.uploadToArgos ?? true;
141
- this.createUploadDirPromise = null;
173
+ this.rootUploadDirectoryPromise = null;
174
+ this.uploadDirectoryPromises = new Map();
142
175
  }
143
176
  /**
144
177
  * Write a file to the temporary directory.
145
178
  */ async writeFile(path, body) {
146
- const uploadDir = await this.getUploadDir();
147
- const dir = dirname(path);
148
- if (dir !== uploadDir) {
149
- await mkdir(dir, {
150
- recursive: true
151
- });
152
- }
179
+ await createDirectory(dirname(path));
153
180
  debug(`Writing file to ${path}`);
154
181
  await writeFile(path, body);
155
182
  debug(`File written to ${path}`);
@@ -157,13 +184,7 @@ class ArgosReporter {
157
184
  /**
158
185
  * Copy a file to the temporary directory.
159
186
  */ async copyFile(from, to) {
160
- const uploadDir = await this.getUploadDir();
161
- const dir = dirname(to);
162
- if (dir !== uploadDir) {
163
- await mkdir(dir, {
164
- recursive: true
165
- });
166
- }
187
+ await createDirectory(dirname(to));
167
188
  debug(`Copying file from ${from} to ${to}`);
168
189
  await copyFile(from, to);
169
190
  debug(`File copied from ${from} to ${to}`);
@@ -176,24 +197,25 @@ class ArgosReporter {
176
197
  await this.copyFile(trace.path, path + ".pw-trace.zip");
177
198
  }
178
199
  }
179
- getAutomaticScreenshotName(test, result) {
180
- let name = test.titlePath().join(" ");
181
- name += result.retry > 0 ? ` #${result.retry + 1}` : "";
182
- name += result.status === "failed" || result.status === "timedOut" ? " (failed)" : "";
183
- return name;
184
- }
185
- getUploadDir() {
186
- if (!this.createUploadDirPromise) {
187
- this.createUploadDirPromise = createUploadDirectory();
200
+ /**
201
+ * Get the root upload directory (cached).
202
+ */ getRootUploadDirectory() {
203
+ if (!this.rootUploadDirectoryPromise) {
204
+ this.rootUploadDirectoryPromise = createTemporaryDirectory();
188
205
  }
189
- return this.createUploadDirPromise;
206
+ return this.rootUploadDirectoryPromise;
190
207
  }
191
208
  onBegin(config, _suite) {
192
209
  debug("ArgosReporter:onBegin");
193
210
  this.playwrightConfig = config;
194
211
  }
195
212
  async onTestEnd(test, result) {
196
- const uploadDir = await this.getUploadDir();
213
+ const buildName = checkIsDynamicBuildName(this.config.buildName) ? this.config.buildName.get(test) : this.config.buildName;
214
+ if (buildName === "") {
215
+ throw new Error('Argos "buildName" cannot be an empty string.');
216
+ }
217
+ const rootUploadDir = await this.getRootUploadDirectory();
218
+ const uploadDir = buildName ? join(rootUploadDir, buildName) : rootUploadDir;
197
219
  debug("ArgosReporter:onTestEnd");
198
220
  await Promise.all(result.attachments.map(async (attachment)=>{
199
221
  if (checkIsArgosScreenshot(attachment) || checkIsArgosScreenshotMetadata(attachment)) {
@@ -207,7 +229,7 @@ class ArgosReporter {
207
229
  // Error screenshots are sent to Argos
208
230
  if (checkIsAutomaticScreenshot(attachment)) {
209
231
  const metadata = await getMetadataFromTestCase(test, result);
210
- const name = this.getAutomaticScreenshotName(test, result);
232
+ const name = getAutomaticScreenshotName(test, result);
211
233
  const path = join(uploadDir, `${name}.png`);
212
234
  await Promise.all([
213
235
  this.writeFile(path + ".argos.json", JSON.stringify(metadata)),
@@ -220,10 +242,10 @@ class ArgosReporter {
220
242
  }
221
243
  async onEnd(_result) {
222
244
  debug("ArgosReporter:onEnd");
223
- const uploadDir = await this.getUploadDir();
245
+ const rootUploadDir = await this.getRootUploadDirectory();
224
246
  if (!this.uploadToArgos) {
225
247
  debug("Not uploading to Argos because uploadToArgos is false.");
226
- debug(`Upload directory: ${uploadDir}`);
248
+ debug(`Upload directory: ${rootUploadDir}`);
227
249
  return;
228
250
  }
229
251
  debug("Getting parallel from config");
@@ -233,17 +255,46 @@ class ArgosReporter {
233
255
  } else {
234
256
  debug("Non-parallel mode");
235
257
  }
258
+ const buildNameConfig = this.config.buildName;
259
+ const uploadOptions = {
260
+ files: [
261
+ "**/*.png"
262
+ ],
263
+ parallel: parallel ?? undefined,
264
+ ...this.config
265
+ };
236
266
  try {
237
- debug("Uploading to Argos");
238
- const res = await upload({
239
- files: [
240
- "**/*.png"
241
- ],
242
- root: uploadDir,
243
- parallel: parallel ?? undefined,
244
- ...this.config
245
- });
246
- console.log(chalk.green(`✅ Argos build created: ${res.build.url}`));
267
+ if (checkIsDynamicBuildName(buildNameConfig)) {
268
+ debug(`Dynamic build names, uploading to Argos for each build name: ${buildNameConfig.values.join()}`);
269
+ const directories = await readdir(rootUploadDir);
270
+ // Check if the buildName.values are consistent with the directories created
271
+ if (directories.some((dir)=>!buildNameConfig.values.includes(dir))) {
272
+ throw new Error(`The \`buildName.values\` (${buildNameConfig.values.join(", ")}) are inconsistent with the \`buildName.get\` returns values (${directories.join(", ")}). Please fix the configuration.`);
273
+ }
274
+ // In non-parallel mode, we iterate over the directories to avoid creating useless builds
275
+ const iteratesOnBuildNames = parallel ? buildNameConfig.values : directories;
276
+ // Iterate over each build name and upload the screenshots
277
+ for (const buildName of iteratesOnBuildNames){
278
+ const uploadDir = join(rootUploadDir, buildName);
279
+ await createDirectory(uploadDir);
280
+ debug(`Uploading to Argos for build: ${buildName}`);
281
+ const res = await upload({
282
+ ...uploadOptions,
283
+ root: uploadDir,
284
+ buildName
285
+ });
286
+ console.log(chalk.green(`✅ Argos "${buildName}" build created: ${res.build.url}`));
287
+ }
288
+ } else {
289
+ debug("Uploading to Argos");
290
+ const uploadDir = buildNameConfig ? join(rootUploadDir, buildNameConfig) : rootUploadDir;
291
+ const res = await upload({
292
+ ...uploadOptions,
293
+ root: uploadDir,
294
+ buildName: buildNameConfig ?? undefined
295
+ });
296
+ console.log(chalk.green(`✅ Argos build created: ${res.build.url}`));
297
+ }
247
298
  } catch (error) {
248
299
  console.error(error);
249
300
  return {
@@ -254,4 +305,4 @@ class ArgosReporter {
254
305
  }
255
306
  }
256
307
 
257
- export { ArgosReporter as default };
308
+ export { createArgosReporterOptions, ArgosReporter as default };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@argos-ci/playwright",
3
3
  "description": "Visual testing solution to avoid visual regression. Playwright commands and utilities for Argos visual testing.",
4
- "version": "3.2.0",
4
+ "version": "3.4.0",
5
5
  "author": "Smooth Code",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -43,13 +43,13 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@argos-ci/browser": "2.1.2",
46
- "@argos-ci/core": "2.3.0",
47
- "@argos-ci/util": "2.0.0",
46
+ "@argos-ci/core": "2.4.0",
47
+ "@argos-ci/util": "2.1.0",
48
48
  "chalk": "^5.3.0",
49
49
  "debug": "^4.3.4"
50
50
  },
51
51
  "devDependencies": {
52
- "@argos-ci/cli": "2.2.0",
52
+ "@argos-ci/cli": "2.3.0",
53
53
  "@argos-ci/playwright": "workspace:.",
54
54
  "@playwright/test": "^1.43.0",
55
55
  "@types/debug": "^4.1.12",
@@ -61,5 +61,5 @@
61
61
  "test": "pnpm exec -- playwright test",
62
62
  "e2e": "UPLOAD_TO_ARGOS=true pnpm run test"
63
63
  },
64
- "gitHead": "0c328bfcd6475ad625c0335cab11e912e856173c"
64
+ "gitHead": "aca82cf842a6d8310611a8594c581794df85cbd8"
65
65
  }