test-inventory 0.8.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 (60) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +16 -11
  3. package/dist/case-titles.d.ts +9 -0
  4. package/dist/case-titles.js +82 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +4 -2
  7. package/dist/config-dependencies.js +19 -0
  8. package/dist/config-runs.d.ts +95 -3
  9. package/dist/config-runs.js +752 -16
  10. package/dist/config.d.ts +24 -0
  11. package/dist/config.js +139 -19
  12. package/dist/diagnostics.js +14 -1
  13. package/dist/extract.d.ts +2 -0
  14. package/dist/extract.js +36 -10
  15. package/dist/files.js +2 -1
  16. package/dist/filter.js +1 -0
  17. package/dist/framework-version.d.ts +4 -2
  18. package/dist/framework-version.js +170 -15
  19. package/dist/frameworks/bun.d.ts +42 -0
  20. package/dist/frameworks/bun.js +272 -0
  21. package/dist/frameworks/common.d.ts +3 -1
  22. package/dist/frameworks/common.js +11 -9
  23. package/dist/frameworks/cypress.d.ts +31 -0
  24. package/dist/frameworks/cypress.js +70 -0
  25. package/dist/frameworks/jasmine.d.ts +28 -0
  26. package/dist/frameworks/jasmine.js +218 -0
  27. package/dist/frameworks/jest.js +1 -1
  28. package/dist/frameworks/mocha.d.ts +50 -0
  29. package/dist/frameworks/mocha.js +306 -0
  30. package/dist/frameworks/node.d.ts +30 -0
  31. package/dist/frameworks/node.js +268 -0
  32. package/dist/frameworks/playwright.d.ts +13 -0
  33. package/dist/frameworks/playwright.js +57 -0
  34. package/dist/frameworks/test-functions.d.ts +26 -1
  35. package/dist/frameworks/test-functions.js +64 -22
  36. package/dist/frameworks/vitest.d.ts +9 -1
  37. package/dist/frameworks/vitest.js +36 -5
  38. package/dist/frameworks/webdriverio.d.ts +27 -0
  39. package/dist/frameworks/webdriverio.js +37 -0
  40. package/dist/model.d.ts +4 -0
  41. package/dist/node-scripts.d.ts +6 -0
  42. package/dist/node-scripts.js +85 -0
  43. package/dist/options.d.ts +13 -4
  44. package/dist/options.js +22 -3
  45. package/dist/records.d.ts +8 -2
  46. package/dist/records.js +50 -13
  47. package/dist/resolve.d.ts +21 -4
  48. package/dist/resolve.js +52 -16
  49. package/dist/scan-file.d.ts +8 -0
  50. package/dist/scan-file.js +16 -2
  51. package/dist/scan.js +42 -7
  52. package/dist/toml.d.ts +13 -0
  53. package/dist/toml.js +172 -0
  54. package/dist/types.d.ts +19 -9
  55. package/dist/values.d.ts +5 -0
  56. package/dist/values.js +13 -0
  57. package/dist/yaml.d.ts +12 -0
  58. package/dist/yaml.js +148 -0
  59. package/package.json +7 -6
  60. package/schema/v1.json +26 -6
