@rpcbase/test 0.308.0 → 0.310.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.d.ts +2 -0
- package/dist/clearDatabase.d.ts.map +1 -0
- package/dist/clearDatabase.js +12 -0
- package/dist/clearDatabase.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +785 -0
- package/dist/cli.js.map +1 -0
- package/dist/coverage/collect.d.ts +5 -0
- package/dist/coverage/collect.d.ts.map +1 -0
- package/dist/coverage/collect.js +113 -0
- package/dist/coverage/collect.js.map +1 -0
- package/dist/coverage/config-loader.d.ts +11 -0
- package/dist/coverage/config-loader.d.ts.map +1 -0
- package/dist/coverage/config-loader.js +292 -0
- package/dist/coverage/config-loader.js.map +1 -0
- package/dist/coverage/config.d.ts +3 -0
- package/dist/coverage/config.d.ts.map +1 -0
- package/dist/coverage/config.js +110 -0
- package/dist/coverage/config.js.map +1 -0
- package/dist/coverage/files.d.ts +4 -0
- package/dist/coverage/files.d.ts.map +1 -0
- package/dist/coverage/files.js +52 -0
- package/dist/coverage/files.js.map +1 -0
- package/dist/coverage/fixtures.d.ts +5 -0
- package/dist/coverage/fixtures.d.ts.map +1 -0
- package/dist/coverage/fixtures.js +42 -0
- package/dist/coverage/fixtures.js.map +1 -0
- package/dist/coverage/global-setup.d.ts +3 -0
- package/dist/coverage/global-setup.d.ts.map +1 -0
- package/dist/coverage/global-setup.js +18 -0
- package/dist/coverage/global-setup.js.map +1 -0
- package/dist/coverage/index.d.ts +10 -0
- package/dist/coverage/index.d.ts.map +1 -0
- package/dist/coverage/index.js +62 -0
- package/dist/coverage/index.js.map +1 -0
- package/dist/coverage/report.d.ts +7 -0
- package/dist/coverage/report.d.ts.map +1 -0
- package/{src → dist}/coverage/report.js +262 -362
- package/dist/coverage/report.js.map +1 -0
- package/dist/coverage/reporter.d.ts +16 -0
- package/dist/coverage/reporter.d.ts.map +1 -0
- package/dist/coverage/reporter.js +57 -0
- package/dist/coverage/reporter.js.map +1 -0
- package/dist/coverage/types.d.ts +47 -0
- package/dist/coverage/types.d.ts.map +1 -0
- package/dist/coverage/v8-tracker.d.ts +6 -0
- package/dist/coverage/v8-tracker.d.ts.map +1 -0
- package/dist/coverage/v8-tracker.js +166 -0
- package/dist/coverage/v8-tracker.js.map +1 -0
- package/dist/defineConfig.d.ts +3 -0
- package/dist/defineConfig.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/serverCoverage.d.ts +8 -0
- package/dist/serverCoverage.d.ts.map +1 -0
- package/dist/serverCoverage.js +42 -0
- package/dist/serverCoverage.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +83 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +25 -14
- package/index.d.ts +0 -64
- package/src/clearDatabase.js +0 -19
- package/src/cli.js +0 -948
- package/src/coverage/collect.js +0 -134
- package/src/coverage/config-loader.js +0 -202
- package/src/coverage/config.js +0 -134
- package/src/coverage/files.js +0 -55
- package/src/coverage/fixtures.js +0 -37
- package/src/coverage/global-setup.js +0 -19
- package/src/coverage/index.js +0 -30
- package/src/coverage/reporter.js +0 -65
- package/src/coverage/v8-tracker.js +0 -205
- package/src/defineConfig.js +0 -129
- package/src/index.js +0 -49
- package/src/vitest.config.mjs +0 -107
- /package/{src → dist}/register-tty.cjs +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, 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 { fileURLToPath } from "node:url";
|
|
8
|
+
import fg from "fast-glob";
|
|
9
|
+
import { createCoverageConfig } from "./coverage/config.js";
|
|
10
|
+
import { createCollectCoverageMatcher, resolveCollectCoverageRoots, isInsideAnyRoot } from "./coverage/collect.js";
|
|
11
|
+
import { loadCoverageOptions } from "./coverage/config-loader.js";
|
|
12
|
+
import { removeCoverageFiles } from "./coverage/files.js";
|
|
13
|
+
import { CoverageThresholdError, collectCoveredFiles, generateCoverageReport } from "./coverage/report.js";
|
|
14
|
+
const require$1 = createRequire(import.meta.url);
|
|
15
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const VITEST_COVERAGE_CANDIDATES = ["src/coverage.json"];
|
|
17
|
+
const COMBINED_COVERAGE_ENV_VAR = "RB_TEST_COMBINED_COVERAGE";
|
|
18
|
+
const isAider = process.env.IS_AIDER === "yes";
|
|
19
|
+
if (process.env.IS_AIDER !== void 0 && process.env.IS_AIDER !== "yes") {
|
|
20
|
+
console.warn("Warning: IS_AIDER is set to a value other than 'yes'.");
|
|
21
|
+
}
|
|
22
|
+
async function runTests() {
|
|
23
|
+
const userArgs = process.argv.slice(2);
|
|
24
|
+
const buildSpecsMap = userArgs.includes("--build-specs-map");
|
|
25
|
+
const auto = userArgs.includes("--auto");
|
|
26
|
+
const showMapping = userArgs.includes("--show-mapping");
|
|
27
|
+
const filteredArgs = userArgs.filter((arg) => arg !== "--build-specs-map" && arg !== "--auto" && arg !== "--show-mapping");
|
|
28
|
+
if (buildSpecsMap && auto) {
|
|
29
|
+
throw new Error("[rb-test] --auto cannot be combined with --build-specs-map");
|
|
30
|
+
}
|
|
31
|
+
if (showMapping && !auto) {
|
|
32
|
+
throw new Error("[rb-test] --show-mapping requires --auto");
|
|
33
|
+
}
|
|
34
|
+
const playwrightCoverage = await loadPlaywrightCoverageConfig();
|
|
35
|
+
const vitestCoverage = await loadVitestCoverageConfig();
|
|
36
|
+
const combinedCoverage = resolveCombinedCoverage(playwrightCoverage, vitestCoverage);
|
|
37
|
+
if (buildSpecsMap) {
|
|
38
|
+
await buildSpecsMapFromCoverage({
|
|
39
|
+
userArgs: filteredArgs,
|
|
40
|
+
combinedCoverage
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const shouldGenerateCoverageReport = combinedCoverage?.enabled && !auto;
|
|
45
|
+
if (shouldGenerateCoverageReport) {
|
|
46
|
+
await cleanCoverageArtifacts(combinedCoverage.config);
|
|
47
|
+
}
|
|
48
|
+
let testError = null;
|
|
49
|
+
try {
|
|
50
|
+
await runVitest(vitestCoverage, combinedCoverage?.config ?? null, { disableCoverage: auto });
|
|
51
|
+
console.log("\nRunning Playwright Tests...");
|
|
52
|
+
const playwrightArgs = auto ? await resolveAutoPlaywrightArgs({
|
|
53
|
+
userArgs: filteredArgs,
|
|
54
|
+
playwrightCoverage,
|
|
55
|
+
vitestCoverage,
|
|
56
|
+
showMapping
|
|
57
|
+
}) : userArgs;
|
|
58
|
+
if (playwrightArgs) {
|
|
59
|
+
await runPlaywright(playwrightArgs, { disableCoverage: auto });
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
testError = error;
|
|
63
|
+
}
|
|
64
|
+
if (shouldGenerateCoverageReport) {
|
|
65
|
+
try {
|
|
66
|
+
await finalizeCoverage(combinedCoverage.config);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!testError) {
|
|
69
|
+
testError = error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (testError) {
|
|
74
|
+
throw testError;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
runTests().then(() => process.exit(0)).catch((error) => {
|
|
78
|
+
if (!(error instanceof CoverageThresholdError)) {
|
|
79
|
+
console.error(error?.stack ?? String(error));
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
83
|
+
async function runVitest(coverage, combinedConfig, { disableCoverage = false } = {}) {
|
|
84
|
+
const vitestArgs = ["run", "--passWithNoTests"];
|
|
85
|
+
const vitestConfig = resolveVitestConfig();
|
|
86
|
+
if (vitestConfig) {
|
|
87
|
+
vitestArgs.push("--config", vitestConfig);
|
|
88
|
+
}
|
|
89
|
+
const launcher = resolveVitestLauncher();
|
|
90
|
+
const env = withRegisterShim(process.env);
|
|
91
|
+
if (disableCoverage) {
|
|
92
|
+
env.RB_DISABLE_COVERAGE = "1";
|
|
93
|
+
}
|
|
94
|
+
if (coverage?.enabled && !disableCoverage) {
|
|
95
|
+
env.NODE_V8_COVERAGE = resolveNodeCoverageDir(combinedConfig ?? coverage.config);
|
|
96
|
+
}
|
|
97
|
+
await spawnWithLogs({
|
|
98
|
+
name: "Vitest",
|
|
99
|
+
launcher,
|
|
100
|
+
args: vitestArgs,
|
|
101
|
+
env,
|
|
102
|
+
successMessage: "Vitest suite passed!",
|
|
103
|
+
failureMessage: "Vitest failed"
|
|
104
|
+
});
|
|
105
|
+
if (coverage?.enabled && !disableCoverage) {
|
|
106
|
+
await convertNodeCoverage({
|
|
107
|
+
config: combinedConfig ?? coverage.config,
|
|
108
|
+
nodeCoverageDir: resolveNodeCoverageDir(combinedConfig ?? coverage.config)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function buildSpecsMapFromCoverage({
|
|
113
|
+
userArgs,
|
|
114
|
+
combinedCoverage
|
|
115
|
+
}) {
|
|
116
|
+
if (!combinedCoverage?.enabled) {
|
|
117
|
+
throw new Error("[specs-map] Coverage must be enabled to build the specs map.");
|
|
118
|
+
}
|
|
119
|
+
const config = combinedCoverage.config;
|
|
120
|
+
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
|
121
|
+
const specSourceFiles = await findSpecSourceFiles(config.rootDir);
|
|
122
|
+
if (specSourceFiles.length === 0) {
|
|
123
|
+
throw new Error("[specs-map] No spec files found under spec/**/*.spec.ts");
|
|
124
|
+
}
|
|
125
|
+
const filesMapDir = path.join(config.testResultsRoot, "files-map");
|
|
126
|
+
await fs.rm(filesMapDir, { recursive: true, force: true });
|
|
127
|
+
await fs.mkdir(filesMapDir, { recursive: true });
|
|
128
|
+
for (const specSourceFile of specSourceFiles) {
|
|
129
|
+
const specProjectPath = path.relative(config.rootDir, specSourceFile);
|
|
130
|
+
const specWorkspacePath = toPosixPath(path.relative(workspaceRoot, specSourceFile));
|
|
131
|
+
const testFile = resolvePlaywrightSpecFile(specProjectPath);
|
|
132
|
+
console.log(`
|
|
133
|
+
[specs-map] Running ${specWorkspacePath}`);
|
|
134
|
+
await removeCoverageFiles(config);
|
|
135
|
+
let error = null;
|
|
136
|
+
let failed = false;
|
|
137
|
+
try {
|
|
138
|
+
await runPlaywright([...userArgs, testFile]);
|
|
139
|
+
} catch (runError) {
|
|
140
|
+
error = runError;
|
|
141
|
+
failed = true;
|
|
142
|
+
console.error(`[specs-map] Failed: ${specWorkspacePath}`);
|
|
143
|
+
console.error(runError?.stack ?? String(runError));
|
|
144
|
+
}
|
|
145
|
+
const coveredFiles = await collectCoveredFiles(config);
|
|
146
|
+
const impactedFiles = coveredFiles.map((filePath) => toPosixPath(path.relative(workspaceRoot, filePath))).filter((relativePath) => relativePath && !relativePath.startsWith("../") && relativePath !== "..").sort();
|
|
147
|
+
const outputFile = path.join(filesMapDir, `${specProjectPath}.json`);
|
|
148
|
+
await fs.mkdir(path.dirname(outputFile), { recursive: true });
|
|
149
|
+
await fs.writeFile(
|
|
150
|
+
outputFile,
|
|
151
|
+
JSON.stringify(
|
|
152
|
+
{
|
|
153
|
+
spec: specWorkspacePath,
|
|
154
|
+
files: impactedFiles,
|
|
155
|
+
failed
|
|
156
|
+
},
|
|
157
|
+
null,
|
|
158
|
+
2
|
|
159
|
+
),
|
|
160
|
+
"utf8"
|
|
161
|
+
);
|
|
162
|
+
if (failed) {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function resolveAutoPlaywrightArgs({
|
|
168
|
+
userArgs,
|
|
169
|
+
playwrightCoverage,
|
|
170
|
+
vitestCoverage,
|
|
171
|
+
showMapping = false
|
|
172
|
+
}) {
|
|
173
|
+
const config = playwrightCoverage?.config ?? vitestCoverage?.config ?? null;
|
|
174
|
+
if (!config) {
|
|
175
|
+
console.warn("[auto] Coverage config not found; running full Playwright suite.");
|
|
176
|
+
return userArgs;
|
|
177
|
+
}
|
|
178
|
+
const filesMapDir = path.join(config.testResultsRoot, "files-map");
|
|
179
|
+
const mapFiles = await findFilesMapJson(filesMapDir);
|
|
180
|
+
if (mapFiles.length === 0) {
|
|
181
|
+
console.warn("[auto] Specs map not found; running full Playwright suite.");
|
|
182
|
+
return userArgs;
|
|
183
|
+
}
|
|
184
|
+
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
|
185
|
+
const gitChanges = getGitChanges(workspaceRoot);
|
|
186
|
+
const renameMap = new Map(
|
|
187
|
+
gitChanges.filter((change) => change.kind === "rename").map((change) => [change.oldPath, change.newPath])
|
|
188
|
+
);
|
|
189
|
+
const specRootAbs = path.join(config.rootDir, "spec");
|
|
190
|
+
const matchesCollectCoverageFrom = createCollectCoverageMatcher(config.collectCoverageFrom, config.rootDir);
|
|
191
|
+
const directSpecChanges = /* @__PURE__ */ new Set();
|
|
192
|
+
const sourceChanges = [];
|
|
193
|
+
for (const change of gitChanges) {
|
|
194
|
+
if (change.kind === "rename") {
|
|
195
|
+
const oldAbs = path.join(workspaceRoot, change.oldPath);
|
|
196
|
+
const newAbs = path.join(workspaceRoot, change.newPath);
|
|
197
|
+
if (isSpecSourceFile(newAbs, specRootAbs) && fs$1.existsSync(newAbs)) {
|
|
198
|
+
directSpecChanges.add(change.newPath);
|
|
199
|
+
}
|
|
200
|
+
const oldMatches = matchesCollectCoverageFrom(oldAbs);
|
|
201
|
+
const newMatches = matchesCollectCoverageFrom(newAbs);
|
|
202
|
+
if (oldMatches || newMatches) {
|
|
203
|
+
sourceChanges.push(change);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const abs = path.join(workspaceRoot, change.path);
|
|
208
|
+
if (isSpecSourceFile(abs, specRootAbs) && fs$1.existsSync(abs)) {
|
|
209
|
+
directSpecChanges.add(change.path);
|
|
210
|
+
}
|
|
211
|
+
if (matchesCollectCoverageFrom(abs)) {
|
|
212
|
+
sourceChanges.push(change);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (directSpecChanges.size === 0 && sourceChanges.length === 0) {
|
|
216
|
+
console.log("[auto] No relevant git changes.");
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const parsedMaps = [];
|
|
220
|
+
for (const file of mapFiles) {
|
|
221
|
+
const json = await readJson(file);
|
|
222
|
+
if (!json) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (json.failed === true) {
|
|
226
|
+
console.warn("[auto] Specs map contains failed entries; running full Playwright suite.");
|
|
227
|
+
return userArgs;
|
|
228
|
+
}
|
|
229
|
+
const spec = typeof json?.spec === "string" ? json.spec : null;
|
|
230
|
+
if (!spec) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const files = Array.isArray(json?.files) ? json.files.filter((entry) => typeof entry === "string") : [];
|
|
234
|
+
parsedMaps.push({ spec, files });
|
|
235
|
+
}
|
|
236
|
+
if (parsedMaps.length === 0) {
|
|
237
|
+
console.warn("[auto] Specs map is empty; running full Playwright suite.");
|
|
238
|
+
return userArgs;
|
|
239
|
+
}
|
|
240
|
+
const specsByImpactedFile = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const entry of parsedMaps) {
|
|
242
|
+
const resolvedSpec = resolveRenamedPath(entry.spec, renameMap);
|
|
243
|
+
for (const file of entry.files) {
|
|
244
|
+
const list = specsByImpactedFile.get(file) ?? [];
|
|
245
|
+
list.push(resolvedSpec);
|
|
246
|
+
specsByImpactedFile.set(file, list);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const unmappedSourceChanges = sourceChanges.filter((change) => {
|
|
250
|
+
if (change.kind === "path") {
|
|
251
|
+
return !specsByImpactedFile.has(change.path);
|
|
252
|
+
}
|
|
253
|
+
return !specsByImpactedFile.has(change.oldPath) && !specsByImpactedFile.has(change.newPath);
|
|
254
|
+
});
|
|
255
|
+
if (unmappedSourceChanges.length > 0) {
|
|
256
|
+
console.warn("[auto] Unmapped source changes detected:");
|
|
257
|
+
for (const change of unmappedSourceChanges) {
|
|
258
|
+
if (change.kind === "path") {
|
|
259
|
+
console.warn(` - ${change.path}`);
|
|
260
|
+
} else {
|
|
261
|
+
console.warn(` - ${change.oldPath} -> ${change.newPath}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const selectedSpecs = new Set(directSpecChanges);
|
|
266
|
+
const triggersBySpec = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const spec of directSpecChanges) {
|
|
268
|
+
if (showMapping) {
|
|
269
|
+
triggersBySpec.set(spec, /* @__PURE__ */ new Set([spec]));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const change of sourceChanges) {
|
|
273
|
+
if (change.kind === "path") {
|
|
274
|
+
const specs = specsByImpactedFile.get(change.path) ?? [];
|
|
275
|
+
specs.forEach((spec) => selectedSpecs.add(spec));
|
|
276
|
+
if (showMapping) {
|
|
277
|
+
for (const spec of specs) {
|
|
278
|
+
const current = triggersBySpec.get(spec) ?? /* @__PURE__ */ new Set();
|
|
279
|
+
current.add(change.path);
|
|
280
|
+
triggersBySpec.set(spec, current);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const oldSpecs = specsByImpactedFile.get(change.oldPath) ?? [];
|
|
286
|
+
oldSpecs.forEach((spec) => selectedSpecs.add(spec));
|
|
287
|
+
const newSpecs = specsByImpactedFile.get(change.newPath) ?? [];
|
|
288
|
+
newSpecs.forEach((spec) => selectedSpecs.add(spec));
|
|
289
|
+
if (showMapping) {
|
|
290
|
+
const key = `${change.oldPath} -> ${change.newPath}`;
|
|
291
|
+
const allSpecs = /* @__PURE__ */ new Set([...oldSpecs, ...newSpecs]);
|
|
292
|
+
for (const spec of allSpecs) {
|
|
293
|
+
const current = triggersBySpec.get(spec) ?? /* @__PURE__ */ new Set();
|
|
294
|
+
current.add(key);
|
|
295
|
+
triggersBySpec.set(spec, current);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const missingSpecs = [];
|
|
300
|
+
const specsToRun = Array.from(selectedSpecs).filter((spec) => {
|
|
301
|
+
const abs = path.join(workspaceRoot, spec);
|
|
302
|
+
if (fs$1.existsSync(abs)) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
missingSpecs.push(spec);
|
|
306
|
+
return false;
|
|
307
|
+
}).sort();
|
|
308
|
+
if (missingSpecs.length > 0) {
|
|
309
|
+
console.warn(`[auto] Ignoring ${missingSpecs.length} missing spec file(s):`);
|
|
310
|
+
for (const spec of missingSpecs.sort()) {
|
|
311
|
+
console.warn(` - ${spec}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (specsToRun.length === 0) {
|
|
315
|
+
console.log("[auto] No impacted specs.");
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (showMapping) {
|
|
319
|
+
console.log("[auto] Mapping:");
|
|
320
|
+
for (const spec of specsToRun) {
|
|
321
|
+
const triggers = Array.from(triggersBySpec.get(spec) ?? []).sort();
|
|
322
|
+
if (triggers.length === 0) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
console.log(` - ${spec}`);
|
|
326
|
+
for (const trigger of triggers) {
|
|
327
|
+
console.log(` <- ${trigger}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const playwrightFiles = specsToRun.map((specWorkspacePath) => path.join(workspaceRoot, specWorkspacePath)).filter((specAbs) => isSubpath(specAbs, config.rootDir)).map((specAbs) => {
|
|
332
|
+
const specProjectPath = path.relative(config.rootDir, specAbs);
|
|
333
|
+
return resolvePlaywrightSpecFile(specProjectPath);
|
|
334
|
+
});
|
|
335
|
+
const totalSpecFiles = (await findSpecSourceFiles(config.rootDir)).length;
|
|
336
|
+
console.log(`[auto] Running ${playwrightFiles.length}/${totalSpecFiles} spec file(s).`);
|
|
337
|
+
return [...userArgs, ...playwrightFiles];
|
|
338
|
+
}
|
|
339
|
+
async function findFilesMapJson(filesMapDir) {
|
|
340
|
+
const patterns = ["spec/**/*.spec.ts.json", "spec/**/*.spec.tsx.json"];
|
|
341
|
+
const matches = await fg(patterns, { cwd: filesMapDir, absolute: true, onlyFiles: true }).catch(() => []);
|
|
342
|
+
return matches.sort();
|
|
343
|
+
}
|
|
344
|
+
function getGitChanges(workspaceRoot) {
|
|
345
|
+
const result = spawnSync("git", ["status", "--porcelain=1", "-z"], {
|
|
346
|
+
cwd: workspaceRoot,
|
|
347
|
+
encoding: "utf8"
|
|
348
|
+
});
|
|
349
|
+
if (result.status !== 0) {
|
|
350
|
+
throw new Error(`[auto] Failed to read git status: ${result.stderr || "unknown error"}`);
|
|
351
|
+
}
|
|
352
|
+
const tokens = String(result.stdout ?? "").split("\0").filter(Boolean);
|
|
353
|
+
const changes = [];
|
|
354
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
355
|
+
const record = tokens[i];
|
|
356
|
+
if (record.length < 4) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const status = record.slice(0, 2);
|
|
360
|
+
const pathPart = toPosixPath(record.slice(3));
|
|
361
|
+
if (isRenameOrCopyStatus(status)) {
|
|
362
|
+
const next = tokens[i + 1];
|
|
363
|
+
if (typeof next !== "string") {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
changes.push({
|
|
367
|
+
kind: "rename",
|
|
368
|
+
oldPath: pathPart,
|
|
369
|
+
newPath: toPosixPath(next)
|
|
370
|
+
});
|
|
371
|
+
i += 1;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
changes.push({
|
|
375
|
+
kind: "path",
|
|
376
|
+
path: pathPart
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return changes;
|
|
380
|
+
}
|
|
381
|
+
function isRenameOrCopyStatus(status) {
|
|
382
|
+
return status.includes("R") || status.includes("C");
|
|
383
|
+
}
|
|
384
|
+
function resolveRenamedPath(original, renameMap) {
|
|
385
|
+
let current = original;
|
|
386
|
+
const visited = /* @__PURE__ */ new Set();
|
|
387
|
+
while (!visited.has(current)) {
|
|
388
|
+
const next = renameMap.get(current);
|
|
389
|
+
if (!next) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
visited.add(current);
|
|
393
|
+
current = next;
|
|
394
|
+
}
|
|
395
|
+
return current;
|
|
396
|
+
}
|
|
397
|
+
function isSubpath(candidate, root) {
|
|
398
|
+
const relative = path.relative(root, candidate);
|
|
399
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
400
|
+
}
|
|
401
|
+
function isSpecSourceFile(absolutePath, specRootAbsolute) {
|
|
402
|
+
if (!isSubpath(absolutePath, specRootAbsolute)) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return absolutePath.endsWith(".spec.ts") || absolutePath.endsWith(".spec.tsx");
|
|
406
|
+
}
|
|
407
|
+
async function findSpecSourceFiles(projectRoot) {
|
|
408
|
+
const patterns = ["spec/**/*.spec.ts", "spec/**/*.spec.tsx"];
|
|
409
|
+
const matches = await fg(patterns, { cwd: projectRoot, absolute: true, onlyFiles: true });
|
|
410
|
+
return matches.sort();
|
|
411
|
+
}
|
|
412
|
+
function resolvePlaywrightSpecFile(specProjectPath) {
|
|
413
|
+
const buildSpecRoot = path.join(process.cwd(), "build", "spec");
|
|
414
|
+
const isBuildSpecProject = fs$1.existsSync(buildSpecRoot);
|
|
415
|
+
if (!isBuildSpecProject) {
|
|
416
|
+
return specProjectPath;
|
|
417
|
+
}
|
|
418
|
+
const builtCandidate = normalizeBuiltSpecPath(path.join("build", specProjectPath));
|
|
419
|
+
const builtAbsolute = path.resolve(process.cwd(), builtCandidate);
|
|
420
|
+
if (!fs$1.existsSync(builtAbsolute)) {
|
|
421
|
+
throw new Error(`[specs-map] Missing built spec file: ${builtCandidate}`);
|
|
422
|
+
}
|
|
423
|
+
return builtCandidate;
|
|
424
|
+
}
|
|
425
|
+
function normalizeBuiltSpecPath(filePath) {
|
|
426
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
|
|
427
|
+
return `${filePath.replace(/\.tsx?$/, "")}.js`;
|
|
428
|
+
}
|
|
429
|
+
return filePath;
|
|
430
|
+
}
|
|
431
|
+
function toPosixPath(input) {
|
|
432
|
+
return String(input ?? "").split(path.sep).join("/");
|
|
433
|
+
}
|
|
434
|
+
function findWorkspaceRoot(projectRoot) {
|
|
435
|
+
let dir = path.resolve(projectRoot);
|
|
436
|
+
while (true) {
|
|
437
|
+
const pkgPath = path.join(dir, "package.json");
|
|
438
|
+
try {
|
|
439
|
+
if (fs$1.existsSync(pkgPath)) {
|
|
440
|
+
const parsed = JSON.parse(fs$1.readFileSync(pkgPath, "utf8"));
|
|
441
|
+
if (parsed && typeof parsed === "object" && parsed.workspaces) {
|
|
442
|
+
return dir;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
const parent = path.dirname(dir);
|
|
448
|
+
if (parent === dir) {
|
|
449
|
+
return path.resolve(projectRoot);
|
|
450
|
+
}
|
|
451
|
+
dir = parent;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function runPlaywright(userArgs, { disableCoverage = false } = {}) {
|
|
455
|
+
const configPath = fs$1.existsSync(
|
|
456
|
+
path.join(process.cwd(), "playwright.config.ts")
|
|
457
|
+
) ? path.join(process.cwd(), "playwright.config.ts") : path.join(moduleDir, "playwright.config.ts");
|
|
458
|
+
const hasCustomConfig = userArgs.some((arg) => {
|
|
459
|
+
if (arg === "--config" || arg === "-c") {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
return arg.startsWith("--config=");
|
|
463
|
+
});
|
|
464
|
+
const playwrightArgs = ["test"];
|
|
465
|
+
if (!hasCustomConfig) {
|
|
466
|
+
playwrightArgs.push("--config", configPath);
|
|
467
|
+
}
|
|
468
|
+
playwrightArgs.push(...userArgs);
|
|
469
|
+
ensureJsxRuntimeShim(process.cwd());
|
|
470
|
+
const launcher = resolvePlaywrightLauncher();
|
|
471
|
+
const env = withRegisterShim(process.env);
|
|
472
|
+
env[COMBINED_COVERAGE_ENV_VAR] = "1";
|
|
473
|
+
if (disableCoverage) {
|
|
474
|
+
env.RB_DISABLE_COVERAGE = "1";
|
|
475
|
+
}
|
|
476
|
+
return spawnWithLogs({
|
|
477
|
+
name: "Playwright",
|
|
478
|
+
launcher,
|
|
479
|
+
args: playwrightArgs,
|
|
480
|
+
env,
|
|
481
|
+
successMessage: "Playwright suite passed!",
|
|
482
|
+
failureMessage: "Playwright failed"
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function resolvePlaywrightLauncher() {
|
|
486
|
+
const cliPath = resolveCliPath();
|
|
487
|
+
if (cliPath) {
|
|
488
|
+
return {
|
|
489
|
+
command: process.execPath,
|
|
490
|
+
args: [cliPath]
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const localBin = path.resolve(process.cwd(), "node_modules/.bin/playwright");
|
|
494
|
+
if (fs$1.existsSync(localBin)) {
|
|
495
|
+
return {
|
|
496
|
+
command: localBin,
|
|
497
|
+
args: []
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
command: "playwright",
|
|
502
|
+
args: []
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function resolveCliPath() {
|
|
506
|
+
const searchRoots = [process.cwd(), moduleDir];
|
|
507
|
+
for (const base of searchRoots) {
|
|
508
|
+
try {
|
|
509
|
+
const pkgPath = require$1.resolve("@playwright/test/package.json", { paths: [base] });
|
|
510
|
+
const cliPath = path.join(path.dirname(pkgPath), "cli.js");
|
|
511
|
+
if (fs$1.existsSync(cliPath)) {
|
|
512
|
+
return cliPath;
|
|
513
|
+
}
|
|
514
|
+
} catch (_error) {
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
function resolveVitestLauncher() {
|
|
520
|
+
const searchRoots = [process.cwd(), moduleDir];
|
|
521
|
+
for (const base of searchRoots) {
|
|
522
|
+
try {
|
|
523
|
+
const pkgPath = require$1.resolve("vitest/package.json", { paths: [base] });
|
|
524
|
+
const pkgDir = path.dirname(pkgPath);
|
|
525
|
+
const pkgJson = JSON.parse(fs$1.readFileSync(pkgPath, "utf8"));
|
|
526
|
+
const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.vitest;
|
|
527
|
+
if (binPath) {
|
|
528
|
+
return {
|
|
529
|
+
command: process.execPath,
|
|
530
|
+
args: [path.join(pkgDir, binPath)]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
} catch (_error) {
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const localBin = path.resolve(process.cwd(), "node_modules/.bin/vitest");
|
|
537
|
+
if (fs$1.existsSync(localBin)) {
|
|
538
|
+
return {
|
|
539
|
+
command: localBin,
|
|
540
|
+
args: []
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
command: "vitest",
|
|
545
|
+
args: []
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function resolveVitestConfig() {
|
|
549
|
+
const userConfig = findVitestConfig(process.cwd());
|
|
550
|
+
if (userConfig) {
|
|
551
|
+
return userConfig;
|
|
552
|
+
}
|
|
553
|
+
const bundledConfig = path.join(moduleDir, "vitest.config.js");
|
|
554
|
+
return fs$1.existsSync(bundledConfig) ? bundledConfig : null;
|
|
555
|
+
}
|
|
556
|
+
function findVitestConfig(baseDir) {
|
|
557
|
+
const candidates = [
|
|
558
|
+
"vitest.config.ts",
|
|
559
|
+
"vitest.config.js",
|
|
560
|
+
"vitest.config.mjs"
|
|
561
|
+
];
|
|
562
|
+
for (const file of candidates) {
|
|
563
|
+
const fullPath = path.join(baseDir, file);
|
|
564
|
+
if (fs$1.existsSync(fullPath)) {
|
|
565
|
+
return fullPath;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
async function loadVitestCoverageConfig() {
|
|
571
|
+
const options = await loadCoverageOptions({
|
|
572
|
+
optional: true,
|
|
573
|
+
candidates: VITEST_COVERAGE_CANDIDATES,
|
|
574
|
+
defaultTestResultsDir: "test-results-vitest"
|
|
575
|
+
});
|
|
576
|
+
if (!options) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const config = createCoverageConfig(options);
|
|
580
|
+
return {
|
|
581
|
+
config,
|
|
582
|
+
enabled: config.coverageEnabled
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
async function loadPlaywrightCoverageConfig() {
|
|
586
|
+
const options = await loadCoverageOptions({ optional: true });
|
|
587
|
+
if (!options) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
const config = createCoverageConfig(options);
|
|
591
|
+
return {
|
|
592
|
+
config,
|
|
593
|
+
enabled: config.coverageEnabled
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function resolveCombinedCoverage(playwrightCoverage, vitestCoverage) {
|
|
597
|
+
if (playwrightCoverage?.enabled) {
|
|
598
|
+
return playwrightCoverage;
|
|
599
|
+
}
|
|
600
|
+
if (vitestCoverage?.enabled) {
|
|
601
|
+
return vitestCoverage;
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
async function cleanCoverageArtifacts(config) {
|
|
606
|
+
await removeCoverageFiles(config);
|
|
607
|
+
await fs.rm(config.coverageReportDir, { recursive: true, force: true });
|
|
608
|
+
await fs.rm(path.join(config.testResultsRoot, "node-coverage"), { recursive: true, force: true });
|
|
609
|
+
}
|
|
610
|
+
function resolveNodeCoverageDir(config) {
|
|
611
|
+
return path.join(config.testResultsRoot, "node-coverage", "vitest");
|
|
612
|
+
}
|
|
613
|
+
async function convertNodeCoverage(coverage) {
|
|
614
|
+
const { config, nodeCoverageDir } = coverage;
|
|
615
|
+
const entries = await fs.readdir(nodeCoverageDir).catch(() => []);
|
|
616
|
+
const scripts = [];
|
|
617
|
+
const scriptRoots = resolveCollectCoverageRoots(config.collectCoverageFrom, config.rootDir);
|
|
618
|
+
for (const entry of entries) {
|
|
619
|
+
if (!entry.endsWith(".json")) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const fullPath = path.join(nodeCoverageDir, entry);
|
|
623
|
+
const payload = await readJson(fullPath);
|
|
624
|
+
const results = Array.isArray(payload?.result) ? payload.result : [];
|
|
625
|
+
for (const script of results) {
|
|
626
|
+
const normalized = normalizeNodeScriptUrl(script.url, config.rootDir);
|
|
627
|
+
if (!normalized) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (isNodeModulesPath(normalized.absolutePath)) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (!isInsideAnyRoot(normalized.absolutePath, scriptRoots)) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const source = await fs.readFile(normalized.absolutePath, "utf8").catch(() => "");
|
|
637
|
+
scripts.push({
|
|
638
|
+
absolutePath: normalized.absolutePath,
|
|
639
|
+
relativePath: normalized.relativePath,
|
|
640
|
+
source,
|
|
641
|
+
functions: script.functions ?? [],
|
|
642
|
+
url: script.url
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (scripts.length === 0) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const outDir = path.join(config.testResultsRoot, "vitest");
|
|
650
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
651
|
+
const outputFile = path.join(outDir, config.coverageFileName);
|
|
652
|
+
await fs.writeFile(outputFile, JSON.stringify({ testId: "vitest", scripts }, null, 2), "utf8");
|
|
653
|
+
}
|
|
654
|
+
async function finalizeCoverage(config) {
|
|
655
|
+
try {
|
|
656
|
+
await generateCoverageReport(config);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error instanceof CoverageThresholdError) {
|
|
659
|
+
console.error(error.message);
|
|
660
|
+
}
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function readJson(filePath) {
|
|
665
|
+
try {
|
|
666
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
667
|
+
return JSON.parse(raw);
|
|
668
|
+
} catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function normalizeNodeScriptUrl(rawUrl, rootDir) {
|
|
673
|
+
if (!rawUrl || rawUrl.startsWith("node:")) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
let absolutePath = null;
|
|
677
|
+
try {
|
|
678
|
+
if (rawUrl.startsWith("file://")) {
|
|
679
|
+
absolutePath = fileURLToPath(rawUrl);
|
|
680
|
+
}
|
|
681
|
+
} catch (_err) {
|
|
682
|
+
}
|
|
683
|
+
if (!absolutePath && path.isAbsolute(rawUrl)) {
|
|
684
|
+
absolutePath = rawUrl;
|
|
685
|
+
}
|
|
686
|
+
if (!absolutePath) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const normalized = path.normalize(absolutePath);
|
|
690
|
+
return {
|
|
691
|
+
absolutePath: normalized,
|
|
692
|
+
relativePath: path.relative(rootDir, normalized)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function isNodeModulesPath(filePath) {
|
|
696
|
+
return path.normalize(String(filePath ?? "")).split(path.sep).includes("node_modules");
|
|
697
|
+
}
|
|
698
|
+
function spawnWithLogs({
|
|
699
|
+
name,
|
|
700
|
+
launcher,
|
|
701
|
+
args,
|
|
702
|
+
env,
|
|
703
|
+
successMessage,
|
|
704
|
+
failureMessage
|
|
705
|
+
}) {
|
|
706
|
+
return new Promise((resolve, reject) => {
|
|
707
|
+
const stdoutBuffer = [];
|
|
708
|
+
const stderrBuffer = [];
|
|
709
|
+
const child = spawn(
|
|
710
|
+
launcher.command,
|
|
711
|
+
[...launcher.args || [], ...args],
|
|
712
|
+
{
|
|
713
|
+
shell: false,
|
|
714
|
+
env
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
child.stdout?.on("data", (data) => {
|
|
718
|
+
if (!isAider) {
|
|
719
|
+
process.stdout.write(data);
|
|
720
|
+
}
|
|
721
|
+
stdoutBuffer.push(data.toString());
|
|
722
|
+
});
|
|
723
|
+
child.stderr?.on("data", (data) => {
|
|
724
|
+
if (!isAider) {
|
|
725
|
+
process.stderr.write(data);
|
|
726
|
+
}
|
|
727
|
+
stderrBuffer.push(data.toString());
|
|
728
|
+
});
|
|
729
|
+
child.on("close", (code) => {
|
|
730
|
+
if (code === 0) {
|
|
731
|
+
if (successMessage) {
|
|
732
|
+
console.log(successMessage);
|
|
733
|
+
}
|
|
734
|
+
resolve();
|
|
735
|
+
} else {
|
|
736
|
+
console.error(failureMessage || `${name} failed:`);
|
|
737
|
+
if (isAider) {
|
|
738
|
+
if (stdoutBuffer.length > 0) {
|
|
739
|
+
console.error(stdoutBuffer.join(""));
|
|
740
|
+
}
|
|
741
|
+
if (stderrBuffer.length > 0) {
|
|
742
|
+
console.error(stderrBuffer.join(""));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
reject(new Error(`${name} failed with exit code: ${code}`));
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
child.on("error", (error) => {
|
|
749
|
+
console.error(`Error spawning ${name}:`, error);
|
|
750
|
+
reject(error);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
function withRegisterShim(baseEnv) {
|
|
755
|
+
const nodeOptions = appendNodeRequire(baseEnv.NODE_OPTIONS, path.join(moduleDir, "register-tty.cjs"));
|
|
756
|
+
return {
|
|
757
|
+
...baseEnv,
|
|
758
|
+
NODE_OPTIONS: nodeOptions
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function ensureJsxRuntimeShim(projectRoot) {
|
|
762
|
+
const shimDir = path.join(projectRoot, "node_modules", "playwright");
|
|
763
|
+
fs$1.mkdirSync(shimDir, { recursive: true });
|
|
764
|
+
const shims = [
|
|
765
|
+
{ file: "jsx-runtime.js", target: "react/jsx-runtime" },
|
|
766
|
+
{ file: "jsx-dev-runtime.js", target: "react/jsx-dev-runtime" }
|
|
767
|
+
];
|
|
768
|
+
for (const { file, target } of shims) {
|
|
769
|
+
const filePath = path.join(shimDir, file);
|
|
770
|
+
if (!fs$1.existsSync(filePath)) {
|
|
771
|
+
const content = `export * from "${target}";
|
|
772
|
+
export { default } from "${target}";
|
|
773
|
+
`;
|
|
774
|
+
fs$1.writeFileSync(filePath, content, "utf8");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function appendNodeRequire(existing, modulePath) {
|
|
779
|
+
const flag = `--require=${modulePath}`;
|
|
780
|
+
if (!existing || existing.length === 0) {
|
|
781
|
+
return flag;
|
|
782
|
+
}
|
|
783
|
+
return `${existing} ${flag}`;
|
|
784
|
+
}
|
|
785
|
+
//# sourceMappingURL=cli.js.map
|