@empiricalrun/test-run 0.11.1 → 0.12.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,16 @@
1
1
  # @empiricalrun/test-run
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 79a4e0f: feat: add blob reporters for sharding
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [79a4e0f]
12
+ - @empiricalrun/r2-uploader@0.6.0
13
+
3
14
  ## 0.11.1
4
15
 
5
16
  ### Patch Changes
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=merge-reports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-reports.d.ts","sourceRoot":"","sources":["../../src/bin/merge-reports.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const dotenv_1 = __importDefault(require("dotenv"));
9
+ const merge_reports_1 = require("../lib/merge-reports");
10
+ dotenv_1.default.config({
11
+ path: [".env.local", ".env"],
12
+ });
13
+ (async function main() {
14
+ commander_1.program
15
+ .option("-b, --blob-dir <blob-dir>", "Path to the blob-report directory")
16
+ .option("-c, --cwd <cwd>", "Working directory")
17
+ .parse(process.argv);
18
+ const options = commander_1.program.opts();
19
+ const { success } = await (0, merge_reports_1.mergeReports)({
20
+ blobDir: options.blobDir,
21
+ cwd: options.cwd,
22
+ });
23
+ if (!success) {
24
+ process.exit(1);
25
+ }
26
+ })();
package/dist/index.d.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  import { JSONReport as PlaywrightJSONReport } from "@playwright/test/reporter";
2
2
  import { spawnCmd } from "./lib/cmd";
3
3
  import { runSpecificTestsCmd } from "./lib/run-specific-test";
4
+ import { parseTestListOutput } from "./stdout-parser";
4
5
  import { Platform, TestCase } from "./types";
5
6
  import { getProjectsFromPlaywrightConfig } from "./utils/config";
6
- export { getProjectsFromPlaywrightConfig, Platform, runSpecificTestsCmd, spawnCmd, };
7
+ export { getProjectsFromPlaywrightConfig, parseTestListOutput, Platform, runSpecificTestsCmd, spawnCmd, };
7
8
  export * from "./glob-matcher";
8
9
  export { filterArrayByGlobMatchersSet, generateProjectFilters } from "./utils";
