@argos-ci/playwright 3.1.1-alpha.0 → 3.3.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.
@@ -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;
@@ -36,11 +60,17 @@ declare class ArgosReporter implements Reporter {
36
60
  */
37
61
  copyTraceIfFound(result: TestResult, path: string): Promise<void>;
38
62
  getAutomaticScreenshotName(test: TestCase, result: TestResult): string;
39
- getUploadDir(): Promise<string>;
63
+ /**
64
+ * Get the root upload directory (cached).
65
+ */
66
+ /**
67
+ * Get the root upload directory (cached).
68
+ */
69
+ getRootUploadDirectory(): Promise<string>;
40
70
  onBegin(config: FullConfig, _suite: Suite): void;
41
71
  onTestEnd(test: TestCase, result: TestResult): Promise<void>;
42
72
  onEnd(_result: FullResult): Promise<{
43
73
  status: "failed";
44
74
  } | undefined>;
45
75
  }
46
- export { ArgosReporter as default, ArgosReporterOptions };
76
+ 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';
@@ -107,16 +107,38 @@ async function getMetadataFromTestCase(testCase, testResult) {
107
107
  const KEY = "@argos-ci/playwright";
108
108
  const debug = createDebug(KEY);
109
109
 
110
- async function createUploadDirectory() {
111
- debug("Creating temporary directory for screenshots");
110
+ const createDirectoryPromises = new Map();
111
+ /**
112
+ * Create a directory if it doesn't exist.
113
+ */ async function createDirectory(pathname) {
114
+ let promise = createDirectoryPromises.get(pathname);
115
+ if (promise) {
116
+ return promise;
117
+ }
118
+ promise = mkdir(pathname, {
119
+ recursive: true
120
+ }).then(()=>{});
121
+ createDirectoryPromises.set(pathname, promise);
122
+ return promise;
123
+ }
124
+ /**
125
+ * Create temporary directory.
126
+ */ async function createTemporaryDirectory() {
127
+ debug("Creating temporary directory");
112
128
  const osTmpDirectory = tmpdir();
113
129
  const path = join(osTmpDirectory, "argos." + randomBytes(16).toString("hex"));
114
- await mkdir(path, {
115
- recursive: true
116
- });
130
+ await createDirectory(path);
117
131
  debug(`Temporary directory created: ${path}`);
118
132
  return path;
119
133
  }
134
+ /**
135
+ * Check if the build name is dynamic.
136
+ */ function checkIsDynamicBuildName(buildName) {
137
+ return Boolean(typeof buildName === "object" && buildName);
138
+ }
139
+ function createArgosReporterOptions(options) {
140
+ return options;
141
+ }
120
142
  async function getParallelFromConfig(config) {
121
143
  if (!config.shard) return null;
122
144
  if (config.shard.total === 1) return null;
@@ -131,25 +153,21 @@ async function getParallelFromConfig(config) {
131
153
  };
132
154
  }
133
155
  class ArgosReporter {
134
- createUploadDirPromise;
156
+ rootUploadDirectoryPromise;
157
+ uploadDirectoryPromises;
135
158
  config;
136
159
  playwrightConfig;
137
160
  uploadToArgos;
138
161
  constructor(config){
139
162
  this.config = config;
140
163
  this.uploadToArgos = config.uploadToArgos ?? true;
141
- this.createUploadDirPromise = null;
164
+ this.rootUploadDirectoryPromise = null;
165
+ this.uploadDirectoryPromises = new Map();
142
166
  }
143
167
  /**
144
168
  * Write a file to the temporary directory.
145
169
  */ 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
- }
170
+ await createDirectory(dirname(path));
153
171
  debug(`Writing file to ${path}`);
154
172
  await writeFile(path, body);
155
173
  debug(`File written to ${path}`);
@@ -157,13 +175,7 @@ class ArgosReporter {
157
175
  /**
158
176
  * Copy a file to the temporary directory.
159
177
  */ 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
- }
178
+ await createDirectory(dirname(to));
167
179
  debug(`Copying file from ${from} to ${to}`);
168
180
  await copyFile(from, to);
169
181
  debug(`File copied from ${from} to ${to}`);
@@ -182,18 +194,25 @@ class ArgosReporter {
182
194
  name += result.status === "failed" || result.status === "timedOut" ? " (failed)" : "";
183
195
  return name;
184
196
  }
185
- getUploadDir() {
186
- if (!this.createUploadDirPromise) {
187
- this.createUploadDirPromise = createUploadDirectory();
197
+ /**
198
+ * Get the root upload directory (cached).
199
+ */ getRootUploadDirectory() {
200
+ if (!this.rootUploadDirectoryPromise) {
201
+ this.rootUploadDirectoryPromise = createTemporaryDirectory();
188
202
  }
189
- return this.createUploadDirPromise;
203
+ return this.rootUploadDirectoryPromise;
190
204
  }
191
205
  onBegin(config, _suite) {
192
206
  debug("ArgosReporter:onBegin");
193
207
  this.playwrightConfig = config;
194
208
  }
195
209
  async onTestEnd(test, result) {
196
- const uploadDir = await this.getUploadDir();
210
+ const buildName = checkIsDynamicBuildName(this.config.buildName) ? this.config.buildName.get(test) : this.config.buildName;
211
+ if (buildName === "") {
212
+ throw new Error('Argos "buildName" cannot be an empty string.');
213
+ }
214
+ const rootUploadDir = await this.getRootUploadDirectory();
215
+ const uploadDir = buildName ? join(rootUploadDir, buildName) : rootUploadDir;
197
216
  debug("ArgosReporter:onTestEnd");
198
217
  await Promise.all(result.attachments.map(async (attachment)=>{
199
218
  if (checkIsArgosScreenshot(attachment) || checkIsArgosScreenshotMetadata(attachment)) {
@@ -220,10 +239,10 @@ class ArgosReporter {
220
239
  }
221
240
  async onEnd(_result) {
222
241
  debug("ArgosReporter:onEnd");
223
- const uploadDir = await this.getUploadDir();
242
+ const rootUploadDir = await this.getRootUploadDirectory();
224
243
  if (!this.uploadToArgos) {
225
244
  debug("Not uploading to Argos because uploadToArgos is false.");
226
- debug(`Upload directory: ${uploadDir}`);
245
+ debug(`Upload directory: ${rootUploadDir}`);
227
246
  return;
228
247
  }
229
248
  debug("Getting parallel from config");
@@ -233,17 +252,46 @@ class ArgosReporter {
233
252
  } else {
234
253
  debug("Non-parallel mode");
235
254
  }
255
+ const buildNameConfig = this.config.buildName;
256
+ const uploadOptions = {
257
+ files: [
258
+ "**/*.png"
259
+ ],
260
+ parallel: parallel ?? undefined,
261
+ ...this.config
262
+ };
236
263
  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}`));
264
+ if (checkIsDynamicBuildName(buildNameConfig)) {
265
+ debug(`Dynamic build names, uploading to Argos for each build name: ${buildNameConfig.values.join()}`);
266
+ const directories = await readdir(rootUploadDir);
267
+ // Check if the buildName.values are consistent with the directories created
268
+ if (directories.some((dir)=>!buildNameConfig.values.includes(dir))) {
269
+ throw new Error(`The \`buildName.values\` (${buildNameConfig.values.join(", ")}) are inconsistent with the \`buildName.get\` returns values (${directories.join(", ")}). Please fix the configuration.`);
270
+ }
271
+ // In non-parallel mode, we iterate over the directories to avoid creating useless builds
272
+ const iteratesOnBuildNames = parallel ? buildNameConfig.values : directories;
273
+ // Iterate over each build name and upload the screenshots
274
+ for (const buildName of iteratesOnBuildNames){
275
+ const uploadDir = join(rootUploadDir, buildName);
276
+ await createDirectory(uploadDir);
277
+ debug(`Uploading to Argos for build: ${buildName}`);
278
+ const res = await upload({
279
+ ...uploadOptions,
280
+ root: uploadDir,
281
+ buildName
282
+ });
283
+ console.log(chalk.green(`✅ Argos "${buildName}" build created: ${res.build.url}`));
284
+ }
285
+ } else {
286
+ debug("Uploading to Argos");
287
+ const uploadDir = buildNameConfig ? join(rootUploadDir, buildNameConfig) : rootUploadDir;
288
+ const res = await upload({
289
+ ...uploadOptions,
290
+ root: uploadDir,
291
+ buildName: buildNameConfig ?? undefined
292
+ });
293
+ console.log(chalk.green(`✅ Argos build created: ${res.build.url}`));
294
+ }
247
295
  } catch (error) {
248
296
  console.error(error);
249
297
  return {
@@ -254,4 +302,4 @@ class ArgosReporter {
254
302
  }
255
303
  }
256
304
 
257
- export { ArgosReporter as default };
305
+ 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.1.1-alpha.0+d1a818a",
4
+ "version": "3.3.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.2.1-alpha.7+d1a818a",
46
+ "@argos-ci/core": "2.3.0",
47
47
  "@argos-ci/util": "2.0.0",
48
48
  "chalk": "^5.3.0",
49
49
  "debug": "^4.3.4"
50
50
  },
51
51
  "devDependencies": {
52
- "@argos-ci/cli": "2.1.1-alpha.7+d1a818a",
52
+ "@argos-ci/cli": "2.2.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": "d1a818aeee5bcba93c0492be43704f8c3682222e"
64
+ "gitHead": "5c09f1d861630af0d98200e52a0f44876bb7f891"
65
65
  }