test-inventory 0.7.0 → 0.9.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 (67) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +16 -10
  3. package/dist/case-titles.d.ts +22 -0
  4. package/dist/case-titles.js +184 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +5 -5
  7. package/dist/config-dependencies.js +27 -3
  8. package/dist/config-runs.d.ts +117 -3
  9. package/dist/config-runs.js +1015 -19
  10. package/dist/config.d.ts +42 -1
  11. package/dist/config.js +287 -21
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +16 -1
  15. package/dist/extract.d.ts +2 -0
  16. package/dist/extract.js +89 -29
  17. package/dist/files.js +4 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +207 -7
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/bun.d.ts +42 -0
  24. package/dist/frameworks/bun.js +272 -0
  25. package/dist/frameworks/common.d.ts +3 -1
  26. package/dist/frameworks/common.js +18 -9
  27. package/dist/frameworks/cypress.d.ts +31 -0
  28. package/dist/frameworks/cypress.js +70 -0
  29. package/dist/frameworks/jasmine.d.ts +28 -0
  30. package/dist/frameworks/jasmine.js +218 -0
  31. package/dist/frameworks/jest.d.ts +46 -0
  32. package/dist/frameworks/jest.js +442 -0
  33. package/dist/frameworks/mocha.d.ts +50 -0
  34. package/dist/frameworks/mocha.js +306 -0
  35. package/dist/frameworks/node.d.ts +30 -0
  36. package/dist/frameworks/node.js +268 -0
  37. package/dist/frameworks/playwright.d.ts +13 -0
  38. package/dist/frameworks/playwright.js +57 -0
  39. package/dist/frameworks/test-functions.d.ts +194 -0
  40. package/dist/frameworks/test-functions.js +534 -0
  41. package/dist/frameworks/vitest.d.ts +15 -62
  42. package/dist/frameworks/vitest.js +69 -389
  43. package/dist/frameworks/webdriverio.d.ts +27 -0
  44. package/dist/frameworks/webdriverio.js +37 -0
  45. package/dist/imported-values.js +15 -1
  46. package/dist/model.d.ts +9 -2
  47. package/dist/node-scripts.d.ts +6 -0
  48. package/dist/node-scripts.js +85 -0
  49. package/dist/options.d.ts +16 -3
  50. package/dist/options.js +22 -3
  51. package/dist/parse.js +17 -2
  52. package/dist/records.d.ts +8 -2
  53. package/dist/records.js +94 -22
  54. package/dist/resolve.d.ts +21 -4
  55. package/dist/resolve.js +91 -52
  56. package/dist/scan-file.d.ts +8 -0
  57. package/dist/scan-file.js +21 -3
  58. package/dist/scan.js +62 -12
  59. package/dist/toml.d.ts +13 -0
  60. package/dist/toml.js +172 -0
  61. package/dist/types.d.ts +27 -11
  62. package/dist/values.d.ts +15 -0
  63. package/dist/values.js +120 -2
  64. package/dist/yaml.d.ts +12 -0
  65. package/dist/yaml.js +148 -0
  66. package/package.json +7 -6
  67. package/schema/v1.json +30 -8
package/dist/model.d.ts CHANGED
@@ -37,6 +37,8 @@ export interface Problem {
37
37
  level?: DiagnosticLevel;
38
38
  /** Raised by code in a test, hook or helper body, which runs after the file is loaded and so can't stop it loading. */
39
39
  afterLoad?: boolean;
40
+ /** The runner still runs the tests the file declared before the call that stops it loading, as WebdriverIO's Mocha. */
41
+ keepsEarlierTests?: boolean;
40
42
  }
