@test-station/adapter-node-test 0.1.7 → 0.2.10
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/README.md +30 -1
- package/package.json +1 -1
- package/src/index.js +221 -8
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install --save-dev @test-station/adapter-node-test
|
|
|
14
14
|
|
|
15
15
|
- runs `node --test` suites and normalizes the output into the shared report model
|
|
16
16
|
- injects Node's built-in `--test-reporter` flag for NDJSON capture
|
|
17
|
-
- supports coverage
|
|
17
|
+
- supports coverage for direct `node --test ...` commands and supported package-script wrappers
|
|
18
18
|
- writes raw NDJSON artifacts under `raw/`
|
|
19
19
|
|
|
20
20
|
## Direct Use
|
|
@@ -26,3 +26,32 @@ const adapter = createNodeTestAdapter();
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Use adapter id `node-test` in `test-station.config.mjs`.
|
|
29
|
+
|
|
30
|
+
## Supported Coverage Patterns
|
|
31
|
+
|
|
32
|
+
Coverage collection works when the adapter can safely resolve the executed command to a single `node --test ...` invocation. Supported patterns are:
|
|
33
|
+
|
|
34
|
+
- direct commands such as `['node', '--test', 'tests/**/*.test.js']`
|
|
35
|
+
- package scripts invoked with `yarn`, `npm run`, or `pnpm run` when the script itself resolves directly to `node --test ...`
|
|
36
|
+
- explicit `suite.coverage.command` values that resolve to one of the patterns above
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
{
|
|
42
|
+
adapter: 'node-test',
|
|
43
|
+
command: ['yarn', 'test:runtime'],
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
{
|
|
49
|
+
adapter: 'node-test',
|
|
50
|
+
command: ['yarn', 'test:runtime'],
|
|
51
|
+
coverage: {
|
|
52
|
+
command: ['node', '--test', 'tests/runtime/**/*.test.js'],
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Shell-heavy wrappers such as chained commands (`&&`, `||`, pipes, redirections) are intentionally not treated as coverage-safe.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -14,10 +14,12 @@ export function createNodeTestAdapter() {
|
|
|
14
14
|
phase: 3,
|
|
15
15
|
async run({ project, suite, execution }) {
|
|
16
16
|
const commandSpec = parseCommandSpec(suite.command);
|
|
17
|
-
const
|
|
18
|
-
const primaryExecution = await executeNodeTestRun(commandSpec, {
|
|
17
|
+
const resolvedCommand = resolveNodeTestCommand(commandSpec, {
|
|
19
18
|
cwd: suite.cwd || project.rootDir,
|
|
20
|
-
|
|
19
|
+
});
|
|
20
|
+
const primaryExecution = await executeNodeTestRun(resolvedCommand.commandSpec, {
|
|
21
|
+
cwd: suite.cwd || project.rootDir,
|
|
22
|
+
suiteEnv: mergeEnvRecords(suite.env, resolvedCommand.env),
|
|
21
23
|
rawRelativePath: `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-node.ndjson`,
|
|
22
24
|
});
|
|
23
25
|
const parsed = parseNodeEvents(parseNdjson(primaryExecution.stdout), suite.cwd || project.rootDir);
|
|
@@ -26,13 +28,20 @@ export function createNodeTestAdapter() {
|
|
|
26
28
|
let coverageArtifact = null;
|
|
27
29
|
|
|
28
30
|
if (execution?.coverage && suite?.coverage?.enabled !== false) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
const coverageCommandSpec = suite?.coverage?.command
|
|
32
|
+
? parseCommandSpec(suite.coverage.command)
|
|
33
|
+
: commandSpec;
|
|
34
|
+
const resolvedCoverageCommand = resolveNodeTestCommand(coverageCommandSpec, {
|
|
35
|
+
cwd: suite.cwd || project.rootDir,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!resolvedCoverageCommand.directNodeTest) {
|
|
39
|
+
warnings.push(createUnsupportedCoverageWarning(Boolean(suite?.coverage?.command)));
|
|
31
40
|
} else {
|
|
32
|
-
const coverageExecution = await executeNodeTestRun(commandSpec, {
|
|
41
|
+
const coverageExecution = await executeNodeTestRun(resolvedCoverageCommand.commandSpec, {
|
|
33
42
|
cwd: suite.cwd || project.rootDir,
|
|
34
43
|
enableCoverage: true,
|
|
35
|
-
suiteEnv: suite.env,
|
|
44
|
+
suiteEnv: mergeEnvRecords(suite.env, resolvedCoverageCommand.env),
|
|
36
45
|
rawRelativePath: `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-node-coverage.ndjson`,
|
|
37
46
|
});
|
|
38
47
|
const coverageParsed = parseNodeEvents(parseNdjson(coverageExecution.stdout), suite.cwd || project.rootDir);
|
|
@@ -153,10 +162,207 @@ function tokenizeCommand(command) {
|
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
function isDirectNodeTestCommand(commandSpec) {
|
|
156
|
-
const binary =
|
|
165
|
+
const binary = normalizeBinaryName(commandSpec.command);
|
|
157
166
|
return binary.startsWith('node') && commandSpec.args.includes('--test');
|
|
158
167
|
}
|
|
159
168
|
|
|
169
|
+
function resolveNodeTestCommand(commandSpec, options = {}) {
|
|
170
|
+
const normalized = extractLeadingEnvAssignments(commandSpec);
|
|
171
|
+
if (isDirectNodeTestCommand(normalized.commandSpec)) {
|
|
172
|
+
return {
|
|
173
|
+
commandSpec: normalized.commandSpec,
|
|
174
|
+
env: normalized.env,
|
|
175
|
+
directNodeTest: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const packageScript = resolvePackageScriptNodeTestCommand(normalized.commandSpec, options);
|
|
180
|
+
if (packageScript) {
|
|
181
|
+
return {
|
|
182
|
+
commandSpec: packageScript.commandSpec,
|
|
183
|
+
env: mergeEnvRecords(normalized.env, packageScript.env),
|
|
184
|
+
directNodeTest: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
commandSpec: normalized.commandSpec,
|
|
190
|
+
env: normalized.env,
|
|
191
|
+
directNodeTest: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolvePackageScriptNodeTestCommand(commandSpec, options = {}) {
|
|
196
|
+
const invocation = parsePackageScriptInvocation(commandSpec);
|
|
197
|
+
if (!invocation) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const packageJsonPath = findClosestPackageJson(options.cwd);
|
|
202
|
+
if (!packageJsonPath) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const seenScripts = options.seenScripts || new Set();
|
|
207
|
+
const scriptKey = `${packageJsonPath}:${invocation.scriptName}`;
|
|
208
|
+
if (seenScripts.has(scriptKey)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
213
|
+
const scriptValue = pkg?.scripts?.[invocation.scriptName];
|
|
214
|
+
if (typeof scriptValue !== 'string' || scriptValue.trim().length === 0) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scriptCommand = parseCommandSpec(scriptValue);
|
|
219
|
+
if (containsShellControlTokens([scriptCommand.command, ...scriptCommand.args])) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const normalizedScript = extractLeadingEnvAssignments(scriptCommand);
|
|
224
|
+
const scriptCommandSpec = appendForwardedArgs(normalizedScript.commandSpec, invocation.forwardedArgs);
|
|
225
|
+
if (isDirectNodeTestCommand(scriptCommandSpec)) {
|
|
226
|
+
return {
|
|
227
|
+
commandSpec: scriptCommandSpec,
|
|
228
|
+
env: normalizedScript.env,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const nested = resolvePackageScriptNodeTestCommand(scriptCommandSpec, {
|
|
233
|
+
cwd: path.dirname(packageJsonPath),
|
|
234
|
+
seenScripts: new Set([...seenScripts, scriptKey]),
|
|
235
|
+
});
|
|
236
|
+
if (!nested) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
commandSpec: nested.commandSpec,
|
|
242
|
+
env: mergeEnvRecords(normalizedScript.env, nested.env),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractLeadingEnvAssignments(commandSpec) {
|
|
247
|
+
const tokens = [commandSpec.command, ...commandSpec.args];
|
|
248
|
+
const env = {};
|
|
249
|
+
let index = 0;
|
|
250
|
+
|
|
251
|
+
while (index < tokens.length && isEnvAssignmentToken(tokens[index])) {
|
|
252
|
+
const token = tokens[index];
|
|
253
|
+
const equalsIndex = token.indexOf('=');
|
|
254
|
+
env[token.slice(0, equalsIndex)] = token.slice(equalsIndex + 1);
|
|
255
|
+
index += 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (index >= tokens.length) {
|
|
259
|
+
return {
|
|
260
|
+
commandSpec,
|
|
261
|
+
env,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
commandSpec: {
|
|
267
|
+
command: tokens[index],
|
|
268
|
+
args: tokens.slice(index + 1),
|
|
269
|
+
},
|
|
270
|
+
env,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parsePackageScriptInvocation(commandSpec) {
|
|
275
|
+
const binary = normalizeBinaryName(commandSpec.command);
|
|
276
|
+
const args = [...commandSpec.args];
|
|
277
|
+
|
|
278
|
+
if (binary === 'yarn') {
|
|
279
|
+
let scriptIndex = 0;
|
|
280
|
+
if (args[0] === 'run') {
|
|
281
|
+
scriptIndex = 1;
|
|
282
|
+
}
|
|
283
|
+
const scriptName = args[scriptIndex];
|
|
284
|
+
if (!scriptName || scriptName.startsWith('-')) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
manager: 'yarn',
|
|
289
|
+
scriptName,
|
|
290
|
+
forwardedArgs: normalizeForwardedArgs(args.slice(scriptIndex + 1)),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (binary === 'npm' || binary === 'pnpm') {
|
|
295
|
+
if (args[0] !== 'run') {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const scriptName = args[1];
|
|
299
|
+
if (!scriptName || scriptName.startsWith('-')) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
manager: binary,
|
|
304
|
+
scriptName,
|
|
305
|
+
forwardedArgs: normalizeForwardedArgs(args.slice(2)),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeForwardedArgs(args) {
|
|
313
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
if (args[0] === '--') {
|
|
317
|
+
return args.slice(1);
|
|
318
|
+
}
|
|
319
|
+
return args;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function appendForwardedArgs(commandSpec, forwardedArgs) {
|
|
323
|
+
if (!Array.isArray(forwardedArgs) || forwardedArgs.length === 0) {
|
|
324
|
+
return commandSpec;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
command: commandSpec.command,
|
|
328
|
+
args: [...commandSpec.args, ...forwardedArgs],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function findClosestPackageJson(startDir) {
|
|
333
|
+
let current = path.resolve(startDir || process.cwd());
|
|
334
|
+
while (true) {
|
|
335
|
+
const candidate = path.join(current, 'package.json');
|
|
336
|
+
if (fs.existsSync(candidate)) {
|
|
337
|
+
return candidate;
|
|
338
|
+
}
|
|
339
|
+
const parent = path.dirname(current);
|
|
340
|
+
if (parent === current) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
current = parent;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function containsShellControlTokens(tokens) {
|
|
348
|
+
return tokens.some((token) => ['&&', '||', ';', '|', '>', '>>', '<', '2>', '2>>', '2>&1'].includes(token));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isEnvAssignmentToken(token) {
|
|
352
|
+
return typeof token === 'string' && /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeBinaryName(command) {
|
|
356
|
+
return path.basename(command || '').toLowerCase().replace(/\.cmd$/, '');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createUnsupportedCoverageWarning(hasCoverageCommand) {
|
|
360
|
+
if (hasCoverageCommand) {
|
|
361
|
+
return 'Coverage pass skipped for suite.coverage.command because it did not resolve to a supported node:test pattern.';
|
|
362
|
+
}
|
|
363
|
+
return 'Coverage pass skipped for wrapped node:test command; use a direct node --test invocation, a supported yarn/npm/pnpm package script, or suite.coverage.command.';
|
|
364
|
+
}
|
|
365
|
+
|
|
160
366
|
function withReporterArgs(args, options = {}) {
|
|
161
367
|
const filtered = [];
|
|
162
368
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -499,6 +705,13 @@ function sanitizeEnv(env) {
|
|
|
499
705
|
return nextEnv;
|
|
500
706
|
}
|
|
501
707
|
|
|
708
|
+
function mergeEnvRecords(...records) {
|
|
709
|
+
return records.reduce((merged, record) => ({
|
|
710
|
+
...merged,
|
|
711
|
+
...normalizeEnvRecord(record),
|
|
712
|
+
}), {});
|
|
713
|
+
}
|
|
714
|
+
|
|
502
715
|
function resolveSuiteEnv(suiteEnv) {
|
|
503
716
|
return {
|
|
504
717
|
...sanitizeEnv(process.env),
|