@rpcbase/test 0.357.0 → 0.358.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clearDatabase.js +12 -10
- package/dist/clearDatabase.js.map +1 -1
- package/dist/cli.js +556 -439
- package/dist/cli.js.map +1 -1
- package/dist/coverage/collect.js +101 -63
- package/dist/coverage/collect.js.map +1 -1
- package/dist/coverage/config-loader.js +230 -180
- package/dist/coverage/config-loader.js.map +1 -1
- package/dist/coverage/config.js +100 -76
- package/dist/coverage/config.js.map +1 -1
- package/dist/coverage/console-text-report.js +220 -175
- package/dist/coverage/console-text-report.js.map +1 -1
- package/dist/coverage/files.js +58 -45
- package/dist/coverage/files.js.map +1 -1
- package/dist/coverage/fixtures.js +38 -27
- package/dist/coverage/fixtures.js.map +1 -1
- package/dist/coverage/global-setup.js +18 -15
- package/dist/coverage/global-setup.js.map +1 -1
- package/dist/coverage/index.js +55 -38
- package/dist/coverage/index.js.map +1 -1
- package/dist/coverage/report.js +466 -341
- package/dist/coverage/report.js.map +1 -1
- package/dist/coverage/reporter.js +60 -47
- package/dist/coverage/reporter.js.map +1 -1
- package/dist/coverage/v8-tracker.js +147 -115
- package/dist/coverage/v8-tracker.js.map +1 -1
- package/dist/index.js +75 -46
- package/dist/index.js.map +1 -1
- package/dist/runners/playwright.js +563 -438
- package/dist/runners/playwright.js.map +1 -1
- package/dist/runners/process.d.ts.map +1 -1
- package/dist/runners/process.js +183 -125
- package/dist/runners/process.js.map +1 -1
- package/dist/runners/vitest.js +171 -124
- package/dist/runners/vitest.js.map +1 -1
- package/dist/serverCoverage.js +42 -28
- package/dist/serverCoverage.js.map +1 -1
- package/dist/vitest.config.d.ts +1 -1
- package/dist/vitest.config.js +74 -62
- package/dist/vitest.config.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,492 +1,609 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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";
|
|
3
8
|
import { createCoverageConfig } from "./coverage/config.js";
|
|
4
|
-
import { removeCoverageFiles } from "./coverage/files.js";
|
|
5
9
|
import { createCollectCoverageMatcher } from "./coverage/collect.js";
|
|
10
|
+
import { loadCoverageOptions } from "./coverage/config-loader.js";
|
|
11
|
+
import { removeCoverageFiles } from "./coverage/files.js";
|
|
6
12
|
import { CoverageThresholdError, collectCoveredFiles, generateCoverageReport } from "./coverage/report.js";
|
|
7
13
|
import { runPlaywright } from "./runners/playwright.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
];
|
|
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"];
|
|
27
25
|
function isRbCliOption(arg) {
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const normalized = arg.toLowerCase();
|
|
27
|
+
return RB_CLI_OPTIONS.some((option) => normalized === `--${option}` || normalized.startsWith(`--${option}=`));
|
|
30
28
|
}
|
|
31
29
|
function parseBooleanCliArg(rawArgs, option) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
});
|
|
41
45
|
}
|
|
42
46
|
function parseCliArgs(rawArgs) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
};
|
|
72
77
|
}
|
|
73
78
|
async function runTests() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
150
|
}
|
|
128
151
|
runTests().then(() => process.exit(0)).catch((error) => {
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
if (!(error instanceof CoverageThresholdError)) {
|
|
153
|
+
console.error(error?.stack ?? String(error));
|
|
154
|
+
}
|
|
155
|
+
process.exit(1);
|
|
131
156
|
});
|
|
132
|
-
async function buildSpecsMapFromCoverage({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
210
|
}
|
|
171
|
-
async function resolveAutoPlaywrightArgs({
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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];
|
|
325
408
|
}
|
|
326
409
|
async function findFilesMapJson(filesMapDir) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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();
|
|
332
417
|
}
|
|
333
418
|
async function listPlaywrightSpecFiles(projectRoot) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
429
|
}
|
|
343
430
|
function getGitChanges(workspaceRoot) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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;
|
|
377
466
|
}
|
|
378
467
|
function isRenameOrCopyStatus(status) {
|
|
379
|
-
|
|
468
|
+
return status.includes("R") || status.includes("C");
|
|
380
469
|
}
|
|
381
470
|
function resolveRenamedPath(original, renameMap) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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;
|
|
391
482
|
}
|
|
392
483
|
function isSubpath(candidate, root) {
|
|
393
|
-
|
|
394
|
-
|
|
484
|
+
const relative = path.relative(root, candidate);
|
|
485
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
395
486
|
}
|
|
396
487
|
function isSpecSourceFile(absolutePath, specRootAbsolute) {
|
|
397
|
-
|
|
398
|
-
|
|
488
|
+
if (!isSubpath(absolutePath, specRootAbsolute)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
return /\.spec(?:\.(?:desktop|mobile))?\.tsx?$/.test(absolutePath);
|
|
399
492
|
}
|
|
400
493
|
async function findSpecSourceFiles(projectRoot) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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();
|
|
406
501
|
}
|
|
407
502
|
function resolvePlaywrightSpecFile(specProjectPath) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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;
|
|
414
514
|
}
|
|
415
515
|
function normalizeBuiltSpecPath(filePath) {
|
|
416
|
-
|
|
417
|
-
|
|
516
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
|
|
517
|
+
return `${filePath.replace(/\.tsx?$/, "")}.js`;
|
|
518
|
+
}
|
|
519
|
+
return filePath;
|
|
418
520
|
}
|
|
419
521
|
function toPosixPath(input) {
|
|
420
|
-
|
|
522
|
+
return String(input ?? "").split(path.sep).join("/");
|
|
421
523
|
}
|
|
422
524
|
function findWorkspaceRoot(projectRoot) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
}
|
|
436
543
|
}
|
|
437
544
|
async function loadVitestCoverageConfig() {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
};
|
|
448
557
|
}
|
|
449
558
|
async function loadPlaywrightCoverageConfig() {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
};
|
|
457
570
|
}
|
|
458
571
|
function resolveCombinedCoverage(playwrightCoverage, vitestCoverage) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
572
|
+
if (playwrightCoverage?.enabled) {
|
|
573
|
+
return playwrightCoverage;
|
|
574
|
+
}
|
|
575
|
+
if (vitestCoverage?.enabled) {
|
|
576
|
+
return vitestCoverage;
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
462
579
|
}
|
|
463
580
|
async function cleanCoverageArtifacts(config) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
+
});
|
|
473
590
|
}
|
|
474
591
|
async function finalizeCoverage(config) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
592
|
+
try {
|
|
593
|
+
await generateCoverageReport(config);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (error instanceof CoverageThresholdError) {
|
|
596
|
+
console.error(error.message);
|
|
597
|
+
}
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
481
600
|
}
|
|
482
601
|
async function readJson(filePath) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
602
|
+
try {
|
|
603
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
604
|
+
return JSON.parse(raw);
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
489
608
|
}
|
|
490
|
-
//#
|
|
491
|
-
|
|
492
|
-
//# sourceMappingURL=cli.js.map
|
|
609
|
+
//# sourceMappingURL=cli.js.map
|