@rpcbase/test 0.343.0 → 0.344.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.
Files changed (40) hide show
  1. package/dist/clearDatabase.js +10 -12
  2. package/dist/clearDatabase.js.map +1 -1
  3. package/dist/cli.js +439 -556
  4. package/dist/cli.js.map +1 -1
  5. package/dist/coverage/collect.js +63 -101
  6. package/dist/coverage/collect.js.map +1 -1
  7. package/dist/coverage/config-loader.js +180 -230
  8. package/dist/coverage/config-loader.js.map +1 -1
  9. package/dist/coverage/config.js +76 -100
  10. package/dist/coverage/config.js.map +1 -1
  11. package/dist/coverage/console-text-report.js +175 -220
  12. package/dist/coverage/console-text-report.js.map +1 -1
  13. package/dist/coverage/files.js +45 -58
  14. package/dist/coverage/files.js.map +1 -1
  15. package/dist/coverage/fixtures.js +27 -38
  16. package/dist/coverage/fixtures.js.map +1 -1
  17. package/dist/coverage/global-setup.js +15 -18
  18. package/dist/coverage/global-setup.js.map +1 -1
  19. package/dist/coverage/index.js +38 -55
  20. package/dist/coverage/index.js.map +1 -1
  21. package/dist/coverage/report.js +341 -466
  22. package/dist/coverage/report.js.map +1 -1
  23. package/dist/coverage/reporter.js +47 -61
  24. package/dist/coverage/reporter.js.map +1 -1
  25. package/dist/coverage/v8-tracker.js +115 -147
  26. package/dist/coverage/v8-tracker.js.map +1 -1
  27. package/dist/index.js +46 -75
  28. package/dist/index.js.map +1 -1
  29. package/dist/runners/playwright.js +392 -490
  30. package/dist/runners/playwright.js.map +1 -1
  31. package/dist/runners/process.js +107 -142
  32. package/dist/runners/process.js.map +1 -1
  33. package/dist/runners/vitest.js +124 -171
  34. package/dist/runners/vitest.js.map +1 -1
  35. package/dist/serverCoverage.js +28 -42
  36. package/dist/serverCoverage.js.map +1 -1
  37. package/dist/vitest.config.d.ts +1 -1
  38. package/dist/vitest.config.js +62 -74
  39. package/dist/vitest.config.js.map +1 -1
  40. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,609 +1,492 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import fs$1 from "node:fs";
4
- import fs from "node:fs/promises";
5
- import path from "node:path";
6
- import { createRequire } from "node:module";
7
- import fg from "fast-glob";
8
- import { createCoverageConfig } from "./coverage/config.js";
9
- import { createCollectCoverageMatcher } from "./coverage/collect.js";
10
2
  import { loadCoverageOptions } from "./coverage/config-loader.js";
3
+ import { createCoverageConfig } from "./coverage/config.js";
11
4
  import { removeCoverageFiles } from "./coverage/files.js";
5
+ import { createCollectCoverageMatcher } from "./coverage/collect.js";
12
6
  import { CoverageThresholdError, collectCoveredFiles, generateCoverageReport } from "./coverage/report.js";
13
7
  import { runPlaywright } from "./runners/playwright.js";
14
- import { runVitest, resolveNodeCoverageDir } from "./runners/vitest.js";
15
- const require$1 = createRequire(import.meta.url);
16
- const shouldForceTty = !process.stdout.isTTY && process.env.FORCE_COLOR === "true";
17
- if (shouldForceTty) {
18
- require$1("./register-tty.cjs");
19
- }
20
- const VITEST_COVERAGE_CANDIDATES = ["src/coverage.json"];
21
- if (process.env.IS_AIDER !== void 0 && process.env.IS_AIDER !== "yes") {
22
- console.warn("Warning: IS_AIDER is set to a value other than 'yes'.");
23
- }
24
- const RB_CLI_OPTIONS = ["build-specs-map", "auto", "full", "show-mapping", "list"];
8
+ import { resolveNodeCoverageDir, runVitest } from "./runners/vitest.js";
9
+ import fsPromises from "node:fs/promises";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { createRequire } from "node:module";
13
+ import fg from "fast-glob";
14
+ import { spawnSync } from "node:child_process";
15
+ //#region src/cli.ts
16
+ var require = createRequire(import.meta.url);
17
+ if (!process.stdout.isTTY && process.env.FORCE_COLOR === "true") require("./register-tty.cjs");
18
+ var VITEST_COVERAGE_CANDIDATES = ["src/coverage.json"];
19
+ if (process.env.IS_AIDER !== void 0 && process.env.IS_AIDER !== "yes") console.warn("Warning: IS_AIDER is set to a value other than 'yes'.");
20
+ var RB_CLI_OPTIONS = [
21
+ "build-specs-map",
22
+ "auto",
23
+ "full",
24
+ "show-mapping",
25
+ "list"
26
+ ];
25
27
  function isRbCliOption(arg) {
26
- const normalized = arg.toLowerCase();
27
- return RB_CLI_OPTIONS.some((option) => normalized === `--${option}` || normalized.startsWith(`--${option}=`));
28
+ const normalized = arg.toLowerCase();
29
+ return RB_CLI_OPTIONS.some((option) => normalized === `--${option}` || normalized.startsWith(`--${option}=`));
28
30
  }
29
31
  function parseBooleanCliArg(rawArgs, option) {
30
- const optionPrefix = `--${option}`;
31
- const optionWithValuePrefix = `${optionPrefix}=`;
32
- return rawArgs.some((arg) => {
33
- if (arg === optionPrefix) {
34
- return true;
35
- }
36
- if (!arg.startsWith(optionWithValuePrefix)) {
37
- return false;
38
- }
39
- const value = arg.slice(optionWithValuePrefix.length).toLowerCase();
40
- if (value === "false" || value === "0") {
41
- return false;
42
- }
43
- return value.length > 0;
44
- });
32
+ const optionPrefix = `--${option}`;
33
+ const optionWithValuePrefix = `${optionPrefix}=`;
34
+ return rawArgs.some((arg) => {
35
+ if (arg === optionPrefix) return true;
36
+ if (!arg.startsWith(optionWithValuePrefix)) return false;
37
+ const value = arg.slice(optionWithValuePrefix.length).toLowerCase();
38
+ if (value === "false" || value === "0") return false;
39
+ return value.length > 0;
40
+ });
45
41
  }
