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
@@ -209,6 +209,7 @@ export class PlaywrightAdapter {
209
209
  imports;
210
210
  followed = new Map();
211
211
  reported = new Set();
212
+ refusedRests = new Set();
212
213
  /** Variables and names of functions read as test objects on trust, each reported once. */
213
214
  assumed = new Set();
214
215
  aliases = new Map();
@@ -259,6 +260,7 @@ export class PlaywrightAdapter {
259
260
  return this.configure(call, context);
260
261
  if (HOOKS.has(key)) {
261
262
  const fn = hookFunction(this.file, context.scopes, call.arguments);
263
+ this.checkFixtureArguments(fn);
262
264
  return {
263
265
  kind: 'hook',
264
266
  name: key,
@@ -270,6 +272,8 @@ export class PlaywrightAdapter {
270
272
  }
271
273
  if (RUN_MEMBERS.has(key))
272
274
  return this.runSetting(call, key, context);
275
+ if (key === 'extend')
276
+ this.checkExtendFixtures(call, context);
273
277
  if (OTHER_MEMBERS.has(key))
274
278
  return { kind: 'other' };
275
279
  return this.invalid(call, names, "is not part of Playwright's API", context);
@@ -476,6 +480,7 @@ export class PlaywrightAdapter {
476
480
  const line = this.file.source.line(call.start);
477
481
  const fn = isFunctionNode(last) ? last : null;
478
482
  const hashed = fn ?? namedFunction(this.file, context.scopes, last);
483
+ this.checkFixtureArguments(hashed);
479
484
  return {
480
485
  kind: 'test',
481
486
  title,
@@ -495,6 +500,56 @@ export class PlaywrightAdapter {
495
500
  problems,
496
501
  };
497
502
  }
503
+ /**
504
+ * Playwright reads which fixtures a function takes from its source, and refuses a rest property among them, as
505
+ * `({ page, ...fixtures })`, failing the whole run. A nested one, as `({ page: { mouse, ...rest } })`, is allowed.
506
+ */
507
+ checkFixtureArguments(fn) {
508
+ const parameter = fn?.params[0];
509
+ const rest = parameter?.type === 'ObjectPattern' ? parameter.properties.find((item) => item.type === 'RestElement') : null;
510
+ if (!rest || this.refusedRests.has(rest))
511
+ return;
512
+ this.refusedRests.add(rest);
513
+ const source = snippet(text(this.file, rest));
514
+ const message = `Playwright refuses the rest property ${source} in fixture arguments, so it fails to load the file and runs no tests at all.`;
515
+ this.problems.push(problem('invalid-fixture-argument', rest, message, source));
516
+ }
517
+ /** The fixture functions of `test.extend({ ... })`, each given as a function or as `[function, options]`. */
518
+ checkExtendFixtures(call, context) {
519
+ const argument = call.arguments.at(0);
520
+ if (argument && argument.type !== 'SpreadElement')
521
+ this.checkFixtureObject(argument, context.scopes, new Set());
522
+ }
523
+ /**
524
+ * An object of fixtures, written in place or held in a constant, with the objects it spreads; `seen` ends a constant
525
+ * that spreads itself, which throws at run time.
526
+ */
527
+ checkFixtureObject(expression, scopes, seen) {
528
+ const node = unwrap(expression);
529
+ if (seen.has(node))
530
+ return;
531
+ seen.add(node);
532
+ if (node.type === 'Identifier') {
533
+ const constant = this.file.values.constantOf(node.name, scopes);
534
+ if (constant)
535
+ this.checkFixtureObject(constant.init, constant.scopes, seen);
536
+ return;
537
+ }
538
+ if (node.type !== 'ObjectExpression')
539
+ return;
540
+ for (const property of node.properties) {
541
+ if (property.type === 'SpreadElement') {
542
+ this.checkFixtureObject(property.argument, scopes, seen);
543
+ continue;
544
+ }
545
+ const value = unwrap(property.value);
546
+ const fn = value.type === 'ArrayExpression' ? value.elements[0] : value;
547
+ if (isFunctionNode(fn))
548
+ this.checkFixtureArguments(fn);
549
+ else if (fn?.type === 'Identifier')
550
+ this.checkFixtureArguments(this.file.values.functionOf(fn.name, scopes));
551
+ }
552
+ }
498
553
  /** `test.step(title, body)` in a test body; a title that can't be worked out is kept as source text. */
499
554
  step(call, isSkipped, context) {
500
555
  const { title } = readTitle(this.file, context.scopes, call.arguments[0], {
@@ -505,6 +560,8 @@ export class PlaywrightAdapter {
505
560
  }
506
561
  modifier(call, state, context) {
507
562
  const [condition, description] = [call.arguments.at(0), call.arguments.at(1)];
563
+ if (isFunctionNode(condition))
564
+ this.checkFixtureArguments(condition);
508
565
  const problems = [];
509
566
  if (isTitleLiteral(condition)) {
510
567
  const callee = snippet(text(this.file, call.callee));
@@ -0,0 +1,194 @@
1
+ import type { Argument, CallExpression, Expression, Node, Program } from 'oxc-parser';
2
+ import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
3
+ import type { ParsedFile } from '../parse.ts';
4
+ import type { HookCounts } from '../types.ts';
5
+ import { type Exported, type FollowImport, type TestObjects } from './bindings.ts';
6
+ import { type Chain, type FileContext, type Link } from './common.ts';
7
+ export type Kind = 'test' | 'describe';
8
+ /** A test or describe function and the links after it, as in `test.skipIf(isCI)`. */
9
+ export interface TestFunction {
10
+ kind: Kind;
11
+ links: Link[];
12
+ }
13
+ /** A name that starts a chain: the function it is, and the links it stands for, as Jest's `fit` stands for `it.only`. */
14
+ export interface Start {
15
+ kind: Kind;
16
+ links: readonly string[];
17
+ }
18
+ /**
19
+ * How a framework with `describe` and `test` functions, imported or used as globals, names them: Vitest and Jest.
20
+ */
21
+ export interface Dialect {
22
+ /** The framework's name in messages. */
23
+ name: string;
24
+ /** The module the functions are imported from. */
25
+ source: string;
26
+ /** Modules of other frameworks, which a file of this one doesn't import. */
27
+ mismatched: ReadonlySet<string>;
28
+ /** The names that start a chain, as exported and as globals; a map, so `constructor` matches nothing inherited. */
29
+ starts: ReadonlyMap<string, Start>;
30
+ /** Each hook's name in code, and the name it is counted under, as Mocha's `before` is counted as `beforeAll`. */
31
+ hooks: ReadonlyMap<string, keyof HookCounts>;
32
+ /** Members that build a test object from another, such as Vitest's `extend`. */
33
+ derivations: ReadonlySet<string>;
34
+ /** Members that give a test function a mode, as `skip` and `only` do, which a variable can hold it with. */
35
+ modifiers: ReadonlySet<string>;
36
+ /** Whether the test function carries the others, as Vitest's `test.describe` and `describe.test` do. */
37
+ carriesKinds: boolean;
38
+ /** Whether an expression is the framework's module, as `import.meta.vitest` or `require('@jest/globals')` are. */
39
+ isModule: (expression: Expression) => boolean;
40
+ /** Why a default import of the module fails; null when it gives the module. */
41
+ defaultImport: string | null;
42
+ /** Why `require()` of the module fails; null when the module can be required. */
43
+ required: string | null;
44
+ /** Other modules the runner reads as its own, as Bun rewrites `@jest/globals` and `vitest` to `bun:test`. */
45
+ aliases?: ReadonlySet<string>;
46
+ /** What the module is when called, as `node:test`'s default export and `require()` are its `test`. */
47
+ callable?: Start;
48
+ /** The module's other members, which a chain on the callable module may start with and is then no test. */
49
+ others?: ReadonlySet<string>;
50
+ /** Whether the functions must be imported, as the runner sets no globals; `starts` are then exports only. */
51
+ importOnly?: boolean;
52
+ }
53
+ /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
54
+ export declare function shared(a: TestFunction | null, b: TestFunction | null): TestFunction | null;
55
+ /** The names a file binds to the framework's test functions, hooks, other exports and module. */
56
+ export declare class SuiteBindings implements TestObjects {
57
+ readonly starts: Map<string, Start>;
58
+ readonly hooks: Map<string, keyof HookCounts>;
59
+ readonly namespaces: Set<string>;
60
+ /** Other names imported from the framework, by their exported name, such as Vitest's `bench` or Jest's `jest`. */
61
+ readonly members: Map<string, string>;
62
+ /** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
63
+ readonly declarations: Set<Node>;
64
+ readonly problems: Problem[];
65
+ private readonly dialect;
66
+ /** Names imported from other modules, which may be test objects there. */
67
+ private readonly imported;
68
+ private readonly follow;
69
+ /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
70
+ private readonly functions;
71
+ private readonly program;
72
+ /** Names the module declares or imports at its top level, which hide the framework's globals. */
73
+ private readonly declared;
74
+ /** Whether the module is one a scanned file imports from, where no walk reads its globals and chains. */
75
+ private readonly isFollowed;
76
+ constructor(program: Program, dialect: Dialect, follow: FollowImport, isFollowed?: boolean);
77
+ isTestName(name: string): boolean;
78
+ /**
79
+ * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
80
+ * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
81
+ * follows in their scopes.
82
+ */
83
+ isTestObject(expression: Expression, followsCalls?: boolean): boolean;
84
+ returnsTestObject(name: string): boolean;
85
+ /**
86
+ * What a name or chain stands for: one bound from the framework, a global the module doesn't hide, a member of the
87
+ * module, or one of those with modifiers, as `it.skip`; null otherwise.
88
+ */
89
+ startOf(expression: Expression): Start | null;
90
+ /**
91
+ * What a value stands for when the file binds it: in a scanned file, a name bound from the framework or a member of
92
+ * its module, as the walk reads globals and chains itself; in a followed module, also globals and chains.
93
+ */
94
+ private boundStart;
95
+ /** A name the module is bound to: its namespace, and its `test` too when the module can be called. */
96
+ private bindModule;
97
+ exportedStart(name: string): Start | null;
98
+ private holdsTestObject;
99
+ reexports(source: string, imported: string, exported: Exported): boolean;
100
+ /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
101
+ private importsTestObject;
102
+ /** Whether an expression is the framework's module: a namespace import, or the module itself. */
103
+ isNamespace(expression: Expression): boolean;
104
+ private bindMember;
105
+ private bind;
106
+ }
107
+ /** Where a chain's names are looked up. */
108
+ export type Lookup = Pick<CallContext, 'scopes' | 'inBody' | 'returnsOf'>;
109
+ /**
110
+ * What Vitest's and Jest's adapters share: how a call's chain is followed to the test or describe function it starts
111
+ * from, through imports, variables, helper functions and globals.
112
+ */
113
+ export declare abstract class SuiteAdapter implements Adapter {
114
+ readonly problems: Problem[];
115
+ readonly fileTags: string[];
116
+ readonly fileTagSpans: Span[];
117
+ usesGlobals: boolean;
118
+ protected readonly file: FileContext;
119
+ protected readonly bindings: SuiteBindings;
120
+ protected readonly dialect: Dialect;
121
+ /** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
122
+ protected readonly suiteParameters: Map<Node, string>;
123
+ private readonly readModule;
124
+ private readonly imports;
125
+ private readonly followed;
126
+ private readonly reported;
127
+ /** Variables and names of functions read as test objects on trust, each reported once. */
128
+ private readonly assumed;
129
+ private readonly aliases;
130
+ /** First members that show a name imported from a local file is used as the framework's `test`. */
131
+ private readonly testMembers;
132
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null, dialect: Dialect, testMembers: Iterable<string>);
133
+ abstract classify(call: CallExpression, context: CallContext): ApiCall | null;
134
+ /** Whether a variable that holds a test function may hold this link after it, as `const skipOnCi = test.skipIf(isCI)`. */
135
+ protected abstract isAliasLink(kind: Kind, link: Link): boolean;
136
+ protected abstract hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
137
+ testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
138
+ /**
139
+ * The function a chain calls and the links after it, or null when it isn't the framework's. An import that can't be
140
+ * followed is reported only outside test and hook bodies, where a call can declare tests.
141
+ */
142
+ protected resolve(chain: Chain, args: readonly Argument[], lookup: Lookup): TestFunction | null;
143
+ private start;
144
+ /**
145
+ * What a chain on the framework's module starts with: the member it names, or for a module that can be called, as
146
+ * `node:test`'s, the module itself, as in `test.skip(...)`.
147
+ */
148
+ private member;
149
+ /**
150
+ * The member a name is destructured from a test or describe function as, with that function's kind and the
151
+ * expression it is taken from: `each`, `test` and `test` in `const { each } = test`; null for any other name.
152
+ */
153
+ protected destructuredMember(name: string, lookup: Lookup): {
154
+ kind: Kind;
155
+ member: string;
156
+ from: Expression;
157
+ } | null;
158
+ /**
159
+ * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
160
+ * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
161
+ * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
162
+ */
163
+ private alias;
164
+ private aliasOf;
165
+ /**
166
+ * The test functions a value may hold, and whether it may also hold something else, as `process.env.CI` may. A test
167
+ * function is never falsy, so `test.skip || test` and `test.skip ?? test` hold `test.skip`, while
168
+ * `process.env.CI && test.skip || test` may hold either. Both branches of `?:` have to be test functions.
169
+ */
170
+ private choices;
171
+ /**
172
+ * The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
173
+ * function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
174
+ * named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
175
+ */
176
+ private returned;
177
+ /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
178
+ private assumeLocal;
179
+ private looksLikeTest;
180
+ /**
181
+ * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
182
+ * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
183
+ * a package extends.
184
+ */
185
+ private worthFollowing;
186
+ /** The framework as followed modules are read. */
187
+ private framework;
188
+ /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
189
+ private follow;
190
+ /** Hooks called directly, such as `beforeEach(...)` imported from the framework or used as a global. */
191
+ protected hookCall(call: CallExpression, context: CallContext): ApiCall | null;
192
+ /** The number of items of a table, also when not all its values are known; see `Values.lengthOf`. */
193
+ protected arrayLength(node: Argument | undefined, context: CallContext): number | null;
194
+ }