test-inventory 0.6.0 → 0.7.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.
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
- import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
4
+ import { atLeast, boundMembers, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
5
5
  const MISMATCHED_SOURCES = new Set(['vitest', '@jest/globals']);
6
6
  // Maps rather than object literals, so names such as `constructor` don't match inherited properties.
7
7
  const DESCRIBES = new Map([
@@ -297,7 +297,9 @@ export class PlaywrightAdapter {
297
297
  const declaration = this.file.values.declarationOf(base.name, scopes);
298
298
  const bound = declaration !== null && this.bindings.declarations.has(declaration);
299
299
  const local = declaration !== null && this.imports.get(base.name)?.node === declaration;
300
- const alias = declaration?.type === 'VariableDeclarator' && !bound && !local ? this.alias(declaration, lookup) : null;
300
+ const alias = declaration?.type === 'VariableDeclarator' && !bound && !local
301
+ ? this.alias(declaration, base.name, lookup)
302
+ : null;
301
303
  // The module from `require()` is both the namespace and `test`, so `pw.test(...)` reads as the namespace.
302
304
  if (bound && this.bindings.namespaces.has(base.name) && links[0]?.name === 'test' && !links[0].args) {
303
305
  return links.slice(1);
@@ -366,9 +368,14 @@ export class PlaywrightAdapter {
366
368
  }
367
369
  /**
368
370
  * The members of `test` a variable holds: those of a describe function, as in `const serialDescribe =
369
- * test.describe.serial`, or none for a test object a function returns.
371
+ * test.describe.serial`, none for a test object a function returns, and the member `name` takes when it is
372
+ * destructured, as in `const { beforeEach } = test`. Playwright binds every member to its test object, so a
373
+ * destructured one works as it does on the object; a test object built with a destructured `extend` is not followed.
370
374
  */
371
- alias(declarator, lookup) {
375
+ alias(declarator, name, lookup) {
376
+ const destructured = boundMembers(declarator.id, name);
377
+ if (!destructured)
378
+ return null;
372
379
  let members = this.aliases.get(declarator);
373
380
  if (members === undefined) {
374
381
  // Set first, so a variable defined through itself ends the lookup.
@@ -376,7 +383,7 @@ export class PlaywrightAdapter {
376
383
  members = declarator.init ? this.members(declarator.init, lookup) : null;
377
384
  this.aliases.set(declarator, members);
378
385
  }
379
- return members;
386
+ return members && [...members, ...destructured];
380
387
  }
381
388
  /** The members of `test` a value holds: none for a test object, or those of a describe function. */
382
389
  members(expression, lookup) {
@@ -480,6 +487,7 @@ export class PlaywrightAdapter {
480
487
  hashed,
481
488
  isParameterized: false,
482
489
  caseCount: null,
490
+ cases: null,
483
491
  testContext: null,
484
492
  fixtures: destructuredNames(hashed, 0),
485
493
  takesAllFixtures: takesWhole(hashed, 0),
@@ -545,6 +553,7 @@ export class PlaywrightAdapter {
545
553
  hashed: fn,
546
554
  isParameterized: false,
547
555
  caseCount: null,
556
+ cases: null,
548
557
  problems,
549
558
  };
550
559
  }
@@ -37,7 +37,11 @@ export declare class VitestAdapter implements Adapter {
37
37
  */
38
38
  private resolve;
39
39
  private start;
40
- /** The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`. */
40
+ /**
41
+ * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
42
+ * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
43
+ * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
44
+ */
41
45
  private alias;
42
46
  private aliasOf;
43
47
  /**
@@ -75,10 +79,16 @@ export declare class VitestAdapter implements Adapter {
75
79
  /** Vitest's order: `only` wins over `skip`, which wins over `todo`. */
76
80
  private states;
77
81
  private readDeclarationOptions;
78
- /** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`; the runner ignores an object before. */
82
+ /** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
79
83
  private retry;
80
84
  private meta;
81
85
  private caseCount;
86
+ /**
87
+ * The cases of a table whose values the scan can work out, as Vitest passes them: `.each` spreads a case that is an
88
+ * array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
89
+ * gives one object per row, keyed by the names in its first line.
90
+ */
91
+ private tableCases;
82
92
  private arrayLength;
83
93
  private tagsUsed;
84
94
  }
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
- import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
4
+ import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, boundMembers, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
5
5
  /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
6
6
  function shared(a, b) {
7
7
  if (!a || a.kind !== b?.kind)
@@ -27,6 +27,7 @@ const MODIFIERS = {
27
27
  };
28
28
  const TABLES = new Set(['each', 'for']);
29
29
  const CONDITIONS = new Set(['skipIf', 'runIf']);
30
+ const STATE_FLAGS = ['only', 'todo', 'fails'];
30
31
  const DERIVATIONS = new Set(['extend', 'override', 'scoped']);
31
32
  /** Hooks, with the position of the test context among their callback's parameters. */
32
33
  const HOOKS = new Map([
@@ -401,7 +402,7 @@ export class VitestAdapter {
401
402
  this.worthFollowing(base.name, links, args) &&
402
403
  this.follow(base.name, !inBody && mayDeclareTests(base.name, links));
403
404
  if (!followed && !this.bindings.declarations.has(declaration)) {
404
- const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, lookup) : null;
405
+ const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, base.name, lookup) : null;
405
406
  if (alias)
406
407
  return { kind: alias.kind, index: 0, modifiers: alias.links };
407
408
  return this.assumeLocal(declaration, base.name, links, args)
@@ -415,8 +416,15 @@ export class VitestAdapter {
415
416
  const kind = this.bindings.kinds.get(base.name);
416
417
  return kind ? { kind, index: 0 } : null;
417
418
  }
418
- /** The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`. */
419
- alias(declarator, lookup) {
419
+ /**
420
+ * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
421
+ * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
422
+ * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
423
+ */
424
+ alias(declarator, name, lookup) {
425
+ const destructured = boundMembers(declarator.id, name);
426
+ if (!destructured)
427
+ return null;
420
428
  let alias = this.aliases.get(declarator);
421
429
  if (alias === undefined) {
422
430
  // Set first, so a variable defined through itself ends the lookup.
@@ -424,7 +432,8 @@ export class VitestAdapter {
424
432
  alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
425
433
  this.aliases.set(declarator, alias);
426
434
  }
427
- return alias;
435
+ const links = destructured.map((member) => ({ name: member, args: null, template: null }));
436
+ return alias && { kind: alias.kind, links: [...alias.links, ...links] };
428
437
  }
429
438
  aliasOf(expression, lookup) {
430
439
  const node = unwrap(expression);
@@ -634,6 +643,17 @@ export class VitestAdapter {
634
643
  const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
635
644
  const flags = new Set(chain);
636
645
  let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
646
+ // From Vitest 5.0.5 a test's `skip`, `skipIf` and `runIf` each set its `skip`, so the last of them decides, and a
647
+ // `skipIf` or `runIf` replaces the skip of a tag or of a describe around. The scan applies that to all of Vitest 5.
648
+ const lastSkip = links.filter((link) => link.name === 'skip' || CONDITIONS.has(link.name)).at(-1);
649
+ const skipsByChain = kind === 'test' && lastSkip !== undefined && atLeast(this.file.version, 5, 0);
650
+ const conditionSetsSkip = skipsByChain && CONDITIONS.has(lastSkip.name);
651
+ if (skipsByChain) {
652
+ flags.delete('skip');
653
+ conditions = conditionSetsSkip ? [this.linkCondition(lastSkip, context)] : [];
654
+ if (!conditionSetsSkip)
655
+ flags.add('skip');
656
+ }
637
657
  // A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
638
658
  // test's mode, and for everything on a describe.
639
659
  for (const [flag, value] of read.flags) {
@@ -653,7 +673,7 @@ export class VitestAdapter {
653
673
  // win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
654
674
  const line = this.file.source.line(call.start);
655
675
  const around = context.passedDown;
656
- const passed = read.skip ? null : around;
676
+ const passed = read.skip || conditionSetsSkip ? null : around;
657
677
  const skips = passed?.condition.applies === 'always' && !flags.has('only');
658
678
  const table = links.find((link) => TABLES.has(link.name)) ?? null;
659
679
  const isTodo = flags.has('todo') || body === undefined;
@@ -683,6 +703,7 @@ export class VitestAdapter {
683
703
  hashed: fn ?? namedFunction(this.file, context.scopes, body),
684
704
  isParameterized: table !== null,
685
705
  caseCount: table ? this.caseCount(call, table, context, problems) : null,
706
+ cases: table ? this.tableCases(table, context) : null,
686
707
  problems,
687
708
  };
688
709
  let declared;
@@ -704,6 +725,10 @@ export class VitestAdapter {
704
725
  fixtures: fixturesOf(base.hashed, index, this.file.version),
705
726
  takesAllFixtures: takesWhole(base.hashed, index),
706
727
  usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
728
+ ownStateOptions: [
729
+ ...(read.skip || conditionSetsSkip ? ['skip'] : []),
730
+ ...STATE_FLAGS.filter((flag) => read.flags.has(flag)),
731
+ ],
707
732
  };
708
733
  }
709
734
  return refusal ? refused(call, refusal, context, () => declared) : declared;
@@ -763,8 +788,14 @@ export class VitestAdapter {
763
788
  return result;
764
789
  const options = readOptions(this.file, context.scopes, node);
765
790
  problems.push(...unresolvedOptions(this.file, options, 'its flags, tags and meta are missing'));
791
+ // From Vitest 5.0.5 an option set to `undefined` replaces nothing; the scan applies that to all of Vitest 5.
792
+ const given = (key) => {
793
+ const option = options.values.get(key);
794
+ const isUndefined = option?.resolved.ok === true && option.resolved.value === undefined;
795
+ return isUndefined && atLeast(this.file.version, 5, 0) ? undefined : option;
796
+ };
766
797
  for (const flag of ['only', 'todo', 'fails', 'concurrent', 'sequential', 'shuffle']) {
767
- const option = options.values.get(flag);
798
+ const option = given(flag);
768
799
  if (option && !option.resolved.ok) {
769
800
  const { importedFrom } = option.resolved;
770
801
  problems.push(unresolvedOption(this.file, flag, option.node, importedFrom, 'it is not applied'));
@@ -776,7 +807,7 @@ export class VitestAdapter {
776
807
  result.flags.set(flag, Boolean(option.resolved.value));
777
808
  }
778
809
  }
779
- const skip = options.values.get('skip');
810
+ const skip = given('skip');
780
811
  if (skip) {
781
812
  result.skip = skip.resolved.ok
782
813
  ? { applies: skip.resolved.value ? 'always' : 'never', text: null }
@@ -806,7 +837,7 @@ export class VitestAdapter {
806
837
  this.retry(retry, details, context, problems);
807
838
  return result;
808
839
  }
809
- /** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`; the runner ignores an object before. */
840
+ /** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
810
841
  retry(option, details, context, problems) {
811
842
  const node = unwrap(option.node);
812
843
  if (node.type !== 'ObjectExpression') {
@@ -815,8 +846,14 @@ export class VitestAdapter {
815
846
  details.run.retry = { count, delay: null, condition: null };
816
847
  return;
817
848
  }
818
- if (!atLeast(this.file.version, 4, 1))
849
+ if (!atLeast(this.file.version, 4, 1)) {
850
+ // Before 4.1 the runner compares the attempt number with the object, so it never runs the test.
851
+ const source = snippet(text(this.file, node));
852
+ const message = `Option retry: ${source} is an object, which Vitest reads from 4.1; before, it never runs the test.`;
853
+ problems.push(problem('unresolved-option', node, message, source));
854
+ details.run.retry = null;
819
855
  return;
856
+ }
820
857
  const values = readOptions(this.file, context.scopes, node);
821
858
  problems.push(...unresolvedOptions(this.file, values, 'the retry is not applied'));
822
859
  const read = (key) => {
@@ -859,6 +896,43 @@ export class VitestAdapter {
859
896
  }
860
897
  return count;
861
898
  }
899
+ /**
900
+ * The cases of a table whose values the scan can work out, as Vitest passes them: `.each` spreads a case that is an
901
+ * array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
902
+ * gives one object per row, keyed by the names in its first line.
903
+ */
904
+ tableCases(table, context) {
905
+ let cases;
906
+ if (table.template) {
907
+ const values = table.template.expressions.map((item) => this.file.values.resolve(item, context.scopes));
908
+ if (!values.every((item) => item.ok))
909
+ return null;
910
+ const header = templateHeader(table.template);
911
+ // Without values, Vitest passes the template's strings as the table.
912
+ cases =
913
+ values.length === 0
914
+ ? table.template.quasis.map((quasi) => quasi.value.cooked ?? undefined)
915
+ : Array.from({ length: Math.floor(values.length / header.length) }, (_, row) => Object.fromEntries(header.map((name, column) => [name, values[row * header.length + column].value])));
916
+ }
917
+ else {
918
+ const first = table.args.at(0);
919
+ const resolved = first && first.type !== 'SpreadElement' ? this.file.values.resolve(first, context.scopes) : null;
920
+ if (!resolved?.ok || !Array.isArray(resolved.value))
921
+ return null;
922
+ cases = resolved.value;
923
+ }
924
+ if (table.name === 'for') {
925
+ return cases.map((item) => ({
926
+ items: item === null || item === undefined ? [] : Array.isArray(item) ? item : [item],
927
+ args: [item],
928
+ }));
929
+ }
930
+ const arraysOnly = cases.every((item) => Array.isArray(item));
931
+ return cases.map((item) => {
932
+ const items = Array.isArray(item) ? item : [item];
933
+ return { items, args: arraysOnly ? items : [item] };
934
+ });
935
+ }
862
936
  arrayLength(node, context) {
863
937
  if (node === undefined || node.type === 'SpreadElement')
864
938
  return null;
@@ -880,13 +954,24 @@ export class VitestAdapter {
880
954
  this.problems.push(problem('config-may-change-state', node, "The file uses Vitest tags, and tags defined in the Vitest config can skip tests or change how they run; the scan can't work out the config's tags."));
881
955
  }
882
956
  }
883
- /** Rows in a tagged-template table such as ``each`a | b ${1} | ${2}` ``: values divided by header columns. */
957
+ /**
958
+ * The column names of a tagged-template table such as ``each`side | qty ${'buy'} | ${100}` ``, as Vitest's
959
+ * `formatTemplateString` reads them: the text of the template with its spaces taken out, its first line split at `|`.
960
+ * Outer pipes, as in `| side | qty |`, give empty names too.
961
+ */
962
+ function templateHeader(template) {
963
+ // A string with an invalid escape, as in `\xZZ`, has no cooked value, which joins as nothing.
964
+ const strings = template.quasis.map((quasi) => quasi.value.cooked ?? '');
965
+ return strings.join('').trim().replace(/ /g, '').split('\n')[0].split('|');
966
+ }
967
+ /**
968
+ * Rows of a tagged-template table: as many as the values fill, an incomplete last row left out, as Vitest does. A
969
+ * template without values isn't split, so its text is the one case.
970
+ */
884
971
  function templateRows(template) {
885
- const header = (template.quasis[0].value.cooked ?? template.quasis[0].value.raw).trim().split('\n')[0];
886
- const columns = header.split('|').filter((column) => column.trim() !== '').length;
887
- if (columns === 0 || template.expressions.length % columns !== 0)
888
- return null;
889
- return template.expressions.length / columns;
972
+ if (template.expressions.length === 0)
973
+ return 1;
974
+ return Math.floor(template.expressions.length / templateHeader(template).length);
890
975
  }
891
976
  function isMetaValue(value) {
892
977
  return value !== undefined;
@@ -0,0 +1,22 @@
1
+ import type { ModuleReader } from './model.ts';
2
+ import { type ImportReader } from './values.ts';
3
+ /**
4
+ * Works out the values project modules export, for the files of one scan: each module is parsed once and each of its
5
+ * exports worked out once, however many test files import it. Installed packages are never read, since the reader
6
+ * only gives project files.
7
+ */
8
+ export declare class ExportedValues {
9
+ private readonly modules;
10
+ private readonly done;
11
+ /** Results of the current lookup, also those that `done` can't keep, so that each module is read once in it. */
12
+ private readonly current;
13
+ private readonly pending;
14
+ /** How many times a lookup was cut short by a cycle or the depth limit. */
15
+ private cutShort;
16
+ /** What a file that reads its imports with `readModule` gets for `import { name } from source`. */
17
+ reader(readModule: ModuleReader | null): ImportReader | null;
18
+ /** What `module` exports under `name`; null when it doesn't export it. */
19
+ private exported;
20
+ private read;
21
+ private open;
22
+ }
@@ -0,0 +1,135 @@
1
+ import { ownExport, parseModule, starReexport } from "./frameworks/bindings.js";
2
+ import { Values } from "./values.js";
3
+ const UNRESOLVED = { ok: false, importedFrom: null };
4
+ /** How many modules deep a name is followed through re-exports and imports, as for test objects. */
5
+ const MAX_DEPTH = 16;
6
+ /**
7
+ * Works out the values project modules export, for the files of one scan: each module is parsed once and each of its
8
+ * exports worked out once, however many test files import it. Installed packages are never read, since the reader
9
+ * only gives project files.
10
+ */
11
+ export class ExportedValues {
12
+ modules = new WeakMap();
13
+ done = new Map();
14
+ /** Results of the current lookup, also those that `done` can't keep, so that each module is read once in it. */
15
+ current = new Map();
16
+ pending = new Set();
17
+ /** How many times a lookup was cut short by a cycle or the depth limit. */
18
+ cutShort = 0;
19
+ /** What a file that reads its imports with `readModule` gets for `import { name } from source`. */
20
+ reader(readModule) {
21
+ if (!readModule)
22
+ return null;
23
+ return (source, name) => {
24
+ const module = readModule(source);
25
+ if ('reason' in module)
26
+ return UNRESOLVED;
27
+ return this.exported(module, name) ?? UNRESOLVED;
28
+ };
29
+ }
30
+ /** What `module` exports under `name`; null when it doesn't export it. */
31
+ exported(module, name) {
32
+ const key = `${module.relativeFilePath}\0${name}`;
33
+ if (this.done.has(key))
34
+ return this.done.get(key);
35
+ if (this.current.has(key)) {
36
+ // It was worked out on a path that was cut short, so what is built on it depends on that path too.
37
+ this.cutShort++;
38
+ return this.current.get(key);
39
+ }
40
+ // A path that comes back to a name being worked out, as `export *` modules that re-export each other do, gives
41
+ // nothing; another path may still give it.
42
+ if (this.pending.has(key) || this.pending.size >= MAX_DEPTH) {
43
+ this.cutShort++;
44
+ return this.pending.has(key) ? null : UNRESOLVED;
45
+ }
46
+ const before = this.cutShort;
47
+ this.pending.add(key);
48
+ const result = this.read(module, name);
49
+ this.pending.delete(key);
50
+ // A result found while a path was cut short depends on where the lookup began, so only this lookup keeps it.
51
+ if (this.cutShort === before)
52
+ this.done.set(key, result);
53
+ else
54
+ this.current.set(key, result);
55
+ if (this.pending.size === 0)
56
+ this.current.clear();
57
+ return result;
58
+ }
59
+ read(module, name) {
60
+ const opened = this.open(module);
61
+ if (!opened)
62
+ return UNRESOLVED;
63
+ if (opened.json)
64
+ return jsonExport(opened.json.value, name);
65
+ const program = opened.program;
66
+ for (const statement of program.body) {
67
+ const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : null;
68
+ if (declaration?.type === 'TSEnumDeclaration' && declaration.id.name === name) {
69
+ return opened.values.resolveName(name, [program]);
70
+ }
71
+ }
72
+ const own = ownExport(program, name);
73
+ if (own?.kind === 'default')
74
+ return opened.values.resolve(own.value, [program]);
75
+ if (own?.kind === 'function')
76
+ return UNRESOLVED;
77
+ if (own?.kind === 'specifier' && own.source !== null) {
78
+ const from = module.readModule(own.source);
79
+ return 'reason' in from ? UNRESOLVED : (this.exported(from, own.local) ?? UNRESOLVED);
80
+ }
81
+ if (own) {
82
+ return opened.values.resolveName(own.kind === 'specifier' ? own.local : name, [program]);
83
+ }
84
+ // `export *` passes on every name but the default. When two modules export the name with different values, Node
85
+ // refuses to load the module and Vite's module runner keeps the first, so the value isn't known.
86
+ if (name === 'default')
87
+ return null;
88
+ let found = null;
89
+ for (const statement of program.body) {
90
+ const source = starReexport(statement);
91
+ const from = source === null ? null : module.readModule(source);
92
+ const result = from && !('reason' in from) ? this.exported(from, name) : null;
93
+ if (!result)
94
+ continue;
95
+ if (found && !(found.ok && result.ok && JSON.stringify(found.value) === JSON.stringify(result.value))) {
96
+ return UNRESOLVED;
97
+ }
98
+ found = result;
99
+ }
100
+ return found;
101
+ }
102
+ open(module) {
103
+ if (this.modules.has(module))
104
+ return this.modules.get(module);
105
+ let opened = null;
106
+ if (module.relativeFilePath.endsWith('.json')) {
107
+ const value = parseJson(module.code);
108
+ opened = value === undefined ? null : { program: null, json: { value }, values: new Values() };
109
+ }
110
+ else if (!module.bytes) {
111
+ const parsed = parseModule(module);
112
+ const values = new Values(this.reader(module.readModule));
113
+ opened = parsed.error ? null : { program: parsed.program, json: null, values };
114
+ }
115
+ this.modules.set(module, opened);
116
+ return opened;
117
+ }
118
+ }
119
+ function parseJson(code) {
120
+ try {
121
+ return JSON.parse(code);
122
+ }
123
+ catch {
124
+ return undefined;
125
+ }
126
+ }
127
+ /** A JSON module's default export is its value; Vite and Playwright also export its top-level keys by name. */
128
+ function jsonExport(value, name) {
129
+ if (name === 'default')
130
+ return { ok: true, value };
131
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.hasOwn(value, name)) {
132
+ return { ok: true, value: value[name] };
133
+ }
134
+ return null;
135
+ }
package/dist/model.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { CallExpression, Expression, Node } from 'oxc-parser';
2
2
  import type { FunctionNode } from './ast.ts';
3
3
  import type { Annotation, DiagnosticCode, DiagnosticLevel, HookCounts, MetaValue } from './types.ts';
4
+ import type { Value } from './values.ts';
4
5
  export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
6
+ /** The Vitest options that set a test's state. */
7
+ export type StateOption = 'skip' | 'only' | 'todo' | 'fails';
5
8
  export type Mode = 'parallel' | 'serial' | 'default';
6
9
  /** A state set by a declaration, a describe or a call such as `test.skip(condition)`. */
7
10
  export interface StateSetting {
@@ -51,12 +54,15 @@ export interface Details {
51
54
  /** Run settings written in one place; a key that is missing is not set there. */
52
55
  export interface RunSettings {
53
56
  timeout?: number;
54
- /** A retry setting replaces the whole `retry` of the describes around, delay and condition included. */
57
+ /**
58
+ * A retry setting replaces the whole `retry` of the describes around, delay and condition included; null when it is
59
+ * set to a value the runner can't use.
60
+ */
55
61
  retry?: {
56
62
  count: number;
57
63
  delay: number | null;
58
64
  condition: string | null;
59
- };
65
+ } | null;
60
66
  repeats?: number;
61
67
  /** Playwright `test.slow()`: the conditions of the calls, any of which makes the test slow; null when one always does. */
62
68
  slow?: {
@@ -77,8 +83,15 @@ interface Declaration {
77
83
  isParameterized: boolean;
78
84
  /** Number of cases of `.each` or `.for` when the table is written out; null otherwise. */
79
85
  caseCount: number | null;
86
+ /** The cases of `.each` or `.for` when the scan can work out every value in them; null otherwise. */
87
+ cases: TableCase[] | null;
80
88
  problems: Problem[];
81
89
  }
90
+ /** One case of a `.each` or `.for` table: the items its title is formatted with, and the arguments its callback gets. */
91
+ export interface TableCase {
92
+ items: Value[];
93
+ args: Value[];
94
+ }
82
95
  export interface TestDeclaration extends Declaration {
83
96
  kind: 'test';
84
97
  title: Title;
@@ -90,6 +103,8 @@ export interface TestDeclaration extends Declaration {
90
103
  takesAllFixtures: boolean;
91
104
  /** Uses Vitest 5's `bench` fixture. */
92
105
  usesBench: boolean;
106
+ /** Vitest: the state options the test sets itself, to `false` too, which replace those of its tags. */
107
+ ownStateOptions?: StateOption[];
93
108
  }
94
109
  export interface SuiteDeclaration extends Declaration {
95
110
  kind: 'describe';
@@ -186,6 +201,8 @@ export interface Returns {
186
201
  /** A module read to see whether it exports a custom `test` object. */
187
202
  export interface ModuleSource {
188
203
  code: string;
204
+ /** The file's bytes when it isn't UTF-8 text, such as an image. */
205
+ bytes?: Uint8Array;
189
206
  relativeFilePath: string;
190
207
  /** Reads the modules this module imports in turn. */
191
208
  readModule: ModuleReader;
package/dist/options.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface ScanOptions {
17
17
  frameworkVersion?: string;
18
18
  /** Which tests get the comments above them. Defaults to `non-active`. */
19
19
  comments?: CommentsMode;
20
+ /**
21
+ * One test or describe per case of a `.each` or `.for` table and per pass of a loop whose values the scan can work
22
+ * out, each with the title the runner gives it. Defaults to `false`: one record per declaration.
23
+ */
24
+ expandCases?: boolean;
20
25
  }
21
26
  /** @inline */
22
27
  export interface ScanSourceOptions {
@@ -28,6 +33,11 @@ export interface ScanSourceOptions {
28
33
  frameworkVersion?: string | null;
29
34
  /** Which tests get the comments above them. Defaults to `non-active`. */
30
35
  comments?: CommentsMode;
36
+ /**
37
+ * One test or describe per case of a `.each` or `.for` table and per pass of a loop whose values the scan can work
38
+ * out, each with the title the runner gives it. Defaults to `false`: one record per declaration.
39
+ */
40
+ expandCases?: boolean;
31
41
  }
32
42
  /** An invalid option, told apart from other errors so that the command line can show it as a usage error. */
33
43
  export declare class OptionsError extends TypeError {
@@ -36,6 +46,7 @@ export declare function invalid(option: string, expected: string): OptionsError;
36
46
  export declare function checkObject(value: unknown, name: string): Record<string, unknown>;
37
47
  export declare function checkFramework(value: unknown, option: string): Framework;
38
48
  export declare function checkComments(value: unknown, option: string): CommentsMode;
49
+ export declare function checkFlag(value: unknown, option: string): boolean;
39
50
  export declare function checkVersion(value: unknown, option: string): string | null;
40
51
  export declare function checkString(value: unknown, option: string): string;
41
52
  export declare function checkStrings(value: unknown, option: string, required: boolean): string[];
package/dist/options.js CHANGED
@@ -25,6 +25,13 @@ export function checkComments(value, option) {
25
25
  throw invalid(option, '"all", "non-active" or "none"');
26
26
  return value;
27
27
  }
28
+ export function checkFlag(value, option) {
29
+ if (value === undefined)
30
+ return false;
31
+ if (typeof value !== 'boolean')
32
+ throw invalid(option, 'true or false');
33
+ return value;
34
+ }
28
35
  export function checkVersion(value, option) {
29
36
  if (value === undefined || value === null)
30
37
  return null;
package/dist/parse.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface ParsedFile {
15
15
  end: number;
16
16
  } | null;
17
17
  }
18
+ /**
19
+ * The offset of the first bracket nested more than `limit` deep, or null. Parentheses, brackets, braces and template
20
+ * substitutions count; strings, comments, regular expressions and template text are skipped. A closing bracket closes
21
+ * up to the one it matches, so a literal read wrongly unbalances only the brackets around it.
22
+ */
23
+ export declare function tooDeeplyNested(text: string, limit: number): number | null;
18
24
  /** The first parser error, located at its first label, or at the start of the file when it has none. */
19
25
  export declare function firstError(errors: readonly OxcError[]): ParsedFile['error'];
20
26
  export declare function parseFile(fileName: string, code: string): ParsedFile;