@@ -0,0 +1,70 @@
1
+ import { unwrap } from "../ast.js";
2
+ import { snippet } from "../source.js";
3
+ import { atLeast, problem, readCount, readOptions, readStrings, text, unresolvedOptions, } from "./common.js";
4
+ import { dialect, MochaAdapter } from "./mocha.js";
5
+ /**
6
+ * Cypress runs its specs on its own copy of Mocha's BDD interface. A test or describe takes Cypress's settings as an
7
+ * object before its function, as `it(title, { retries: 2 }, fn)`, and Cypress sets no Mocha timeout or retries from
8
+ * `this`.
9
+ */
10
+ export class CypressAdapter extends MochaAdapter {
11
+ readsContextSettings = false;
12
+ refusedEffect = 'Cypress fails the spec and runs none of its tests';
13
+ constructor(parsed, file, readModule) {
14
+ super(parsed, file, readModule, 'bdd', { ...dialect('bdd'), name: 'Cypress', source: 'cypress' });
15
+ }
16
+ splitArguments(args) {
17
+ // Cypress takes the config only before a function: `it(title, { retries: 2 })` gives Mocha the object as the test's
18
+ // function, so the test runs, and fails.
19
+ if (args.length >= 3)
20
+ return { body: args.at(2), options: args.at(1) };
21
+ return { body: args.at(1), options: null };
22
+ }
23
+ /**
24
+ * Cypress's test config: `retries`, a count or `{ runMode, openMode }`, of which `cypress run` takes `runMode`;
25
+ * `browser`, which skips the test in the browsers it leaves out; `@cypress/grep`'s `tags`; and `env`, which Cypress
26
+ * 16 refuses in a test's config.
27
+ */
28
+ withOptions(declared, options, context) {
29
+ const read = readOptions(this.file, context.scopes, options);
30
+ const { problems, details } = declared;
31
+ problems.push(...unresolvedOptions(this.file, read, 'its retries, tags and browser are missing'));
32
+ const retries = read.values.get('retries');
33
+ if (retries) {
34
+ const count = this.retryCount(retries, context, problems);
35
+ if (count !== null)
36
+ details.run = { ...details.run, retry: { count, delay: null, condition: null } };
37
+ }
38
+ const tags = read.values.get('tags');
39
+ if (tags)
40
+ details.tags.push(...readStrings(this.file, context.scopes, tags).values);
41
+ const browser = read.values.get('browser');
42
+ if (browser) {
43
+ const line = this.file.source.line(browser.node.start);
44
+ const condition = `browser: ${snippet(text(this.file, browser.node))}`;
45
+ declared.states.push({ state: 'skip', condition, reason: null, hasDynamicReason: false, line });
46
+ }
47
+ const env = read.values.get('env');
48
+ if (env && atLeast(this.file.version, 16, 0)) {
49
+ const source = snippet(text(this.file, env.node));
50
+ const message = "Cypress 16 no longer takes env in a test's or describe's config, and fails each test it applies to when the test runs; expose: { KEY: value } sets a value the browser reads.";
51
+ problems.push(problem('removed-api', env.node, message, source));
52
+ }
53
+ }
54
+ /**
55
+ * A retry count, or `runMode` of `{ runMode, openMode }`, which `cypress run` uses; null when it is `null`, which
56
+ * Cypress takes as no retries set, or can't be worked out.
57
+ */
58
+ retryCount(option, context, problems) {
59
+ const node = unwrap(option.node);
60
+ if (node.type !== 'ObjectExpression')
61
+ return this.count('retries', option, problems);
62
+ const runMode = readOptions(this.file, context.scopes, node).values.get('runMode');
63
+ return runMode ? this.count('retries.runMode', runMode, problems) : null;
64
+ }
65
+ count(name, option, problems) {
66
+ if (option.resolved.ok && option.resolved.value === null)
67
+ return null;
68
+ return readCount(this.file, name, option, problems);
69
+ }
70
+ }
@@ -0,0 +1,28 @@
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 } from './common.ts';
6
+ import { SuiteAdapter } from './test-functions.ts';
7
+ export declare class JasmineAdapter extends SuiteAdapter {
8
+ /** What the runner does with a call it refuses at the top level of a file, as the end of a sentence. */
9
+ protected readonly refusedEffect: string;
10
+ /** Whether a number after a spec's timeout is a retry count, as WebdriverIO reads it. */
11
+ protected readonly trailingRetries: boolean;
12
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
13
+ classify(call: CallExpression, context: CallContext): ApiCall | null;
14
+ protected isAliasLink(): boolean;
15
+ protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
16
+ /**
17
+ * `pending(reason)` in a spec or its `beforeEach` or `afterEach` hook, and from Jasmine 7 `notApplicable(reason)`,
18
+ * which mark the spec as not run, with the reason (checked with Jasmine 4.6.0, 5.13.0 and 7.0.0).
19
+ */
20
+ private pendingCall;
21
+ private chainCall;
22
+ private declaration;
23
+ /**
24
+ * A call Jasmine refuses: written at the top level of a file, it stops Jasmine before it runs any spec; inside a
25
+ * describe, Jasmine catches it, reports the describe as failed, and registers none of the specs written after it there.
26
+ */
27
+ private refusal;
28
+ }
@@ -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
+ }
@@ -19,7 +19,7 @@ const JEST = {
19
19
  ['fdescribe', { kind: 'describe', links: ['only'] }],
20
20
  ['xdescribe', { kind: 'describe', links: ['skip'] }],
21
21
  ]),
