@empiricalrun/playwright-utils 0.36.0 → 0.37.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @empiricalrun/playwright-utils
2
2
 
3
+ ## 0.37.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 14a5406: feat: adopt dash proxy in email client
8
+ - 11c0b24: refactor: uploader abstraction in playwright reporter
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [3ee1aec]
13
+ - @empiricalrun/r2-uploader@0.8.0
14
+ - @empiricalrun/test-gen@0.78.6
15
+
3
16
  ## 0.36.0
4
17
 
5
18
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE1E,MAAM,MAAM,KAAK,GAAG,cAAc,CAAC;AAEnC,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAKF,KAAK,kBAAkB,GAAG;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,WAAW;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,SAAa;IACrB,OAAO,CAAC,MAAM,CAAkB;gBAEpB,IAAI,GAAE,kBAAuB;IA2BzC,UAAU,IAAI,MAAM;IAId,YAAY,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC;CAGzD"}
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE1E,MAAM,MAAM,KAAK,GAAG,cAAc,CAAC;AAEnC,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAKF,KAAK,kBAAkB,GAAG;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,WAAW;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,SAAa;IACrB,OAAO,CAAC,MAAM,CAAkB;gBAEpB,IAAI,GAAE,kBAAuB;IAwCzC,UAAU,IAAI,MAAM;IAId,YAAY,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC;CAGzD"}
package/dist/email.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EmailClient = void 0;
4
+ const dashboard_1 = require("@empiricalrun/test-gen/dashboard");
4
5
  const mailosaur_client_1 = require("./mailosaur-client");
5
6
  const DEFAULT_TIMEOUT = 30_000;
6
7
  const SERVER_ID = "pnyrwq5o";