9
- export declare function runSingleTest({ testName, suites, filePath, projects, envOverrides, repoDir, }: {
10
+ export declare function runSingleTest({ testName, suites, filePath, projects, envOverrides, repoDir, stdout, stderr, }: {
10
11
  testName: string;
11
12
  suites: string[];
12
13
  filePath: string;
13
14
  projects: string[];
14
15
  envOverrides?: Record<string, string>;
15
16
  repoDir: string;
17
+ stdout?: NodeJS.WritableStream;
18
+ stderr?: NodeJS.WritableStream;
16
19
  }): Promise<{
17
20
  hasTestPassed: boolean;
18
21
  summaryJson: PlaywrightJSONReport;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAI/E,OAAO,EAAkB,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAMjE,OAAO,EACL,+BAA+B,EAC/B,QAAQ,EACR,mBAAmB,EACnB,QAAQ,GACT,CAAC;AACF,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAc/E,wBAAsB,aAAa,CAAC,EAClC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC;IACV,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,oBAAoB,CAAC;CACnC,CAAC,CAiBD;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACnE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;CACjD,CAAC,CAaD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAI/E,OAAO,EAAkB,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAMjE,OAAO,EACL,+BAA+B,EAC/B,mBAAmB,EACnB,QAAQ,EACR,mBAAmB,EACnB,QAAQ,GACT,CAAC;AACF,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAc/E,wBAAsB,aAAa,CAAC,EAClC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,MAAM,EACN,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GAAG,OAAO,CAAC;IACV,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,oBAAoB,CAAC;CACnC,CAAC,CAoBD;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACnE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;CACjD,CAAC,CAeD"}
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.spawnCmd = exports.runSpecificTestsCmd = exports.Platform = exports.getProjectsFromPlaywrightConfig = void 0;
20
+ exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.spawnCmd = exports.runSpecificTestsCmd = exports.Platform = exports.parseTestListOutput = exports.getProjectsFromPlaywrightConfig = void 0;
21
21
  exports.runSingleTest = runSingleTest;
22
22
  exports.listProjectsAndTests = listProjectsAndTests;
23
23
  const fs_1 = __importDefault(require("fs"));
@@ -26,7 +26,8 @@ const cmd_1 = require("./lib/cmd");
26
26
  Object.defineProperty(exports, "spawnCmd", { enumerable: true, get: function () { return cmd_1.spawnCmd; } });
27
27
  const run_specific_test_1 = require("./lib/run-specific-test");
28
28
  Object.defineProperty(exports, "runSpecificTestsCmd", { enumerable: true, get: function () { return run_specific_test_1.runSpecificTestsCmd; } });
29
- const parser_1 = require("./parser");
29
+ const stdout_parser_1 = require("./stdout-parser");
30
+ Object.defineProperty(exports, "parseTestListOutput", { enumerable: true, get: function () { return stdout_parser_1.parseTestListOutput; } });
30
31
  const types_1 = require("./types");
31
32
  Object.defineProperty(exports, "Platform", { enumerable: true, get: function () { return types_1.Platform; } });
32
33
  const utils_1 = require("./utils");
@@ -46,7 +47,7 @@ function getSummaryJsonPath(repoDir) {
46
47
  ? pathForPlaywright147
47
48
  : pathForOtherPlaywrightVersions;
48
49
  }
49
- async function runSingleTest({ testName, suites, filePath, projects, envOverrides, repoDir, }) {
50
+ async function runSingleTest({ testName, suites, filePath, projects, envOverrides, repoDir, stdout, stderr, }) {
50
51
  const testDir = "tests";
51
52
  const commandToRun = await (0, run_specific_test_1.runSpecificTestsCmd)({
52
53
  tests: [{ name: testName, dir: testDir, filePath, suites }],
@@ -55,7 +56,10 @@ async function runSingleTest({ testName, suites, filePath, projects, envOverride
55
56
  platform: supportedPlatform,
56
57
  repoDir,
57
58
  });
58
- const { hasTestPassed } = await (0, cmd_1.runTestsForCmd)(commandToRun, repoDir);
59
+ const { hasTestPassed } = await (0, cmd_1.runTestsForCmd)(commandToRun, repoDir, {
60
+ stdout,
61
+ stderr,
62
+ });
59
63
  const jsonFilePath = getSummaryJsonPath(repoDir);
60
64
  const jsonFileContents = fs_1.default.readFileSync(jsonFilePath, "utf8");
61
65
  const summaryJson = JSON.parse(jsonFileContents);
@@ -69,12 +73,14 @@ async function listProjectsAndTests(repoDir) {
69
73
  const args = [testRunner, "test", "--list"];
70
74
  const { output, code } = await (0, cmd_1.spawnCmd)("npx", args, {
71
75
  cwd: repoDir,
72
- envOverrides: {},
76
+ envOverrides: {
77
+ NODE_PATH: path_1.default.join(repoDir, "node_modules"),
78
+ },
73
79
  captureOutput: true,
74
80
  throwOnError: true,
75
81
  });
76
82
  if (!output) {
77
83
  throw new Error(`Failed to run list command; exit code: ${code}`);
78
84
  }
79
- return (0, parser_1.parseTestListOutput)(output);
85
+ return (0, stdout_parser_1.parseTestListOutput)(output);
80
86
  }
package/dist/lib/cmd.d.ts CHANGED
@@ -3,7 +3,10 @@ export declare function getCommandFromString(command: string): {
3
3
  command: string;
4
4
  args: string[];
5
5
  };
6
- export declare function runTestsForCmd({ command, args, env }: CommandToRun, cwd: string): Promise<{
6
+ export declare function runTestsForCmd({ command, args, env }: CommandToRun, cwd: string, options?: {
7
+ stdout?: NodeJS.WritableStream;
8
+ stderr?: NodeJS.WritableStream;
9
+ }): Promise<{
7
10
  hasTestPassed: boolean;
8
11
  }>;
9
12
  export declare function spawnCmd(command: string, args: string[], options: {
@@ -11,6 +14,8 @@ export declare function spawnCmd(command: string, args: string[], options: {
11
14
  envOverrides: Record<string, string>;
12
15
  captureOutput: boolean;
13
16
  throwOnError: boolean;
17
+ stdout?: NodeJS.WritableStream;
18
+ stderr?: NodeJS.WritableStream;
14
19
  }): Promise<{
15
20
  code: number;
16
21
  output?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/lib/cmd.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAeA;AAED,wBAAsB,cAAc,CAClC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,YAAY,EACpC,GAAG,EAAE,MAAM;;GAeZ;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6C5C"}
1
+ {"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/lib/cmd.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAeA;AAED,wBAAsB,cAAc,CAClC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,YAAY,EACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC;;GAiBF;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GACA,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+C5C"}
package/dist/lib/cmd.js CHANGED
@@ -19,7 +19,7 @@ function getCommandFromString(command) {
19
19
  }),
20
20
  };
21
21
  }
22
- async function runTestsForCmd({ command, args, env }, cwd) {
22
+ async function runTestsForCmd({ command, args, env }, cwd, options) {
23
23
  logger_1.logger.debug(`Running cmd: ${command} with args: ${args}`);
24
24
  let hasTestPassed = true;
25
25
  try {
@@ -28,6 +28,8 @@ async function runTestsForCmd({ command, args, env }, cwd) {
28
28
  envOverrides: env,
29
29
  captureOutput: false,
30
30
  throwOnError: true,
31
+ stdout: options?.stdout,
32
+ stderr: options?.stderr,
31
33
  });
32
34
  }
33
35
  catch {
@@ -53,7 +55,8 @@ async function spawnCmd(command, args, options) {
53
55
  output += log;
54
56
  }
55
57
  else {
56
- process.stdout.write(log);
58
+ const stdout = options.stdout || process.stdout;
59
+ stdout.write(log);
57
60
  }
58
61
  if (log.includes("Error")) {
59
62
  errorLogs.push(log);
@@ -61,7 +64,8 @@ async function spawnCmd(command, args, options) {
61
64
  });
62
65
  p.stderr.on("data", (x) => {
63
66
  const log = x.toString();
64
- process.stderr.write(log);
67
+ const stderr = options.stderr || process.stderr;
68
+ stderr.write(log);
65
69
  errorLogs.push(log);
66
70
  });
67
71
  p.on("exit", (code) => {
@@ -0,0 +1,26 @@
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): 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-reports.d.ts","sourceRoot":"","sources":["../../src/lib/merge-reports.ts"],"names":[],"mappings":"AAYA,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;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA2B/B;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BxB;AAED,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAqFf;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAuBf;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"}
@@ -0,0 +1,231 @@
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 adm_zip_1 = __importDefault(require("adm-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
+ async function runPlaywrightMergeReports(options) {
19
+ const { blobDir, outputDir, cwd } = options;
20
+ logger_1.logger.debug(`[Merge Reports] Running playwright merge-reports`);
21
+ logger_1.logger.debug(`[Merge Reports] Blob dir: ${blobDir}`);
22
+ logger_1.logger.debug(`[Merge Reports] Output dir: ${outputDir}`);
23
+ try {
24
+ await (0, cmd_1.spawnCmd)("npx", ["playwright", "merge-reports", blobDir, "--reporter", "html,json"], {
25
+ cwd,
26
+ envOverrides: {
27
+ PLAYWRIGHT_HTML_OPEN: "never",
28
+ PLAYWRIGHT_HTML_OUTPUT_DIR: outputDir,
29
+ PLAYWRIGHT_JSON_OUTPUT_NAME: path_1.default.join(cwd, "summary.json"),
30
+ },
31
+ captureOutput: false,
32
+ throwOnError: true,
33
+ });
34
+ return { success: true };
35
+ }
36
+ catch (error) {
37
+ logger_1.logger.error(`[Merge Reports] Failed to merge reports:`, error);
38
+ return { success: false };
39
+ }
40
+ }
41
+ function extractUrlMappingsFromBlobs(blobDir) {
42
+ const combinedMap = {};
43
+ const files = fs_1.default.readdirSync(blobDir);
44
+ for (const fileName of files.filter((f) => f.endsWith(".zip"))) {
45
+ const zipPath = path_1.default.join(blobDir, fileName);
46
+ try {
47
+ const zip = new adm_zip_1.default(zipPath);
48
+ const urlsEntry = zip.getEntry("_empirical_urls.json");
49
+ if (urlsEntry) {
50
+ const content = JSON.parse(urlsEntry.getData().toString("utf8"));
51
+ Object.assign(combinedMap, content);
52
+ logger_1.logger.debug(`[Merge Reports] Extracted ${Object.keys(content).length} URL mappings from ${fileName}`);
53
+ }
54
+ }
55
+ catch (error) {
56
+ logger_1.logger.error(`[Merge Reports] Failed to extract URL mappings from ${fileName}:`, error);
57
+ }
58
+ }
59
+ logger_1.logger.info(`[Merge Reports] Total URL mappings: ${Object.keys(combinedMap).length}`);
60
+ return combinedMap;
61
+ }
62
+ async function patchMergedHtmlReport(htmlFilePath, urlMappings) {
63
+ if (Object.keys(urlMappings).length === 0) {
64
+ logger_1.logger.debug(`[Merge Reports] No URL mappings to apply`);
65
+ return;
66
+ }
67
+ let htmlContent;
68
+ try {
69
+ htmlContent = await fs_1.default.promises.readFile(htmlFilePath, "utf8");
70
+ }
71
+ catch (error) {
72
+ logger_1.logger.error(`[Merge Reports] Failed to read HTML file:`, error);
73
+ return;
74
+ }
75
+ const oldFormatMatch = htmlContent.match(/window\.playwrightReportBase64\s*=\s*"(?:data:application\/zip;base64,)?([^"]+)"/);
76
+ const newFormatMatch = htmlContent.match(/<script\s+id="playwrightReportBase64"[^>]*>(?:data:application\/zip;base64,)?([^<]+)<\/script>/);
77
+ const base64 = oldFormatMatch?.[1] || newFormatMatch?.[1];
78
+ if (!base64) {
79
+ logger_1.logger.error(`[Merge Reports] Base64 zip data not found in HTML`);
80
+ return;
81
+ }
82
+ const htmlDir = path_1.default.dirname(path_1.default.resolve(htmlFilePath));
83
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(htmlDir, "merge-patch-"));
84
+ const zipPath = path_1.default.join(tempDir, "archive.zip");
85
+ try {
86
+ await fs_1.default.promises.writeFile(zipPath, Buffer.from(base64, "base64"));
87
+ const zip = new adm_zip_1.default(zipPath);
88
+ zip.extractAllTo(tempDir, true);
89
+ await fs_1.default.promises.unlink(zipPath);
90
+ const jsonFiles = (await fs_1.default.promises.readdir(tempDir)).filter((f) => f.endsWith(".json"));
91
+ for (const file of jsonFiles) {
92
+ const filePath = path_1.default.join(tempDir, file);
93
+ const content = await fs_1.default.promises.readFile(filePath, "utf8");
94
+ let modified = content;
95
+ for (const [resourcePath, url] of Object.entries(urlMappings)) {
96
+ const resourceFileName = resourcePath.replace(/^resources\//, "");
97
+ const regex = new RegExp(`[^"]*${resourceFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g");
98
+ modified = modified.replace(regex, url);
99
+ }
100
+ if (modified !== content) {
101
+ await fs_1.default.promises.writeFile(filePath, modified, "utf8");
102
+ logger_1.logger.debug(`[Merge Reports] Patched ${file}`);
103
+ }
104
+ }
105
+ const newZip = new adm_zip_1.default();
106
+ newZip.addLocalFolder(tempDir);
107
+ const newBase64 = newZip.toBuffer().toString("base64");
108
+ let updatedHtml;
109
+ if (oldFormatMatch) {
110
+ updatedHtml = htmlContent.replace(/(window\.playwrightReportBase64\s*=\s*")(?:data:application\/zip;base64,)?[^"]*(")/, `$1data:application/zip;base64,${newBase64}$2`);
111
+ }
112
+ else {
113
+ updatedHtml = htmlContent.replace(/(<script\s+id="playwrightReportBase64"[^>]*>)(?:data:application\/zip;base64,)?[^<]*(<\/script>)/, `$1data:application/zip;base64,${newBase64}$2`);
114
+ }
115
+ await fs_1.default.promises.writeFile(htmlFilePath, updatedHtml, "utf8");
116
+ logger_1.logger.info(`[Merge Reports] HTML file patched successfully`);
117
+ }
118
+ catch (error) {
119
+ logger_1.logger.error(`[Merge Reports] Failed to patch HTML:`, error);
120
+ }
121
+ finally {
122
+ await fs_1.default.promises.rm(tempDir, { recursive: true, force: true });
123
+ }
124
+ }
125
+ async function patchSummaryJson(jsonFilePath, urlMappings) {
126
+ if (Object.keys(urlMappings).length === 0) {
127
+ logger_1.logger.debug(`[Merge Reports] No URL mappings to apply to summary.json`);
128
+ return;
129
+ }
130
+ try {
131
+ let content = await fs_1.default.promises.readFile(jsonFilePath, "utf8");
132
+ for (const [resourcePath, url] of Object.entries(urlMappings)) {
133
+ const resourceFileName = resourcePath.replace(/^resources\//, "");
134
+ const regex = new RegExp(`[^"]*${resourceFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g");
135
+ content = content.replace(regex, url);
136
+ }
137
+ await fs_1.default.promises.writeFile(jsonFilePath, content, "utf8");
138
+ logger_1.logger.info(`[Merge Reports] summary.json patched successfully`);
139
+ }
140
+ catch (error) {
141
+ logger_1.logger.error(`[Merge Reports] Failed to patch summary.json:`, error);
142
+ }
143
+ }
144
+ async function uploadMergedReports(cwd, outputDir, uploadOptions) {
145
+ const { projectName, runId, baseUrl, uploadBucket } = uploadOptions;
146
+ const destinationDir = path_1.default.join(projectName, runId);
147
+ const htmlFilePath = path_1.default.join(outputDir, "index.html");
148
+ const jsonFilePath = path_1.default.join(cwd, "summary.json");
149
+ if (fs_1.default.existsSync(htmlFilePath)) {
150
+ logger_1.logger.debug(`[Merge Reports] Uploading HTML report`);
151
+ const task = (0, r2_uploader_1.createUploadTask)({
152
+ sourceDir: outputDir,
153
+ fileList: [htmlFilePath],
154
+ destinationDir,
155
+ uploadBucket,
156
+ baseUrl,
157
+ });
158
+ void (0, r2_uploader_1.sendTaskToQueue)(task);
159
+ }
160
+ if (fs_1.default.existsSync(jsonFilePath)) {
161
+ logger_1.logger.debug(`[Merge Reports] Uploading summary.json`);
162
+ const task = (0, r2_uploader_1.createUploadTask)({
163
+ sourceDir: cwd,
164
+ fileList: [jsonFilePath],
165
+ destinationDir,
166
+ uploadBucket,
167
+ baseUrl,
168
+ });
169
+ void (0, r2_uploader_1.sendTaskToQueue)(task);
170
+ }
171
+ const traceDir = path_1.default.join(outputDir, "trace");
172
+ if (fs_1.default.existsSync(traceDir)) {
173
+ logger_1.logger.debug(`[Merge Reports] Uploading trace folder`);
174
+ const task = (0, r2_uploader_1.createUploadTask)({
175
+ sourceDir: traceDir,
176
+ destinationDir: path_1.default.join(destinationDir, "trace"),
177
+ uploadBucket,
178
+ baseUrl,
179
+ });
180
+ void (0, r2_uploader_1.sendTaskToQueue)(task);
181
+ }
182
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
183
+ const reportUrl = `${baseUrl}/${destinationDir}/index.html`;
184
+ const jsonUrl = `${baseUrl}/${destinationDir}/summary.json`;
185
+ logger_1.logger.info(`[Merge Reports] All uploads completed`);
186
+ logger_1.logger.info(`[Merge Reports] HTML Report: ${reportUrl}`);
187
+ logger_1.logger.info(`[Merge Reports] Summary JSON: ${jsonUrl}`);
188
+ }
189
+ async function mergeReports(options) {
190
+ const cwd = options.cwd || process.cwd();
191
+ const blobDir = options.blobDir || path_1.default.join(cwd, "blob-report");
192
+ const outputDir = path_1.default.join(cwd, "playwright-report");
193
+ const projectName = process.env.PROJECT_NAME;
194
+ const runId = process.env.TEST_RUN_GITHUB_ACTION_ID;
195
+ if (!projectName || !runId) {
196
+ logger_1.logger.error(`[Merge Reports] PROJECT_NAME and TEST_RUN_GITHUB_ACTION_ID must be set`);
197
+ return { success: false };
198
+ }
199
+ if (!fs_1.default.existsSync(blobDir)) {
200
+ logger_1.logger.error(`[Merge Reports] Blob directory does not exist: ${blobDir}`);
201
+ return { success: false };
202
+ }
203
+ const urlMappings = extractUrlMappingsFromBlobs(blobDir);
204
+ const { success } = await runPlaywrightMergeReports({
205
+ blobDir,
206
+ outputDir,
207
+ cwd,
208
+ });
209
+ if (!success) {
210
+ return { success: false };
211
+ }
212
+ const htmlFilePath = path_1.default.join(outputDir, "index.html");
213
+ const jsonFilePath = path_1.default.join(cwd, "summary.json");
214
+ await patchMergedHtmlReport(htmlFilePath, urlMappings);
215
+ await patchSummaryJson(jsonFilePath, urlMappings);
216
+ const hasR2Creds = process.env.R2_ACCOUNT_ID &&
217
+ process.env.R2_ACCESS_KEY_ID &&
218
+ process.env.R2_SECRET_ACCESS_KEY;
219
+ if (hasR2Creds) {
220
+ await uploadMergedReports(cwd, outputDir, {
221
+ projectName,
222
+ runId,
223
+ baseUrl: "https://reports.empirical.run",
224
+ uploadBucket: "test-report",
225
+ });
226
+ }
227
+ else {
228
+ logger_1.logger.info(`[Merge Reports] R2 credentials not found, skipping upload`);
229
+ }
230
+ return { success: true };
231
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"run-specific-test.d.ts","sourceRoot":"","sources":["../../src/lib/run-specific-test.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAY5D,wBAAsB,mBAAmB,CAAC,EACxC,KAAU,EACV,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,YAAY,EACZ,OAAO,GACR,EAAE;IACD,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,YAAY,CAAC,CA0FxB"}
1
+ {"version":3,"file":"run-specific-test.d.ts","sourceRoot":"","sources":["../../src/lib/run-specific-test.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAY5D,wBAAsB,mBAAmB,CAAC,EACxC,KAAU,EACV,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,YAAY,EACZ,OAAO,GACR,EAAE;IACD,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2FxB"}
@@ -33,7 +33,7 @@ async function runSpecificTestsCmd({ tests = [], projects, passthroughArgs, plat
33
33
  }
34
34
  }
35
35
  if (!matchingFilePath) {
36
- const suitesPrefix = testCase.suites
36
+ const suitesPrefix = testCase.suites && testCase.suites.length > 0
37
37
  ? `${testCase.suites.join(" > ")} > `
38
38
  : "";
39
39
  const fullTestName = `${suitesPrefix}${testCase.name}`;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/stdout-parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;CACjD,CA0CA"}
@@ -1,4 +1,4 @@
1
- import { PlaywrightProject } from "@empiricalrun/shared-types";
1
+ import { PlaywrightProject } from "@empiricalrun/shared-types/tool-results";
2
2
  import { Platform } from "../types";
3
3
  export declare function getProjectsFromPlaywrightConfig(platform: Platform, repoDir: string): Promise<PlaywrightProject[]>;
4
4
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAK/D,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyE9B"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAK5E,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyE9B"}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-run",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
7
7
  },
8
8
  "bin": {
9
- "@empiricalrun/test-run": "dist/bin/index.js"
9
+ "@empiricalrun/test-run": "dist/bin/index.js",
10
+ "@empiricalrun/merge-reports": "dist/bin/merge-reports.js"
10
11
  },
11
12
  "main": "dist/index.js",
12
13
  "exports": {
@@ -25,26 +26,29 @@
25
26
  },
26
27
  "author": "Empirical Team <hey@empirical.run>",
27
28
  "dependencies": {
29
+ "adm-zip": "^0.5.16",
28
30
  "async-retry": "^1.3.3",
29
31
  "commander": "^12.1.0",
30
32
  "console-log-level": "^1.4.1",
31
33
  "dotenv": "^16.4.5",
32
34
  "minimatch": "^10.0.1",
33
- "ts-morph": "^23.0.0"
35
+ "ts-morph": "^23.0.0",
36
+ "@empiricalrun/r2-uploader": "^0.6.0"
34
37
  },
35
38
  "devDependencies": {
39
+ "@playwright/test": "1.53.2",
40
+ "@types/adm-zip": "^0.5.7",
36
41
  "@types/async-retry": "^1.4.8",
37
42
  "@types/console-log-level": "^1.4.5",
38
43
  "@types/node": "^22.5.5",
39
- "@playwright/test": "1.53.2",
40
44
  "memfs": "^4.17.1",
41
- "@empiricalrun/shared-types": "0.10.1"
45
+ "@empiricalrun/shared-types": "0.12.0"
42
46
  },
43
47
  "scripts": {
44
48
  "dev": "tsc --build --watch",
45
49
  "build": "tsc --build",
46
50
  "clean": "tsc --build --clean",
47
- "lint": "eslint .",
51
+ "lint": "biome check --unsafe",
48
52
  "test": "vitest run",
49
53
  "test:watch": "vitest"
50
54
  }
@@ -1 +1 @@
1
- {"root":["./src/dashboard.ts","./src/glob-matcher.ts","./src/index.ts","./src/logger.ts","./src/bin/index.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/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/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 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;CACjD,CA0CA"}
package/eslint.config.mjs DELETED
@@ -1,16 +0,0 @@
1
- import libraryConfig from "../eslint-config/library.mjs";
2
- import tsParser from "@typescript-eslint/parser";
3
-
4
- export default [
5
- ...libraryConfig,
6
- {
7
- files: ["src/**/*.ts", "src/**/*.tsx"],
8
- languageOptions: {
9
- parser: tsParser,
10
- parserOptions: {
11
- project: "./tsconfig.lint.json",
12
- tsconfigRootDir: import.meta.dirname,
13
- },
14
- },
15
- },
16
- ];
File without changes
File without changes