@empiricalrun/test-run 0.13.0 → 0.13.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +42 -3
  3. package/dist/bin/commands/estimate-time-shard.d.ts +3 -0
  4. package/dist/bin/commands/estimate-time-shard.d.ts.map +1 -0
  5. package/dist/bin/commands/estimate-time-shard.js +122 -0
  6. package/dist/bin/commands/failed-list.d.ts +3 -0
  7. package/dist/bin/commands/failed-list.d.ts.map +1 -0
  8. package/dist/bin/commands/failed-list.js +34 -0
  9. package/dist/bin/commands/merge.d.ts +3 -0
  10. package/dist/bin/commands/merge.d.ts.map +1 -0
  11. package/dist/bin/commands/merge.js +20 -0
  12. package/dist/bin/commands/optimize-shards.d.ts +3 -0
  13. package/dist/bin/commands/optimize-shards.d.ts.map +1 -0
  14. package/dist/bin/commands/optimize-shards.js +400 -0
  15. package/dist/bin/commands/run.d.ts +3 -0
  16. package/dist/bin/commands/run.d.ts.map +1 -0
  17. package/dist/bin/commands/run.js +132 -0
  18. package/dist/bin/index.js +15 -132
  19. package/dist/failed-test-list.d.ts +35 -0
  20. package/dist/failed-test-list.d.ts.map +1 -0
  21. package/dist/failed-test-list.js +267 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -1
  25. package/dist/lib/cancellation-watcher.js +1 -1
  26. package/dist/lib/merge-reports/html.d.ts +2 -0
  27. package/dist/lib/merge-reports/html.d.ts.map +1 -0
  28. package/dist/lib/merge-reports/html.js +113 -0
  29. package/dist/lib/merge-reports/index.d.ts +16 -0
  30. package/dist/lib/merge-reports/index.d.ts.map +1 -0
  31. package/dist/lib/merge-reports/index.js +189 -0
  32. package/dist/lib/merge-reports/json.d.ts +2 -0
  33. package/dist/lib/merge-reports/json.d.ts.map +1 -0
  34. package/dist/lib/merge-reports/json.js +67 -0
  35. package/dist/lib/merge-reports/types.d.ts +15 -0
  36. package/dist/lib/merge-reports/types.d.ts.map +1 -0
  37. package/dist/lib/merge-reports/types.js +11 -0
  38. package/package.json +4 -4
  39. package/tsconfig.tsbuildinfo +1 -1
  40. package/dist/bin/merge-reports.d.ts +0 -3
  41. package/dist/bin/merge-reports.d.ts.map +0 -1
  42. package/dist/bin/merge-reports.js +0 -26
  43. package/dist/lib/merge-reports.d.ts +0 -26
  44. package/dist/lib/merge-reports.d.ts.map +0 -1
  45. package/dist/lib/merge-reports.js +0 -248
package/dist/bin/index.js CHANGED
@@ -6,138 +6,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
8
  const dotenv_1 = __importDefault(require("dotenv"));
9
- const dashboard_1 = require("../dashboard");
10
- const cmd_1 = require("../lib/cmd");
11
- const run_all_tests_1 = require("../lib/run-all-tests");
12
- const run_specific_test_1 = require("../lib/run-specific-test");
13
- const types_1 = require("../types");
14
- const utils_1 = require("../utils");
15
- const config_parser_1 = require("../utils/config-parser");
9
+ const estimate_time_shard_1 = require("./commands/estimate-time-shard");
10
+ const failed_list_1 = require("./commands/failed-list");
11
+ const merge_1 = require("./commands/merge");
12
+ const optimize_shards_1 = require("./commands/optimize-shards");
13
+ const run_1 = require("./commands/run");
16
14
  dotenv_1.default.config({
17
15
  path: [".env.local", ".env"],
18
16
  });