22
- hooks: new Set(['beforeEach', 'afterEach', 'beforeAll', 'afterAll']),
22
+ hooks: new Map(['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].map((name) => [name, name])),
23
23
  derivations: new Set(),
24
24
  modifiers: new Set(['only', 'skip', 'todo', 'concurrent', 'failing']),
25
25
  carriesKinds: false,
@@ -0,0 +1,50 @@
1
+ import type { Argument, CallExpression } from 'oxc-parser';
2
+ import type { ApiCall, CallContext, ModuleReader, SuiteDeclaration, TestDeclaration } 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 Dialect, type Kind } from './test-functions.ts';
7
+ /** The interfaces Mocha's `ui` option names whose globals the scan reads; QUnit and exports declare tests otherwise. */
8
+ export type MochaInterface = 'bdd' | 'tdd';
9
+ export declare function dialect(ui: MochaInterface): Dialect;
10
+ export declare class MochaAdapter extends SuiteAdapter {
11
+ /**
12
+ * Whether `this.timeout()` and `this.retries()` set the tests' settings, as in Mocha. Cypress reads neither, and its
13
+ * `retries()` throws.
14
+ */
15
+ protected readonly readsContextSettings: boolean;
16
+ /** What the runner does with a call it refuses while it loads the file, as the end of a sentence. */
17
+ protected readonly refusedEffect: string;
18
+ /** Whether a number after a test's function is a retry count, as WebdriverIO reads it. */
19
+ protected readonly trailingRetries: boolean;
20
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null, ui?: MochaInterface, runner?: Dialect);
21
+ /** A declaration's function, and the options written before it; Mocha takes none. */
22
+ protected splitArguments(args: CallExpression['arguments']): {
23
+ body: Argument | undefined;
24
+ options: Argument | null;
25
+ };
26
+ /** Sets on a declaration what the options written before its function set; Mocha takes none. */
27
+ protected withOptions?(declared: TestDeclaration | SuiteDeclaration, options: Argument, context: CallContext): void;
28
+ classify(call: CallExpression, context: CallContext): ApiCall | null;
29
+ protected isAliasLink(_kind: Kind, link: Link): boolean;
30
+ protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
31
+ /**
32
+ * `this.timeout(n)`, `this.retries(n)` and `this.skip()` where `this` is Mocha's context: in a describe written as a
33
+ * `function`, they set its tests' timeout and retries; in a test, its own; `this.skip()` in a test or hook skips the
34
+ * tests it runs for. In a hook, `this.timeout()` sets the hook's own timeout, which no test record holds.
35
+ */
36
+ private contextCall;
37
+ /**
38
+ * `it(title, fn).timeout(n)` and `.retries(n)`, which Mocha's `it` and `describe` return their test or suite for: the
39
+ * declaration with the setting. A describe's tests are added before the call, and take its retries when they are
40
+ * added, so `describe(...).retries(n)` reaches none of them; its timeout reaches them from Mocha 12, which reads it as
41
+ * each test runs (checked with 10.8.2, 11.7.5 and 12.0.3).
42
+ */
43
+ private chainedSetting;
44
+ /** A timeout or a retry count from its argument; null, with a warning, when it can't be worked out. */
45
+ private setting;
46
+ private chainCall;
47
+ private declaration;
48
+ }
49
+ /** A timeout in milliseconds or a retry count as Mocha reads it; null for a value it doesn't take. */
50
+ export declare function durationOf(value: unknown, name: 'timeout' | 'retries'): number | null;