@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 +13 -0
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +23 -9
- package/dist/mailosaur-client.d.ts +11 -4
- package/dist/mailosaur-client.d.ts.map +1 -1
- package/dist/mailosaur-client.js +61 -36
- package/dist/reporter/empirical-reporter.d.ts +2 -6
- package/dist/reporter/empirical-reporter.d.ts.map +1 -1
- package/dist/reporter/empirical-reporter.js +45 -80
- package/dist/reporter/uploader.d.ts +21 -2
- package/dist/reporter/uploader.d.ts.map +1 -1
- package/dist/reporter/uploader.js +94 -2
- package/package.json +3 -3
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
|
package/dist/email.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
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"}
|
package/dist/mailosaur-client.js
CHANGED
|
@@ -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
|
|
40
|
-
let
|
|
74
|
+
const path = `/messages/search?${searchParams.toString()}`;
|
|
75
|
+
let result;
|
|
41
76
|
try {
|
|
42
|
-
|
|
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 (
|
|
56
|
-
|
|
86
|
+
if (result.status === 401) {
|
|
87
|
+
throw new Error("Email provider error: Authentication error");
|
|
57
88
|
}
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
let
|
|
117
|
+
const path = `/messages/${messageId}`;
|
|
118
|
+
let result;
|
|
87
119
|
try {
|
|
88
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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.
|
|
111
|
+
if (!this._uploader)
|
|
137
112
|
return;
|
|
138
|
-
}
|
|
139
113
|
logger_1.logger.debug(`[Empirical Reporter] Test run completed with status: ${result.status}`);
|
|
140
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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":"
|
|
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.
|
|
6
|
+
exports.Uploader = void 0;
|
|
7
|
+
exports.createUploader = createUploader;
|
|
4
8
|
const r2_uploader_1 = require("@empiricalrun/r2-uploader");
|
|
5
|
-
|
|
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.
|
|
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.
|
|
47
|
-
"@empiricalrun/test-gen": "^0.78.
|
|
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",
|