19
- const repoDir = process.cwd();
20
- (async function main() {
21
- commander_1.program
22
- .option("-n, --name <test-name>", "Name of the test to run")
23
- .option("-s, --suites <suites>", "Suites under which the test is defined")
24
- .option("-d, --dir <test-dir>", "Path to the test directory")
25
- .option("-f, --file <test-file-path>", "Path to the test file")
26
- .option("-p, --project <project-name...>", "Test projects to run")
27
- .option("--payload <payload>", "Payload to run tests")
28
- .option("--skip-teardown", "This options skips running teardown tests")
29
- .option("--forbid-only", `This options forbids the use of ".only" in the test files`)
30
- .allowUnknownOption();
31
- commander_1.program.parse(process.argv);
32
- // Accessing the extracted parameters
33
- const options = commander_1.program.opts();
34
- if (options.name && options.forbidOnly) {
35
- console.error("--name and --forbid-only options cannot be used together");
36
- process.exit(1);
37
- }
38
- options.project = options.project || ["*"];
39
- const optionsToStrip = [
40
- "-n",
41
- "--name",
42
- "--skip-teardown",
43
- "-f",
44
- "--file",
45
- "-d",
46
- "--dir",
47
- "-p",
48
- "--project",
49
- "-s",
50
- "--suites",
51
- "--payload",
52
- options.skipTeardown,
53
- options.name,
54
- options.dir,
55
- options.file,
56
- options.suites,
57
- options.payload,
58
- ...options.project, // an array of comma separated project names/pattern matching globs *,premium-*,super-premium-*,safari
59
- ];
60
- const pwOptions = process.argv
61
- .slice(2)
62
- .filter((arg) => !optionsToStrip.includes(arg));
63
- const projectName = process.env.PROJECT_NAME || (await (0, utils_1.pickNameFromPackageJson)());
64
- if (!projectName) {
65
- throw new Error("Project name is required");
66
- }
67
- const directory = options.dir || "tests";
68
- const suites = options.suites && options.suites.trim() !== ""
69
- ? options.suites?.split(",")
70
- : undefined;
71
- let tests = (0, config_parser_1.parseToken)(options.payload)?.tests ||
72
- (options.name
73
- ? [
74
- {
75
- name: options.name,
76
- dir: directory,
77
- filePath: options.file,
78
- suites,
79
- },
80
- ]
81
- : undefined);
82
- const environmentSlug = process.env.TEST_RUN_ENVIRONMENT || "";
83
- // Fetch environment variables from dashboard
84
- const environmentVariables = await (0, dashboard_1.fetchEnvironmentVariables)();
85
- const envOverrides = {};
86
- // Convert environment variables to key-value pairs for process.env
87
- environmentVariables.forEach((envVar) => {
88
- envOverrides[envVar.name] = envVar.value;
89
- });
90
- if (Object.keys(envOverrides).length > 0) {
91
- console.log(`Loaded environment variables: ${Object.keys(envOverrides).join(", ")}`);
92
- }
93
- let environmentSpecificProjects = [];
94
- let platform = types_1.Platform.WEB;
95
- try {
96
- if (environmentSlug) {
97
- const { environment, build: latestBuild } = await (0, dashboard_1.fetchEnvironmentAndBuild)(projectName, environmentSlug);
98
- platform = environment.platform;
99
- environmentSpecificProjects = environment.playwright_projects;
100
- if (!process.env.BUILD_URL) {
101
- process.env.BUILD_URL = latestBuild?.build_url; // pick the one coming from dispatch, otherwise use the latest build from DB
102
- }
103
- const buildUrl = process.env.BUILD_URL;
104
- await (0, utils_1.downloadBuild)(buildUrl);
105
- }
106
- const projectFilters = await (0, utils_1.generateProjectFilters)({
107
- platform,
108
- filteringSets: [...options.project, ...environmentSpecificProjects],
109
- repoDir,
110
- });
111
- if (options.skipTeardown) {
112
- await (0, utils_1.handleTeardownSkipFlag)(directory, repoDir);
113
- }
114
- const hasTestsFilter = tests && tests.length > 0;
115
- let commandToRun;
116
- if (hasTestsFilter) {
117
- commandToRun = await (0, run_specific_test_1.runSpecificTestsCmd)({
118
- tests,
119
- projects: projectFilters,
120
- passthroughArgs: pwOptions.join(" "),
121
- platform,
122
- envOverrides,
123
- repoDir,
124
- });
125
- }
126
- else {
127
- commandToRun = (0, run_all_tests_1.runAllTestsCmd)({
128
- projects: projectFilters,
129
- passthroughArgs: pwOptions.join(" "),
130
- platform,
131
- envOverrides,
132
- });
133
- }
134
- const { hasTestPassed } = await (0, cmd_1.runTestsForCmd)(commandToRun, repoDir);
135
- if (!hasTestPassed) {
136
- process.exit(1);
137
- }
138
- }
139
- catch (error) {
140
- console.error("Error while running playwright test:", error.message);
141
- process.exit(1);
142
- }
143
- })();
17
+ commander_1.program
18
+ .name("test-run")
19
+ .description("Empirical test runner CLI")
20
+ .version("0.13.1");
21
+ (0, run_1.registerRunCommand)(commander_1.program);
22
+ (0, merge_1.registerMergeCommand)(commander_1.program);
23
+ (0, failed_list_1.registerFailedListCommand)(commander_1.program);
24
+ (0, estimate_time_shard_1.registerEstimateTimeShardCommand)(commander_1.program);
25
+ (0, optimize_shards_1.registerOptimizeShardsCommand)(commander_1.program);
26
+ commander_1.program.parse(process.argv);
@@ -0,0 +1,35 @@
1
+ export interface TestRunResponse {
2
+ data: {
3
+ test_run: {
4
+ project: {
5
+ repo_name: string;
6
+ };
7
+ testRun: {
8
+ summary_url: string | null;
9
+ run_id: number | null;
10
+ };
11
+ };
12
+ } | null;
13
+ error?: {
14
+ message: string;
15
+ };
16
+ }
17
+ export interface FailedTest {
18
+ projectName: string;
19
+ file: string;
20
+ title: string;
21
+ suites: string[];
22
+ }
23
+ export interface BuildTestListOptions {
24
+ outputPath?: string;
25
+ verbose?: boolean;
26
+ repoName?: string;
27
+ repoPath?: string;
28
+ }
29
+ export interface BuildTestListResult {
30
+ failedTests: FailedTest[];
31
+ testListContent: string;
32
+ outputPath: string;
33
+ }
34
+ export declare function buildTestListFromFailedTestRun(runId: string, options?: BuildTestListOptions): Promise<BuildTestListResult>;
35
+ //# sourceMappingURL=failed-test-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failed-test-list.d.ts","sourceRoot":"","sources":["../src/failed-test-list.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QACJ,QAAQ,EAAE;YACR,OAAO,EAAE;gBACP,SAAS,EAAE,MAAM,CAAC;aACnB,CAAC;YACF,OAAO,EAAE;gBACP,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;gBAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;aACvB,CAAC;SACH,CAAC;KACH,GAAG,IAAI,CAAC;IACT,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AASD,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAgPD,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,8BAA8B,CAClD,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAiH9B"}
@@ -0,0 +1,267 @@
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.buildTestListFromFailedTestRun = buildTestListFromFailedTestRun;
7
+ const reporter_1 = require("@empiricalrun/reporter");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const utils_1 = require("./utils");
11
+ const DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
12
+ const SUITES_DELIMITER = " › ";
13
+ const REPORTS_BASE_URL = "https://reports.empirical.run";
14
+ function buildSummaryUrl(repoName, runId) {
15
+ const projectSlug = repoName.replace("-tests", "");
16
+ return `${REPORTS_BASE_URL}/${projectSlug}/${runId}/summary.json`;
17
+ }
18
+ async function fetchTestRun(runId, options) {
19
+ if (!process.env.EMPIRICALRUN_API_KEY) {
20
+ throw new Error("EMPIRICALRUN_API_KEY environment variable is required");
21
+ }
22
+ const params = new URLSearchParams();
23
+ if (options.repoName) {
24
+ params.set("repo_name", options.repoName);
25
+ }
26
+ const queryString = params.toString();
27
+ const url = `${DOMAIN}/api/test-runs/${runId}${queryString ? `?${queryString}` : ""}`;
28
+ if (options.verbose) {
29
+ console.log(`Fetching test run from: ${url}`);
30
+ }
31
+ const response = await fetch(url, {
32
+ method: "GET",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ Authorization: `Bearer ${process.env.EMPIRICALRUN_API_KEY}`,
36
+ },
37
+ });
38
+ if (!response.ok) {
39
+ if (options.verbose) {
40
+ const text = await response.text();
41
+ console.log(`Response status: ${response.status}`);
42
+ console.log(`Response body: ${text}`);
43
+ }
44
+ throw new Error(`Failed to fetch test run: ${response.status}`);
45
+ }
46
+ const data = await response.json();
47
+ return data;
48
+ }
49
+ function getFailedTests(specs) {
50
+ const failedTests = [];
51
+ for (const spec of specs) {
52
+ for (const test of spec.tests) {
53
+ const status = (0, reporter_1.deriveTestStatus)(test.results);
54
+ if (status === "fail") {
55
+ const suites = spec.nesting.slice(1, -1);
56
+ failedTests.push({
57
+ projectName: test.projectName,
58
+ file: spec.file,
59
+ title: spec.title,
60
+ suites,
61
+ });
62
+ }
63
+ }
64
+ }
65
+ return failedTests;
66
+ }
67
+ function findTestFile(repoPath, relativePath) {
68
+ const directPath = path_1.default.join(repoPath, relativePath);
69
+ if (fs_1.default.existsSync(directPath)) {
70
+ return { absolutePath: directPath, repoRelativePath: relativePath };
71
+ }
72
+ const testsRelative = path_1.default.join("tests", relativePath);
73
+ const testDirPath = path_1.default.join(repoPath, testsRelative);
74
+ if (fs_1.default.existsSync(testDirPath)) {
75
+ return { absolutePath: testDirPath, repoRelativePath: testsRelative };
76
+ }
77
+ return null;
78
+ }
79
+ async function getSerialBlockInfo(test, repoPath, verbose) {
80
+ const fileInfo = findTestFile(repoPath, test.file);
81
+ if (!fileInfo) {
82
+ if (verbose) {
83
+ console.log(`File not found: ${test.file} (searched in repo and tests dir)`);
84
+ }
85
+ return null;
86
+ }
87
+ const isFileSerial = await (0, utils_1.hasTopLevelDescribeConfigureWithSerialMode)(fileInfo.absolutePath);
88
+ if (isFileSerial) {
89
+ if (verbose) {
90
+ console.log(`File ${test.file} is marked as serial at top level`);
91
+ }
92
+ return { file: test.file, serialDescribeName: null, isFileSerial: true };
93
+ }
94
+ const { testCaseNode } = await (0, utils_1.getTestCaseNode)({
95
+ filePath: fileInfo.repoRelativePath,
96
+ scenarioName: test.title,
97
+ suites: test.suites,
98
+ repoDir: repoPath,
99
+ });
100
+ if (testCaseNode) {
101
+ const parentDescribe = (0, utils_1.findFirstSerialDescribeBlock)(testCaseNode);
102
+ if (parentDescribe) {
103
+ const describeName = (0, utils_1.getDescribeBlockName)(parentDescribe);
104
+ if (verbose) {
105
+ console.log(`Test "${test.title}" is in serial describe block: ${describeName}`);
106
+ }
107
+ return {
108
+ file: test.file,
109
+ serialDescribeName: describeName ?? null,
110
+ isFileSerial: false,
111
+ };
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ function formatTestListLine(test) {
117
+ const suitesAndTitle = test.suites.length > 0
118
+ ? [...test.suites, test.title].join(SUITES_DELIMITER)
119
+ : test.title;
120
+ return `[${test.projectName}] › ${test.file} › ${suitesAndTitle}`;
121
+ }
122
+ function getAllTestsFromSpecs(specs) {
123
+ const tests = [];
124
+ for (const spec of specs) {
125
+ for (const test of spec.tests) {
126
+ const suites = spec.nesting.slice(1, -1);
127
+ tests.push({
128
+ projectName: test.projectName,
129
+ file: spec.file,
130
+ title: spec.title,
131
+ suites,
132
+ });
133
+ }
134
+ }
135
+ return tests;
136
+ }
137
+ function isTestInSerialBlock(test, serialBlock) {
138
+ if (test.file !== serialBlock.file) {
139
+ return false;
140
+ }
141
+ if (serialBlock.isFileSerial) {
142
+ return true;
143
+ }
144
+ if (serialBlock.serialDescribeName) {
145
+ return test.suites.includes(serialBlock.serialDescribeName);
146
+ }
147
+ return false;
148
+ }
149
+ function generateTestListContent(failedTests, allTests, serialBlocks = []) {
150
+ const lines = [
151
+ `# Failed tests from test run`,
152
+ `# Generated: ${new Date().toISOString()}`,
153
+ `# Total failed tests: ${failedTests.length}`,
154
+ "",
155
+ ];
156
+ const addedEntries = new Set();
157
+ for (const serialBlock of serialBlocks) {
158
+ const projectNames = [
159
+ ...new Set(failedTests
160
+ .filter((t) => t.file === serialBlock.file)
161
+ .map((t) => t.projectName)),
162
+ ];
163
+ for (const projectName of projectNames) {
164
+ const testsInBlock = allTests.filter((t) => t.projectName === projectName && isTestInSerialBlock(t, serialBlock));
165
+ for (const test of testsInBlock) {
166
+ const line = formatTestListLine(test);
167
+ if (!addedEntries.has(line)) {
168
+ lines.push(line);
169
+ addedEntries.add(line);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ for (const test of failedTests) {
175
+ const isInSerialBlock = serialBlocks.some((s) => s.file === test.file);
176
+ if (!isInSerialBlock) {
177
+ const line = formatTestListLine(test);
178
+ if (!addedEntries.has(line)) {
179
+ lines.push(line);
180
+ addedEntries.add(line);
181
+ }
182
+ }
183
+ }
184
+ return lines.join("\n");
185
+ }
186
+ async function buildTestListFromFailedTestRun(runId, options = {}) {
187
+ const verbose = options.verbose ?? false;
188
+ const testRunResponse = await fetchTestRun(runId, {
189
+ verbose,
190
+ repoName: options.repoName,
191
+ });
192
+ if (!testRunResponse.data) {
193
+ throw new Error(testRunResponse.error?.message || "Failed to fetch test run");
194
+ }
195
+ const { testRun, project } = testRunResponse.data.test_run;
196
+ let summaryUrl = testRun.summary_url;
197
+ if (!summaryUrl && testRun.run_id && project.repo_name) {
198
+ summaryUrl = buildSummaryUrl(project.repo_name, testRun.run_id);
199
+ if (verbose) {
200
+ console.log(`Summary URL not in DB, built from run_id: ${summaryUrl}`);
201
+ }
202
+ }
203
+ if (verbose) {
204
+ console.log(`Summary URL: ${summaryUrl}`);
205
+ }
206
+ if (!summaryUrl) {
207
+ throw new Error("Test run does not have a summary URL and cannot be built (missing run_id)");
208
+ }
209
+ if (verbose) {
210
+ console.log(`Fetching report from: ${summaryUrl}`);
211
+ }
212
+ const report = await (0, reporter_1.fetchReport)(summaryUrl);
213
+ if (!report) {
214
+ throw new Error("Failed to fetch report from summary URL");
215
+ }
216
+ if (verbose) {
217
+ console.log(`Report has ${report.suites.length} top-level suites`);
218
+ }
219
+ const flattenedSpecs = (0, reporter_1.getFlattenedTestList)(report.suites);
220
+ const allTests = getAllTestsFromSpecs(flattenedSpecs);
221
+ const failedTests = getFailedTests(flattenedSpecs);
222
+ if (failedTests.length === 0) {
223
+ throw new Error("No failed tests found in the report");
224
+ }
225
+ if (verbose) {
226
+ console.log(`Found ${failedTests.length} failed tests, ${allTests.length} total tests`);
227
+ }
228
+ const serialBlocks = [];
229
+ if (options.repoPath) {
230
+ if (verbose) {
231
+ console.log(`Checking for serial blocks in repo: ${options.repoPath}`);
232
+ }
233
+ for (const test of failedTests) {
234
+ try {
235
+ const serialInfo = await getSerialBlockInfo(test, options.repoPath, verbose);
236
+ if (serialInfo) {
237
+ const alreadyAdded = serialBlocks.some((s) => s.file === serialInfo.file &&
238
+ s.serialDescribeName === serialInfo.serialDescribeName &&
239
+ s.isFileSerial === serialInfo.isFileSerial);
240
+ if (!alreadyAdded) {
241
+ serialBlocks.push(serialInfo);
242
+ }
243
+ }
244
+ }
245
+ catch (error) {
246
+ if (verbose) {
247
+ console.log(`Error checking serial block for ${test.title}: ${error}`);
248
+ }
249
+ }
250
+ }
251
+ if (verbose && serialBlocks.length > 0) {
252
+ console.log(`Found ${serialBlocks.length} serial blocks to expand`);
253
+ }
254
+ }
255
+ const testListContent = generateTestListContent(failedTests, allTests, serialBlocks);
256
+ const outputPath = options.outputPath || path_1.default.join(process.cwd(), `failed-tests-${runId}.txt`);
257
+ const outputDir = path_1.default.dirname(outputPath);
258
+ if (!fs_1.default.existsSync(outputDir)) {
259
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
260
+ }
261
+ fs_1.default.writeFileSync(outputPath, testListContent, "utf-8");
262
+ return {
263
+ failedTests,
264
+ testListContent,
265
+ outputPath,
266
+ };
267
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { parseTestListOutput } from "./stdout-parser";
5
5
  import { Platform, TestCase } from "./types";
6
6
  import { getProjectsFromPlaywrightConfig } from "./utils/config";
7
7
  export { getProjectsFromPlaywrightConfig, parseTestListOutput, Platform, runSpecificTestsCmd, spawnCmd, };
8
+ export { type BuildTestListOptions, type BuildTestListResult, buildTestListFromFailedTestRun, type FailedTest, } from "./failed-test-list";
8
9
  export * from "./glob-matcher";
9
10
  export { type CancellationWatcher, startCancellationWatcher, } from "./lib/cancellation-watcher";
10
11
  export { filterArrayByGlobMatchersSet, generateProjectFilters } from "./utils";
@@ -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;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,EACL,KAAK,mBAAmB,EACxB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,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"}
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,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,8BAA8B,EAC9B,KAAK,UAAU,GAChB,MAAM,oBAAoB,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EACL,KAAK,mBAAmB,EACxB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,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.startCancellationWatcher = exports.spawnCmd = exports.runSpecificTestsCmd = exports.Platform = exports.parseTestListOutput = exports.getProjectsFromPlaywrightConfig = void 0;
20
+ exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.startCancellationWatcher = exports.buildTestListFromFailedTestRun = 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"));
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "getProjectsFromPlaywrightConfig", { enumerable:
36
36
  // For test-run package, the library entrypoint, we only support web platform
37
37
  // The bin entrypoint has support for mobile also
38
38
  const supportedPlatform = types_1.Platform.WEB;
39
+ var failed_test_list_1 = require("./failed-test-list");
40
+ Object.defineProperty(exports, "buildTestListFromFailedTestRun", { enumerable: true, get: function () { return failed_test_list_1.buildTestListFromFailedTestRun; } });
39
41
  __exportStar(require("./glob-matcher"), exports);
40
42
  var cancellation_watcher_1 = require("./lib/cancellation-watcher");
41
43
  Object.defineProperty(exports, "startCancellationWatcher", { enumerable: true, get: function () { return cancellation_watcher_1.startCancellationWatcher; } });
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.startCancellationWatcher = startCancellationWatcher;
4
4
  const DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
5
- const DEFAULT_POLL_INTERVAL_MS = 5000;
5
+ const DEFAULT_POLL_INTERVAL_MS = 30_000;
6
6
  async function checkTestRunStatus(testRunId, apiKey) {
7
7
  const url = `${DOMAIN}/api/test-runs/${testRunId}/status`;
8
8
  try {
@@ -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":"AAcA,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;AA6BlE,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,CAsDf;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,CA8DhC"}