41
43
  export interface Title {
42
44
  text: string;
@@ -87,9 +89,10 @@ interface Declaration {
87
89
  cases: TableCase[] | null;
88
90
  problems: Problem[];
89
91
  }
90
- /** One case of a `.each` or `.for` table: the items its title is formatted with, and the arguments its callback gets. */
92
+ /** One case of a `.each` or `.for` table: the title the runner gives it, and the arguments its callback gets. */
91
93
  export interface TableCase {
92
- items: Value[];
94
+ /** Null when the scan can't format it the way the runner does, as for a title that shows an object. */
95
+ title: string | null;
93
96
  args: Value[];
94
97
  }
95
98
  export interface TestDeclaration extends Declaration {
@@ -139,6 +142,8 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
139
142
  settings: RunSettings;
140
143
  /** Source text of the first argument. */ argument: string | null;
141
144
  problems: Problem[];
145
+ /** Jest's `jest.retryTimes()`, which applies to every test of the file; a later call replaces it. */
146
+ fileWide?: boolean;
142
147
  } | {
143
148
  kind: 'hook';
144
149
  name: keyof HookCounts;
@@ -219,6 +224,8 @@ export interface Adapter {
219
224
  /** Where each of `fileTags` is written. */
220
225
  fileTagSpans: Span[];
221
226
  usesGlobals: boolean;
227
+ /** Whether a test declared in a test's body is a subtest the runner runs as the test does, as in `node:test`. */
228
+ nestsTests?: boolean;
222
229
  classify(call: CallExpression, context: CallContext): ApiCall | null;
223
230
  /**
224
231
  * The test or describe function a value holds, in the adapter's own terms, as `it.skip` in `return it.skip`; null
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The `node --test` commands of a package.json script, each as the files and globs it gives; empty for a command
3
+ * that gives none. A command runs node:test when `--test` comes before any file, as `node build.js --test` passes it on
4
+ * to the script.
5
+ */
6
+ export declare function nodeTestCommands(script: string): string[][];
@@ -0,0 +1,85 @@
1
+ /** Node.js options that take their value as the next argument, so it is not a file to test. */
2
+ const VALUE_OPTIONS = new Set([
3
+ '--allow-fs-read',
4
+ '--allow-fs-write',
5
+ '--conditions',
6
+ '-C',
7
+ '--config-file',
8
+ '--cpu-prof-dir',
9
+ '--cpu-prof-name',
10
+ '--diagnostic-dir',
11
+ '--disable-warning',
12
+ '--dns-result-order',
13
+ '--env-file',
14
+ '--env-file-if-exists',
15
+ '--experimental-config-file',
16
+ '--experimental-loader',
17
+ '--heap-prof-dir',
18
+ '--heap-prof-name',
19
+ '--heapsnapshot-signal',
20
+ '--icu-data-dir',
21
+ '--import',
22
+ '--input-type',
23
+ '--loader',
24
+ '--localstorage-file',
25
+ '--max-http-header-size',
26
+ '--openssl-config',
27
+ '--redirect-warnings',
28
+ '--report-dir',
29
+ '--report-filename',
30
+ '--require',
31
+ '-r',
32
+ '--secure-heap',
33
+ '--secure-heap-min',
34
+ '--test-concurrency',
35
+ '--test-coverage-branches',
36
+ '--test-coverage-exclude',
37
+ '--test-coverage-functions',
38
+ '--test-coverage-include',
39
+ '--test-coverage-lines',
40
+ '--test-global-setup',
41
+ '--test-isolation',
42
+ '--test-name-pattern',
43
+ '--test-random-seed',
44
+ '--test-reporter',
45
+ '--test-reporter-destination',
46
+ '--test-rerun-failures',
47
+ '--test-shard',
48
+ '--test-skip-pattern',
49
+ '--test-timeout',
50
+ '--title',
51
+ '--unhandled-rejections',
52
+ '--watch-kill-signal',
53
+ '--watch-path',
54
+ ]);
55
+ /**
56
+ * The `node --test` commands of a package.json script, each as the files and globs it gives; empty for a command
57
+ * that gives none. A command runs node:test when `--test` comes before any file, as `node build.js --test` passes it on
58
+ * to the script.
59
+ */
60
+ export function nodeTestCommands(script) {
61
+ const commands = [];
62
+ for (const command of script.split(/&&|\|\||[;|]/)) {
63
+ const words = [...command.matchAll(/'([^']*)'|"([^"]*)"|(\S+)/g)].map((match) => match[1] ?? match[2] ?? match[3]);
64
+ const node = words.findIndex((word) => /(^|[/\\])node(\.exe)?$/.test(word));
65
+ if (node === -1)
66
+ continue;
67
+ let runsTests = false;
68
+ const files = [];
69
+ for (let index = node + 1; index < words.length; index++) {
70
+ const word = words[index];
71
+ if (word === '--test')
72
+ runsTests = true;
73
+ else if (VALUE_OPTIONS.has(word))
74
+ index++;
75
+ else if (!word.startsWith('-')) {
76
+ if (!runsTests)
77
+ break;
78
+ files.push(word);
79
+ }
80
+ }
81
+ if (runsTests)
82
+ commands.push(files);
83
+ }
84
+ return commands;
85
+ }
package/dist/options.d.ts CHANGED
@@ -4,11 +4,18 @@ export type { CommentsMode };
4
4
  /** @inline */
5
5
  export interface ScanOptions {
6
6
  /**
7
- * Glob patterns for the test files, relative to `root`. As in Playwright and Vitest, they match names that start with
8
- * a dot too. Without them, the test files are those the configs in the root and the folders below it run.
7
+ * Glob patterns for the test files, relative to `root`. As in the runners, they match names that start with a dot
8
+ * too. Without them, the test files are those the configs in the root and the folders below it run, or for node:test
9
+ * and Bun those `node --test` and `bun test` run.
9
10
  */
10
11
  patterns?: string[];
11
- framework: Framework;
12
+ /**
13
+ * The runner whose tests are listed. Without it, the one the project uses: whose package a package.json in the root
14
+ * or below lists, whose config is there, or for node:test and Bun, run by a package.json script, else one a
15
+ * package.json above the root lists. With none of them or
16
+ * several, the scan throws a `TypeError` that names them.
17
+ */
18
+ framework?: Framework;
12
19
  /** The project root. Paths in the output are relative to it. Defaults to the current directory. */
13
20
  root?: string;
14
21
  /** Glob patterns to leave out, in addition to `node_modules` and `.git`. */
@@ -22,6 +29,11 @@ export interface ScanOptions {
22
29
  * out, each with the title the runner gives it. Defaults to `false`: one record per declaration.
23
30
  */
24
31
  expandCases?: boolean;
32
+ /**
33
+ * Stops the scan when it aborts: the scan checks it before it starts and between files, and then rejects with the
34
+ * signal's `reason`, as `fetch` does.
35
+ */
36
+ signal?: AbortSignal;
25
37
  }
26
38
  /** @inline */
27
39
  export interface ScanSourceOptions {
@@ -47,6 +59,7 @@ export declare function checkObject(value: unknown, name: string): Record<string
47
59
  export declare function checkFramework(value: unknown, option: string): Framework;
48
60
  export declare function checkComments(value: unknown, option: string): CommentsMode;
49
61
  export declare function checkFlag(value: unknown, option: string): boolean;
62
+ export declare function checkSignal(value: unknown, option: string): AbortSignal | null;
50
63
  export declare function checkVersion(value: unknown, option: string): string | null;
51
64
  export declare function checkString(value: unknown, option: string): string;
52
65
  export declare function checkStrings(value: unknown, option: string, required: boolean): string[];
package/dist/options.js CHANGED
@@ -1,4 +1,14 @@
1
- const FRAMEWORKS = ['playwright', 'vitest'];
1
+ const FRAMEWORKS = [
2
+ 'playwright',
3
+ 'vitest',
4
+ 'jest',
5
+ 'mocha',
6
+ 'jasmine',
7
+ 'cypress',
8
+ 'webdriverio',
9
+ 'node',
10
+ 'bun',
11
+ ];
2
12
  const COMMENTS = ['all', 'non-active', 'none'];
3
13
  // A prerelease and build metadata each appear once, so a long input can't make the match backtrack for long.
4
14
  const VERSION = /^v?(\d+)\.(\d+)(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -14,8 +24,10 @@ export function checkObject(value, name) {
14
24
  return value;
15
25
  }
16
26
  export function checkFramework(value, option) {
17
- if (!FRAMEWORKS.includes(value))
18
- throw invalid(option, '"playwright" or "vitest"');
27
+ if (!FRAMEWORKS.includes(value)) {
28
+ const names = FRAMEWORKS.map((name) => `"${name}"`);
29
+ throw invalid(option, `${names.slice(0, -1).join(', ')} or ${names.at(-1)}`);
30
+ }
19
31
  return value;
20
32
  }
21
33
  export function checkComments(value, option) {
@@ -32,6 +44,13 @@ export function checkFlag(value, option) {
32
44
  throw invalid(option, 'true or false');
33
45
  return value;
34
46
  }
47
+ export function checkSignal(value, option) {
48
+ if (value === undefined)
49
+ return null;
50
+ if (!(value instanceof AbortSignal))
51
+ throw invalid(option, 'an AbortSignal');
52
+ return value;
53
+ }
35
54
  export function checkVersion(value, option) {
36
55
  if (value === undefined || value === null)
37
56
  return null;
package/dist/parse.js CHANGED
@@ -222,10 +222,25 @@ export function parseFile(fileName, code) {
222
222
  error: { message, start: tooDeep, end: tooDeep + 1 },
223
223
  };
224
224
  }
225
- const result = parseSync(fileName, text, parserOptions(fileName));
225
+ let result;
226
+ let program;
227
+ try {
228
+ result = parseSync(fileName, text, parserOptions(fileName));
229
+ // The tree is handed over when it is first read.
230
+ program = result.program;
231
+ }
232
+ catch (error) {
233
+ // The parser can fail to hand over the tree of invalid code, as for a JSX name written as a Unicode escape.
234
+ return {
235
+ source: new SourceText(text),
236
+ program: parseSync(fileName, '', parserOptions(fileName)).program,
237
+ comments: [],
238
+ error: { message: error.message, start: 0, end: 0 },
239
+ };
240
+ }
226
241
  return {
227
242
  source: new SourceText(text),
228
- program: result.program,
243
+ program,
229
244
  comments: result.comments,
230
245
  error: firstError(result.errors),
231
246
  };
package/dist/records.d.ts CHANGED
@@ -52,8 +52,14 @@ export interface VitestTags {
52
52
  }
53
53
  /** What the config the file uses gives it. */
54
54
  export interface FileConfig {
55
- /** Whether a `.only` fails the run: Playwright's `forbidOnly`, always, never, or under the condition with this text. */
56
- forbidOnly: boolean | string;
55
+ /**
56
+ * Why a `.only` fails, from Playwright's `forbidOnly` or Vitest's `allowOnly`, and whether it surely does; null when
57
+ * it doesn't.
58
+ */
59
+ forbidOnly: {
60
+ message: string;
61
+ isSure: boolean;
62
+ } | null;
57
63
  /** The names of the projects that run a test, null when unknown, and what they set for it. */
58
64
  forTest(test: TestRecord, suites: SuiteRecord[]): {
59
65
  projects: string[] | null;
package/dist/records.js CHANGED
@@ -1,4 +1,4 @@
1
- import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
1
+ import { LEVELS, REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
2
2
  import { anySlow, eitherCondition, noHooks, } from "./extract.js";
3
3
  import { problem } from "./frameworks/common.js";
4
4
  import { hashNodes, shortHash } from "./hash.js";
@@ -184,9 +184,16 @@ class RecordBuilder {
184
184
  fileName;
185
185
  ordinals = new Map();
186
186
  suiteIds = new Map();
187
+ testIds = new Map();
187
188
  diagnostics = [];
188
189
  /** The line of the first problem for which the runner refuses the file; null when it loads. */
189
190
  rejectedAt = null;
191
+ /** Whether the tests declared before the line that stops the file loading still run. */
192
+ keepsEarlierTests = false;
193
+ /** Jest: the describes with a test of their own that uses `.only`, which then keep their mode from their others. */
194
+ focused = new Set();
195
+ /** Jasmine: scopes with a focused spec or describe below them, whose own focus Jasmine gives up for the inner one. */
196
+ focusedBelow = new Set();
190
197
  constructor(parsed, options) {
191
198
  this.parsed = parsed;
192
199
  this.options = options;
@@ -203,12 +210,24 @@ class RecordBuilder {
203
210
  ...extraction.problems.filter((item) => tags === null || isTagMissing || item.code !== 'config-may-change-state'),
204
211
  ...undefinedUses.map((use) => use.problem),
205
212
  ];
206
- const rejecting = [...problems, ...testProblems, ...duplicates.map((item) => item.problem)].filter((item) => REJECTS_FILE[this.options.framework].has(item.code) && item.code !== 'undefined-tag' && item.afterLoad !== true);
213
+ const rejecting = [...problems, ...testProblems, ...duplicates.map((item) => item.problem)].filter((item) => REJECTS_FILE[this.options.framework].has(item.code) &&
214
+ item.code !== 'undefined-tag' &&
215
+ item.afterLoad !== true &&
216
+ // A warning of a code that rejects files, as for Jest's .each without a body, leaves the file loading.
217
+ (item.level ?? LEVELS[item.code]) === 'error');
207
218
  // Each of these problems points at the code that causes it. Vitest meets the tags in the order they are listed.
208
219
  const firstTag = undefinedUses.find((use) => use.isSure)?.problem;
209
220
  const lines = [...rejecting, ...(firstTag ? [firstTag] : [])].map((item) => this.parsed.source.line(item.node.start));
210
221
  if (lines.length > 0)
211
222
  this.rejectedAt = Math.min(...lines);
223
+ this.keepsEarlierTests = rejecting.length > 0 && rejecting.every((item) => item.keepsEarlierTests === true);
224
+ this.focused = new Set(extraction.tests.filter((test) => test.declaration.isOnly).map((test) => test.scope));
225
+ const focusedAt = [
226
+ ...extraction.tests.filter((test) => test.declaration.isOnly).map((test) => test.scope),
227
+ // A describe's scope is inside the file's, so it has a parent.
228
+ ...extraction.suites.filter((suite) => suite.declaration.isOnly).map((suite) => suite.scope.parent),
229
+ ];
230
+ this.focusedBelow = new Set(focusedAt.flatMap((scope) => outward(scope)));
212
231
  const suites = extraction.suites.map((suite) => this.suite(suite));
213
232
  const tests = extraction.tests.map((test) => this.test(test, suites));
214
233
  for (const item of problems) {
@@ -258,15 +277,12 @@ class RecordBuilder {
258
277
  * warning. `call` is the `.only` call when it is in this file, whose callee the diagnostic shows.
259
278
  */
260
279
  forbidOnly(located, call) {
261
- const forbid = this.options.config?.forbidOnly ?? false;
262
- if (forbid === false)
280
+ const forbid = this.options.config?.forbidOnly ?? null;
281
+ if (forbid === null)
263
282
  return;
264
- const message = forbid === true
265
- ? 'The config sets forbidOnly, so this .only fails the run before any test runs.'
266
- : `The config sets forbidOnly when ${forbid}, so then this .only fails the run before any test runs.`;
267
283
  const callee = call && snippet(this.parsed.source.text.slice(call.callee.start, call.callee.end));
268
- const found = problem('forbidden-only', located, message, callee);
269
- this.diagnostics.push(this.diagnostic(forbid === true ? found : { ...found, level: 'warning' }));
284
+ const found = problem('forbidden-only', located, forbid.message, callee);
285
+ this.diagnostics.push(this.diagnostic(forbid.isSure ? found : { ...found, level: 'warning' }));
270
286
  }
271
287
  suite(pending) {
272
288
  const { scope, declaration, call } = pending;
@@ -336,6 +352,39 @@ class RecordBuilder {
336
352
  merged[option] = undefined;
337
353
  return merged;
338
354
  }
355
+ /**
356
+ * A Jest test's state, as jest-circus decides it: a describe takes its own `.only` or `.skip`, else the mode around
357
+ * it; a test without a mode of its own takes its describe's, unless that describe is `.only` and has a test of its
358
+ * own that uses `.only`. A skipped describe skips every test in it, a todo or a `.only` too, and a `.only` there
359
+ * focuses nothing.
360
+ */
361
+ jestState(pending, scopes) {
362
+ const { declaration } = pending;
363
+ const marked = scopes.find((scope) => scope.isOnly || scope.states.some((setting) => setting.state === 'skip'));
364
+ const skippedBy = marked && !marked.isOnly ? marked.states.find((setting) => setting.state === 'skip') : undefined;
365
+ const own = (state) => declaration.states.find((setting) => setting.state === state);
366
+ const passesOnly = marked?.isOnly === true && !this.focused.has(pending.scope);
367
+ if (skippedBy) {
368
+ return {
369
+ chosen: own('skip')
370
+ ? { setting: own('skip'), source: 'test' }
371
+ : { setting: skippedBy, source: 'suite' },
372
+ isOnly: false,
373
+ };
374
+ }
375
+ const setting = own('skip') ?? own('todo') ?? own('expectedToFail');
376
+ return { chosen: setting ? { setting, source: 'test' } : null, isOnly: declaration.isOnly || passesOnly };
377
+ }
378
+ /**
379
+ * Whether Jasmine runs a spec as focused: a `fit`, or a spec in an `fdescribe` with no focused spec or describe
380
+ * below it, as Jasmine runs only the innermost focus (checked with Jasmine 4.6.0 and 7.0.0).
381
+ */
382
+ jasmineFocus(pending, scopes) {
383
+ if (pending.declaration.isOnly)
384
+ return true;
385
+ const nearest = scopes.find((scope) => scope.isOnly);
386
+ return nearest !== undefined && !this.focusedBelow.has(nearest);
387
+ }
339
388
  test(pending, suites) {
340
389
  const { declaration } = pending;
341
390
  const dependencies = this.options.dependencies(pending);
@@ -344,7 +393,14 @@ class RecordBuilder {
344
393
  const scopes = outward(pending.scope);
345
394
  const describes = scopes.filter((scope) => scope.kind === 'describe').reverse();
346
395
  const title = declaration.title;
347
- const id = this.id([this.describeTitles(describes), title.withoutTags]);
396
+ const parents = [];
397
+ for (let parent = pending.parent; parent; parent = parent.parent)
398
+ parents.unshift(parent);
399
+ const parentTitles = parents.map((parent) => parent.declaration.title.withoutTags);
400
+ const id = this.id(parents.length > 0
401
+ ? [this.describeTitles(describes), parentTitles, title.withoutTags]
402
+ : [this.describeTitles(describes), title.withoutTags]);
403
+ this.testIds.set(pending, id);
348
404
  const fileScope = scopes[scopes.length - 1];
349
405
  const tags = unique([
350
406
  ...(this.options.config?.configTags ?? []),
@@ -356,19 +412,32 @@ class RecordBuilder {
356
412
  // In Vitest 5 the options of a test's tags win over those the describes pass down; before, the describes' win.
357
413
  const tagsFirst = this.options.config?.tags?.beatsDescribes ?? true;
358
414
  const tagged = (own, tag, around) => tagsFirst ? [...own, ...tag, ...around] : [...own, ...around, ...tag];
359
- const chosen = chooseState([
360
- ...declaredStates(pending.states.slice(0, declaration.states.length), fromTags, declaration),
361
- ...pending.states.slice(declaration.states.length).map((setting) => ({ setting, source: 'test' })),
362
- ...scopes.flatMap((scope) => scope.states.map((setting) => ({
363
- setting,
364
- source: scope.kind === 'file' ? 'file' : 'suite',
365
- }))),
366
- ]);
367
- // When the runner refuses the file, no test in it runs, whatever else is written.
368
- const rejected = this.rejectedAt !== null;
415
+ const jest = this.options.framework === 'jest' ? this.jestState(pending, scopes) : null;
416
+ const focused = this.options.framework === 'jasmine' ? this.jasmineFocus(pending, scopes) : null;
417
+ // A focused Jasmine spec runs inside an `xdescribe`, whose own skip doesn't reach it; `pending()` still does.
418
+ const declaredSkip = (scope, setting) => focused === true && scope.kind === 'describe' && suiteOf(scope).declaration.states.includes(setting);
419
+ const chosen = jest
420
+ ? jest.chosen
421
+ : chooseState([
422
+ ...declaredStates(pending.states.slice(0, declaration.states.length), fromTags, declaration),
423
+ ...pending.states.slice(declaration.states.length).map((setting) => ({ setting, source: 'test' })),
424
+ // A test the declaration skips never runs its body, so its subtests never run; one its body skips still runs it.
425
+ ...parents.flatMap((parent) => parent.declaration.states.map((setting) => ({ setting, source: 'suite' }))),
426
+ ...scopes.flatMap((scope) => scope.states
427
+ .filter((setting) => !declaredSkip(scope, setting))
428
+ .map((setting) => ({
429
+ setting,
430
+ source: scope.kind === 'file' ? 'file' : 'suite',
431
+ }))),
432
+ ]);
433
+ // When the runner refuses the file, no test in it runs, whatever else is written, or none after the refused call.
434
+ const rejected = this.rejectedAt !== null && !(this.keepsEarlierTests && this.parsed.source.line(located.start) < this.rejectedAt);
369
435
  const state = rejected ? 'notLoaded' : (chosen?.setting.state ?? 'active');
370
436
  const setting = rejected ? null : (chosen?.setting ?? null);
371
- const suitePath = describes.flatMap((scope) => suiteOf(scope).declaration.title?.text ?? []);
437
+ const suitePath = [
438
+ ...describes.flatMap((scope) => suiteOf(scope).declaration.title?.text ?? []),
439
+ ...parents.map((parent) => parent.declaration.title.text),
440
+ ];
372
441
  const tagConcurrent = this.options.config?.tags?.concurrentBeatsTags ? undefined : fromTags?.concurrent;
373
442
  const tagMode = tagConcurrent === undefined ? null : tagConcurrent ? 'parallel' : 'default';
374
443
  const [mode = null] = tagged([declaration.mode], [tagMode], scopes.map((scope) => scope.mode)).filter((item) => item !== null);
@@ -376,7 +445,9 @@ class RecordBuilder {
376
445
  for (const values of [fromTags?.meta, ...describes.map((scope) => scope.details.meta), declaration.details.meta]) {
377
446
  Object.assign(meta, values);
378
447
  }
379
- const isOnly = declaration.isOnly || scopes.some((scope) => scope.isOnly) || fromTags?.only === true;
448
+ const isOnly = jest
449
+ ? jest.isOnly
450
+ : (focused ?? (declaration.isOnly || scopes.some((scope) => scope.isOnly) || fromTags?.only === true));
380
451
  const comments = this.options.comments === 'all' || (this.options.comments === 'non-active' && state !== 'active')
381
452
  ? pending.comments
382
453
  : [];
@@ -407,6 +478,7 @@ class RecordBuilder {
407
478
  dependencies: dependencies.files,
408
479
  hasUnfollowedDependencies: !dependencies.isComplete,
409
480
  suiteId: describes.length > 0 ? this.suiteIds.get(describes[describes.length - 1]) : null,
481
+ parentTestId: pending.parent ? this.testIds.get(pending.parent) : null,
410
482
  suitePath,
411
483
  relativeFilePath: this.options.relativeFilePath,
412
484
  fileName: this.fileName,
package/dist/resolve.d.ts CHANGED
@@ -2,8 +2,25 @@ import type { ModuleReader } from './model.ts';
2
2
  /** Parses JSON written with comments and trailing commas, as tsconfig files often are; null when it isn't JSON. */
3
3
  export declare function parseJsonc(text: string): unknown;
4
4
  /**
5
- * Returns a reader for the modules a test file imports. It follows relative paths, tsconfig `paths` and `baseUrl` from
6
- * the tsconfig nearest the importing file, package.json `imports` (`#name`), and workspace packages whose entry is
7
- * TypeScript source. Each module it reads carries a reader for the modules that module imports in turn.
5
+ * How a runner's config maps imports, for every module a test imports: aliases, tried first, as Vite's
6
+ * `resolve.alias` and Jest's `moduleNameMapper`, and folders a bare name is looked for in, as Jest's `modulePaths` and
7
+ * `moduleDirectories`.
8
8
  */
9
- export declare function moduleResolver(root: string): (fromFile: string) => ModuleReader;
9
+ export interface ImportMap {
10
+ /** A string `find` maps the name itself and the names below it, as `@` maps `@/x`; a regex maps what it matches. */
11
+ aliases: {
12
+ find: string | RegExp;
13
+ replacements: string[];
14
+ }[];
15
+ /** Folders a bare name is looked for in. */
16
+ paths: string[];
17
+ /** Names of folders a bare name is looked for in, in the importing file's folder and each folder above it. */
18
+ directories: string[];
19
+ }
20
+ /**
21
+ * Returns a reader for the modules a test file imports. It follows the import map of the config the file runs with,
22
+ * relative paths, tsconfig `paths` and `baseUrl` from the tsconfig nearest the importing file, package.json `imports`
23
+ * (`#name`), and workspace packages whose entry is TypeScript source. Each module it reads carries a reader for the
24
+ * modules that module imports in turn, with the same import map.
25
+ */
26
+ export declare function moduleResolver(root: string): (fromFile: string, map?: ImportMap | null) => ModuleReader;