@empiricalrun/test-run 0.15.0 → 0.16.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,18 @@
1
1
  # @empiricalrun/test-run
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [3a315aa]
8
+ - @empiricalrun/reporter@0.28.1
9
+
10
+ ## 0.16.0
11
+
12
+ ### Minor Changes
13
+
14
+ - ab403b8: fix: move json report location to avoid accidental agent commits
15
+
3
16
  ## 0.15.0
4
17
 
5
18
  ### Minor Changes
@@ -23,7 +23,7 @@ function registerFailedListCommand(program) {
23
23
  repoPath: options.repoPath,
24
24
  verbose: options.verbose,
25
25
  });
26
- console.log(`Found ${result.failedTests.length} failed tests`);
26
+ console.log(`Found ${result.testsToRun.length} failed tests`);
27
27
  console.log(`Test list written to: ${result.outputPath}`);
28
28
  }
29
29
  catch (error) {
@@ -1,9 +1,28 @@
1
- export interface FailedTest {
1
+ /**
2
+ * Normalize a test line for comparison between manifest and ran tests.
3
+ * Strips `:line:col` suffixes from file paths.
4
+ *
5
+ * e.g. `[chromium] › foo/bar.spec.ts:4:5 › has title`
6
+ * → `[chromium] › foo/bar.spec.ts › has title`
7
+ */
8
+ export declare function normalizeTestLine(line: string): string;
9
+ export declare function parseTestListLine(line: string): TestList | null;
10
+ /**
11
+ * Runs `npx playwright test --list <file>` and returns all test lines for the file.
12
+ */
13
+ export declare function getAllTestsForFile(file: string, repoPath: string): Promise<string[]>;
14
+ export interface TestList {
2
15
  projectName: string;
3
16
  file: string;
4
17
  title: string;
5
18
  suites: string[];
6
19
  }
20
+ export interface SerialBlockInfo {
21
+ file: string;
22
+ serialDescribeName: string | null;
23
+ isFileSerial: boolean;
24
+ }
25
+ export declare function getSerialBlockInfo(test: TestList, repoPath: string, verbose: boolean): Promise<SerialBlockInfo | null>;
7
26
  export interface BuildTestListOptions {
8
27
  outputPath?: string;
9
28
  verbose?: boolean;
@@ -11,9 +30,13 @@ export interface BuildTestListOptions {
11
30
  repoPath?: string;
12
31
  }
13
32
  export interface BuildTestListResult {
14
- failedTests: FailedTest[];
33
+ testsToRun: TestList[];
15
34
  testListContent: string;
16
35
  outputPath: string;
17
36
  }
37
+ export declare function buildTestList(testsToRun: TestList[], allTests: TestList[], options: BuildTestListOptions & {
38
+ verbose: boolean;
39
+ }): Promise<BuildTestListResult>;
18
40
  export declare function buildTestListFromFailedTestRun(runId: string, options?: BuildTestListOptions): Promise<BuildTestListResult>;
41
+ export declare function expandSerialDependencies(testListContent: string, outputPath: string, repoPath: string): Promise<string>;
19
42
  //# sourceMappingURL=failed-test-list.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"failed-test-list.d.ts","sourceRoot":"","sources":["../src/failed-test-list.ts"],"names":[],"mappings":"AAoBA,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,CAyG9B"}
1
+ {"version":3,"file":"failed-test-list.d.ts","sourceRoot":"","sources":["../src/failed-test-list.ts"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAgB/D;AAID;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,EAAE,CAAC,CAsBnB;AAED,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAmED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;CACvB;AAoBD,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA+CjC;AA0FD,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,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AA0DD,wBAAsB,aAAa,CACjC,UAAU,EAAE,QAAQ,EAAE,EACtB,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,EAAE,oBAAoB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACnD,OAAO,CAAC,mBAAmB,CAAC,CA0D9B;AAED,wBAAsB,8BAA8B,CAClD,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAa9B;AAED,wBAAsB,wBAAwB,CAC5C,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAmCjB"}
@@ -3,13 +3,65 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.normalizeTestLine = normalizeTestLine;
7
+ exports.parseTestListLine = parseTestListLine;
8
+ exports.getAllTestsForFile = getAllTestsForFile;
9
+ exports.getSerialBlockInfo = getSerialBlockInfo;
10
+ exports.buildTestList = buildTestList;
6
11
  exports.buildTestListFromFailedTestRun = buildTestListFromFailedTestRun;
12
+ exports.expandSerialDependencies = expandSerialDependencies;
13
+ const node_child_process_1 = require("node:child_process");
14
+ const node_util_1 = require("node:util");
7
15
  const reporter_1 = require("@empiricalrun/reporter");
8
16
  const fs_1 = __importDefault(require("fs"));
9
17
  const path_1 = __importDefault(require("path"));
10
18
  const utils_1 = require("./utils");
11
19
  const DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
12
20
  const SUITES_DELIMITER = " › ";
21
+ /**
22
+ * Normalize a test line for comparison between manifest and ran tests.
23
+ * Strips `:line:col` suffixes from file paths.
24
+ *
25
+ * e.g. `[chromium] › foo/bar.spec.ts:4:5 › has title`
26
+ * → `[chromium] › foo/bar.spec.ts › has title`
27
+ */
28
+ function normalizeTestLine(line) {
29
+ return line.replace(/(\.spec\.ts|\.test\.ts|\.ts|\.js):\d+:\d+/g, "$1");
30
+ }
31
+ function parseTestListLine(line) {
32
+ const trimmed = normalizeTestLine(line.trim());
33
+ if (!trimmed || trimmed.startsWith("#"))
34
+ return null;
35
+ const match = trimmed.match(/^\[(.*?)\]\s*[›>]\s*(.*)/);
36
+ if (!match?.[1] || !match[2])
37
+ return null;
38
+ const projectName = match[1];
39
+ const parts = match[2].split(/\s*[›>]\s*/);
40
+ if (parts.length < 2)
41
+ return null;
42
+ const file = parts[0].trim();
43
+ const suites = parts.slice(1, -1);
44
+ const title = parts[parts.length - 1].trim();
45
+ return { projectName, file, suites, title };
46
+ }
47
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
48
+ /**
49
+ * Runs `npx playwright test --list <file>` and returns all test lines for the file.
50
+ */
51
+ async function getAllTestsForFile(file, repoPath) {
52
+ try {
53
+ const playwrightCli = path_1.default.join(repoPath, "node_modules", "@playwright", "test", "cli.js");
54
+ const { stdout } = await execFileAsync("node", [playwrightCli, "test", "--list", file], { cwd: repoPath });
55
+ return stdout
56
+ .split("\n")
57
+ .map((line) => line.trim())
58
+ .filter((line) => line.includes(" › "));
59
+ }
60
+ catch (error) {
61
+ console.error(`Failed to list tests for file: ${file}`, error);
62
+ throw error;
63
+ }
64
+ }
13
65
  async function fetchTestRun(runId, options) {
14
66
  if (!process.env.EMPIRICALRUN_API_KEY) {
15
67
  throw new Error("EMPIRICALRUN_API_KEY environment variable is required");
@@ -45,8 +97,12 @@ function getFailedTests(specs) {
45
97
  const failedTests = [];
46
98
  for (const spec of specs) {
47
99
  for (const test of spec.tests) {
48
- const status = (0, reporter_1.deriveTestStatus)(test.results);
49
- if (status === "fail") {
100
+ const retries = test.results.map((r) => ({
101
+ status: r.status ?? "unknown",
102
+ expectedStatus: test.expectedStatus,
103
+ }));
104
+ const status = (0, reporter_1.getTestStatusFromRetries)(retries);
105
+ if (status === "failed") {
50
106
  const suites = spec.nesting.slice(1, -1);
51
107
  failedTests.push({
52
108
  projectName: test.projectName,
@@ -141,17 +197,12 @@ function isTestInSerialBlock(test, serialBlock) {
141
197
  }
142
198
  return false;
143
199
  }
144
- function generateTestListContent(failedTests, allTests, serialBlocks = []) {
145
- const lines = [
146
- `# Failed tests from test run`,
147
- `# Generated: ${new Date().toISOString()}`,
148
- `# Total failed tests: ${failedTests.length}`,
149
- "",
150
- ];
200
+ function generateTestListContent(testsToRun, allTests, serialBlocks = []) {
201
+ const lines = [];
151
202
  const addedEntries = new Set();
152
203
  for (const serialBlock of serialBlocks) {
153
204
  const projectNames = [
154
- ...new Set(failedTests
205
+ ...new Set(testsToRun
155
206
  .filter((t) => t.file === serialBlock.file)
156
207
  .map((t) => t.projectName)),
157
208
  ];
@@ -166,7 +217,7 @@ function generateTestListContent(failedTests, allTests, serialBlocks = []) {
166
217
  }
167
218
  }
168
219
  }
169
- for (const test of failedTests) {
220
+ for (const test of testsToRun) {
170
221
  const isInSerialBlock = serialBlocks.some((s) => s.file === test.file);
171
222
  if (!isInSerialBlock) {
172
223
  const line = formatTestListLine(test);
@@ -178,8 +229,8 @@ function generateTestListContent(failedTests, allTests, serialBlocks = []) {
178
229
  }
179
230
  return lines.join("\n");
180
231
  }
181
- async function buildTestListFromFailedTestRun(runId, options = {}) {
182
- const verbose = options.verbose ?? false;
232
+ async function fetchFailedTestList(runId, options) {
233
+ const { verbose } = options;
183
234
  const testRunResponse = await fetchTestRun(runId, {
184
235
  verbose,
185
236
  repoName: options.repoName,
@@ -214,12 +265,16 @@ async function buildTestListFromFailedTestRun(runId, options = {}) {
214
265
  if (verbose) {
215
266
  console.log(`Found ${failedTests.length} failed tests, ${allTests.length} total tests`);
216
267
  }
268
+ return { failedTests, allTests };
269
+ }
270
+ async function buildTestList(testsToRun, allTests, options) {
271
+ const { verbose } = options;
217
272
  const serialBlocks = [];
218
273
  if (options.repoPath) {
219
274
  if (verbose) {
220
275
  console.log(`Checking for serial blocks in repo: ${options.repoPath}`);
221
276
  }
222
- for (const test of failedTests) {
277
+ for (const test of testsToRun) {
223
278
  try {
224
279
  const serialInfo = await getSerialBlockInfo(test, options.repoPath, verbose);
225
280
  if (serialInfo) {
@@ -241,16 +296,59 @@ async function buildTestListFromFailedTestRun(runId, options = {}) {
241
296
  console.log(`Found ${serialBlocks.length} serial blocks to expand`);
242
297
  }
243
298
  }
244
- const testListContent = generateTestListContent(failedTests, allTests, serialBlocks);
245
- const outputPath = options.outputPath || path_1.default.join(process.cwd(), `failed-tests-${runId}.txt`);
299
+ const testListContent = generateTestListContent(testsToRun, allTests, serialBlocks);
300
+ const outputPath = options.outputPath || path_1.default.join(process.cwd(), "failed-tests.txt");
246
301
  const outputDir = path_1.default.dirname(outputPath);
247
302
  if (!fs_1.default.existsSync(outputDir)) {
248
303
  fs_1.default.mkdirSync(outputDir, { recursive: true });
249
304
  }
250
305
  fs_1.default.writeFileSync(outputPath, testListContent, "utf-8");
251
306
  return {
252
- failedTests,
307
+ testsToRun,
253
308
  testListContent,
254
309
  outputPath,
255
310
  };
256
311
  }
312
+ async function buildTestListFromFailedTestRun(runId, options = {}) {
313
+ const verbose = options.verbose ?? false;
314
+ const { failedTests, allTests } = await fetchFailedTestList(runId, {
315
+ verbose,
316
+ repoName: options.repoName,
317
+ });
318
+ return buildTestList(failedTests, allTests, {
319
+ ...options,
320
+ verbose,
321
+ outputPath: options.outputPath ||
322
+ path_1.default.join(process.cwd(), `failed-tests-${runId}.txt`),
323
+ });
324
+ }
325
+ async function expandSerialDependencies(testListContent, outputPath, repoPath) {
326
+ const lines = testListContent.split("\n");
327
+ const testsToRun = [];
328
+ for (const line of lines) {
329
+ const parsed = parseTestListLine(line);
330
+ if (parsed)
331
+ testsToRun.push(parsed);
332
+ }
333
+ if (testsToRun.length === 0) {
334
+ console.log("testListContent could not be parsed. Returning original tests");
335
+ fs_1.default.writeFileSync(outputPath, testListContent, "utf-8");
336
+ return testListContent;
337
+ }
338
+ const uniqueFiles = [...new Set(testsToRun.map((t) => t.file))];
339
+ const allTests = [];
340
+ for (const file of uniqueFiles) {
341
+ const allLines = await getAllTestsForFile(file, repoPath);
342
+ for (const line of allLines) {
343
+ const parsed = parseTestListLine(line);
344
+ if (parsed)
345
+ allTests.push(parsed);
346
+ }
347
+ }
348
+ const result = await buildTestList(testsToRun, allTests, {
349
+ repoPath,
350
+ outputPath,
351
+ verbose: false,
352
+ });
353
+ return result.testListContent;
354
+ }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ import { parseTestListOutput } from "./stdout-parser";
5
5
  import { TestCase } from "./types";
6
6
  import { getProjectsFromPlaywrightConfig } from "./utils/config";
7
7
  export { getProjectsFromPlaywrightConfig, parseTestListOutput, runSpecificTestsCmd, spawnCmd, };
8
- export { type BuildTestListOptions, type BuildTestListResult, buildTestListFromFailedTestRun, type FailedTest, } from "./failed-test-list";
8
+ export { type BuildTestListOptions, type BuildTestListResult, buildTestList, buildTestListFromFailedTestRun, expandSerialDependencies, getAllTestsForFile, normalizeTestLine, parseTestListLine, type TestList, } from "./failed-test-list";
9
9
  export * from "./glob-matcher";
10
10
  export { type CancellationWatcher, startCancellationWatcher, } from "./lib/cancellation-watcher";
11
11
  export { filterArrayByGlobMatchersSet, generateProjectFilters, generateProjectFiltersV2, } 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,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EACL,+BAA+B,EAC/B,mBAAmB,EACnB,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,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,SAAS,CAAC;AAcjB,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,CAmBD;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,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EACL,+BAA+B,EAC/B,mBAAmB,EACnB,mBAAmB,EACnB,QAAQ,GACT,CAAC;AACF,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,aAAa,EACb,8BAA8B,EAC9B,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,QAAQ,GACd,MAAM,oBAAoB,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EACL,KAAK,mBAAmB,EACxB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,SAAS,CAAC;AAQjB,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,CAmBD;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.generateProjectFiltersV2 = exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.startCancellationWatcher = exports.buildTestListFromFailedTestRun = exports.spawnCmd = exports.runSpecificTestsCmd = exports.parseTestListOutput = exports.getProjectsFromPlaywrightConfig = void 0;
20
+ exports.generateProjectFiltersV2 = exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.startCancellationWatcher = exports.parseTestListLine = exports.normalizeTestLine = exports.getAllTestsForFile = exports.expandSerialDependencies = exports.buildTestListFromFailedTestRun = exports.buildTestList = exports.spawnCmd = exports.runSpecificTestsCmd = exports.parseTestListOutput = exports.getProjectsFromPlaywrightConfig = void 0;
21
21
  exports.runSingleTest = runSingleTest;
22
22
  exports.listProjectsAndTests = listProjectsAndTests;
23
23
  const fs_1 = __importDefault(require("fs"));
@@ -31,7 +31,12 @@ Object.defineProperty(exports, "parseTestListOutput", { enumerable: true, get: f
31
31
  const config_1 = require("./utils/config");
32
32
  Object.defineProperty(exports, "getProjectsFromPlaywrightConfig", { enumerable: true, get: function () { return config_1.getProjectsFromPlaywrightConfig; } });
33
33
  var failed_test_list_1 = require("./failed-test-list");
34
+ Object.defineProperty(exports, "buildTestList", { enumerable: true, get: function () { return failed_test_list_1.buildTestList; } });
34
35
  Object.defineProperty(exports, "buildTestListFromFailedTestRun", { enumerable: true, get: function () { return failed_test_list_1.buildTestListFromFailedTestRun; } });
36
+ Object.defineProperty(exports, "expandSerialDependencies", { enumerable: true, get: function () { return failed_test_list_1.expandSerialDependencies; } });
37
+ Object.defineProperty(exports, "getAllTestsForFile", { enumerable: true, get: function () { return failed_test_list_1.getAllTestsForFile; } });
38
+ Object.defineProperty(exports, "normalizeTestLine", { enumerable: true, get: function () { return failed_test_list_1.normalizeTestLine; } });
39
+ Object.defineProperty(exports, "parseTestListLine", { enumerable: true, get: function () { return failed_test_list_1.parseTestListLine; } });
35
40
  __exportStar(require("./glob-matcher"), exports);
36
41
  var cancellation_watcher_1 = require("./lib/cancellation-watcher");
37
42
  Object.defineProperty(exports, "startCancellationWatcher", { enumerable: true, get: function () { return cancellation_watcher_1.startCancellationWatcher; } });
@@ -40,11 +45,9 @@ Object.defineProperty(exports, "filterArrayByGlobMatchersSet", { enumerable: tru
40
45
  Object.defineProperty(exports, "generateProjectFilters", { enumerable: true, get: function () { return utils_1.generateProjectFilters; } });
41
46
  Object.defineProperty(exports, "generateProjectFiltersV2", { enumerable: true, get: function () { return utils_1.generateProjectFiltersV2; } });
42
47
  function getSummaryJsonPath(repoDir) {
43
- const pathForPlaywright147 = path_1.default.join(repoDir, "playwright-report", "summary.json");
44
- const pathForOtherPlaywrightVersions = path_1.default.join(repoDir, "summary.json");
45
- return fs_1.default.existsSync(pathForPlaywright147)
46
- ? pathForPlaywright147
47
- : pathForOtherPlaywrightVersions;
48
+ const newPath = path_1.default.join(repoDir, "playwright-report", "summary.json");
49
+ const legacyPath = path_1.default.join(repoDir, "summary.json");
50
+ return fs_1.default.existsSync(newPath) ? newPath : legacyPath;
48
51
  }
49
52
  async function runSingleTest({ testName, suites, filePath, projects, envOverrides, repoDir, stdout, stderr, }) {
50
53
  const testDir = "tests";
package/dist/lib/cmd.js CHANGED
@@ -122,9 +122,16 @@ async function spawnCmd(command, args, options) {
122
122
  function setupProcessSignalHandlers(proc) {
123
123
  const handleSignal = async (signal) => {
124
124
  logger_1.logger.debug(`\nReceived ${signal}, gracefully shutting down...`);
125
- if (proc && !proc.killed) {
126
- // Forward the signal to the child process
127
- proc.kill(signal);
125
+ if (proc && !proc.killed && proc.pid) {
126
+ // Forward the signal to the entire process group so all children
127
+ // (including Playwright workers and reporters) receive it
128
+ try {
129
+ process.kill(-proc.pid, signal);
130
+ }
131
+ catch {
132
+ // Process group may have already exited
133
+ proc.kill(signal);
134
+ }
128
135
  // Wait for the child process to exit
129
136
  await new Promise((resolve) => {
130
137
  proc.once("exit", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/lib/merge-reports/html.ts"],"names":[],"mappings":"AAaA,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC,CAgIf"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/lib/merge-reports/html.ts"],"names":[],"mappings":"AAaA,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC,CAsIf"}
@@ -24,7 +24,11 @@ async function patchMergedHtmlReport(htmlFilePath, summaryJsonFilePath) {
24
24
  // Ensure attachment.name includes a file extension so that
25
25
  // Playwright's downloadFileNameForAttachment() uses it as-is
26
26
  // instead of parsing the (now URL) path for an extension.
27
- if (attachment.path && !attachment.name.includes(".")) {
27
+ // Exception: keep "trace" as-is because the HTML reporter checks
28
+ // `attachment.name === 'trace'` to show the "View Trace" button.
29
+ if (attachment.path &&
30
+ attachment.name !== "trace" &&
31
+ !attachment.name.includes(".")) {
28
32
  const ext = attachment.path.match(/\.[^/.]+$/)?.[0];
29
33
  if (ext) {
30
34
  return { ...attachment, name: attachment.name + ext };
@@ -1 +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;AA0DlE,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,CA+EhC"}
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;AA0DlE,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA+B/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,CA+EhC"}
@@ -75,7 +75,7 @@ async function runPlaywrightMergeReports(options) {
75
75
  envOverrides: {
76
76
  PLAYWRIGHT_HTML_OPEN: "never",
77
77
  PLAYWRIGHT_HTML_OUTPUT_DIR: outputDir,
78
- PLAYWRIGHT_JSON_OUTPUT_NAME: path_1.default.join(cwd, "summary.json"),
78
+ PLAYWRIGHT_JSON_OUTPUT_NAME: path_1.default.join(cwd, "playwright-report", "summary.json"),
79
79
  },
80
80
  captureOutput: false,
81
81
  throwOnError: true,
@@ -116,7 +116,7 @@ async function uploadMergedReports(cwd, outputDir, uploadOptions) {
116
116
  const { projectName, runId, baseUrl, uploadBucket, credentials } = uploadOptions;
117
117
  const destinationDir = path_1.default.join(projectName, runId);
118
118
  const htmlFilePath = path_1.default.join(outputDir, "index.html");
119
- const jsonFilePath = path_1.default.join(cwd, "summary.json");
119
+ const jsonFilePath = path_1.default.join(outputDir, "summary.json");
120
120
  if (fs_1.default.existsSync(htmlFilePath)) {
121
121
  logger_1.logger.debug(`[Merge Reports] Uploading HTML report`);
122
122
  const task = (0, r2_uploader_1.createUploadTask)({
@@ -132,7 +132,7 @@ async function uploadMergedReports(cwd, outputDir, uploadOptions) {
132
132
  if (fs_1.default.existsSync(jsonFilePath)) {
133
133
  logger_1.logger.debug(`[Merge Reports] Uploading summary.json`);
134
134
  const task = (0, r2_uploader_1.createUploadTask)({
135
- sourceDir: cwd,
135
+ sourceDir: outputDir,
136
136
  fileList: [jsonFilePath],
137
137
  destinationDir,
138
138
  uploadBucket,
@@ -193,7 +193,7 @@ async function mergeReports(options) {
193
193
  return { success: false };
194
194
  }
195
195
  const htmlFilePath = path_1.default.join(outputDir, "index.html");
196
- const jsonFilePath = path_1.default.join(cwd, "summary.json");
196
+ const jsonFilePath = path_1.default.join(outputDir, "summary.json");
197
197
  // Patch summary.json first (replaces local paths with URLs),
198
198
  // then patch the HTML report (reads the patched summary.json to build an attachment map).
199
199
  await (0, json_1.patchSummaryJson)(jsonFilePath, urlMappings);
@@ -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,MAAM,UAAU,CAAC;AAYlD,wBAAsB,mBAAmB,CAAC,EACxC,KAAU,EACV,QAAQ,EACR,eAAe,EACf,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,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,CAsFxB"}
1
+ {"version":3,"file":"run-specific-test.d.ts","sourceRoot":"","sources":["../../src/lib/run-specific-test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIlD,wBAAsB,mBAAmB,CAAC,EACxC,KAAU,EACV,QAAQ,EACR,eAAe,EACf,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,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,CAkFxB"}
@@ -1,10 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.runSpecificTestsCmd = runSpecificTestsCmd;
7
- const path_1 = __importDefault(require("path"));
4
+ const failed_test_list_1 = require("../failed-test-list");
8
5
  const utils_1 = require("../utils");
9
6
  const run_all_tests_1 = require("./run-all-tests");
10
7
  async function runSpecificTestsCmd({ tests = [], projects, passthroughArgs, envOverrides, repoDir, }) {
@@ -16,6 +13,7 @@ async function runSpecificTestsCmd({ tests = [], projects, passthroughArgs, envO
16
13
  for (const testCase of tests) {
17
14
  // TODO: Why do we have this getAllFilePaths call?
18
15
  // TODO: Can we remove `dir` from the test case entity?
16
+ // TODO: After 1.57 is present in all repo, replace grep with --test-list
19
17
  const files = await (0, utils_1.getAllFilePaths)(testCase.dir, repoDir, {
20
18
  filePath: testCase.filePath,
21
19
  });
@@ -43,24 +41,17 @@ async function runSpecificTestsCmd({ tests = [], projects, passthroughArgs, envO
43
41
  else {
44
42
  filesToRun.push(matchingFilePath);
45
43
  }
46
- const { testCaseNode } = await (0, utils_1.getTestCaseNode)({
47
- filePath: matchingFilePath,
48
- scenarioName: testCase.name,
49
- suites: testCase.suites,
50
- repoDir,
51
- });
52
- const isFileMarkedSerial = await (0, utils_1.hasTopLevelDescribeConfigureWithSerialMode)(path_1.default.join(repoDir, matchingFilePath));
53
- if (!isFileMarkedSerial && testCaseNode) {
54
- const parentDescribe = (0, utils_1.findFirstSerialDescribeBlock)(testCaseNode);
55
- if (!parentDescribe) {
56
- patternsToGrep.push(testCase.name);
57
- }
58
- else {
59
- const parentDescribeName = (0, utils_1.getDescribeBlockName)(parentDescribe);
60
- if (parentDescribeName) {
61
- patternsToGrep.push(parentDescribeName);
62
- }
63
- }
44
+ const serialInfo = await (0, failed_test_list_1.getSerialBlockInfo)({
45
+ file: matchingFilePath,
46
+ title: testCase.name,
47
+ suites: testCase.suites ?? [],
48
+ projectName: "",
49
+ }, repoDir, false);
50
+ if (!serialInfo) {
51
+ patternsToGrep.push(testCase.name);
52
+ }
53
+ else if (!serialInfo.isFileSerial && serialInfo.serialDescribeName) {
54
+ patternsToGrep.push(serialInfo.serialDescribeName);
64
55
  }
65
56
  }
66
57
  const teardownLabels = await (0, utils_1.labelTeardownProjects)(projects, repoDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-run",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -37,7 +37,7 @@
37
37
  "minimatch": "^10.0.1",
38
38
  "ts-morph": "^23.0.0",
39
39
  "@empiricalrun/r2-uploader": "^0.9.1",
40
- "@empiricalrun/reporter": "^0.28.0"
40
+ "@empiricalrun/reporter": "^0.28.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@playwright/test": "1.57.0",
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: ".",
5
+ projects: [{ name: "chromium" }],
6
+ });
@@ -0,0 +1,17 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test.describe("Serial Tests", () => {
4
+ test.describe.configure({ mode: "serial" });
5
+
6
+ test("Test 1 should pass", async () => {
7
+ expect(true).toBe(true);
8
+ });
9
+
10
+ test("Test 2 should pass", async () => {
11
+ expect(1 + 1).toBe(2);
12
+ });
13
+
14
+ test("Test 3 should fail", async () => {
15
+ expect(true).toBe(true);
16
+ });
17
+ });
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.ts"],
6
+ },
7
+ });