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
@@ -1,26 +1,8 @@
1
1
  import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
2
2
  import { snippet } from "../source.js";
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, boundMembers, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
5
- /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
6
- function shared(a, b) {
7
- if (!a || a.kind !== b?.kind)
8
- return null;
9
- const same = (link, index) => {
10
- const other = b.links.at(index);
11
- return other?.name === link.name && !link.args && !link.template && !other.args && !other.template;
12
- };
13
- const differs = a.links.findIndex((link, index) => !same(link, index));
14
- return { kind: a.kind, links: differs === -1 ? a.links : a.links.slice(0, differs) };
15
- }
16
- // Maps rather than object literals, so names such as `constructor` don't match inherited properties.
17
- const KINDS = new Map([
18
- ['test', 'test'],
19
- ['it', 'test'],
20
- ['describe', 'describe'],
21
- ['suite', 'describe'],
22
- ]);
23
- const MISMATCHED_SOURCES = new Set(['@playwright/test', 'playwright/test', '@jest/globals']);
3
+ import { vitestCaseTitle } from "../case-titles.js";
4
+ import { atLeast, hookFunction, problem, readChain, readCondition, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, origin, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
5
+ import { SuiteAdapter } from "./test-functions.js";
24
6
  const MODIFIERS = {
25
7
  test: new Set(['concurrent', 'sequential', 'only', 'skip', 'todo', 'fails']),
26
8
  describe: new Set(['concurrent', 'sequential', 'shuffle', 'skip', 'only', 'todo']),
@@ -44,7 +26,25 @@ function isHook(name) {
44
26
  /** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
45
27
  const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
46
28
  /** First members that show a name imported from a local file is used as Vitest's `test`. */
47
- const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
29
+ const TEST_MEMBERS = [...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS];
30
+ const VITEST = {
31
+ name: 'Vitest',
32
+ source: 'vitest',
33
+ mismatched: new Set(['@playwright/test', 'playwright/test', '@jest/globals']),
34
+ starts: new Map([
35
+ ['test', { kind: 'test', links: [] }],
36
+ ['it', { kind: 'test', links: [] }],
37
+ ['describe', { kind: 'describe', links: [] }],
38
+ ['suite', { kind: 'describe', links: [] }],
39
+ ]),
40
+ hooks: new Map([...HOOKS.keys()].map((name) => [name, name])),
41
+ derivations: DERIVATIONS,
42
+ modifiers: new Set([...MODIFIERS.test, ...MODIFIERS.describe]),
43
+ carriesKinds: true,
44
+ isModule: isImportMetaVitest,
45
+ defaultImport: "Vitest has no default export; import { test, describe } from 'vitest' instead.",
46
+ required: 'Vitest cannot be loaded with require(); tests that use it are missing.',
47
+ };
48
48
  /** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
49
49
  const HOISTED_OBJECTS = new Set(['vi', 'vitest']);
50
50
  const HOISTED_METHODS = new Set(['mock', 'unmock', 'hoisted']);
@@ -112,186 +112,30 @@ function fixturesOf(fn, index, version) {
112
112
  return !since || !atLeast(version, ...since);
113
113
  });
114
114
  }
115
- /** The names a file binds to Vitest's `test`, `describe`, hooks and namespace. */
116
- class VitestBindings {
117
- kinds = new Map();
118
- hooks = new Map();
119
- namespaces = new Set();
120
- /** Names `bench` is imported as from Vitest 5, which no longer exports it. */
121
- benches = new Set();
122
- /** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
123
- declarations = new Set();
124
- problems = [];
125
- /** Names imported from other modules, which may be test objects there. */
126
- imported;
127
- follow;
128
- /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
129
- functions;
130
- constructor(program, version, follow) {
131
- this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
132
- this.follow = follow;
133
- this.functions = new ModuleFunctions(program, this.imported, follow);
134
- for (const statement of program.body) {
135
- if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
136
- continue;
137
- const source = statement.source.value;
138
- if (MISMATCHED_SOURCES.has(source)) {
139
- this.problems.push(problem('framework-mismatch', statement, `The file imports '${source}', but the scan is for Vitest.`));
140
- }
141
- if (source !== 'vitest')
142
- continue;
143
- for (const specifier of statement.specifiers) {
144
- this.declarations.add(specifier);
145
- if (specifier.type === 'ImportNamespaceSpecifier') {
146
- this.namespaces.add(specifier.local.name);
147
- }
148
- else if (specifier.type === 'ImportDefaultSpecifier') {
149
- this.problems.push(problem('invalid-import', specifier, "Vitest has no default export; import { test, describe } from 'vitest' instead."));
150
- }
151
- else {
152
- this.bindImported(exportName(specifier.imported), specifier.local.name, version);
153
- }
154
- }
155
- }
156
- const declarators = allDeclarators(program);
157
- for (const declarator of declarators) {
158
- if (declarator.init && requiredModule(declarator.init) === 'vitest') {
159
- this.problems.push(problem('invalid-import', declarator, 'Vitest cannot be loaded with require(); tests that use it are missing.'));
160
- }
161
- }
162
- bindAll(declarators, (declarator) => {
163
- const bound = this.bind(declarator);
164
- if (bound)
165
- this.declarations.add(declarator);
166
- return bound;
167
- });
168
- this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
169
- }
170
- isTestName(name) {
171
- return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
172
- }
173
- /**
174
- * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
175
- * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
176
- * follows in their scopes.
177
- */
178
- isTestObject(expression, followsCalls = true) {
179
- const node = unwrap(expression);
180
- if (node.type === 'Identifier') {
181
- const { name } = node;
182
- return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
183
- }
184
- if (node.type === 'MemberExpression') {
185
- return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
186
- }
187
- // `process.env.CI ? vitestTest : skippedTest` is a test object either way.
188
- if (node.type === 'ConditionalExpression') {
189
- return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
190
- }
191
- if (node.type !== 'CallExpression')
192
- return false;
193
- const callee = unwrap(node.callee);
194
- // `defineBrokerTest()`, a function that returns a test object.
195
- if (callee.type === 'Identifier')
196
- return followsCalls && this.returnsTestObject(callee.name);
197
- return (callee.type === 'MemberExpression' &&
198
- DERIVATIONS.has(propertyName(callee) ?? '') &&
199
- this.isTestObject(callee.object, followsCalls));
200
- }
201
- returnsTestObject(name) {
202
- return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
203
- }
204
- holdsTestObject(name) {
205
- return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
206
- }
207
- reexports(source, imported, exported) {
208
- if (source !== 'vitest')
209
- return this.follow(source, imported, exported);
210
- return exported === 'test object' && KINDS.has(imported);
211
- }
212
- /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
213
- importsTestObject(name) {
214
- const from = this.imported.get(name);
215
- return from !== undefined && this.follow(from.source, from.imported);
216
- }
217
- isNamespace(expression) {
218
- const node = unwrap(expression);
219
- return node.type === 'Identifier' && this.namespaces.has(node.name);
220
- }
221
- bindImported(imported, local, version) {
222
- const kind = KINDS.get(imported);
223
- if (kind) {
224
- this.kinds.set(local, kind);
225
- }
226
- else if (isHook(imported)) {
227
- this.hooks.set(local, imported);
228
- }
229
- else if (imported === 'bench' && atLeast(version, 5, 0)) {
230
- this.benches.add(local);
231
- }
232
- }
233
- bind(declarator) {
234
- const { id, init } = declarator;
235
- if (!init)
236
- return false;
237
- if (id.type === 'Identifier' && this.isTestObject(init, false)) {
238
- this.kinds.set(id.name, 'test');
239
- return true;
240
- }
241
- if (id.type !== 'ObjectPattern' || !isImportMetaVitest(init))
242
- return false;
243
- // In-source tests: const { it, describe } = import.meta.vitest
244
- for (const property of id.properties) {
245
- if (property.type === 'RestElement' || property.value.type !== 'Identifier')
246
- continue;
247
- const key = propertyKey(property);
248
- const kind = KINDS.get(key ?? '');
249
- if (kind)
250
- this.kinds.set(property.value.name, kind);
251
- if (key !== null && isHook(key))
252
- this.hooks.set(property.value.name, key);
253
- }
115
+ /**
116
+ * Whether a member of `test` or `describe` reads the function it is called on through `this`, so that it throws when
117
+ * destructured, as each version of Vitest runs it (checked with 3.2.4, 4.0.18, 4.1.12, 5.0.0 and 5.1.0).
118
+ */
119
+ function readsItsFunction(kind, member, version) {
120
+ if (kind === 'describe')
121
+ return member === 'each' || (member === 'for' && atLeast(version, 5, 0));
122
+ if (['each', 'for', 'skipIf', 'runIf'].includes(member))
254
123
  return true;
255
- }
124
+ return ['extend', 'scoped', 'override'].includes(member) && atLeast(version, 4, 1);
256
125
  }
257
- export class VitestAdapter {
258
- problems;
259
- fileTags = [];
260
- fileTagSpans = [];
261
- usesGlobals = false;
262
- file;
263
- readModule;
264
- bindings;
265
- imports;
266
- followed = new Map();
267
- reported = new Set();
268
- /** Variables and names of functions read as test objects on trust, each reported once. */
269
- assumed = new Set();
270
- aliases = new Map();
271
- /** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
272
- suiteParameters = new Map();
126
+ export class VitestAdapter extends SuiteAdapter {
273
127
  warnedAboutTags = false;
274
- constructor(parsed, file, readModule) {
275
- this.file = file;
276
- this.readModule = readModule;
277
- // A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
278
- // a call through the import itself is what reports an import that can't be followed.
279
- const followed = new Map();
280
- this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
281
- const key = `${source}\0${exported}\0${imported}`;
282
- if (!followed.has(key)) {
283
- followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
284
- }
285
- return followed.get(key) === true;
286
- });
287
- this.problems = [...this.bindings.problems];
128
+ /** The length a value in a case title is shortened to, as the config sets it. */
129
+ titleTruncate;
130
+ constructor(parsed, file, readModule, titleTruncate = 40) {
131
+ super(parsed, file, readModule, VITEST, TEST_MEMBERS);
132
+ this.titleTruncate = titleTruncate;
288
133
  if (atLeast(file.version, 5, 0)) {
289
134
  for (const call of nestedHoistedCalls(parsed.program)) {
290
135
  const callee = snippet(text(this.file, call.callee));
291
136
  this.problems.push(problem('removed-api', call, `Vitest 5 fails a file that calls ${callee}(...) anywhere but its top level, because the call is hoisted there.`, callee));
292
137
  }
293
138
  }
294
- this.imports = moduleImports(parsed.program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
295
139
  if (atLeast(file.version, 4, 1)) {
296
140
  // Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
297
141
  for (const match of parsed.source.text.matchAll(/(\/\/|\*)\s*@module-tag\s+([\w\-/]+)\b/g)) {
@@ -302,19 +146,22 @@ export class VitestAdapter {
302
146
  }
303
147
  }
304
148
  }
149
+ isAliasLink(kind, link) {
150
+ return MODIFIERS[kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name);
151
+ }
305
152
  classify(call, context) {
306
153
  return (this.benchCall(call, context) ??
307
154
  this.hookCall(call, context) ??
308
155
  this.contextSkip(call, context) ??
156
+ this.unboundCall(call, context) ??
309
157
  this.chainCall(call, context));
310
158
  }
311
- testFunction(expression, scopes, returnsOf) {
312
- return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
313
- }
314
159
  /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
315
160
  benchCall(call, context) {
316
161
  const base = readChain(call.callee)?.base;
317
- if (base?.type !== 'Identifier' || !this.bindings.benches.has(base.name))
162
+ if (base?.type !== 'Identifier' || this.bindings.members.get(base.name) !== 'bench')
163
+ return null;
164
+ if (!atLeast(this.file.version, 5, 0))
318
165
  return null;
319
166
  const declaration = this.file.values.declarationOf(base.name, context.scopes);
320
167
  if (declaration === null || !this.bindings.declarations.has(declaration))
@@ -325,6 +172,22 @@ export class VitestAdapter {
325
172
  problem: problem('removed-api', call, `Vitest 5 no longer exports bench from 'vitest'; ${callee}(...) fails. Benchmarks are not tests.`, callee),
326
173
  };
327
174
  }
175
+ /**
176
+ * A call of a member Vitest calls on its test or describe function, destructured from it, as `each` in
177
+ * `const { each } = test`: the member reads the function it is called on, which a plain call doesn't give it, so the
178
+ * call throws while the file loads. Which members do that depends on the version.
179
+ */
180
+ unboundCall(call, context) {
181
+ const callee = unwrap(call.callee);
182
+ if (callee.type !== 'Identifier')
183
+ return null;
184
+ const found = this.destructuredMember(callee.name, context);
185
+ if (!found || !readsItsFunction(found.kind, found.member, this.file.version))
186
+ return null;
187
+ const from = snippet(text(this.file, found.from));
188
+ const message = `${callee.name} is destructured from ${from}, so it loses the test object it reads, and Vitest fails the file.`;
189
+ return { kind: 'invalid', problem: problem('invalid-chain', call, message, callee.name) };
190
+ }
328
191
  chainCall(call, context) {
329
192
  const chain = readChain(call.callee);
330
193
  const resolved = chain ? this.resolve(chain, call.arguments, context) : null;
@@ -358,176 +221,6 @@ export class VitestAdapter {
358
221
  // Which of its parts the chain means is not known, so it declares a plain test or describe.
359
222
  return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
360
223
  }
361
- /**
362
- * The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
363
- * followed is reported only outside test and hook bodies, where a call can declare tests.
364
- */
365
- resolve(chain, args, lookup) {
366
- const start = this.start(chain, args, lookup);
367
- if (!start)
368
- return null;
369
- let kind = start.kind;
370
- let links = [...(start.modifiers ?? []), ...chain.links.slice(start.index)];
371
- // A test object derived inline, as in `test.extend({ ... }).skip(...)`.
372
- links = links.slice(links.findLastIndex((link) => DERIVATIONS.has(link.name) && link.args !== null) + 1);
373
- // Vitest's test object also carries `describe` and `suite`, and they carry `test` and `it`.
374
- while (links.length > 0 && KINDS.has(links[0].name) && !links[0].args) {
375
- kind = KINDS.get(links[0].name);
376
- links = links.slice(1);
377
- }
378
- return { kind, links };
379
- }
380
- start(chain, args, lookup) {
381
- const { base, links } = chain;
382
- const { scopes, inBody } = lookup;
383
- if (base.type === 'CallExpression') {
384
- const returned = this.returned(base, links, args, lookup);
385
- return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
386
- }
387
- if (base.type !== 'Identifier')
388
- return null;
389
- // A name the file doesn't declare is one of Vitest's globals; a local variable or parameter with the same name,
390
- // such as `(test) => test.result()`, is not Vitest's.
391
- const declaration = this.file.values.declarationOf(base.name, scopes);
392
- if (declaration === null) {
393
- const global = KINDS.get(base.name);
394
- if (global)
395
- this.usesGlobals = true;
396
- return global ? { kind: global, index: 0 } : null;
397
- }
398
- // A parameter's declaration is its function: `describe('market orders', (test) => { test(...) })`.
399
- if (this.suiteParameters.get(declaration) === base.name)
400
- return { kind: 'test', index: 0 };
401
- const followed = this.imports.get(base.name)?.node === declaration &&
402
- this.worthFollowing(base.name, links, args) &&
403
- this.follow(base.name, !inBody && mayDeclareTests(base.name, links));
404
- if (!followed && !this.bindings.declarations.has(declaration)) {
405
- const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, base.name, lookup) : null;
406
- if (alias)
407
- return { kind: alias.kind, index: 0, modifiers: alias.links };
408
- return this.assumeLocal(declaration, base.name, links, args)
409
- ? { kind: KINDS.get(base.name) ?? 'test', index: 0 }
410
- : null;
411
- }
412
- if (this.bindings.namespaces.has(base.name)) {
413
- const kind = KINDS.get(links.at(0)?.name ?? '');
414
- return kind ? { kind, index: 1 } : null;
415
- }
416
- const kind = this.bindings.kinds.get(base.name);
417
- return kind ? { kind, index: 0 } : null;
418
- }
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;
428
- let alias = this.aliases.get(declarator);
429
- if (alias === undefined) {
430
- // Set first, so a variable defined through itself ends the lookup.
431
- this.aliases.set(declarator, null);
432
- alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
433
- this.aliases.set(declarator, alias);
434
- }
435
- const links = destructured.map((member) => ({ name: member, args: null, template: null }));
436
- return alias && { kind: alias.kind, links: [...alias.links, ...links] };
437
- }
438
- aliasOf(expression, lookup) {
439
- const node = unwrap(expression);
440
- if (node.type === 'ConditionalExpression') {
441
- // `isCI ? describe.concurrent : describe`: only the modifiers both branches have apply.
442
- return shared(this.aliasOf(node.consequent, lookup), this.aliasOf(node.alternate, lookup));
443
- }
444
- const chain = readChain(node);
445
- const resolved = chain ? this.resolve(chain, [], lookup) : null;
446
- const allowed = (link) => resolved !== null &&
447
- (MODIFIERS[resolved.kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name));
448
- return resolved?.links.every(allowed) ? resolved : null;
449
- }
450
- /**
451
- * The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
452
- * function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
453
- * named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
454
- */
455
- returned(call, links, args, lookup) {
456
- const callee = unwrap(call.callee);
457
- if (callee.type !== 'Identifier')
458
- return null;
459
- const returns = lookup.returnsOf(callee.name, lookup.scopes);
460
- if (returns)
461
- return returnedFunction(returns, shared);
462
- if (!isTestName(callee.name) || !this.looksLikeTest(links, args))
463
- return null;
464
- if (!this.assumed.has(callee.name)) {
465
- this.assumed.add(callee.name);
466
- this.problems.push(returnedTestObject(this.file, call));
467
- }
468
- return { kind: KINDS.get(callee.name) ?? 'test', links: [] };
469
- }
470
- /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
471
- assumeLocal(declaration, name, links, args) {
472
- if (declaration.type !== 'VariableDeclarator' || !mayHoldTestObject(declaration) || !isTestName(name))
473
- return false;
474
- if (!this.looksLikeTest(links, args))
475
- return false;
476
- if (!this.assumed.has(declaration)) {
477
- this.assumed.add(declaration);
478
- this.problems.push(localTestObject(this.file, name, declaration));
479
- }
480
- return true;
481
- }
482
- looksLikeTest(links, args) {
483
- return links.length === 0 ? looksLikeDeclaration(args) : TEST_MEMBERS.has(links[0].name);
484
- }
485
- /**
486
- * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
487
- * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
488
- * a package extends.
489
- */
490
- worthFollowing(name, links, args) {
491
- return this.looksLikeTest(links, args) || (isTestName(name) && looksLikeDeclaration(args));
492
- }
493
- /** Vitest as followed modules are read, with the rules of the scanned version. */
494
- framework() {
495
- const version = this.file.version;
496
- return {
497
- name: 'Vitest',
498
- isSource: (source) => source === 'vitest',
499
- testObjects: (program, follow) => new VitestBindings(program, version, follow),
500
- };
501
- }
502
- /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
503
- follow(name, report) {
504
- const imported = this.imports.get(name);
505
- let result = this.followed.get(name);
506
- if (!result) {
507
- result = followImport(name, imported, this.readModule, this.file, this.framework());
508
- this.followed.set(name, result);
509
- if (result.exported) {
510
- this.bindings.kinds.set(name, KINDS.get(imported.imported) ?? 'test');
511
- this.bindings.declarations.add(imported.node);
512
- }
513
- }
514
- if (report && result.problem && !this.reported.has(name)) {
515
- this.reported.add(name);
516
- this.problems.push(result.problem);
517
- }
518
- return result.exported;
519
- }
520
- /** Hooks called directly, such as `beforeEach(...)` imported from Vitest or used as a global. */
521
- hookCall(call, context) {
522
- const callee = unwrap(call.callee);
523
- if (callee.type !== 'Identifier')
524
- return null;
525
- const declaration = this.file.values.declarationOf(callee.name, context.scopes);
526
- const hook = declaration === null
527
- ? isHook(callee.name) && callee.name
528
- : this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
529
- return hook ? this.hook(call, hook, context) : null;
530
- }
531
224
  hook(call, name, context) {
532
225
  const fn = hookFunction(this.file, context.scopes, call.arguments);
533
226
  const index = HOOKS.get(name);
@@ -703,7 +396,7 @@ export class VitestAdapter {
703
396
  hashed: fn ?? namedFunction(this.file, context.scopes, body),
704
397
  isParameterized: table !== null,
705
398
  caseCount: table ? this.caseCount(call, table, context, problems) : null,
706
- cases: table ? this.tableCases(table, context) : null,
399
+ cases: table ? this.tableCases(table, title, context) : null,
707
400
  problems,
708
401
  };
709
402
  let declared;
@@ -827,12 +520,12 @@ export class VitestAdapter {
827
520
  if (meta)
828
521
  details.meta = this.meta(meta, context, problems);
829
522
  for (const key of ['timeout', 'repeats']) {
830
- const option = options.values.get(key);
523
+ const option = given(key);
831
524
  const value = option ? readCount(this.file, key, option, problems) : null;
832
525
  if (value !== null)
833
526
  details.run[key] = value;
834
527
  }
835
- const retry = options.values.get('retry');
528
+ const retry = given('retry');
836
529
  if (retry)
837
530
  this.retry(retry, details, context, problems);
838
531
  return result;
@@ -901,7 +594,7 @@ export class VitestAdapter {
901
594
  * array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
902
595
  * gives one object per row, keyed by the names in its first line.
903
596
  */
904
- tableCases(table, context) {
597
+ tableCases(table, title, context) {
905
598
  let cases;
906
599
  if (table.template) {
907
600
  const values = table.template.expressions.map((item) => this.file.values.resolve(item, context.scopes));
@@ -921,32 +614,19 @@ export class VitestAdapter {
921
614
  return null;
922
615
  cases = resolved.value;
923
616
  }
617
+ const caseTitle = (items, index) => title.isDynamic ? null : vitestCaseTitle(title.text, items, index, this.file.version, this.titleTruncate);
924
618
  if (table.name === 'for') {
925
- return cases.map((item) => ({
926
- items: item === null || item === undefined ? [] : Array.isArray(item) ? item : [item],
619
+ return cases.map((item, index) => ({
620
+ title: caseTitle(item === null || item === undefined ? [] : Array.isArray(item) ? item : [item], index),
927
621
  args: [item],
928
622
  }));
929
623
  }
930
624
  const arraysOnly = cases.every((item) => Array.isArray(item));
931
- return cases.map((item) => {
625
+ return cases.map((item, index) => {
932
626
  const items = Array.isArray(item) ? item : [item];
933
- return { items, args: arraysOnly ? items : [item] };
627
+ return { title: caseTitle(items, index), args: arraysOnly ? items : [item] };
934
628
  });
935
629
  }
936
- arrayLength(node, context) {
937
- if (node === undefined || node.type === 'SpreadElement')
938
- return null;
939
- const resolved = this.file.values.resolve(node, context.scopes);
940
- if (resolved.ok && Array.isArray(resolved.value))
941
- return resolved.value.length;
942
- const expression = unwrap(node);
943
- // The cases may hold values that can't be worked out, such as functions, while the list itself is written out.
944
- if (expression.type === 'ArrayExpression' &&
945
- expression.elements.every((element) => element?.type !== 'SpreadElement')) {
946
- return expression.elements.length;
947
- }
948
- return null;
949
- }
950
630
  tagsUsed(node) {
951
631
  if (this.warnedAboutTags)
952
632
  return;
@@ -0,0 +1,27 @@
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 { FileContext } from './common.ts';
5
+ import { JasmineAdapter } from './jasmine.ts';
6
+ import { MochaAdapter, type MochaInterface } from './mocha.ts';
7
+ /** The framework a WebdriverIO config runs its specs with; Cucumber's are `.feature` files, which the scan doesn't read. */
8
+ export type WebdriverIOFramework = 'mocha' | 'jasmine';
9
+ /**
10
+ * WebdriverIO on Mocha: Mocha's calls, where a number after a test's function is a retry count. WebdriverIO catches
11
+ * a call Mocha refuses while it loads the file, runs the tests declared before it, and then fails the file.
12
+ */
13
+ export declare class WebdriverIOMochaAdapter extends MochaAdapter {
14
+ protected readonly trailingRetries = true;
15
+ protected readonly refusedEffect = "WebdriverIO runs only the tests declared above it in the file, and none of the spec files after it in its worker";
16
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null, ui?: MochaInterface);
17
+ classify(call: CallExpression, context: CallContext): ApiCall | null;
18
+ }
19
+ /**
20
+ * WebdriverIO on Jasmine: Jasmine's calls, where a second number after a spec's function, after the timeout, is a
21
+ * retry count. A call Jasmine refuses at the top level of a file fails the whole worker that runs it.
22
+ */
23
+ export declare class WebdriverIOJasmineAdapter extends JasmineAdapter {
24
+ protected readonly trailingRetries = true;
25
+ protected readonly refusedEffect = "WebdriverIO fails the worker that runs the spec file, which runs none of its specs, nor those of the files grouped with it";
26
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
27
+ }
@@ -0,0 +1,37 @@
1
+ import { JasmineAdapter } from "./jasmine.js";
2
+ import { MochaAdapter } from "./mocha.js";
3
+ /**
4
+ * The file as the framework WebdriverIO runs it with sees it: the version is that of the Mocha or Jasmine its adapter
5
+ * depends on, Mocha 12 and Jasmine 6 from WebdriverIO 10, Mocha 10.8 and Jasmine 5 before.
6
+ */
7
+ function bundled(file, from10, before) {
8
+ if (file.version === null)
9
+ return file;
10
+ return { ...file, version: file.version[0] >= 10 ? from10 : before };
11
+ }
12
+ /**
13
+ * WebdriverIO on Mocha: Mocha's calls, where a number after a test's function is a retry count. WebdriverIO catches
14
+ * a call Mocha refuses while it loads the file, runs the tests declared before it, and then fails the file.
15
+ */
16
+ export class WebdriverIOMochaAdapter extends MochaAdapter {
17
+ trailingRetries = true;
18
+ refusedEffect = 'WebdriverIO runs only the tests declared above it in the file, and none of the spec files after it in its worker';
19
+ constructor(parsed, file, readModule, ui = 'bdd') {
20
+ super(parsed, bundled(file, [12, 0], [10, 8]), readModule, ui);
21
+ }
22
+ classify(call, context) {
23
+ const found = super.classify(call, context);
24
+ return found?.kind === 'invalid' ? { ...found, problem: { ...found.problem, keepsEarlierTests: true } } : found;
25
+ }
26
+ }
27
+ /**
28
+ * WebdriverIO on Jasmine: Jasmine's calls, where a second number after a spec's function, after the timeout, is a
29
+ * retry count. A call Jasmine refuses at the top level of a file fails the whole worker that runs it.
30
+ */
31
+ export class WebdriverIOJasmineAdapter extends JasmineAdapter {
32
+ trailingRetries = true;
33
+ refusedEffect = 'WebdriverIO fails the worker that runs the spec file, which runs none of its specs, nor those of the files grouped with it';
34
+ constructor(parsed, file, readModule) {
35
+ super(parsed, bundled(file, [6, 3], [5, 0]), readModule);
36
+ }
37
+ }
@@ -1,4 +1,4 @@
1
- import { ownExport, parseModule, starReexport } from "./frameworks/bindings.js";
1
+ import { commonJsExport, commonJsNames, ownExport, parseModule, starReexport } from "./frameworks/bindings.js";
2
2
  import { Values } from "./values.js";
3
3
  const UNRESOLVED = { ok: false, importedFrom: null };
4
4
  /** How many modules deep a name is followed through re-exports and imports, as for test objects. */
@@ -81,6 +81,20 @@ export class ExportedValues {
81
81
  if (own) {
82
82
  return opened.values.resolveName(own.kind === 'specifier' ? own.local : name, [program]);
83
83
  }
84
+ const commonJs = commonJsExport(program, name);
85
+ if (commonJs)
86
+ return opened.values.resolve(commonJs, [program]);
87
+ // What `require()` gives for a module that exports its names one by one: an object of them.
88
+ const names = name === 'default' ? commonJsNames(program) : [];
89
+ if (names.length > 0) {
90
+ const entries = names.map((item) => [item, opened.values.resolve(commonJsExport(program, item), [program])]);
91
+ if (!entries.every(([, value]) => value.ok))
92
+ return UNRESOLVED;
93
+ return {
94
+ ok: true,
95
+ value: Object.fromEntries(entries.map(([key, value]) => [key, value.value])),
96
+ };
97
+ }
84
98
  // `export *` passes on every name but the default. When two modules export the name with different values, Node
85
99
  // refuses to load the module and Vite's module runner keeps the first, so the value isn't known.
86
100
  if (name === 'default')