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
@@ -0,0 +1,218 @@
1
+ import { isFunctionNode, unwrap } from "../ast.js";
2
+ import { snippet } from "../source.js";
3
+ import { atLeast, hookFunction, namedFunction, problem, readChain, readCount, readReason, readTitle, refused, text, } from "./common.js";
4
+ import { SuiteAdapter } from "./test-functions.js";
5
+ const HOOKS = ['beforeEach', 'afterEach', 'beforeAll', 'afterAll'];
6
+ const JASMINE = {
7
+ name: 'Jasmine',
8
+ // Jasmine has no module to import its functions from: they are globals, set by the jasmine command, Karma or a
9
+ // browser page.
10
+ source: 'jasmine-core',
11
+ mismatched: new Set(['@playwright/test', 'playwright/test', 'vitest', '@jest/globals', 'mocha']),
12
+ starts: new Map([
13
+ ['describe', { kind: 'describe', links: [] }],
14
+ ['xdescribe', { kind: 'describe', links: ['skip'] }],
15
+ ['fdescribe', { kind: 'describe', links: ['only'] }],
16
+ ['it', { kind: 'test', links: [] }],
17
+ ['xit', { kind: 'test', links: ['skip'] }],
18
+ ['fit', { kind: 'test', links: ['only'] }],
19
+ ]),
20
+ hooks: new Map(HOOKS.map((name) => [name, name])),
21
+ derivations: new Set(),
22
+ modifiers: new Set(),
23
+ carriesKinds: false,
24
+ isModule: () => false,
25
+ defaultImport: null,
26
+ required: null,
27
+ };
28
+ /**
29
+ * Marks what runs with each spec, the spec and its `beforeEach` and `afterEach` hooks, where `pending()` marks the spec
30
+ * pending, and a describe, around which Jasmine catches what its callback throws.
31
+ */
32
+ const IN_SPEC = Symbol('in a spec');
33
+ const IN_DESCRIBE = Symbol('in a describe');
34
+ export class JasmineAdapter extends SuiteAdapter {
35
+ /** What the runner does with a call it refuses at the top level of a file, as the end of a sentence. */
36
+ refusedEffect = 'Jasmine stops before it runs any spec';
37
+ /** Whether a number after a spec's timeout is a retry count, as WebdriverIO reads it. */
38
+ trailingRetries = false;
39
+ constructor(parsed, file, readModule) {
40
+ super(parsed, file, readModule, JASMINE, []);
41
+ }
42
+ classify(call, context) {
43
+ return this.pendingCall(call, context) ?? this.hookCall(call, context) ?? this.chainCall(call, context);
44
+ }
45
+ isAliasLink() {
46
+ return false;
47
+ }
48
+ hook(call, name, context) {
49
+ const fn = hookFunction(this.file, context.scopes, call.arguments);
50
+ // In `beforeAll` and `afterAll`, `pending()` is an error of the describe, and the specs keep the state they get.
51
+ const testContext = name === 'beforeEach' || name === 'afterEach' ? IN_SPEC : null;
52
+ return { kind: 'hook', name, fn, testContext, fixtures: [], takesAllFixtures: false };
53
+ }
54
+ /**
55
+ * `pending(reason)` in a spec or its `beforeEach` or `afterEach` hook, and from Jasmine 7 `notApplicable(reason)`,
56
+ * which mark the spec as not run, with the reason (checked with Jasmine 4.6.0, 5.13.0 and 7.0.0).
57
+ */
58
+ pendingCall(call, context) {
59
+ const callee = unwrap(call.callee);
60
+ if (callee.type !== 'Identifier' || context.testContext !== IN_SPEC)
61
+ return null;
62
+ const isPending = callee.name === 'pending';
63
+ const isNotApplicable = callee.name === 'notApplicable' && atLeast(this.file.version, 7, 0);
64
+ if (!isPending && !isNotApplicable)
65
+ return null;
66
+ if (this.file.values.declarationOf(callee.name, context.scopes) !== null)
67
+ return null;
68
+ const reason = readReason(this.file, context.scopes, call.arguments.at(0));
69
+ return {
70
+ kind: 'modifier',
71
+ state: 'skip',
72
+ condition: { applies: 'always', text: null },
73
+ reason: reason.reason,
74
+ hasDynamicReason: reason.isDynamic,
75
+ problems: reason.problem ? [reason.problem] : [],
76
+ };
77
+ }
78
+ chainCall(call, context) {
79
+ const chain = readChain(call.callee);
80
+ if (!chain)
81
+ return null;
82
+ const resolved = this.resolve(chain, call.arguments, context);
83
+ if (!resolved)
84
+ return null;
85
+ const { kind, links } = resolved;
86
+ const callee = snippet(text(this.file, call.callee));
87
+ // Jasmine's functions have no members: `xit` and `fit` carry their modifier in the name.
88
+ if (chain.links.length === 0)
89
+ return this.declaration(call, kind, links, context);
90
+ const refusal = this.refusal('invalid-chain', call, `${callee} is not part of Jasmine's API`, context);
91
+ return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
92
+ }
93
+ declaration(call, kind, links, context) {
94
+ const args = call.arguments;
95
+ const callee = snippet(text(this.file, call.callee));
96
+ // Jasmine registers `it()` as a pending spec titled '', and refuses a describe without a function.
97
+ if (args.length === 0 && kind === 'describe') {
98
+ return {
99
+ kind: 'invalid',
100
+ problem: this.refusal('test-without-body', call, `${callee}() has no function`, context),
101
+ };
102
+ }
103
+ const flags = new Set(links.map((link) => link.name));
104
+ const problems = [];
105
+ let title = { text: '', withoutTags: '', isDynamic: false };
106
+ if (args.length > 0) {
107
+ const read = readTitle(this.file, context.scopes, args[0], { stripTags: false, functionNames: false });
108
+ title = read.title;
109
+ if (read.problem)
110
+ problems.push(read.problem);
111
+ }
112
+ const spread = args.some((arg) => arg.type === 'SpreadElement');
113
+ const written = spread ? null : args.at(1);
114
+ // `undefined` given as the function is no function, as when none is given.
115
+ const body = written?.type === 'Identifier' && written.name === 'undefined' ? undefined : written;
116
+ const fn = isFunctionNode(body) ? body : null;
117
+ const line = this.file.source.line(call.start);
118
+ // A spec without a function is pending, unless it is focused: `fit` needs one.
119
+ const isSkipped = flags.has('skip') || (kind === 'test' && body === undefined);
120
+ const base = {
121
+ title,
122
+ states: isSkipped
123
+ ? [{ state: 'skip', condition: null, reason: null, hasDynamicReason: false, line }]
124
+ : [],
125
+ isOnly: flags.has('only'),
126
+ mode: null,
127
+ details: { tags: [], annotations: [], locks: [], meta: null, run: {} },
128
+ fn,
129
+ hashed: fn ?? namedFunction(this.file, context.scopes, body ?? undefined),
130
+ isParameterized: false,
131
+ caseCount: null,
132
+ cases: null,
133
+ problems,
134
+ };
135
+ if (kind === 'test') {
136
+ const declared = {
137
+ kind: 'test',
138
+ ...base,
139
+ testContext: IN_SPEC,
140
+ fixtures: [],
141
+ takesAllFixtures: false,
142
+ usesBench: false,
143
+ };
144
+ const timeout = spread ? undefined : args.at(2);
145
+ if (timeout && timeout.type !== 'SpreadElement') {
146
+ const option = { node: timeout, resolved: this.file.values.resolve(timeout, context.scopes) };
147
+ const value = readCount(this.file, 'timeout', option, problems);
148
+ if (value !== null)
149
+ declared.details.run = { timeout: value };
150
+ }
151
+ const retries = this.trailingRetries && !spread ? args.at(3) : undefined;
152
+ if (retries && retries.type !== 'SpreadElement') {
153
+ const option = { node: retries, resolved: this.file.values.resolve(retries, context.scopes) };
154
+ const count = readCount(this.file, 'retries', option, problems);
155
+ if (count !== null)
156
+ declared.details.run = { ...declared.details.run, retry: { count, delay: null, condition: null } };
157
+ }
158
+ if (body === null || (body === undefined ? !flags.has('only') : isFunctionLike(body)))
159
+ return declared;
160
+ const reason = body === undefined ? `${callee}() has no function` : `${callee}() takes a function second`;
161
+ return refused(call, this.refusal('test-without-body', call, reason, context), context, () => declared);
162
+ }
163
+ const declared = { kind: 'describe', ...base, isShuffled: false, passesDown: IN_DESCRIBE };
164
+ if (body === null)
165
+ return declared;
166
+ if (body === undefined || !isFunctionLike(body)) {
167
+ const refusal = this.refusal('test-without-body', call, `${callee}() has no function`, context);
168
+ return refused(call, refusal, context, () => declared);
169
+ }
170
+ if (fn?.async) {
171
+ const refusal = this.refusal('async-describe', fn, 'The describe callback is async, which Jasmine refuses', context);
172
+ return refused(call, refusal, context, () => declared);
173
+ }
174
+ if (fn && declaresNothing(fn)) {
175
+ const refusal = this.refusal('describe-throws', fn, 'The describe declares no spec or describe, which Jasmine refuses', context);
176
+ return refused(call, refusal, context, () => declared);
177
+ }
178
+ if (fn && fn.params.length > 0) {
179
+ const refusal = this.refusal('describe-throws', fn, 'The describe callback takes arguments, which Jasmine refuses', context);
180
+ return refused(call, refusal, context, () => declared);
181
+ }
182
+ return declared;
183
+ }
184
+ /**
185
+ * A call Jasmine refuses: written at the top level of a file, it stops Jasmine before it runs any spec; inside a
186
+ * describe, Jasmine catches it, reports the describe as failed, and registers none of the specs written after it there.
187
+ */
188
+ refusal(code, node, what, context) {
189
+ const source = snippet(text(this.file, node));
190
+ if (context.passedDown !== IN_DESCRIBE) {
191
+ return problem(code, node, `${what}, so ${this.refusedEffect}.`, source);
192
+ }
193
+ const message = `${what}, so the describe around it throws: Jasmine reports it as failed, and registers none of the specs written after this call in it.`;
194
+ return { ...problem(code, node, message, source), afterLoad: true };
195
+ }
196
+ }
197
+ /**
198
+ * Whether a describe callback surely declares nothing: its body is empty or holds only hook calls. A call to anything
199
+ * else, as a helper that declares specs, may declare some.
200
+ */
201
+ function declaresNothing(fn) {
202
+ if (fn.body?.type !== 'BlockStatement')
203
+ return false;
204
+ return fn.body.body.every((statement) => {
205
+ if (statement.type !== 'ExpressionStatement')
206
+ return false;
207
+ const expression = unwrap(statement.expression);
208
+ if (expression.type !== 'CallExpression')
209
+ return false;
210
+ const callee = unwrap(expression.callee);
211
+ return callee.type === 'Identifier' && HOOKS.includes(callee.name);
212
+ });
213
+ }
214
+ /** Whether an argument may be a function: anything but a value that can't be one, as a number or an object. */
215
+ function isFunctionLike(node) {
216
+ const value = unwrap(node);
217
+ return !['Literal', 'ObjectExpression', 'ArrayExpression', 'TemplateLiteral'].includes(value.type);
218
+ }
@@ -0,0 +1,46 @@
1
+ import type { CallExpression } from 'oxc-parser';
2
+ import type { ApiCall, CallContext, ModuleReader } from '../model.ts';
3
+ import type { ParsedFile } from '../parse.ts';
4
+ import type { HookCounts } from '../types.ts';
5
+ import { type FileContext, type Link } from './common.ts';
6
+ import { SuiteAdapter, type Kind } from './test-functions.ts';
7
+ export declare class JestAdapter extends SuiteAdapter {
8
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
9
+ classify(call: CallExpression, context: CallContext): ApiCall | null;
10
+ protected isAliasLink(_kind: Kind, link: Link): boolean;
11
+ protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
12
+ /**
13
+ * `jest.retryTimes(n, options)`. Jest keeps one count for the file and reads it as each test runs, so the last call
14
+ * made while the file loads applies to every test, wherever it is written. From 30.5, `entireDescribe` keeps the
15
+ * count for the describe it is called in instead, and retries the describe as a whole.
16
+ */
17
+ private retryCall;
18
+ /** Whether an expression is Jest's `jest` object: the global, a name imported from Jest, or a member of the module. */
19
+ private isJest;
20
+ private chainCall;
21
+ private declaration;
22
+ /** Whether a table makes `describe.each` throw, as jest-each does on an empty array or a value that isn't a table. */
23
+ private isInvalidTable;
24
+ /** Jest's order: `todo` and `skip` set the test's mode, `only` runs it, and `failing` applies when it runs. */
25
+ private states;
26
+ /**
27
+ * How many tests a table registers. No table, an empty array, or a tagged template whose first line isn't a line of names or
28
+ * whose values don't fill its rows, makes Jest register one test with the title as written, which fails.
29
+ */
30
+ private caseCount;
31
+ /**
32
+ * The cases of a table whose values the scan can work out, as Jest passes them: a case that is an array is spread
33
+ * into the callback's arguments when every case is one; otherwise each case is passed whole. A tagged template gives
34
+ * one object per row, keyed by the names in its first line.
35
+ */
36
+ private tableCases;
37
+ }
38
+ /**
39
+ * What jest-each's heading pattern, `/^\n\s*[^\s]+\s*(\|\s*[^\s]+\s*)*\n/`, matches at the start of a table's first
40
+ * string. The pattern backtracks exponentially on a long run of `|` it can't match, which a scanned file could hang the
41
+ * scan with, so its tries are worked out once per position, from the end. `heading[i]` is where the match ends when a
42
+ * heading, `\s*[^\s]+\s*`, starts at `i`, and `rest[i]` where it ends when the heading before ends at `i`: on a `|`,
43
+ * another heading first, else the line break. The pattern tries the whole word first, with its spaces given back one by
44
+ * one, then shorter words, of which only one that ends before a `|` can go on.
45
+ */
46
+ export declare function headingsMatch(text: string): string | null;