@empiricalrun/test-run 0.13.0 → 0.13.1

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,12 @@
1
1
  # @empiricalrun/test-run
2
2
 
3
+ ## 0.13.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [3ee1aec]
8
+ - @empiricalrun/r2-uploader@0.8.0
9
+
3
10
  ## 0.13.0
4
11
 
5
12
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export declare function patchMergedHtmlReport(htmlFilePath: string, urlMappings: Record<string, string>): Promise<void>;
2
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/lib/merge-reports/html.ts"],"names":[],"mappings":"AAwEA,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CA8Gf"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchMergedHtmlReport = patchMergedHtmlReport;
7
+ const zip_1 = require("@empiricalrun/r2-uploader/zip");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const logger_1 = require("../../logger");
11
+ const types_1 = require("./types");
12
+ function patchHtmlReportAttachments(report, urlMap) {
13
+ let patchCount = 0;
14
+ // report.json has files[].tests[], individual test files have tests[] directly
15
+ const tests = report.files
16
+ ? report.files.flatMap((f) => f.tests)
17
+ : report.tests || [];
18
+ for (const test of tests) {
19
+ for (const result of test.results || []) {
20
+ for (const attachment of result.attachments || []) {
21
+ if (attachment.path) {
22
+ for (const [fileName, url] of urlMap) {
23
+ if (attachment.path.endsWith(fileName)) {
24
+ attachment.path = url;
25
+ patchCount++;
26
+ break;
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return patchCount;
34
+ }
35
+ async function patchMergedHtmlReport(htmlFilePath, urlMappings) {
36
+ if (Object.keys(urlMappings).length === 0) {
37
+ logger_1.logger.debug(`[Merge Reports] No URL mappings to apply`);
38
+ return;
39
+ }
40
+ let htmlContent;
41
+ const startTime = Date.now();
42
+ logger_1.logger.info(`[Merge Reports] Starting HTML patch...`);
43
+ try {
44
+ htmlContent = await fs_1.default.promises.readFile(htmlFilePath, "utf8");
45
+ logger_1.logger.info(`[Merge Reports] HTML file read: ${(htmlContent.length / 1024 / 1024).toFixed(2)} MB in ${Date.now() - startTime}ms`);
46
+ }
47
+ catch (error) {
48
+ logger_1.logger.error(`[Merge Reports] Failed to read HTML file:`, error);
49
+ return;
50
+ }
51
+ // Support both old format (1.53.x) and new format (1.57.0+)
52
+ const oldFormatMatch = htmlContent.match(/window\.playwrightReportBase64\s*=\s*"(?:data:application\/zip;base64,)?([^"]+)"/);
53
+ const newFormatMatch = htmlContent.match(/<script\s+id="playwrightReportBase64"[^>]*>(?:data:application\/zip;base64,)?([^<]+)<\/script>/);
54
+ const base64 = oldFormatMatch?.[1] || newFormatMatch?.[1];
55
+ if (!base64) {
56
+ logger_1.logger.error(`[Merge Reports] Base64 zip data not found in HTML`);
57
+ return;
58
+ }
59
+ const htmlDir = path_1.default.dirname(path_1.default.resolve(htmlFilePath));
60
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(htmlDir, "merge-patch-"));
61
+ const zipPath = path_1.default.join(tempDir, "archive.zip");
62
+ try {
63
+ let stepTime = Date.now();
64
+ await fs_1.default.promises.writeFile(zipPath, Buffer.from(base64, "base64"));
65
+ await (0, zip_1.extractZipToDirectory)(zipPath, tempDir);
66
+ await fs_1.default.promises.unlink(zipPath);
67
+ logger_1.logger.info(`[Merge Reports] Zip extracted in ${Date.now() - stepTime}ms`);
68
+ const jsonFiles = (await fs_1.default.promises.readdir(tempDir)).filter((f) => f.endsWith(".json"));
69
+ logger_1.logger.info(`[Merge Reports] Patching ${jsonFiles.length} JSON files with ${Object.keys(urlMappings).length} mappings`);
70
+ const urlMap = (0, types_1.buildUrlMap)(urlMappings);
71
+ stepTime = Date.now();
72
+ let totalPatchCount = 0;
73
+ const patchResults = await Promise.allSettled(jsonFiles.map(async (file) => {
74
+ const filePath = path_1.default.join(tempDir, file);
75
+ const content = await fs_1.default.promises.readFile(filePath, "utf8");
76
+ const report = JSON.parse(content);
77
+ const patchCount = patchHtmlReportAttachments(report, urlMap);
78
+ if (patchCount > 0) {
79
+ await fs_1.default.promises.writeFile(filePath, JSON.stringify(report), "utf8");
80
+ logger_1.logger.debug(`[Merge Reports] Patched ${file} (${patchCount} paths)`);
81
+ }
82
+ return patchCount;
83
+ }));
84
+ for (const result of patchResults) {
85
+ if (result.status === "rejected") {
86
+ logger_1.logger.error(`[Merge Reports] Failed to patch JSON file:`, result.reason);
87
+ }
88
+ else {
89
+ totalPatchCount += result.value;
90
+ }
91
+ }
92
+ logger_1.logger.info(`[Merge Reports] JSON patching completed in ${Date.now() - stepTime}ms (${totalPatchCount} paths patched)`);
93
+ stepTime = Date.now();
94
+ const newBuffer = await (0, zip_1.createZipFromDirectory)(tempDir);
95
+ const newBase64 = newBuffer.toString("base64");
96
+ logger_1.logger.info(`[Merge Reports] New zip created in ${Date.now() - stepTime}ms`);
97
+ let updatedHtml;
98
+ if (oldFormatMatch) {
99
+ updatedHtml = htmlContent.replace(/(window\.playwrightReportBase64\s*=\s*")(?:data:application\/zip;base64,)?[^"]*(")/, `$1data:application/zip;base64,${newBase64}$2`);
100
+ }
101
+ else {
102
+ updatedHtml = htmlContent.replace(/(<script\s+id="playwrightReportBase64"[^>]*>)(?:data:application\/zip;base64,)?[^<]*(<\/script>)/, `$1data:application/zip;base64,${newBase64}$2`);
103
+ }
104
+ await fs_1.default.promises.writeFile(htmlFilePath, updatedHtml, "utf8");
105
+ logger_1.logger.info(`[Merge Reports] HTML file patched successfully`);
106
+ }
107
+ catch (error) {
108
+ logger_1.logger.error(`[Merge Reports] Failed to patch HTML:`, error);
109
+ }
110
+ finally {
111
+ await fs_1.default.promises.rm(tempDir, { recursive: true, force: true });
112
+ }
113
+ }
@@ -0,0 +1,16 @@
1
+ import type { MergeReportsOptions, UploadOptions } from "./types";
2
+ export { patchMergedHtmlReport } from "./html";
3
+ export { patchSummaryJson } from "./json";
4
+ export type { MergeReportsOptions, UploadOptions } from "./types";
5
+ export declare function runPlaywrightMergeReports(options: MergeReportsOptions): Promise<{
6
+ success: boolean;
7
+ }>;
8
+ export declare function extractUrlMappingsFromBlobs(blobDir: string): Promise<Record<string, string>>;
9
+ export declare function uploadMergedReports(cwd: string, outputDir: string, uploadOptions: UploadOptions): Promise<void>;
10
+ export declare function mergeReports(options: {
11
+ blobDir?: string;
12
+ cwd?: string;
13
+ }): Promise<{
14
+ success: boolean;
15
+ }>;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/merge-reports/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAElE,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAC1C,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAElE,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA2B/B;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoCjC;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAyDhC"}
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchSummaryJson = exports.patchMergedHtmlReport = void 0;
7
+ exports.runPlaywrightMergeReports = runPlaywrightMergeReports;
8
+ exports.extractUrlMappingsFromBlobs = extractUrlMappingsFromBlobs;
9
+ exports.uploadMergedReports = uploadMergedReports;
10
+ exports.mergeReports = mergeReports;
11
+ const r2_uploader_1 = require("@empiricalrun/r2-uploader");
12
+ const zip_1 = require("@empiricalrun/r2-uploader/zip");
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const logger_1 = require("../../logger");
16
+ const cmd_1 = require("../cmd");
17
+ const html_1 = require("./html");
18
+ const json_1 = require("./json");
19
+ var html_2 = require("./html");
20
+ Object.defineProperty(exports, "patchMergedHtmlReport", { enumerable: true, get: function () { return html_2.patchMergedHtmlReport; } });
21
+ var json_2 = require("./json");
22
+ Object.defineProperty(exports, "patchSummaryJson", { enumerable: true, get: function () { return json_2.patchSummaryJson; } });
23
+ async function runPlaywrightMergeReports(options) {
24
+ const { blobDir, outputDir, cwd } = options;
25
+ logger_1.logger.debug(`[Merge Reports] Running playwright merge-reports`);
26
+ logger_1.logger.debug(`[Merge Reports] Blob dir: ${blobDir}`);
27
+ logger_1.logger.debug(`[Merge Reports] Output dir: ${outputDir}`);
28
+ try {
29
+ await (0, cmd_1.spawnCmd)("npx", ["playwright", "merge-reports", blobDir, "--reporter", "html,json"], {
30
+ cwd,
31
+ envOverrides: {
32
+ PLAYWRIGHT_HTML_OPEN: "never",
33
+ PLAYWRIGHT_HTML_OUTPUT_DIR: outputDir,
34
+ PLAYWRIGHT_JSON_OUTPUT_NAME: path_1.default.join(cwd, "summary.json"),
35
+ },
36
+ captureOutput: false,
37
+ throwOnError: true,
38
+ });
39
+ return { success: true };
40
+ }
41
+ catch (error) {
42
+ logger_1.logger.error(`[Merge Reports] Failed to merge reports:`, error);
43
+ return { success: false };
44
+ }
45
+ }
46
+ async function extractUrlMappingsFromBlobs(blobDir) {
47
+ const combinedMap = {};
48
+ const files = fs_1.default.readdirSync(blobDir);
49
+ const zipFiles = files.filter((f) => f.endsWith(".zip"));
50
+ const results = await Promise.allSettled(zipFiles.map(async (fileName) => {
51
+ const zipPath = path_1.default.join(blobDir, fileName);
52
+ const buffer = await (0, zip_1.readZipEntry)(zipPath, "_empirical_urls.json");
53
+ if (buffer) {
54
+ const content = JSON.parse(buffer.toString("utf8"));
55
+ logger_1.logger.debug(`[Merge Reports] Extracted ${Object.keys(content).length} URL mappings from ${fileName}`);
56
+ return content;
57
+ }
58
+ return null;
59
+ }));
60
+ for (const result of results) {
61
+ if (result.status === "fulfilled" && result.value) {
62
+ Object.assign(combinedMap, result.value);
63
+ }
64
+ else if (result.status === "rejected") {
65
+ logger_1.logger.error(`[Merge Reports] Failed to extract URL mappings:`, result.reason);
66
+ }
67
+ }
68
+ logger_1.logger.info(`[Merge Reports] Total URL mappings: ${Object.keys(combinedMap).length}`);
69
+ return combinedMap;
70
+ }
71
+ async function uploadMergedReports(cwd, outputDir, uploadOptions) {
72
+ const { projectName, runId, baseUrl, uploadBucket } = uploadOptions;
73
+ const destinationDir = path_1.default.join(projectName, runId);
74
+ const htmlFilePath = path_1.default.join(outputDir, "index.html");
75
+ const jsonFilePath = path_1.default.join(cwd, "summary.json");
76
+ if (fs_1.default.existsSync(htmlFilePath)) {
77
+ logger_1.logger.debug(`[Merge Reports] Uploading HTML report`);
78
+ const task = (0, r2_uploader_1.createUploadTask)({
79
+ sourceDir: outputDir,
80
+ fileList: [htmlFilePath],
81
+ destinationDir,
82
+ uploadBucket,
83
+ baseUrl,
84
+ });
85
+ void (0, r2_uploader_1.sendTaskToQueue)(task);
86
+ }
87
+ if (fs_1.default.existsSync(jsonFilePath)) {
88
+ logger_1.logger.debug(`[Merge Reports] Uploading summary.json`);
89
+ const task = (0, r2_uploader_1.createUploadTask)({
90
+ sourceDir: cwd,
91
+ fileList: [jsonFilePath],
92
+ destinationDir,
93
+ uploadBucket,
94
+ baseUrl,
95
+ });
96
+ void (0, r2_uploader_1.sendTaskToQueue)(task);
97
+ }
98
+ const traceDir = path_1.default.join(outputDir, "trace");
99
+ if (fs_1.default.existsSync(traceDir)) {
100
+ logger_1.logger.debug(`[Merge Reports] Uploading trace folder`);
101
+ const task = (0, r2_uploader_1.createUploadTask)({
102
+ sourceDir: traceDir,
103
+ destinationDir: path_1.default.join(destinationDir, "trace"),
104
+ uploadBucket,
105
+ baseUrl,
106
+ });
107
+ void (0, r2_uploader_1.sendTaskToQueue)(task);
108
+ }
109
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
110
+ const reportUrl = `${baseUrl}/${destinationDir}/index.html`;
111
+ const jsonUrl = `${baseUrl}/${destinationDir}/summary.json`;
112
+ logger_1.logger.info(`[Merge Reports] All uploads completed`);
113
+ logger_1.logger.info(`[Merge Reports] HTML Report: ${reportUrl}`);
114
+ logger_1.logger.info(`[Merge Reports] Summary JSON: ${jsonUrl}`);
115
+ }
116
+ async function mergeReports(options) {
117
+ const cwd = options.cwd || process.cwd();
118
+ const blobDir = options.blobDir || path_1.default.join(cwd, "blob-report");
119
+ const outputDir = path_1.default.join(cwd, "playwright-report");
120
+ const projectName = process.env.PROJECT_NAME;
121
+ const runId = process.env.TEST_RUN_GITHUB_ACTION_ID;
122
+ if (!projectName || !runId) {
123
+ logger_1.logger.error(`[Merge Reports] PROJECT_NAME and TEST_RUN_GITHUB_ACTION_ID must be set`);
124
+ return { success: false };
125
+ }
126
+ if (!fs_1.default.existsSync(blobDir)) {
127
+ logger_1.logger.error(`[Merge Reports] Blob directory does not exist: ${blobDir}`);
128
+ return { success: false };
129
+ }
130
+ const urlMappings = await extractUrlMappingsFromBlobs(blobDir);
131
+ const { success } = await runPlaywrightMergeReports({
132
+ blobDir,
133
+ outputDir,
134
+ cwd,
135
+ });
136
+ if (!success) {
137
+ return { success: false };
138
+ }
139
+ const htmlFilePath = path_1.default.join(outputDir, "index.html");
140
+ const jsonFilePath = path_1.default.join(cwd, "summary.json");
141
+ await Promise.all([
142
+ (0, html_1.patchMergedHtmlReport)(htmlFilePath, urlMappings),
143
+ (0, json_1.patchSummaryJson)(jsonFilePath, urlMappings),
144
+ ]);
145
+ const hasR2Creds = process.env.R2_ACCOUNT_ID &&
146
+ process.env.R2_ACCESS_KEY_ID &&
147
+ process.env.R2_SECRET_ACCESS_KEY;
148
+ if (hasR2Creds) {
149
+ await uploadMergedReports(cwd, outputDir, {
150
+ projectName,
151
+ runId,
152
+ baseUrl: "https://reports.empirical.run",
153
+ uploadBucket: "test-report",
154
+ });
155
+ }
156
+ else {
157
+ logger_1.logger.info(`[Merge Reports] R2 credentials not found, skipping upload`);
158
+ }
159
+ return { success: true };
160
+ }
@@ -0,0 +1,2 @@
1
+ export declare function patchSummaryJson(jsonFilePath: string, urlMappings: Record<string, string>): Promise<void>;
2
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../../src/lib/merge-reports/json.ts"],"names":[],"mappings":"AA8EA,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAyCf"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchSummaryJson = patchSummaryJson;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const logger_1 = require("../../logger");
9
+ const types_1 = require("./types");
10
+ function patchAttachmentPaths(report, urlMap) {
11
+ let patchCount = 0;
12
+ function processSuite(suite) {
13
+ for (const spec of suite.specs) {
14
+ for (const test of spec.tests) {
15
+ for (const result of test.results) {
16
+ for (const attachment of result.attachments) {
17
+ if (attachment.path) {
18
+ for (const [fileName, url] of urlMap) {
19
+ if (attachment.path.endsWith(fileName)) {
20
+ attachment.path = url;
21
+ patchCount++;
22
+ break;
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ if (suite.suites) {
31
+ for (const nested of suite.suites) {
32
+ processSuite(nested);
33
+ }
34
+ }
35
+ }
36
+ for (const suite of report.suites) {
37
+ processSuite(suite);
38
+ }
39
+ return patchCount;
40
+ }
41
+ async function patchSummaryJson(jsonFilePath, urlMappings) {
42
+ if (Object.keys(urlMappings).length === 0) {
43
+ logger_1.logger.debug(`[Merge Reports] No URL mappings to apply to summary.json`);
44
+ return;
45
+ }
46
+ const startTime = Date.now();
47
+ logger_1.logger.info(`[Merge Reports] Starting summary.json patch...`);
48
+ try {
49
+ let stepTime = Date.now();
50
+ const content = await fs_1.default.promises.readFile(jsonFilePath, "utf8");
51
+ logger_1.logger.info(`[Merge Reports] summary.json read: ${(content.length / 1024).toFixed(2)} KB in ${Date.now() - stepTime}ms`);
52
+ stepTime = Date.now();
53
+ const report = JSON.parse(content);
54
+ logger_1.logger.info(`[Merge Reports] summary.json parsed in ${Date.now() - stepTime}ms`);
55
+ stepTime = Date.now();
56
+ const urlMap = (0, types_1.buildUrlMap)(urlMappings);
57
+ const patchCount = patchAttachmentPaths(report, urlMap);
58
+ logger_1.logger.info(`[Merge Reports] summary.json patched ${patchCount} attachment paths in ${Date.now() - stepTime}ms`);
59
+ stepTime = Date.now();
60
+ await fs_1.default.promises.writeFile(jsonFilePath, JSON.stringify(report), "utf8");
61
+ logger_1.logger.info(`[Merge Reports] summary.json written in ${Date.now() - stepTime}ms`);
62
+ logger_1.logger.info(`[Merge Reports] summary.json patched successfully (total: ${Date.now() - startTime}ms)`);
63
+ }
64
+ catch (error) {
65
+ logger_1.logger.error(`[Merge Reports] Failed to patch summary.json:`, error);
66
+ }
67
+ }
@@ -0,0 +1,13 @@
1
+ export interface MergeReportsOptions {
2
+ blobDir: string;
3
+ outputDir: string;
4
+ cwd: string;
5
+ }
6
+ export interface UploadOptions {
7
+ projectName: string;
8
+ runId: string;
9
+ baseUrl: string;
10
+ uploadBucket: string;
11
+ }
12
+ export declare function buildUrlMap(urlMappings: Record<string, string>): Map<string, string>;
13
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/merge-reports/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOrB"}
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildUrlMap = buildUrlMap;
4
+ function buildUrlMap(urlMappings) {
5
+ const map = new Map();
6
+ for (const [resourcePath, url] of Object.entries(urlMappings)) {
7
+ const fileName = resourcePath.replace(/^resources\//, "");
8
+ map.set(fileName, url);
9
+ }
10
+ return map;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-run",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -32,7 +32,7 @@
32
32
  "dotenv": "^16.4.5",
33
33
  "minimatch": "^10.0.1",
34
34
  "ts-morph": "^23.0.0",
35
- "@empiricalrun/r2-uploader": "^0.7.0"
35
+ "@empiricalrun/r2-uploader": "^0.8.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@playwright/test": "1.53.2",
@@ -1 +1 @@
1
- {"root":["./src/dashboard.ts","./src/glob-matcher.ts","./src/index.ts","./src/logger.ts","./src/bin/index.ts","./src/bin/merge-reports.ts","./src/lib/cancellation-watcher.ts","./src/lib/cmd.ts","./src/lib/merge-reports.ts","./src/lib/run-all-tests.ts","./src/lib/run-specific-test.ts","./src/lib/memfs/read-hello-world.ts","./src/stdout-parser/index.ts","./src/types/index.ts","./src/utils/config-parser.ts","./src/utils/config.ts","./src/utils/index.ts"],"version":"5.8.3"}
1
+ {"root":["./src/dashboard.ts","./src/glob-matcher.ts","./src/index.ts","./src/logger.ts","./src/bin/index.ts","./src/bin/merge-reports.ts","./src/lib/cancellation-watcher.ts","./src/lib/cmd.ts","./src/lib/run-all-tests.ts","./src/lib/run-specific-test.ts","./src/lib/memfs/read-hello-world.ts","./src/lib/merge-reports/html.ts","./src/lib/merge-reports/index.ts","./src/lib/merge-reports/json.ts","./src/lib/merge-reports/types.ts","./src/stdout-parser/index.ts","./src/types/index.ts","./src/utils/config-parser.ts","./src/utils/config.ts","./src/utils/index.ts"],"version":"5.8.3"}
@@ -1,26 +0,0 @@
1
- interface MergeReportsOptions {
2
- blobDir: string;
3
- outputDir: string;
4
- cwd: string;
5
- }
6
- interface UploadOptions {
7
- projectName: string;
8
- runId: string;
9
- baseUrl: string;
10
- uploadBucket: string;
11
- }
12
- export declare function runPlaywrightMergeReports(options: MergeReportsOptions): Promise<{
13
- success: boolean;
14
- }>;
15
- export declare function extractUrlMappingsFromBlobs(blobDir: string): Promise<Record<string, string>>;
16
- export declare function patchMergedHtmlReport(htmlFilePath: string, urlMappings: Record<string, string>): Promise<void>;
17
- export declare function patchSummaryJson(jsonFilePath: string, urlMappings: Record<string, string>): Promise<void>;
18
- export declare function uploadMergedReports(cwd: string, outputDir: string, uploadOptions: UploadOptions): Promise<void>;
19
- export declare function mergeReports(options: {
20
- blobDir?: string;
21
- cwd?: string;
22
- }): Promise<{
23
- success: boolean;
24
- }>;
25
- export {};
26
- //# sourceMappingURL=merge-reports.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"merge-reports.d.ts","sourceRoot":"","sources":["../../src/lib/merge-reports.ts"],"names":[],"mappings":"AAgBA,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AA+BD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA2B/B;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA2BjC;AAED,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CA8Ff;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAuDhC"}
@@ -1,248 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.runPlaywrightMergeReports = runPlaywrightMergeReports;
7
- exports.extractUrlMappingsFromBlobs = extractUrlMappingsFromBlobs;
8
- exports.patchMergedHtmlReport = patchMergedHtmlReport;
9
- exports.patchSummaryJson = patchSummaryJson;
10
- exports.uploadMergedReports = uploadMergedReports;
11
- exports.mergeReports = mergeReports;
12
- const r2_uploader_1 = require("@empiricalrun/r2-uploader");
13
- const zip_1 = require("@empiricalrun/r2-uploader/zip");
14
- const fs_1 = __importDefault(require("fs"));
15
- const path_1 = __importDefault(require("path"));
16
- const logger_1 = require("../logger");
17
- const cmd_1 = require("./cmd");
18
- function buildMappingPatterns(urlMappings) {
19
- return Object.entries(urlMappings).map(([resourcePath, url]) => {
20
- const resourceFileName = resourcePath.replace(/^resources\//, "");
21
- const escaped = resourceFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
- const regex = new RegExp(`[^"]*${escaped}`, "g");
23
- return { regex, resourceFileName, url };
24
- });
25
- }
26
- function applyMappingPatterns(content, patterns) {
27
- let modified = content;
28
- for (const { regex, resourceFileName, url } of patterns) {
29
- if (!modified.includes(resourceFileName))
30
- continue;
31
- modified = modified.replace(regex, url);
32
- }
33
- return modified;
34
- }
35
- async function runPlaywrightMergeReports(options) {
36
- const { blobDir, outputDir, cwd } = options;
37
- logger_1.logger.debug(`[Merge Reports] Running playwright merge-reports`);
38
- logger_1.logger.debug(`[Merge Reports] Blob dir: ${blobDir}`);
39
- logger_1.logger.debug(`[Merge Reports] Output dir: ${outputDir}`);
40
- try {
41
- await (0, cmd_1.spawnCmd)("npx", ["playwright", "merge-reports", blobDir, "--reporter", "html,json"], {
42
- cwd,
43
- envOverrides: {
44
- PLAYWRIGHT_HTML_OPEN: "never",
45
- PLAYWRIGHT_HTML_OUTPUT_DIR: outputDir,
46
- PLAYWRIGHT_JSON_OUTPUT_NAME: path_1.default.join(cwd, "summary.json"),
47
- },
48
- captureOutput: false,
49
- throwOnError: true,
50
- });
51
- return { success: true };
52
- }
53
- catch (error) {
54
- logger_1.logger.error(`[Merge Reports] Failed to merge reports:`, error);
55
- return { success: false };
56
- }
57
- }
58
- async function extractUrlMappingsFromBlobs(blobDir) {
59
- const combinedMap = {};
60
- const files = fs_1.default.readdirSync(blobDir);
61
- for (const fileName of files.filter((f) => f.endsWith(".zip"))) {
62
- const zipPath = path_1.default.join(blobDir, fileName);
63
- try {
64
- const buffer = await (0, zip_1.readZipEntry)(zipPath, "_empirical_urls.json");
65
- if (buffer) {
66
- const content = JSON.parse(buffer.toString("utf8"));
67
- Object.assign(combinedMap, content);
68
- logger_1.logger.debug(`[Merge Reports] Extracted ${Object.keys(content).length} URL mappings from ${fileName}`);
69
- }
70
- }
71
- catch (error) {
72
- logger_1.logger.error(`[Merge Reports] Failed to extract URL mappings from ${fileName}:`, error);
73
- }
74
- }
75
- logger_1.logger.info(`[Merge Reports] Total URL mappings: ${Object.keys(combinedMap).length}`);
76
- return combinedMap;
77
- }
78
- async function patchMergedHtmlReport(htmlFilePath, urlMappings) {
79
- if (Object.keys(urlMappings).length === 0) {
80
- logger_1.logger.debug(`[Merge Reports] No URL mappings to apply`);
81
- return;
82
- }
83
- let htmlContent;
84
- const startTime = Date.now();
85
- logger_1.logger.info(`[Merge Reports] Starting HTML patch...`);
86
- try {
87
- htmlContent = await fs_1.default.promises.readFile(htmlFilePath, "utf8");
88
- logger_1.logger.info(`[Merge Reports] HTML file read: ${(htmlContent.length / 1024 / 1024).toFixed(2)} MB in ${Date.now() - startTime}ms`);
89
- }
90
- catch (error) {
91
- logger_1.logger.error(`[Merge Reports] Failed to read HTML file:`, error);
92
- return;
93
- }
94
- const oldFormatMatch = htmlContent.match(/window\.playwrightReportBase64\s*=\s*"(?:data:application\/zip;base64,)?([^"]+)"/);
95
- const newFormatMatch = htmlContent.match(/<script\s+id="playwrightReportBase64"[^>]*>(?:data:application\/zip;base64,)?([^<]+)<\/script>/);
96
- const base64 = oldFormatMatch?.[1] || newFormatMatch?.[1];
97
- if (!base64) {
98
- logger_1.logger.error(`[Merge Reports] Base64 zip data not found in HTML`);
99
- return;
100
- }
101
- const htmlDir = path_1.default.dirname(path_1.default.resolve(htmlFilePath));
102
- const tempDir = fs_1.default.mkdtempSync(path_1.default.join(htmlDir, "merge-patch-"));
103
- const zipPath = path_1.default.join(tempDir, "archive.zip");
104
- try {
105
- let stepTime = Date.now();
106
- await fs_1.default.promises.writeFile(zipPath, Buffer.from(base64, "base64"));
107
- await (0, zip_1.extractZipToDirectory)(zipPath, tempDir);
108
- await fs_1.default.promises.unlink(zipPath);
109
- logger_1.logger.info(`[Merge Reports] Zip extracted in ${Date.now() - stepTime}ms`);
110
- const jsonFiles = (await fs_1.default.promises.readdir(tempDir)).filter((f) => f.endsWith(".json"));
111
- logger_1.logger.info(`[Merge Reports] Patching ${jsonFiles.length} JSON files with ${Object.keys(urlMappings).length} mappings`);
112
- const mappingPatterns = buildMappingPatterns(urlMappings);
113
- stepTime = Date.now();
114
- for (const file of jsonFiles) {
115
- const filePath = path_1.default.join(tempDir, file);
116
- const content = await fs_1.default.promises.readFile(filePath, "utf8");
117
- const modified = applyMappingPatterns(content, mappingPatterns);
118
- if (modified !== content) {
119
- await fs_1.default.promises.writeFile(filePath, modified, "utf8");
120
- logger_1.logger.debug(`[Merge Reports] Patched ${file}`);
121
- }
122
- }
123
- logger_1.logger.info(`[Merge Reports] JSON patching completed in ${Date.now() - stepTime}ms`);
124
- stepTime = Date.now();
125
- const newBuffer = await (0, zip_1.createZipFromDirectory)(tempDir);
126
- const newBase64 = newBuffer.toString("base64");
127
- logger_1.logger.info(`[Merge Reports] New zip created in ${Date.now() - stepTime}ms`);
128
- let updatedHtml;
129
- if (oldFormatMatch) {
130
- updatedHtml = htmlContent.replace(/(window\.playwrightReportBase64\s*=\s*")(?:data:application\/zip;base64,)?[^"]*(")/, `$1data:application/zip;base64,${newBase64}$2`);
131
- }
132
- else {
133
- updatedHtml = htmlContent.replace(/(<script\s+id="playwrightReportBase64"[^>]*>)(?:data:application\/zip;base64,)?[^<]*(<\/script>)/, `$1data:application/zip;base64,${newBase64}$2`);
134
- }
135
- await fs_1.default.promises.writeFile(htmlFilePath, updatedHtml, "utf8");
136
- logger_1.logger.info(`[Merge Reports] HTML file patched successfully`);
137
- }
138
- catch (error) {
139
- logger_1.logger.error(`[Merge Reports] Failed to patch HTML:`, error);
140
- }
141
- finally {
142
- await fs_1.default.promises.rm(tempDir, { recursive: true, force: true });
143
- }
144
- }
145
- async function patchSummaryJson(jsonFilePath, urlMappings) {
146
- if (Object.keys(urlMappings).length === 0) {
147
- logger_1.logger.debug(`[Merge Reports] No URL mappings to apply to summary.json`);
148
- return;
149
- }
150
- try {
151
- const content = await fs_1.default.promises.readFile(jsonFilePath, "utf8");
152
- const mappingPatterns = buildMappingPatterns(urlMappings);
153
- const modified = applyMappingPatterns(content, mappingPatterns);
154
- await fs_1.default.promises.writeFile(jsonFilePath, modified, "utf8");
155
- logger_1.logger.info(`[Merge Reports] summary.json patched successfully`);
156
- }
157
- catch (error) {
158
- logger_1.logger.error(`[Merge Reports] Failed to patch summary.json:`, error);
159
- }
160
- }
161
- async function uploadMergedReports(cwd, outputDir, uploadOptions) {
162
- const { projectName, runId, baseUrl, uploadBucket } = uploadOptions;
163
- const destinationDir = path_1.default.join(projectName, runId);
164
- const htmlFilePath = path_1.default.join(outputDir, "index.html");
165
- const jsonFilePath = path_1.default.join(cwd, "summary.json");
166
- if (fs_1.default.existsSync(htmlFilePath)) {
167
- logger_1.logger.debug(`[Merge Reports] Uploading HTML report`);
168
- const task = (0, r2_uploader_1.createUploadTask)({
169
- sourceDir: outputDir,
170
- fileList: [htmlFilePath],
171
- destinationDir,
172
- uploadBucket,
173
- baseUrl,
174
- });
175
- void (0, r2_uploader_1.sendTaskToQueue)(task);
176
- }
177
- if (fs_1.default.existsSync(jsonFilePath)) {
178
- logger_1.logger.debug(`[Merge Reports] Uploading summary.json`);
179
- const task = (0, r2_uploader_1.createUploadTask)({
180
- sourceDir: cwd,
181
- fileList: [jsonFilePath],
182
- destinationDir,
183
- uploadBucket,
184
- baseUrl,
185
- });
186
- void (0, r2_uploader_1.sendTaskToQueue)(task);
187
- }
188
- const traceDir = path_1.default.join(outputDir, "trace");
189
- if (fs_1.default.existsSync(traceDir)) {
190
- logger_1.logger.debug(`[Merge Reports] Uploading trace folder`);
191
- const task = (0, r2_uploader_1.createUploadTask)({
192
- sourceDir: traceDir,
193
- destinationDir: path_1.default.join(destinationDir, "trace"),
194
- uploadBucket,
195
- baseUrl,
196
- });
197
- void (0, r2_uploader_1.sendTaskToQueue)(task);
198
- }
199
- await (0, r2_uploader_1.waitForTaskQueueToFinish)();
200
- const reportUrl = `${baseUrl}/${destinationDir}/index.html`;
201
- const jsonUrl = `${baseUrl}/${destinationDir}/summary.json`;
202
- logger_1.logger.info(`[Merge Reports] All uploads completed`);
203
- logger_1.logger.info(`[Merge Reports] HTML Report: ${reportUrl}`);
204
- logger_1.logger.info(`[Merge Reports] Summary JSON: ${jsonUrl}`);
205
- }
206
- async function mergeReports(options) {
207
- const cwd = options.cwd || process.cwd();
208
- const blobDir = options.blobDir || path_1.default.join(cwd, "blob-report");
209
- const outputDir = path_1.default.join(cwd, "playwright-report");
210
- const projectName = process.env.PROJECT_NAME;
211
- const runId = process.env.TEST_RUN_GITHUB_ACTION_ID;
212
- if (!projectName || !runId) {
213
- logger_1.logger.error(`[Merge Reports] PROJECT_NAME and TEST_RUN_GITHUB_ACTION_ID must be set`);
214
- return { success: false };
215
- }
216
- if (!fs_1.default.existsSync(blobDir)) {
217
- logger_1.logger.error(`[Merge Reports] Blob directory does not exist: ${blobDir}`);
218
- return { success: false };
219
- }
220
- const urlMappings = await extractUrlMappingsFromBlobs(blobDir);
221
- const { success } = await runPlaywrightMergeReports({
222
- blobDir,
223
- outputDir,
224
- cwd,
225
- });
226
- if (!success) {
227
- return { success: false };
228
- }
229
- const htmlFilePath = path_1.default.join(outputDir, "index.html");
230
- const jsonFilePath = path_1.default.join(cwd, "summary.json");
231
- await patchMergedHtmlReport(htmlFilePath, urlMappings);
232
- await patchSummaryJson(jsonFilePath, urlMappings);
233
- const hasR2Creds = process.env.R2_ACCOUNT_ID &&
234
- process.env.R2_ACCESS_KEY_ID &&
235
- process.env.R2_SECRET_ACCESS_KEY;
236
- if (hasR2Creds) {
237
- await uploadMergedReports(cwd, outputDir, {
238
- projectName,
239
- runId,
240
- baseUrl: "https://reports.empirical.run",
241
- uploadBucket: "test-report",
242
- });
243
- }
244
- else {
245
- logger_1.logger.info(`[Merge Reports] R2 credentials not found, skipping upload`);
246
- }
247
- return { success: true };
248
- }