46
42
  function parseCliArgs(rawArgs) {
47
- const yargs = require$1("yargs/yargs");
48
- const parsed = yargs(rawArgs).help(false).version(false).strict(false).exitProcess(false).parserConfiguration({
49
- "unknown-options-as-args": true,
50
- "populate--": true,
51
- "strip-dashed": true,
52
- "strip-aliased": true
53
- }).option("build-specs-map", {
54
- type: "boolean",
55
- default: false
56
- }).option("auto", {
57
- type: "boolean",
58
- default: false
59
- }).option("full", {
60
- type: "boolean",
61
- default: false
62
- }).option("show-mapping", {
63
- type: "boolean",
64
- default: false
65
- }).option("list", {
66
- type: "boolean",
67
- default: false
68
- }).parseSync();
69
- const passthroughArgs = [...Array.isArray(parsed._) ? parsed._ : [], ...Array.isArray(parsed["--"]) ? parsed["--"] : []].map((entry) => String(entry)).filter((arg) => !isRbCliOption(arg));
70
- return {
71
- buildSpecsMap: parseBooleanCliArg(rawArgs, "build-specs-map"),
72
- auto: parseBooleanCliArg(rawArgs, "auto") && !parseBooleanCliArg(rawArgs, "full"),
73
- showMapping: parseBooleanCliArg(rawArgs, "show-mapping"),
74
- list: parseBooleanCliArg(rawArgs, "list"),
75
- passthroughArgs
76
- };
43
+ const parsed = require("yargs/yargs")(rawArgs).help(false).version(false).strict(false).exitProcess(false).parserConfiguration({
44
+ "unknown-options-as-args": true,
45
+ "populate--": true,
46
+ "strip-dashed": true,
47
+ "strip-aliased": true
48
+ }).option("build-specs-map", {
49
+ type: "boolean",
50
+ default: false
51
+ }).option("auto", {
52
+ type: "boolean",
53
+ default: false
54
+ }).option("full", {
55
+ type: "boolean",
56
+ default: false
57
+ }).option("show-mapping", {
58
+ type: "boolean",
59
+ default: false
60
+ }).option("list", {
61
+ type: "boolean",
62
+ default: false
63
+ }).parseSync();
64
+ const passthroughArgs = [...Array.isArray(parsed._) ? parsed._ : [], ...Array.isArray(parsed["--"]) ? parsed["--"] : []].map((entry) => String(entry)).filter((arg) => !isRbCliOption(arg));
65
+ return {
66
+ buildSpecsMap: parseBooleanCliArg(rawArgs, "build-specs-map"),
67
+ auto: parseBooleanCliArg(rawArgs, "auto") && !parseBooleanCliArg(rawArgs, "full"),
68
+ showMapping: parseBooleanCliArg(rawArgs, "show-mapping"),
69
+ list: parseBooleanCliArg(rawArgs, "list"),
70
+ passthroughArgs
71
+ };
77
72
  }
78
73
  async function runTests() {
79
- const args = parseCliArgs(process.argv.slice(2));
80
- const buildSpecsMap = args.buildSpecsMap;
81
- const auto = args.auto && !buildSpecsMap;
82
- const showMapping = args.showMapping;
83
- const list = args.list;
84
- const filteredArgs = args.passthroughArgs;
85
- if (showMapping && !auto) {
86
- throw new Error("[rb-test] --show-mapping requires --auto");
87
- }
88
- const playwrightCoverage = await loadPlaywrightCoverageConfig();
89
- const vitestCoverage = await loadVitestCoverageConfig();
90
- const combinedCoverage = resolveCombinedCoverage(playwrightCoverage, vitestCoverage);
91
- if (buildSpecsMap) {
92
- await buildSpecsMapFromCoverage({
93
- userArgs: filteredArgs,
94
- combinedCoverage
95
- });
96
- return;
97
- }
98
- if (list) {
99
- if (auto) {
100
- await resolveAutoPlaywrightArgs({
101
- userArgs: filteredArgs,
102
- playwrightCoverage,
103
- vitestCoverage,
104
- showMapping,
105
- listOnly: true
106
- });
107
- } else {
108
- await listPlaywrightSpecFiles(combinedCoverage?.config.rootDir ?? process.cwd());
109
- }
110
- return;
111
- }
112
- const shouldGenerateCoverageReport = combinedCoverage?.enabled && !auto;
113
- if (shouldGenerateCoverageReport) {
114
- await cleanCoverageArtifacts(combinedCoverage.config);
115
- }
116
- let testError = null;
117
- try {
118
- await runVitest(vitestCoverage, combinedCoverage?.config ?? null, filteredArgs, {
119
- disableCoverage: auto
120
- });
121
- console.log("\nRunning Playwright Tests...");
122
- const playwrightArgs = auto ? await resolveAutoPlaywrightArgs({
123
- userArgs: filteredArgs,
124
- playwrightCoverage,
125
- vitestCoverage,
126
- showMapping
127
- }) : filteredArgs;
128
- if (playwrightArgs) {
129
- await runPlaywright(playwrightArgs, {
130
- disableCoverage: auto
131
- });
132
- }
133
- } catch (error) {
134
- testError = error;
135
- }
136
- if (shouldGenerateCoverageReport) {
137
- if (testError) {
138
- console.warn("[coverage] skipping report generation because tests failed");
139
- } else {
140
- try {
141
- await finalizeCoverage(combinedCoverage.config);
142
- } catch (error) {
143
- testError = error;
144
- }
145
- }
146
- }
147
- if (testError) {
148
- throw testError;
149
- }
74
+ const args = parseCliArgs(process.argv.slice(2));
75
+ const buildSpecsMap = args.buildSpecsMap;
76
+ const auto = args.auto && !buildSpecsMap;
77
+ const showMapping = args.showMapping;
78
+ const list = args.list;
79
+ const filteredArgs = args.passthroughArgs;
80
+ if (showMapping && !auto) throw new Error("[rb-test] --show-mapping requires --auto");
81
+ const playwrightCoverage = await loadPlaywrightCoverageConfig();
82
+ const vitestCoverage = await loadVitestCoverageConfig();
83
+ const combinedCoverage = resolveCombinedCoverage(playwrightCoverage, vitestCoverage);
84
+ if (buildSpecsMap) {
85
+ await buildSpecsMapFromCoverage({
86
+ userArgs: filteredArgs,
87
+ playwrightCoverage,
88
+ vitestCoverage,
89
+ combinedCoverage
90
+ });
91
+ return;
92
+ }
93
+ if (list) {
94
+ if (auto) await resolveAutoPlaywrightArgs({
95
+ userArgs: filteredArgs,
96
+ playwrightCoverage,
97
+ vitestCoverage,
98
+ showMapping,
99
+ listOnly: true
100
+ });
101
+ else await listPlaywrightSpecFiles(combinedCoverage?.config.rootDir ?? process.cwd());
102
+ return;
103
+ }
104
+ const shouldGenerateCoverageReport = combinedCoverage?.enabled && !auto;
105
+ if (shouldGenerateCoverageReport) await cleanCoverageArtifacts(combinedCoverage.config);
106
+ let testError = null;
107
+ try {
108
+ await runVitest(vitestCoverage, combinedCoverage?.config ?? null, filteredArgs, { disableCoverage: auto });
109
+ console.log("\nRunning Playwright Tests...");
110
+ const playwrightArgs = auto ? await resolveAutoPlaywrightArgs({
111
+ userArgs: filteredArgs,
112
+ playwrightCoverage,
113
+ vitestCoverage,
114
+ showMapping
115
+ }) : filteredArgs;
116
+ if (playwrightArgs) await runPlaywright(playwrightArgs, { disableCoverage: auto });
117
+ } catch (error) {
118
+ testError = error;
119
+ }
120
+ if (shouldGenerateCoverageReport) if (testError) console.warn("[coverage] skipping report generation because tests failed");
121
+ else try {
122
+ await finalizeCoverage(combinedCoverage.config);
123
+ } catch (error) {
124
+ testError = error;
125
+ }
126
+ if (testError) throw testError;
150
127
  }