@@ -10,17 +11,30 @@ class EmailClient {
10
11
  serverId = SERVER_ID;
11
12
  client;
12
13
  constructor(opts = {}) {
13
- const apiKey = process.env.MAILOSAUR_API_KEY;
14
- if (!apiKey) {
15
- throw new Error("'apiKey' must be set");
16
- }
17
14
  const { timeout = DEFAULT_TIMEOUT, emailId: knownEmailId } = opts;
18
15
  this.timeout = timeout;
19
- this.client = new mailosaur_client_1.MailosaurClient({
20
- apiKey,
21
- serverId: this.serverId,
22
- timeout: this.timeout,
23
- });
16
+ const empiricalApiKey = process.env.EMPIRICALRUN_API_KEY;
17
+ if (empiricalApiKey) {
18
+ const apiClient = new dashboard_1.DashboardAPIClient({
19
+ authType: "project-api-key",
20
+ });
21
+ this.client = new mailosaur_client_1.MailosaurClient({
22
+ apiClient,
23
+ serverId: this.serverId,
24
+ timeout: this.timeout,
25
+ });
26
+ }
27
+ else {
28
+ const apiKey = process.env.MAILOSAUR_API_KEY;
29
+ if (!apiKey) {
30
+ throw new Error("Either EMPIRICALRUN_API_KEY or MAILOSAUR_API_KEY must be set");
31
+ }
32
+ this.client = new mailosaur_client_1.MailosaurClient({
33
+ apiKey,
34
+ serverId: this.serverId,
35
+ timeout: this.timeout,
36
+ });
37
+ }
24
38
  const emailId = knownEmailId ||
25
39
  [...Array(7)].map(() => Math.random().toString(36)[2]).join("");
26
40
  if (emailId.includes("@")) {
@@ -1,3 +1,4 @@
1
+ import type { IDashboardAPIClient } from "@empiricalrun/shared-types/api/base";
1
2
  type Link = {
2
3
  text?: string;
3
4
  href?: string;
@@ -10,15 +11,22 @@ export type MailosaurEmail = {
10
11
  codes: string[];
11
12
  };
12
13
  type MailosaurClientOptions = {
13
- apiKey: string;
14
14
  serverId: string;
15
15
  timeout: number;
16
- };
16
+ } & ({
17
+ apiKey: string;
18
+ apiClient?: never;
19
+ } | {
20
+ apiKey?: never;
21
+ apiClient: IDashboardAPIClient;
22
+ });
17
23
  export declare class MailosaurClient {
18
- private apiKey;
24
+ private apiKey?;
25
+ private apiClient?;
19
26
  private serverId;
20
27
  private timeout;
21
28
  constructor(opts: MailosaurClientOptions);
29
+ private makeRequest;
22
30
  waitForEmail(address: string, filter?: {
23
31
  from?: string;
24
32
  subject?: string;
@@ -26,7 +34,6 @@ export declare class MailosaurClient {
26
34
  }): Promise<MailosaurEmail>;
27
35
  private searchWithPolling;
28
36
  private getMessageById;
29
- private handleMailosaurError;
30
37
  private parseEmailResponse;
31
38
  private sleep;
32
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mailosaur-client.d.ts","sourceRoot":"","sources":["../src/mailosaur-client.ts"],"names":[],"mappings":"AAAA,KAAK,IAAI,GAAG;IACV,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AA6BF,KAAK,sBAAsB,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,sBAAsB;IAMlC,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1D,OAAO,CAAC,cAAc,CAAC;YAqBZ,iBAAiB;YAyEjB,cAAc;YAuBd,oBAAoB;IAqBlC,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,KAAK;CAGd"}
1
+ {"version":3,"file":"mailosaur-client.d.ts","sourceRoot":"","sources":["../src/mailosaur-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE/E,KAAK,IAAI,GAAG;IACV,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAkCF,KAAK,sBAAsB,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,CACA;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAE,GACrC;IAAE,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,mBAAmB,CAAA;CAAE,CACrD,CAAC;AAEF,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,SAAS,CAAC,CAAsB;IACxC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,sBAAsB;YAW1B,WAAW;IA0CnB,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1D,OAAO,CAAC,cAAc,CAAC;YAqBZ,iBAAiB;YA2EjB,cAAc;IA+B5B,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,KAAK;CAGd"}
@@ -5,12 +5,47 @@ const MAILOSAUR_BASE_URL = "https://mailosaur.com/api";
5
5
  const DEFAULT_POLL_DELAY = 1000;
6
6
  class MailosaurClient {
7
7
  apiKey;
8
+ apiClient;
8
9
  serverId;
9
10
  timeout;
10
11
  constructor(opts) {
11
- this.apiKey = opts.apiKey;
12
12
  this.serverId = opts.serverId;
13
13
  this.timeout = opts.timeout;
14
+ if ("apiClient" in opts && opts.apiClient) {
15
+ this.apiClient = opts.apiClient;
16
+ }
17
+ else {
18
+ this.apiKey = opts.apiKey;
19
+ }
20
+ }
21
+ async makeRequest(method, path, body) {
22
+ if (this.apiClient) {
23
+ const result = await this.apiClient.callMailosaurProxy({
24
+ method,
25
+ path,
26
+ body,
27
+ });
28
+ return {
29
+ data: result.data,
30
+ headers: result.headers || {},
31
+ status: 200,
32
+ };
33
+ }
34
+ const url = `${MAILOSAUR_BASE_URL}${path}`;
35
+ const response = await fetch(url, {
36
+ method,
37
+ headers: {
38
+ Authorization: `Basic ${Buffer.from(`${this.apiKey}:`).toString("base64")}`,
39
+ "Content-Type": "application/json",
40
+ },
41
+ body: body ? JSON.stringify(body) : undefined,
42
+ });
43
+ const responseHeaders = {};
44
+ response.headers.forEach((value, key) => {
45
+ responseHeaders[key] = value;
46
+ });
47
+ const data = await response.json().catch(() => null);
48
+ return { data, headers: responseHeaders, status: response.status };
14
49
  }
15
50
  async waitForEmail(address, filter) {
16
51
  const now = new Date();
@@ -36,31 +71,29 @@ class MailosaurClient {
36
71
  itemsPerPage: "1",
37
72
  receivedAfter: receivedAfter.toISOString(),
38
73
  });
39
- const url = `${MAILOSAUR_BASE_URL}/messages/search?${searchParams.toString()}`;
40
- let response;
74
+ const path = `/messages/search?${searchParams.toString()}`;
75
+ let result;
41
76
  try {
42
- response = await fetch(url, {
43
- method: "POST",
44
- headers: {
45
- Authorization: `Basic ${Buffer.from(`${this.apiKey}:`).toString("base64")}`,
46
- "Content-Type": "application/json",
47
- },
48
- body: JSON.stringify(criteria),
49
- });
77
+ result = await this.makeRequest("POST", path, criteria);
50
78
  }
51
79
  catch (err) {
52
80
  console.error("Mailosaur network error during search:", err);
81
+ if (err instanceof Error && "status" in err && err.status === 401) {
82
+ throw new Error("Email provider error: Authentication error");
83
+ }
53
84
  throw new Error("Email provider error: Network request failed");
54
85
  }
55
- if (!response.ok) {
56
- await this.handleMailosaurError(response, "search");
86
+ if (result.status === 401) {
87
+ throw new Error("Email provider error: Authentication error");
57
88
  }
58
- const body = (await response.json());
59
- if (body.items && body.items.length > 0 && body.items[0]?.id) {
89
+ if (result.status >= 400) {
90
+ throw new Error(`Email provider error (search): HTTP ${result.status}`);
91
+ }
92
+ const body = result.data;
93
+ if (body?.items && body.items.length > 0 && body.items[0]?.id) {
60
94
  return body.items[0].id;
61
95
  }
62
- // Parse delay from response header, filtering out invalid values
63
- const header = response.headers.get("x-ms-delay");
96
+ const header = result.headers["x-ms-delay"];
64
97
  let delay = DEFAULT_POLL_DELAY;
65
98
  if (header) {
66
99
  const pattern = header
@@ -73,7 +106,6 @@ class MailosaurClient {
73
106
  }
74
107
  }
75
108
  pollCount += 1;
76
- // Check if we'll exceed timeout
77
109
  const elapsed = Date.now() - startTime;
78
110
  if (elapsed + delay > this.timeout) {
79
111
  throw new Error(`Email not received at ${address} within ${this.timeout}ms`);
@@ -82,32 +114,25 @@ class MailosaurClient {
82
114
  }
83
115
  }
84
116
  async getMessageById(messageId) {
85
- const url = `${MAILOSAUR_BASE_URL}/messages/${messageId}`;
86
- let response;
117
+ const path = `/messages/${messageId}`;
118
+ let result;
87
119
  try {
88
- response = await fetch(url, {
89
- method: "GET",
90
- headers: {
91
- Authorization: `Basic ${Buffer.from(`${this.apiKey}:`).toString("base64")}`,
92
- },
93
- });
120
+ result = await this.makeRequest("GET", path);
94
121
  }
95
122
  catch (err) {
96
123
  console.error("Mailosaur network error during getMessageById:", err);
124
+ if (err instanceof Error && "status" in err && err.status === 401) {
125
+ throw new Error("Email provider error: Authentication error");
126
+ }
97
127
  throw new Error("Email provider error: Network request failed");
98
128
  }
99
- if (!response.ok) {
100
- await this.handleMailosaurError(response, "getMessageById");
101
- }
102
- return (await response.json());
103
- }
104
- async handleMailosaurError(response, context) {
105
- const errorBody = await response.text().catch(() => "");
106
- console.error(`Mailosaur error in ${context}:`, errorBody || response.statusText);
107
- if (response.status === 401) {
129
+ if (result.status === 401) {
108
130
  throw new Error("Email provider error: Authentication error");
109
131
  }
110
- throw new Error(`Email provider error (${context}): HTTP ${response.status}${errorBody ? ` - ${errorBody}` : ""}`);
132
+ if (result.status >= 400) {
133
+ throw new Error(`Email provider error (getMessageById): HTTP ${result.status}`);
134
+ }
135
+ return result.data;
111
136
  }
112
137
  parseEmailResponse(email) {
113
138
  const codesAsString = email.html?.codes
@@ -1,21 +1,17 @@
1
1
  import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
2
2
  declare class EmpiricalReporter implements Reporter {
3
- private _destinationDir;
4
- private _uploadBucket;
5
3
  private _currentWorkingDir;
6
- private _willUploadArtifacts;
7
4
  private _testResultSourceDir;
8
- private _baseUrl;
9
- private _urlPrefix;
10
5
  private _pendingTestCaseEvents;
11
6
  private _hasSharding;
12
7
  private _attachmentUrlMap;
13
8
  private _testRetryCounts;
14
- private checkR2Creds;
9
+ private _uploader;
15
10
  constructor();
16
11
  private enqueTestAttachmentUploadTask;
17
12
  onTestEnd(test: TestCase, result: TestResult): void;
18
13
  onEnd(result: FullResult): Promise<void>;
14
+ private _logMissingFiles;
19
15
  private _patchBlobReport;
20
16
  private _uploadBlobReport;
21
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAkBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,UAAU,CACwE;IAC1F,OAAO,CAAC,sBAAsB,CAAuB;IAErD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAE1D,OAAO,CAAC,YAAY;;IAyBpB,OAAO,CAAC,6BAA6B,CA4BnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAuFtC,KAAK,CAAC,MAAM,EAAE,UAAU;YA4HhB,gBAAgB;YAOhB,iBAAiB;CAWhC;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAkBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;;IAM1C,OAAO,CAAC,6BAA6B,CAkCnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAoFtC,KAAK,CAAC,MAAM,EAAE,UAAU;IAuG9B,OAAO,CAAC,gBAAgB;YAoBV,gBAAgB;YAOhB,iBAAiB;CAMhC;AAED,eAAe,iBAAiB,CAAC"}
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const r2_uploader_1 = require("@empiricalrun/r2-uploader");
7
6
  const fs_1 = __importDefault(require("fs"));
8
7
  const path_1 = __importDefault(require("path"));
9
8
  const logger_1 = require("../logger");
@@ -11,61 +10,38 @@ const blob_utils_1 = require("./blob-utils");
11
10
  const uploader_1 = require("./uploader");
12
11
  const util_1 = require("./util");
13
12
  class EmpiricalReporter {
14
- _destinationDir;
15
- _uploadBucket = "test-report";
16
13
  _currentWorkingDir = process.cwd();
17
- _willUploadArtifacts = false;
18
14
  _testResultSourceDir = path_1.default.join(this._currentWorkingDir, "test-results");
19
- _baseUrl = `https://reports.empirical.run`;
20
- _urlPrefix = `${this._baseUrl}/${process.env.PROJECT_NAME}/${process.env.TEST_RUN_GITHUB_ACTION_ID}`;
21
15
  _pendingTestCaseEvents = [];
22
16
  _hasSharding = process.env.HAS_SHARDING === "true";
23
17
  _attachmentUrlMap = new Map();
24
18
  _testRetryCounts = new Map();
25
- checkR2Creds() {
26
- if (!process.env.R2_ACCOUNT_ID ||
27
- !process.env.R2_ACCESS_KEY_ID ||
28
- !process.env.R2_SECRET_ACCESS_KEY) {
29
- logger_1.logger.debug("R2 credentials not found, skipping artifact upload");
30
- return false;
31
- }
32
- return true;
33
- }
19
+ _uploader = null;
34
20
  constructor() {
35
- if (!process.env.PROJECT_NAME || !process.env.TEST_RUN_GITHUB_ACTION_ID) {
36
- logger_1.logger.debug("PROJECT_NAME and TEST_RUN_GITHUB_ACTION_ID must be set");
37
- this._destinationDir = "";
38
- return;
39
- }
40
- this._destinationDir = path_1.default.join(process.env.PROJECT_NAME, process.env.TEST_RUN_GITHUB_ACTION_ID);
41
- this._willUploadArtifacts = this.checkR2Creds();
21
+ this._uploader = (0, uploader_1.createUploader)();
42
22
  }
43
23
  enqueTestAttachmentUploadTask = async (attachment) => {
24
+ if (!this._uploader)
25
+ return;
44
26
  if (!attachment.path) {
45
27
  logger_1.logger.warn(`[Empirical Reporter] Skipping attachment without path: ${attachment.name}`);
46
28
  return;
47
29
  }
48
30
  const exists = await (0, util_1.checkFileExistsAsync)(attachment.path);
49
- if (exists) {
50
- logger_1.logger.debug(`[Empirical Reporter] Processing attachment: File exists`);
51
- }
52
- else {
31
+ if (!exists) {
53
32
  logger_1.logger.error(`[Empirical Reporter] Attachment does not exist: ${attachment.path}`);
54
33
  return;
55
34
  }
56
- const uploadTask = (0, uploader_1.createUploadTaskV2)({
57
- sourceDir: this._testResultSourceDir,
58
- fileList: [attachment.path],
59
- destinationDir: path_1.default.join(this._destinationDir, "data"),
60
- uploadBucket: this._uploadBucket,
61
- baseUrl: this._baseUrl,
62
- });
63
- return (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
35
+ const relativePath = path_1.default.relative(this._testResultSourceDir, attachment.path);
36
+ const destinationPath = `data/${relativePath}`;
37
+ const publicUrl = await this._uploader.uploadFile(attachment.path, destinationPath);
38
+ if (publicUrl) {
39
+ return { [attachment.path]: publicUrl };
40
+ }
64
41
  };
65
42
  onTestEnd(test, result) {
66
- if (!this._willUploadArtifacts) {
43
+ if (!this._uploader)
67
44
  return;
68
- }
69
45
  if (!process.env.TEST_RUN_GITHUB_ACTION_ID) {
70
46
  logger_1.logger.debug("[Empirical Reporter] TEST_RUN_GITHUB_ACTION_ID is not set");
71
47
  return;
@@ -126,18 +102,17 @@ class EmpiricalReporter {
126
102
  }
127
103
  };
128
104
  this._pendingTestCaseEvents.push(testCaseRunEventTask());
129
- return;
130
105
  }
131
106
  catch (error) {
132
107
  logger_1.logger.error(`[Empirical Reporter] Error in onTestEnd for ${test.title}:`, error);
133
108
  }
134
109
  }
135
110
  async onEnd(result) {
136
- if (!this._willUploadArtifacts) {
111
+ if (!this._uploader)
137
112
  return;
138
- }
139
113
  logger_1.logger.debug(`[Empirical Reporter] Test run completed with status: ${result.status}`);
140
- const startTime = new Date().getTime();
114
+ logger_1.logger.debug(`[Empirical Reporter] State: cwd=${this._currentWorkingDir}, hasSharding=${this._hasSharding}`);
115
+ const startTime = Date.now();
141
116
  logger_1.logger.debug("[Empirical Reporter] Starting final report upload at: ", new Intl.DateTimeFormat("en-US", {
142
117
  hour: "2-digit",
143
118
  minute: "2-digit",
@@ -147,34 +122,23 @@ class EmpiricalReporter {
147
122
  const jsonExists = fs_1.default.existsSync(jsonFilePath);
148
123
  const htmlFilePath = path_1.default.join(this._currentWorkingDir, "playwright-report/index.html");
149
124
  const htmlExists = await (0, util_1.checkFileExistsAsync)(htmlFilePath);
125
+ if (!htmlExists || !jsonExists) {
126
+ this._logMissingFiles();
127
+ }
150
128
  if (htmlExists && jsonExists) {
151
129
  logger_1.logger.debug("[Empirical Reporter] Updating the HTML Zip");
152
130
  await (0, util_1.updateHtmlZipFileAttachmentPaths)(jsonFilePath, htmlFilePath, this._testResultSourceDir);
153
131
  logger_1.logger.debug("[Empirical Reporter] Uploading HTML file");
154
- const uploadHtmlTask = (0, uploader_1.createUploadTaskV2)({
155
- sourceDir: path_1.default.join(this._currentWorkingDir, "playwright-report"),
156
- fileList: [htmlFilePath],
157
- destinationDir: this._destinationDir,
158
- uploadBucket: this._uploadBucket,
159
- baseUrl: this._baseUrl,
160
- });
161
- void (0, r2_uploader_1.sendTaskToQueue)(uploadHtmlTask);
132
+ await this._uploader.uploadFile(htmlFilePath, "index.html");
162
133
  }
163
134
  else {
164
135
  logger_1.logger.error(`[Empirical Reporter] playwright-report/index.html does not exist at: ${htmlFilePath}`);
165
136
  }
166
137
  if (jsonExists) {
167
138
  logger_1.logger.debug("[Empirical Reporter] Updating the JSON file");
168
- await (0, util_1.updateSummaryJsonAttachmentPaths)(jsonFilePath, this._testResultSourceDir, `${this._urlPrefix}/data`);
139
+ await (0, util_1.updateSummaryJsonAttachmentPaths)(jsonFilePath, this._testResultSourceDir, `${this._uploader.urlPrefix}/data`);
169
140
  logger_1.logger.debug("[Empirical Reporter] Uploading JSON file");
170
- const uploadJsonTask = (0, uploader_1.createUploadTaskV2)({
171
- sourceDir: this._currentWorkingDir,
172
- fileList: [jsonFilePath],
173
- destinationDir: this._destinationDir,
174
- uploadBucket: this._uploadBucket,
175
- baseUrl: this._baseUrl,
176
- });
177
- void (0, r2_uploader_1.sendTaskToQueue)(uploadJsonTask);
141
+ await this._uploader.uploadFile(jsonFilePath, "summary.json");
178
142
  }
179
143
  else {
180
144
  logger_1.logger.error(`[Empirical Reporter] summary.json does not exist at: ${jsonFilePath}`);
@@ -183,18 +147,12 @@ class EmpiricalReporter {
183
147
  const traceExists = await (0, util_1.checkFileExistsAsync)(traceFilePath);
184
148
  if (traceExists) {
185
149
  logger_1.logger.debug("[Empirical Reporter] Uploading Trace folder");
186
- const uploadTraceTask = (0, uploader_1.createUploadTaskV2)({
187
- sourceDir: traceFilePath,
188
- destinationDir: path_1.default.join(this._destinationDir, "trace"),
189
- uploadBucket: this._uploadBucket,
190
- baseUrl: this._baseUrl,
191
- });
192
- void (0, r2_uploader_1.sendTaskToQueue)(uploadTraceTask);
150
+ await this._uploader.uploadDirectory(traceFilePath, "trace");
193
151
  }
194
152
  else {
195
153
  logger_1.logger.debug(`[Empirical Reporter] playwright-report/trace does not exist at: ${traceFilePath}`);
196
154
  }
197
- await (0, r2_uploader_1.waitForTaskQueueToFinish)();
155
+ await this._uploader.waitForUploads();
198
156
  await Promise.allSettled(this._pendingTestCaseEvents);
199
157
  if (this._hasSharding) {
200
158
  const blobDir = path_1.default.join(this._currentWorkingDir, "blob-report");
@@ -207,15 +165,26 @@ class EmpiricalReporter {
207
165
  }
208
166
  }
209
167
  logger_1.logger.debug("[Empirical Reporter] All uploads finished");
210
- const endTime = new Date().getTime();
211
- logger_1.logger.debug("[Empirical Reporter] Finished final report upload at: ", new Intl.DateTimeFormat("en-US", {
212
- hour: "2-digit",
213
- minute: "2-digit",
214
- second: "2-digit",
215
- }).format(endTime));
216
- const timeDiff = endTime - startTime;
168
+ const timeDiff = Date.now() - startTime;
217
169
  logger_1.logger.debug("[Empirical Reporter] Time taken to upload after tests ended: ", timeDiff, "ms");
218
170
  }
171
+ _logMissingFiles() {
172
+ try {
173
+ const cwdFiles = fs_1.default.readdirSync(this._currentWorkingDir);
174
+ logger_1.logger.debug(`[Empirical Reporter] Files in cwd: ${cwdFiles.join(", ")}`);
175
+ const reportDir = path_1.default.join(this._currentWorkingDir, "playwright-report");
176
+ if (fs_1.default.existsSync(reportDir)) {
177
+ const reportFiles = fs_1.default.readdirSync(reportDir);
178
+ logger_1.logger.debug(`[Empirical Reporter] Files in playwright-report: ${reportFiles.join(", ")}`);
179
+ }
180
+ else {
181
+ logger_1.logger.debug(`[Empirical Reporter] playwright-report directory does not exist`);
182
+ }
183
+ }
184
+ catch (e) {
185
+ logger_1.logger.debug(`[Empirical Reporter] Error listing files: ${e}`);
186
+ }
187
+ }
219
188
  async _patchBlobReport(blobDir) {
220
189
  const files = await fs_1.default.promises.readdir(blobDir);
221
190
  for (const fileName of files.filter((f) => f.endsWith(".zip"))) {
@@ -223,14 +192,10 @@ class EmpiricalReporter {
223
192
  }
224
193
  }
225
194
  async _uploadBlobReport(blobDir) {
226
- const uploadTask = (0, uploader_1.createUploadTaskV2)({
227
- sourceDir: blobDir,
228
- destinationDir: path_1.default.join(this._destinationDir, "blobs"),
229
- uploadBucket: this._uploadBucket,
230
- baseUrl: this._baseUrl,
231
- });
232
- await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
233
- await (0, r2_uploader_1.waitForTaskQueueToFinish)();
195
+ if (!this._uploader)
196
+ return;
197
+ await this._uploader.uploadDirectory(blobDir, "blobs");
198
+ await this._uploader.waitForUploads();
234
199
  logger_1.logger.debug("[Empirical Reporter] Blob report uploaded");
235
200
  }
236
201
  }
@@ -1,3 +1,22 @@
1
- import { createUploadTask } from "@empiricalrun/r2-uploader";
2
- export { createUploadTask as createUploadTaskV2 };
1
+ export interface UploaderConfig {
2
+ projectName: string;
3
+ runId: string;
4
+ baseUrl?: string;
5
+ uploadBucket?: string;
6
+ }
7
+ export declare class Uploader {
8
+ private _baseUrl;
9
+ private _uploadBucket;
10
+ private _destinationDir;
11
+ constructor(config: UploaderConfig);
12
+ private static _hasR2Credentials;
13
+ get isEnabled(): boolean;
14
+ get baseUrl(): string;
15
+ get destinationDir(): string;
16
+ get urlPrefix(): string;
17
+ uploadFile(filePath: string, destinationPath: string): Promise<string | null>;
18
+ uploadDirectory(sourceDir: string, destinationPrefix: string): Promise<void>;
19
+ waitForUploads(): Promise<void>;
20
+ }
21
+ export declare function createUploader(): Uploader | null;
3
22
  //# sourceMappingURL=uploader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,gBAAgB,IAAI,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;gBAEpB,MAAM,EAAE,cAAc;IAUlC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAQhC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiCnB,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC;IAqBV,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CAGtC;AAED,wBAAgB,cAAc,IAAI,QAAQ,GAAG,IAAI,CAiBhD"}
@@ -1,5 +1,97 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createUploadTaskV2 = void 0;
6
+ exports.Uploader = void 0;
7
+ exports.createUploader = createUploader;
4
8
  const r2_uploader_1 = require("@empiricalrun/r2-uploader");
5
- Object.defineProperty(exports, "createUploadTaskV2", { enumerable: true, get: function () { return r2_uploader_1.createUploadTask; } });
9
+ const path_1 = __importDefault(require("path"));
10
+ const logger_1 = require("../logger");
11
+ class Uploader {
12
+ _baseUrl;
13
+ _uploadBucket;
14
+ _destinationDir;
15
+ constructor(config) {
16
+ this._baseUrl = config.baseUrl || "https://reports.empirical.run";
17
+ this._uploadBucket = config.uploadBucket || "test-report";
18
+ this._destinationDir = `${config.projectName}/${config.runId}`;
19
+ logger_1.logger.debug(`[Uploader] Initialized with destination: ${this._destinationDir}`);
20
+ }
21
+ static _hasR2Credentials() {
22
+ return !!(process.env.R2_ACCOUNT_ID &&
23
+ process.env.R2_ACCESS_KEY_ID &&
24
+ process.env.R2_SECRET_ACCESS_KEY);
25
+ }
26
+ get isEnabled() {
27
+ return Uploader._hasR2Credentials();
28
+ }
29
+ get baseUrl() {
30
+ return this._baseUrl;
31
+ }
32
+ get destinationDir() {
33
+ return this._destinationDir;
34
+ }
35
+ get urlPrefix() {
36
+ return `${this._baseUrl}/${this._destinationDir}`;
37
+ }
38
+ async uploadFile(filePath, destinationPath) {
39
+ if (!this.isEnabled) {
40
+ logger_1.logger.debug("[Uploader] R2 credentials not found, skipping upload");
41
+ return null;
42
+ }
43
+ try {
44
+ const fullDestinationPath = path_1.default.join(this._destinationDir, destinationPath);
45
+ const sourceDir = path_1.default.dirname(filePath);
46
+ const uploadTask = (0, r2_uploader_1.createUploadTask)({
47
+ sourceDir,
48
+ fileList: [filePath],
49
+ destinationDir: path_1.default.dirname(fullDestinationPath),
50
+ uploadBucket: this._uploadBucket,
51
+ baseUrl: this._baseUrl,
52
+ });
53
+ const result = await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
54
+ if (result) {
55
+ const url = Object.values(result)[0];
56
+ return url || null;
57
+ }
58
+ return null;
59
+ }
60
+ catch (err) {
61
+ logger_1.logger.error(`[Uploader] Error uploading file:`, err);
62
+ return null;
63
+ }
64
+ }
65
+ async uploadDirectory(sourceDir, destinationPrefix) {
66
+ if (!this.isEnabled) {
67
+ logger_1.logger.debug("[Uploader] R2 credentials not found, skipping upload");
68
+ return;
69
+ }
70
+ const fullDestinationDir = path_1.default.join(this._destinationDir, destinationPrefix);
71
+ const uploadTask = (0, r2_uploader_1.createUploadTask)({
72
+ sourceDir,
73
+ destinationDir: fullDestinationDir,
74
+ uploadBucket: this._uploadBucket,
75
+ baseUrl: this._baseUrl,
76
+ });
77
+ await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
78
+ }
79
+ async waitForUploads() {
80
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
81
+ }
82
+ }
83
+ exports.Uploader = Uploader;
84
+ function createUploader() {
85
+ if (!process.env.PROJECT_NAME || !process.env.TEST_RUN_GITHUB_ACTION_ID) {
86
+ logger_1.logger.debug("[Uploader] PROJECT_NAME and TEST_RUN_GITHUB_ACTION_ID must be set");
87
+ return null;
88
+ }
89
+ if (!Uploader["_hasR2Credentials"]()) {
90
+ logger_1.logger.debug("[Uploader] R2 credentials not found");
91
+ return null;
92
+ }
93
+ return new Uploader({
94
+ projectName: process.env.PROJECT_NAME,
95
+ runId: process.env.TEST_RUN_GITHUB_ACTION_ID,
96
+ });
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -43,8 +43,8 @@
43
43
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
44
44
  "rimraf": "^6.0.1",
45
45
  "@empiricalrun/llm": "^0.25.1",
46
- "@empiricalrun/r2-uploader": "^0.7.0",
47
- "@empiricalrun/test-gen": "^0.78.5"
46
+ "@empiricalrun/r2-uploader": "^0.8.0",
47
+ "@empiricalrun/test-gen": "^0.78.6"
48
48
  },
49
49
  "scripts": {
50
50
  "dev": "tsc --build --watch",