151
128
  runTests().then(() => process.exit(0)).catch((error) => {
152
- if (!(error instanceof CoverageThresholdError)) {
153
- console.error(error?.stack ?? String(error));
154
- }
155
- process.exit(1);
129
+ if (!(error instanceof CoverageThresholdError)) console.error(error?.stack ?? String(error));
130
+ process.exit(1);
156
131
  });
157
- async function buildSpecsMapFromCoverage({
158
- userArgs,
159
- combinedCoverage
160
- }) {
161
- if (!combinedCoverage?.enabled) {
162
- throw new Error("[specs-map] Coverage must be enabled to build the specs map.");
163
- }
164
- const config = combinedCoverage.config;
165
- const workspaceRoot = findWorkspaceRoot(process.cwd());
166
- const specSourceFiles = await findSpecSourceFiles(config.rootDir);
167
- if (specSourceFiles.length === 0) {
168
- throw new Error("[specs-map] No spec files found under spec/**/*.spec{,.desktop,.mobile}.ts");
169
- }
170
- const filesMapDir = path.join(config.testResultsRoot, "files-map");
171
- await fs.rm(filesMapDir, {
172
- recursive: true,
173
- force: true
174
- });
175
- await fs.mkdir(filesMapDir, {
176
- recursive: true
177
- });
178
- for (const specSourceFile of specSourceFiles) {
179
- const specProjectPath = path.relative(config.rootDir, specSourceFile);
180
- const specWorkspacePath = toPosixPath(path.relative(workspaceRoot, specSourceFile));
181
- const testFile = resolvePlaywrightSpecFile(specProjectPath);
182
- console.log(`
183
- [specs-map] Running ${specWorkspacePath}`);
184
- await removeCoverageFiles(config);
185
- let error = null;
186
- let failed = false;
187
- try {
188
- await runPlaywright([...userArgs, testFile]);
189
- } catch (runError) {
190
- error = runError;
191
- failed = true;
192
- console.error(`[specs-map] Failed: ${specWorkspacePath}`);
193
- console.error(runError?.stack ?? String(runError));
194
- }
195
- const coveredFiles = await collectCoveredFiles(config);
196
- const impactedFiles = coveredFiles.map((filePath) => toPosixPath(path.relative(workspaceRoot, filePath))).filter((relativePath) => relativePath && !relativePath.startsWith("../") && relativePath !== "..").sort();
197
- const outputFile = path.join(filesMapDir, `${specProjectPath}.json`);
198
- await fs.mkdir(path.dirname(outputFile), {
199
- recursive: true
200
- });
201
- await fs.writeFile(outputFile, JSON.stringify({
202
- spec: specWorkspacePath,
203
- files: impactedFiles,
204
- failed
205
- }, null, 2), "utf8");
206
- if (failed) {
207
- throw error;
208
- }
209
- }
132
+ async function buildSpecsMapFromCoverage({ userArgs, combinedCoverage }) {
133
+ if (!combinedCoverage?.enabled) throw new Error("[specs-map] Coverage must be enabled to build the specs map.");
134
+ const config = combinedCoverage.config;
135
+ const workspaceRoot = findWorkspaceRoot(process.cwd());
136
+ const specSourceFiles = await findSpecSourceFiles(config.rootDir);
137
+ if (specSourceFiles.length === 0) throw new Error("[specs-map] No spec files found under spec/**/*.spec{,.desktop,.mobile}.ts");
138
+ const filesMapDir = path.join(config.testResultsRoot, "files-map");
139
+ await fsPromises.rm(filesMapDir, {
140
+ recursive: true,
141
+ force: true
142
+ });
143
+ await fsPromises.mkdir(filesMapDir, { recursive: true });
144
+ for (const specSourceFile of specSourceFiles) {
145
+ const specProjectPath = path.relative(config.rootDir, specSourceFile);
146
+ const specWorkspacePath = toPosixPath(path.relative(workspaceRoot, specSourceFile));
147
+ const testFile = resolvePlaywrightSpecFile(specProjectPath);
148
+ console.log(`\n[specs-map] Running ${specWorkspacePath}`);
149
+ await removeCoverageFiles(config);
150
+ let error = null;
151
+ let failed = false;
152
+ try {
153
+ await runPlaywright([...userArgs, testFile]);
154
+ } catch (runError) {
155
+ error = runError;
156
+ failed = true;
157
+ console.error(`[specs-map] Failed: ${specWorkspacePath}`);
158
+ console.error(runError?.stack ?? String(runError));
159
+ }
160
+ const impactedFiles = (await collectCoveredFiles(config)).map((filePath) => toPosixPath(path.relative(workspaceRoot, filePath))).filter((relativePath) => relativePath && !relativePath.startsWith("../") && relativePath !== "..").sort();
161
+ const outputFile = path.join(filesMapDir, `${specProjectPath}.json`);
162
+ await fsPromises.mkdir(path.dirname(outputFile), { recursive: true });
163
+ await fsPromises.writeFile(outputFile, JSON.stringify({
164
+ spec: specWorkspacePath,
165
+ files: impactedFiles,
166
+ failed
167
+ }, null, 2), "utf8");
168
+ if (failed) throw error;
169
+ }
210
170
  }
211
- async function resolveAutoPlaywrightArgs({
212
- userArgs,
213
- playwrightCoverage,
214
- vitestCoverage,
215
- showMapping = false,
216
- listOnly = false
217
- }) {
218
- const config = playwrightCoverage?.config ?? vitestCoverage?.config ?? null;
219
- if (!config) {
220
- console.warn("[auto] Coverage config not found; running full Playwright suite.");
221
- return userArgs;
222
- }
223
- const filesMapDir = path.join(config.testResultsRoot, "files-map");
224
- const mapFiles = await findFilesMapJson(filesMapDir);
225
- if (mapFiles.length === 0) {
226
- console.warn("[auto] Specs map not found; running full Playwright suite.");
227
- return userArgs;
228
- }
229
- const workspaceRoot = findWorkspaceRoot(process.cwd());
230
- const gitChanges = getGitChanges(workspaceRoot);
231
- const renameMap = new Map(gitChanges.filter((change) => change.kind === "rename").map((change) => [change.oldPath, change.newPath]));
232
- const specRootAbs = path.join(config.rootDir, "spec");
233
- const matchesCollectCoverageFrom = createCollectCoverageMatcher(config.collectCoverageFrom, config.rootDir);
234
- const directSpecChanges = /* @__PURE__ */ new Set();
235
- const sourceChanges = [];
236
- for (const change of gitChanges) {
237
- if (change.kind === "rename") {
238
- const oldAbs = path.join(workspaceRoot, change.oldPath);
239
- const newAbs = path.join(workspaceRoot, change.newPath);
240
- if (isSpecSourceFile(newAbs, specRootAbs) && fs$1.existsSync(newAbs)) {
241
- directSpecChanges.add(change.newPath);
242
- }
243
- const oldMatches = matchesCollectCoverageFrom(oldAbs);
244
- const newMatches = matchesCollectCoverageFrom(newAbs);
245
- if (oldMatches || newMatches) {
246
- sourceChanges.push(change);
247
- }
248
- continue;
249
- }
250
- const abs = path.join(workspaceRoot, change.path);
251
- if (isSpecSourceFile(abs, specRootAbs) && fs$1.existsSync(abs)) {
252
- directSpecChanges.add(change.path);
253
- }
254
- if (matchesCollectCoverageFrom(abs)) {
255
- sourceChanges.push(change);
256
- }
257
- }
258
- if (directSpecChanges.size === 0 && sourceChanges.length === 0) {
259
- if (listOnly) {
260
- console.log("[auto] No relevant git changes.");
261
- console.log("[list] No matched spec files.");
262
- return null;
263
- }
264
- console.warn("[auto] No relevant git changes; running full Playwright suite.");
265
- return userArgs;
266
- }
267
- const parsedMaps = [];
268
- for (const file of mapFiles) {
269
- const json = await readJson(file);
270
- if (!json) {
271
- continue;
272
- }
273
- if (json.failed === true) {
274
- console.warn("[auto] Specs map contains failed entries; running full Playwright suite.");
275
- return userArgs;
276
- }
277
- const spec = typeof json?.spec === "string" ? json.spec : null;
278
- if (!spec) {
279
- continue;
280
- }
281
- const files = Array.isArray(json?.files) ? json.files.filter((entry) => typeof entry === "string") : [];
282
- parsedMaps.push({
283
- spec,
284
- files
285
- });
286
- }
287
- if (parsedMaps.length === 0) {
288
- console.warn("[auto] Specs map is empty; running full Playwright suite.");
289
- return userArgs;
290
- }
291
- const specsByImpactedFile = /* @__PURE__ */ new Map();
292
- for (const entry of parsedMaps) {
293
- const resolvedSpec = resolveRenamedPath(entry.spec, renameMap);
294
- for (const file of entry.files) {
295
- const list = specsByImpactedFile.get(file) ?? [];
296
- list.push(resolvedSpec);
297
- specsByImpactedFile.set(file, list);
298
- }
299
- }
300
- const unmappedSourceChanges = sourceChanges.filter((change) => {
301
- if (change.kind === "path") {
302
- return !specsByImpactedFile.has(change.path);
303
- }
304
- return !specsByImpactedFile.has(change.oldPath) && !specsByImpactedFile.has(change.newPath);
305
- });
306
- if (unmappedSourceChanges.length > 0) {
307
- console.warn("[auto] Unmapped source changes detected:");
308
- for (const change of unmappedSourceChanges) {
309
- if (change.kind === "path") {
310
- console.warn(` - ${change.path}`);
311
- } else {
312
- console.warn(` - ${change.oldPath} -> ${change.newPath}`);
313
- }
314
- }
315
- }
316
- const selectedSpecs = new Set(directSpecChanges);
317
- const triggersBySpec = /* @__PURE__ */ new Map();
318
- for (const spec of directSpecChanges) {
319
- if (showMapping) {
320
- triggersBySpec.set(spec, /* @__PURE__ */ new Set([spec]));
321
- }
322
- }
323
- for (const change of sourceChanges) {
324
- if (change.kind === "path") {
325
- const specs = specsByImpactedFile.get(change.path) ?? [];
326
- specs.forEach((spec) => selectedSpecs.add(spec));
327
- if (showMapping) {
328
- for (const spec of specs) {
329
- const current = triggersBySpec.get(spec) ?? /* @__PURE__ */ new Set();
330
- current.add(change.path);
331
- triggersBySpec.set(spec, current);
332
- }
333
- }
334
- continue;
335
- }
336
- const oldSpecs = specsByImpactedFile.get(change.oldPath) ?? [];
337
- oldSpecs.forEach((spec) => selectedSpecs.add(spec));
338
- const newSpecs = specsByImpactedFile.get(change.newPath) ?? [];
339
- newSpecs.forEach((spec) => selectedSpecs.add(spec));
340
- if (showMapping) {
341
- const key = `${change.oldPath} -> ${change.newPath}`;
342
- const allSpecs = /* @__PURE__ */ new Set([...oldSpecs, ...newSpecs]);
343
- for (const spec of allSpecs) {
344
- const current = triggersBySpec.get(spec) ?? /* @__PURE__ */ new Set();
345
- current.add(key);
346
- triggersBySpec.set(spec, current);
347
- }
348
- }
349
- }
350
- const missingSpecs = [];
351
- const specsToRun = Array.from(selectedSpecs).filter((spec) => {
352
- const abs = path.join(workspaceRoot, spec);
353
- if (fs$1.existsSync(abs)) {
354
- return true;
355
- }
356
- missingSpecs.push(spec);
357
- return false;
358
- }).sort();
359
- if (missingSpecs.length > 0) {
360
- console.warn(`[auto] Ignoring ${missingSpecs.length} missing spec file(s):`);
361
- for (const spec of missingSpecs.sort()) {
362
- console.warn(` - ${spec}`);
363
- }
364
- }
365
- if (specsToRun.length === 0) {
366
- if (listOnly) {
367
- console.log("[list] No matched spec files.");
368
- return null;
369
- }
370
- console.log("[auto] No impacted specs.");
371
- return null;
372
- }
373
- if (showMapping) {
374
- console.log("[auto] Mapping:");
375
- for (const spec of specsToRun) {
376
- const triggers = Array.from(triggersBySpec.get(spec) ?? []).sort();
377
- if (triggers.length === 0) {
378
- continue;
379
- }
380
- console.log(` - ${spec}`);
381
- for (const trigger of triggers) {
382
- console.log(` <- ${trigger}`);
383
- }
384
- }
385
- }
386
- const playwrightFiles = specsToRun.map((specWorkspacePath) => path.join(workspaceRoot, specWorkspacePath)).filter((specAbs) => isSubpath(specAbs, config.rootDir)).map((specAbs) => {
387
- const specProjectPath = path.relative(config.rootDir, specAbs);
388
- return resolvePlaywrightSpecFile(specProjectPath);
389
- });
390
- if (playwrightFiles.length === 0) {
391
- if (listOnly) {
392
- console.log("[list] No matched spec files.");
393
- return null;
394
- }
395
- console.log("[auto] No impacted specs.");
396
- return null;
397
- }
398
- const totalSpecFiles = (await findSpecSourceFiles(config.rootDir)).length;
399
- if (listOnly) {
400
- console.log(`[auto] Selected ${playwrightFiles.length}/${totalSpecFiles} spec file(s):`);
401
- for (const playwrightFile of playwrightFiles) {
402
- console.log(` - ${playwrightFile}`);
403
- }
404
- return null;
405
- }
406
- console.log(`[auto] Running ${playwrightFiles.length}/${totalSpecFiles} spec file(s).`);
407
- return [...userArgs, ...playwrightFiles];
171
+ async function resolveAutoPlaywrightArgs({ userArgs, playwrightCoverage, vitestCoverage, showMapping = false, listOnly = false }) {
172
+ const config = playwrightCoverage?.config ?? vitestCoverage?.config ?? null;
173
+ if (!config) {
174
+ console.warn("[auto] Coverage config not found; running full Playwright suite.");
175
+ return userArgs;
176
+ }
177
+ const mapFiles = await findFilesMapJson(path.join(config.testResultsRoot, "files-map"));
178
+ if (mapFiles.length === 0) {
179
+ console.warn("[auto] Specs map not found; running full Playwright suite.");
180
+ return userArgs;
181
+ }
182
+ const workspaceRoot = findWorkspaceRoot(process.cwd());
183
+ const gitChanges = getGitChanges(workspaceRoot);
184
+ const renameMap = new Map(gitChanges.filter((change) => change.kind === "rename").map((change) => [change.oldPath, change.newPath]));
185
+ const specRootAbs = path.join(config.rootDir, "spec");
186
+ const matchesCollectCoverageFrom = createCollectCoverageMatcher(config.collectCoverageFrom, config.rootDir);
187
+ const directSpecChanges = /* @__PURE__ */ new Set();
188
+ const sourceChanges = [];
189
+ for (const change of gitChanges) {
190
+ if (change.kind === "rename") {
191
+ const oldAbs = path.join(workspaceRoot, change.oldPath);
192
+ const newAbs = path.join(workspaceRoot, change.newPath);
193
+ if (isSpecSourceFile(newAbs, specRootAbs) && fs.existsSync(newAbs)) directSpecChanges.add(change.newPath);
194
+ const oldMatches = matchesCollectCoverageFrom(oldAbs);
195
+ const newMatches = matchesCollectCoverageFrom(newAbs);
196
+ if (oldMatches || newMatches) sourceChanges.push(change);
197
+ continue;
198
+ }
199
+ const abs = path.join(workspaceRoot, change.path);
200
+ if (isSpecSourceFile(abs, specRootAbs) && fs.existsSync(abs)) directSpecChanges.add(change.path);
201
+ if (matchesCollectCoverageFrom(abs)) sourceChanges.push(change);
202
+ }
203
+ if (directSpecChanges.size === 0 && sourceChanges.length === 0) {
204
+ if (listOnly) {
205
+ console.log("[auto] No relevant git changes.");
206
+ console.log("[list] No matched spec files.");
207
+ return null;
208
+ }
209
+ console.warn("[auto] No relevant git changes; running full Playwright suite.");
210
+ return userArgs;
211
+ }
212
+ const parsedMaps = [];
213
+ for (const file of mapFiles) {
214
+ const json = await readJson(file);
215
+ if (!json) continue;
216
+ if (json.failed === true) {
217
+ console.warn("[auto] Specs map contains failed entries; running full Playwright suite.");
218
+ return userArgs;
219
+ }
220
+ const spec = typeof json?.spec === "string" ? json.spec : null;
221
+ if (!spec) continue;
222
+ const files = Array.isArray(json?.files) ? json.files.filter((entry) => typeof entry === "string") : [];
223
+ parsedMaps.push({
224
+ spec,
225
+ files
226
+ });
227
+ }
228
+ if (parsedMaps.length === 0) {
229
+ console.warn("[auto] Specs map is empty; running full Playwright suite.");
230
+ return userArgs;
231
+ }
232
+ const specsByImpactedFile = /* @__PURE__ */ new Map();
233
+ for (const entry of parsedMaps) {
234
+ const resolvedSpec = resolveRenamedPath(entry.spec, renameMap);
235
+ for (const file of entry.files) {
236
+ const list = specsByImpactedFile.get(file) ?? [];
237
+ list.push(resolvedSpec);
238
+ specsByImpactedFile.set(file, list);
239
+ }
240
+ }
241
+ const unmappedSourceChanges = sourceChanges.filter((change) => {
242
+ if (change.kind === "path") return !specsByImpactedFile.has(change.path);
243
+ return !specsByImpactedFile.has(change.oldPath) && !specsByImpactedFile.has(change.newPath);
244
+ });
245
+ if (unmappedSourceChanges.length > 0) {
246
+ console.warn("[auto] Unmapped source changes detected:");
247
+ for (const change of unmappedSourceChanges) if (change.kind === "path") console.warn(` - ${change.path}`);
248
+ else console.warn(` - ${change.oldPath} -> ${change.newPath}`);
249
+ }
250
+ const selectedSpecs = new Set(directSpecChanges);
251
+ const triggersBySpec = /* @__PURE__ */ new Map();
252
+ for (const spec of directSpecChanges) if (showMapping) triggersBySpec.set(spec, new Set([spec]));
253
+ for (const change of sourceChanges) {
254
+ if (change.kind === "path") {
255
+ const specs = specsByImpactedFile.get(change.path) ?? [];
256
+ specs.forEach((spec) => selectedSpecs.add(spec));
257
+ if (showMapping) for (const spec of specs) {
258
+ const current = triggersBySpec.get(spec) ?? /* @__PURE__ */ new Set();
259
+ current.add(change.path);
260
+ triggersBySpec.set(spec, current);
261
+ }
262
+ continue;
263
+ }
264
+ const oldSpecs = specsByImpactedFile.get(change.oldPath) ?? [];
265
+ oldSpecs.forEach((spec) => selectedSpecs.add(spec));
266
+ const newSpecs = specsByImpactedFile.get(change.newPath) ?? [];
267
+ newSpecs.forEach((spec) => selectedSpecs.add(spec));
268
+ if (showMapping) {
269
+ const key = `${change.oldPath} -> ${change.newPath}`;
270
+ const allSpecs = new Set([...oldSpecs, ...newSpecs]);
271
+ for (const spec of allSpecs) {
272
+ const current = triggersBySpec.get(spec) ?? /* @__PURE__ */ new Set();
273
+ current.add(key);
274
+ triggersBySpec.set(spec, current);
275
+ }
276
+ }
277
+ }
278
+ const missingSpecs = [];
279
+ const specsToRun = Array.from(selectedSpecs).filter((spec) => {
280
+ const abs = path.join(workspaceRoot, spec);
281
+ if (fs.existsSync(abs)) return true;
282
+ missingSpecs.push(spec);
283
+ return false;
284
+ }).sort();
285
+ if (missingSpecs.length > 0) {
286
+ console.warn(`[auto] Ignoring ${missingSpecs.length} missing spec file(s):`);
287
+ for (const spec of missingSpecs.sort()) console.warn(` - ${spec}`);
288
+ }
289
+ if (specsToRun.length === 0) {
290
+ if (listOnly) {
291
+ console.log("[list] No matched spec files.");
292
+ return null;
293
+ }
294
+ console.log("[auto] No impacted specs.");
295
+ return null;
296
+ }
297
+ if (showMapping) {
298
+ console.log("[auto] Mapping:");
299
+ for (const spec of specsToRun) {
300
+ const triggers = Array.from(triggersBySpec.get(spec) ?? []).sort();
301
+ if (triggers.length === 0) continue;
302
+ console.log(` - ${spec}`);
303
+ for (const trigger of triggers) console.log(` <- ${trigger}`);
304
+ }
305
+ }
306
+ const playwrightFiles = specsToRun.map((specWorkspacePath) => path.join(workspaceRoot, specWorkspacePath)).filter((specAbs) => isSubpath(specAbs, config.rootDir)).map((specAbs) => {
307
+ return resolvePlaywrightSpecFile(path.relative(config.rootDir, specAbs));
308
+ });
309
+ if (playwrightFiles.length === 0) {
310
+ if (listOnly) {
311
+ console.log("[list] No matched spec files.");
312
+ return null;
313
+ }
314
+ console.log("[auto] No impacted specs.");
315
+ return null;
316
+ }
317
+ const totalSpecFiles = (await findSpecSourceFiles(config.rootDir)).length;
318
+ if (listOnly) {
319
+ console.log(`[auto] Selected ${playwrightFiles.length}/${totalSpecFiles} spec file(s):`);
320
+ for (const playwrightFile of playwrightFiles) console.log(` - ${playwrightFile}`);
321
+ return null;
322
+ }
323
+ console.log(`[auto] Running ${playwrightFiles.length}/${totalSpecFiles} spec file(s).`);
324
+ return [...userArgs, ...playwrightFiles];
408
325
  }
409
326
  async function findFilesMapJson(filesMapDir) {
410
- const patterns = ["spec/**/*.spec{,.desktop,.mobile}.ts.json", "spec/**/*.spec{,.desktop,.mobile}.tsx.json"];
411
- const matches = await fg(patterns, {
412
- cwd: filesMapDir,
413
- absolute: true,
414
- onlyFiles: true
415
- }).catch(() => []);
416
- return matches.sort();
327
+ return (await fg(["spec/**/*.spec{,.desktop,.mobile}.ts.json", "spec/**/*.spec{,.desktop,.mobile}.tsx.json"], {
328
+ cwd: filesMapDir,
329
+ absolute: true,
330
+ onlyFiles: true
331
+ }).catch(() => [])).sort();
417
332
  }
418
333
  async function listPlaywrightSpecFiles(projectRoot) {
419
- const specSourceFiles = await findSpecSourceFiles(projectRoot);
420
- if (specSourceFiles.length === 0) {
421
- console.log("[list] No spec files found.");
422
- return;
423
- }
424
- const playwrightFiles = specSourceFiles.map((specSourceFile) => path.relative(projectRoot, specSourceFile)).map((specProjectPath) => resolvePlaywrightSpecFile(specProjectPath));
425
- console.log(`[list] Selected ${playwrightFiles.length} spec file(s):`);
426
- for (const playwrightFile of playwrightFiles) {
427
- console.log(` - ${playwrightFile}`);
428
- }
334
+ const specSourceFiles = await findSpecSourceFiles(projectRoot);
335
+ if (specSourceFiles.length === 0) {
336
+ console.log("[list] No spec files found.");
337
+ return;
338
+ }
339
+ const playwrightFiles = specSourceFiles.map((specSourceFile) => path.relative(projectRoot, specSourceFile)).map((specProjectPath) => resolvePlaywrightSpecFile(specProjectPath));
340
+ console.log(`[list] Selected ${playwrightFiles.length} spec file(s):`);
341
+ for (const playwrightFile of playwrightFiles) console.log(` - ${playwrightFile}`);
429
342
  }
430
343
  function getGitChanges(workspaceRoot) {
431
- const result = spawnSync("git", ["status", "--porcelain=1", "-z"], {
432
- cwd: workspaceRoot,
433
- encoding: "utf8"
434
- });
435
- if (result.status !== 0) {
436
- throw new Error(`[auto] Failed to read git status: ${result.stderr || "unknown error"}`);
437
- }
438
- const tokens = String(result.stdout ?? "").split("\0").filter(Boolean);
439
- const changes = [];
440
- for (let i = 0; i < tokens.length; i += 1) {
441
- const record = tokens[i];
442
- if (record.length < 4) {
443
- continue;
444
- }
445
- const status = record.slice(0, 2);
446
- const pathPart = toPosixPath(record.slice(3));
447
- if (isRenameOrCopyStatus(status)) {
448
- const next = tokens[i + 1];
449
- if (typeof next !== "string") {
450
- continue;
451
- }
452
- changes.push({
453
- kind: "rename",
454
- oldPath: pathPart,
455
- newPath: toPosixPath(next)
456
- });
457
- i += 1;
458
- continue;
459
- }
460
- changes.push({
461
- kind: "path",
462
- path: pathPart
463
- });
464
- }
465
- return changes;
344
+ const result = spawnSync("git", [
345
+ "status",
346
+ "--porcelain=1",
347
+ "-z"
348
+ ], {
349
+ cwd: workspaceRoot,
350
+ encoding: "utf8"
351
+ });
352
+ if (result.status !== 0) throw new Error(`[auto] Failed to read git status: ${result.stderr || "unknown error"}`);
353
+ const tokens = String(result.stdout ?? "").split("\0").filter(Boolean);
354
+ const changes = [];
355
+ for (let i = 0; i < tokens.length; i += 1) {
356
+ const record = tokens[i];
357
+ if (record.length < 4) continue;
358
+ const status = record.slice(0, 2);
359
+ const pathPart = toPosixPath(record.slice(3));
360
+ if (isRenameOrCopyStatus(status)) {
361
+ const next = tokens[i + 1];
362
+ if (typeof next !== "string") continue;
363
+ changes.push({
364
+ kind: "rename",
365
+ oldPath: pathPart,
366
+ newPath: toPosixPath(next)
367
+ });
368
+ i += 1;
369
+ continue;
370
+ }
371
+ changes.push({
372
+ kind: "path",
373
+ path: pathPart
374
+ });
375
+ }
376
+ return changes;
466
377
  }
467
378
  function isRenameOrCopyStatus(status) {
468
- return status.includes("R") || status.includes("C");
379
+ return status.includes("R") || status.includes("C");
469
380
  }
470
381
  function resolveRenamedPath(original, renameMap) {
471
- let current = original;
472
- const visited = /* @__PURE__ */ new Set();
473
- while (!visited.has(current)) {
474
- const next = renameMap.get(current);
475
- if (!next) {
476
- break;
477
- }
478
- visited.add(current);
479
- current = next;
480
- }
481
- return current;
382
+ let current = original;
383
+ const visited = /* @__PURE__ */ new Set();
384
+ while (!visited.has(current)) {
385
+ const next = renameMap.get(current);
386
+ if (!next) break;
387
+ visited.add(current);
388
+ current = next;
389
+ }
390
+ return current;
482
391
  }
483
392
  function isSubpath(candidate, root) {
484
- const relative = path.relative(root, candidate);
485
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
393
+ const relative = path.relative(root, candidate);
394
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
486
395
  }
487
396
  function isSpecSourceFile(absolutePath, specRootAbsolute) {
488
- if (!isSubpath(absolutePath, specRootAbsolute)) {
489
- return false;
490
- }
491
- return /\.spec(?:\.(?:desktop|mobile))?\.tsx?$/.test(absolutePath);
397
+ if (!isSubpath(absolutePath, specRootAbsolute)) return false;
398
+ return /\.spec(?:\.(?:desktop|mobile))?\.tsx?$/.test(absolutePath);
492
399
  }
493
400
  async function findSpecSourceFiles(projectRoot) {
494
- const patterns = ["spec/**/*.spec{,.desktop,.mobile}.ts", "spec/**/*.spec{,.desktop,.mobile}.tsx"];
495
- const matches = await fg(patterns, {
496
- cwd: projectRoot,
497
- absolute: true,
498
- onlyFiles: true
499
- });
500
- return matches.sort();
401
+ return (await fg(["spec/**/*.spec{,.desktop,.mobile}.ts", "spec/**/*.spec{,.desktop,.mobile}.tsx"], {
402
+ cwd: projectRoot,
403
+ absolute: true,
404
+ onlyFiles: true
405
+ })).sort();
501
406
  }
502
407
  function resolvePlaywrightSpecFile(specProjectPath) {
503
- const buildSpecRoot = path.join(process.cwd(), "build", "spec");
504
- const isBuildSpecProject = fs$1.existsSync(buildSpecRoot);
505
- if (!isBuildSpecProject) {
506
- return specProjectPath;
507
- }
508
- const builtCandidate = normalizeBuiltSpecPath(path.join("build", specProjectPath));
509
- const builtAbsolute = path.resolve(process.cwd(), builtCandidate);
510
- if (!fs$1.existsSync(builtAbsolute)) {
511
- throw new Error(`[specs-map] Missing built spec file: ${builtCandidate}`);
512
- }
513
- return builtCandidate;
408
+ const buildSpecRoot = path.join(process.cwd(), "build", "spec");
409
+ if (!fs.existsSync(buildSpecRoot)) return specProjectPath;
410
+ const builtCandidate = normalizeBuiltSpecPath(path.join("build", specProjectPath));
411
+ const builtAbsolute = path.resolve(process.cwd(), builtCandidate);
412
+ if (!fs.existsSync(builtAbsolute)) throw new Error(`[specs-map] Missing built spec file: ${builtCandidate}`);
413
+ return builtCandidate;
514
414
  }
515
415
  function normalizeBuiltSpecPath(filePath) {
516
- if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
517
- return `${filePath.replace(/\.tsx?$/, "")}.js`;
518
- }
519
- return filePath;
416
+ if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return `${filePath.replace(/\.tsx?$/, "")}.js`;
417
+ return filePath;
520
418
  }
521
419
  function toPosixPath(input) {
522
- return String(input ?? "").split(path.sep).join("/");
420
+ return String(input ?? "").split(path.sep).join("/");
523
421
  }
524
422
  function findWorkspaceRoot(projectRoot) {
525
- let dir = path.resolve(projectRoot);
526
- while (true) {
527
- const pkgPath = path.join(dir, "package.json");
528
- try {
529
- if (fs$1.existsSync(pkgPath)) {
530
- const parsed = JSON.parse(fs$1.readFileSync(pkgPath, "utf8"));
531
- if (parsed && typeof parsed === "object" && parsed.workspaces) {
532
- return dir;
533
- }
534
- }
535
- } catch {
536
- }
537
- const parent = path.dirname(dir);
538
- if (parent === dir) {
539
- return path.resolve(projectRoot);
540
- }
541
- dir = parent;
542
- }
423
+ let dir = path.resolve(projectRoot);
424
+ while (true) {
425
+ const pkgPath = path.join(dir, "package.json");
426
+ try {
427
+ if (fs.existsSync(pkgPath)) {
428
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
429
+ if (parsed && typeof parsed === "object" && parsed.workspaces) return dir;
430
+ }
431
+ } catch {}
432
+ const parent = path.dirname(dir);
433
+ if (parent === dir) return path.resolve(projectRoot);
434
+ dir = parent;
435
+ }
543
436
  }
544
437
  async function loadVitestCoverageConfig() {
545
- const options = await loadCoverageOptions({
546
- optional: true,
547
- candidates: VITEST_COVERAGE_CANDIDATES
548
- });
549
- if (!options) {
550
- return null;
551
- }
552
- const config = createCoverageConfig(options);
553
- return {
554
- config,
555
- enabled: config.coverageEnabled
556
- };
438
+ const options = await loadCoverageOptions({
439
+ optional: true,
440
+ candidates: VITEST_COVERAGE_CANDIDATES
441
+ });
442
+ if (!options) return null;
443
+ const config = createCoverageConfig(options);
444
+ return {
445
+ config,
446
+ enabled: config.coverageEnabled
447
+ };
557
448
  }
558
449
  async function loadPlaywrightCoverageConfig() {
559
- const options = await loadCoverageOptions({
560
- optional: true
561
- });
562
- if (!options) {
563
- return null;
564
- }
565
- const config = createCoverageConfig(options);
566
- return {
567
- config,
568
- enabled: config.coverageEnabled
569
- };
450
+ const options = await loadCoverageOptions({ optional: true });
451
+ if (!options) return null;
452
+ const config = createCoverageConfig(options);
453
+ return {
454
+ config,
455
+ enabled: config.coverageEnabled
456
+ };
570
457
  }
571
458
  function resolveCombinedCoverage(playwrightCoverage, vitestCoverage) {
572
- if (playwrightCoverage?.enabled) {
573
- return playwrightCoverage;
574
- }
575
- if (vitestCoverage?.enabled) {
576
- return vitestCoverage;
577
- }
578
- return null;
459
+ if (playwrightCoverage?.enabled) return playwrightCoverage;
460
+ if (vitestCoverage?.enabled) return vitestCoverage;
461
+ return null;
579
462
  }
580
463
  async function cleanCoverageArtifacts(config) {
581
- await removeCoverageFiles(config);
582
- await fs.rm(config.coverageReportDir, {
583
- recursive: true,
584
- force: true
585
- });
586
- await fs.rm(resolveNodeCoverageDir(config), {
587
- recursive: true,
588
- force: true
589
- });
464
+ await removeCoverageFiles(config);
465
+ await fsPromises.rm(config.coverageReportDir, {
466
+ recursive: true,
467
+ force: true
468
+ });
469
+ await fsPromises.rm(resolveNodeCoverageDir(config), {
470
+ recursive: true,
471
+ force: true
472
+ });
590
473
  }
591
474
  async function finalizeCoverage(config) {
592
- try {
593
- await generateCoverageReport(config);
594
- } catch (error) {
595
- if (error instanceof CoverageThresholdError) {
596
- console.error(error.message);
597
- }
598
- throw error;
599
- }
475
+ try {
476
+ await generateCoverageReport(config);
477
+ } catch (error) {
478
+ if (error instanceof CoverageThresholdError) console.error(error.message);
479
+ throw error;
480
+ }
600
481
  }
601
482
  async function readJson(filePath) {
602
- try {
603
- const raw = await fs.readFile(filePath, "utf8");
604
- return JSON.parse(raw);
605
- } catch {
606
- return null;
607
- }
483
+ try {
484
+ const raw = await fsPromises.readFile(filePath, "utf8");
485
+ return JSON.parse(raw);
486
+ } catch {
487
+ return null;
488
+ }
608
489
  }
609
- //# sourceMappingURL=cli.js.map
490
+ //#endregion
491
+
492
+ //# sourceMappingURL=cli.js.map