test-inventory 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
- import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, requiredModule, } from "./bindings.js";
4
- import { atLeast, findFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, readCondition, readOptions, readReason, readStrings, readTitle, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
3
+ import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
+ import { atLeast, findFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
5
5
  const MISMATCHED_SOURCES = new Set(['vitest', '@jest/globals']);
6
6
  // Maps rather than object literals, so names such as `constructor` don't match inherited properties.
7
7
  const DESCRIBES = new Map([
@@ -24,6 +24,10 @@ const STATES = new Map([
24
24
  ]);
25
25
  /** Members of `test` that are known but don't declare or modify tests. */
26
26
  const OTHER_MEMBERS = new Set(['use', 'slow', 'setTimeout', 'extend']);
27
+ /** Members that set how the tests run where they are called. */
28
+ const RUN_MEMBERS = new Set(['slow', 'setTimeout']);
29
+ /** First members of chains that never declare a test, even when Playwright refuses the rest of the chain. */
30
+ const NOT_DECLARING = new Set(['step', 'abort', ...HOOKS, ...OTHER_MEMBERS]);
27
31
  /** First members that show a name imported from a local file is used as Playwright's `test`. */
28
32
  const TEST_MEMBERS = new Set(['describe', 'only', 'skip', 'fixme', 'fail', 'step', ...HOOKS]);
29
33
  /** The members of `test` a value chosen at run time holds either way: those both have, if both are the same kind. */
@@ -57,9 +61,12 @@ class PlaywrightBindings {
57
61
  /** Names imported from other modules, which may be test objects there. */
58
62
  imported;
59
63
  follow;
64
+ /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
65
+ functions;
60
66
  constructor(program, follow) {
61
67
  this.imported = moduleImports(program, (source) => isPlaywrightSource(source) || MISMATCHED_SOURCES.has(source));
62
68
  this.follow = follow;
69
+ this.functions = new ModuleFunctions(program, this.imported, follow);
63
70
  for (const statement of program.body) {
64
71
  if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
65
72
  continue;
@@ -85,41 +92,59 @@ class PlaywrightBindings {
85
92
  }
86
93
  }
87
94
  }
88
- bindAll(allDeclarators(program), (declarator) => {
95
+ const declarators = allDeclarators(program);
96
+ bindAll(declarators, (declarator) => {
89
97
  const bound = this.bind(declarator);
90
98
  if (bound)
91
99
  this.declarations.add(declarator);
92
100
  return bound;
93
101
  });
102
+ this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
94
103
  }
95
104
  isTestName(name) {
96
- return this.testNames.has(name) || this.importsTestObject(name);
105
+ return this.testNames.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
97
106
  }
98
- isTestObject(expression) {
107
+ /**
108
+ * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
109
+ * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
110
+ * follows in their scopes.
111
+ */
112
+ isTestObject(expression, followsCalls = true) {
99
113
  const node = unwrap(expression);
100
114
  switch (node.type) {
101
115
  case 'Identifier':
102
- return this.testNames.has(node.name) || this.importsTestObject(node.name);
116
+ return (this.testNames.has(node.name) ||
117
+ this.importsTestObject(node.name) ||
118
+ (followsCalls && this.holdsTestObject(node.name)));
103
119
  case 'MemberExpression':
104
120
  return propertyName(node) === 'test' && this.isModule(node.object);
105
121
  case 'ConditionalExpression':
106
122
  // `process.env.CI ? ciTest : test` is a test object either way.
107
- return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
123
+ return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
108
124
  case 'CallExpression': {
109
125
  const callee = unwrap(node.callee);
110
126
  if (callee.type === 'MemberExpression' && propertyName(callee) === 'extend') {
111
- return this.isTestObject(callee.object);
127
+ return this.isTestObject(callee.object, followsCalls);
112
128
  }
129
+ // `mergeTests(a, b)`, or `defineBrokerTest()`, a function that returns a test object.
113
130
  return callee.type === 'Identifier'
114
- ? this.mergeTestsNames.has(callee.name)
131
+ ? this.mergeTestsNames.has(callee.name) || (followsCalls && this.returnsTestObject(callee.name))
115
132
  : callee.type === 'MemberExpression' && propertyName(callee) === 'mergeTests' && this.isModule(callee.object);
116
133
  }
117
134
  default:
118
135
  return false;
119
136
  }
120
137
  }
121
- reexports(source, imported) {
122
- return isPlaywrightSource(source) ? imported === 'test' || imported === 'default' : this.follow(source, imported);
138
+ returnsTestObject(name) {
139
+ return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
140
+ }
141
+ holdsTestObject(name) {
142
+ return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
143
+ }
144
+ reexports(source, imported, exported) {
145
+ if (!isPlaywrightSource(source))
146
+ return this.follow(source, imported, exported);
147
+ return exported === 'test object' && (imported === 'test' || imported === 'default');
123
148
  }
124
149
  /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
125
150
  importsTestObject(name) {
@@ -148,7 +173,7 @@ class PlaywrightBindings {
148
173
  }
149
174
  return true;
150
175
  }
151
- if (this.isTestObject(init)) {
176
+ if (this.isTestObject(init, false)) {
152
177
  this.testNames.add(id.name);
153
178
  return true;
154
179
  }
@@ -192,10 +217,11 @@ export class PlaywrightAdapter {
192
217
  // A test object the file builds from an imported one, as in `const test = base.extend({...})`, is followed
193
218
  // quietly: a call through the import itself is what reports an import that can't be followed.
194
219
  const followed = new Map();
195
- this.bindings = new PlaywrightBindings(program, (source, imported) => {
196
- const key = `${source}\0${imported}`;
197
- if (!followed.has(key))
198
- followed.set(key, followModule(source, imported, readModule, PLAYWRIGHT).exported);
220
+ this.bindings = new PlaywrightBindings(program, (source, imported, exported = 'test object') => {
221
+ const key = `${source}\0${exported}\0${imported}`;
222
+ if (!followed.has(key)) {
223
+ followed.set(key, followModule(source, imported, readModule, PLAYWRIGHT, exported).exported);
224
+ }
199
225
  return followed.get(key) === true;
200
226
  });
201
227
  this.problems = [...this.bindings.mismatches];
@@ -210,14 +236,18 @@ export class PlaywrightAdapter {
210
236
  const key = names.join('.');
211
237
  const addedIn = ADDED_IN.get(key);
212
238
  if (addedIn && !atLeast(this.file.version, ...addedIn)) {
213
- return this.invalid(call, `needs Playwright ${addedIn.join('.')} or newer`);
239
+ return this.invalid(call, names, `needs Playwright ${addedIn.join('.')} or newer`, context);
214
240
  }
215
241
  if (names[0] === 'info')
216
242
  return this.annotationsPush(call, links, context);
243
+ if ((key === 'step' || key === 'step.skip') && context.inTest && call.arguments.length > 0) {
244
+ return this.step(call, key === 'step.skip', context);
245
+ }
217
246
  if (names[0] === 'expect' || names[0] === 'step' || key === 'abort')
218
247
  return { kind: 'other' };
219
- if (links.some((link) => link.args ?? link.template))
220
- return this.invalid(call, "is not part of Playwright's API");
248
+ if (links.some((link) => link.args ?? link.template)) {
249
+ return this.invalid(call, names, "is not part of Playwright's API", context);
250
+ }
221
251
  if (key === '' || key === 'only' || key === 'skip' || key === 'fixme' || key === 'fail' || key === 'fail.only') {
222
252
  return this.testCall(call, key, context);
223
253
  }
@@ -226,11 +256,15 @@ export class PlaywrightAdapter {
226
256
  return this.describeCall(call, describe, context);
227
257
  if (key === 'describe.configure')
228
258
  return this.configure(call, context);
229
- if (HOOKS.has(key))
230
- return { kind: 'hook', fn: findFunction(call.arguments), testContext: null };
259
+ if (HOOKS.has(key)) {
260
+ const fn = findFunction(call.arguments);
261
+ return { kind: 'hook', name: key, fn, testContext: null, fixtures: destructuredNames(fn, 0) };
262
+ }
263
+ if (RUN_MEMBERS.has(key))
264
+ return this.runSetting(call, key, context);
231
265
  if (OTHER_MEMBERS.has(key))
232
266
  return { kind: 'other' };
233
- return this.invalid(call, "is not part of Playwright's API");
267
+ return this.invalid(call, names, "is not part of Playwright's API", context);
234
268
  }
235
269
  /**
236
270
  * The links after the `test` object, or null when the chain doesn't start from Playwright's `test`. An import that
@@ -283,7 +317,7 @@ export class PlaywrightAdapter {
283
317
  */
284
318
  returned(call, links, args, lookup) {
285
319
  const callee = unwrap(call.callee);
286
- if (callee.type !== 'Identifier' || !lookup.returnsOf)
320
+ if (callee.type !== 'Identifier')
287
321
  return null;
288
322
  const returns = lookup.returnsOf(callee.name, lookup.scopes);
289
323
  if (returns)
@@ -296,8 +330,8 @@ export class PlaywrightAdapter {
296
330
  }
297
331
  return [];
298
332
  }
299
- testFunction(expression, scopes) {
300
- return this.members(expression, { scopes, inBody: false, returnsOf: null });
333
+ testFunction(expression, scopes, returnsOf) {
334
+ return this.members(expression, { scopes, inBody: false, returnsOf });
301
335
  }
302
336
  /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
303
337
  assumeLocal(declaration, name, links, args) {
@@ -343,7 +377,7 @@ export class PlaywrightAdapter {
343
377
  // `IS_PLATFORM ? test.describe.serial : test.describe`: only the members both branches have apply.
344
378
  return sharedMembers(this.members(node.consequent, lookup), this.members(node.alternate, lookup));
345
379
  }
346
- if (this.bindings.isTestObject(node))
380
+ if (this.bindings.isTestObject(node, false))
347
381
  return [];
348
382
  const chain = readChain(node);
349
383
  if (!chain || chain.links.some((link) => link.args ?? link.template))
@@ -376,9 +410,15 @@ export class PlaywrightAdapter {
376
410
  const resolved = this.file.values.resolve(node, context.scopes);
377
411
  return resolved.ok && typeof resolved.value === 'string';
378
412
  }
379
- invalid(call, reason) {
413
+ /** A chain Playwright refuses; one that starts like a declaration also declares a plain test or describe. */
414
+ invalid(call, names, reason, context) {
380
415
  const callee = snippet(text(this.file, call.callee));
381
- return { kind: 'invalid', problem: problem('invalid-chain', call, `${callee} ${reason}.`, callee) };
416
+ const refusal = problem('invalid-chain', call, `${callee} ${reason}.`, callee);
417
+ return refused(call, refusal, context, () => {
418
+ if (NOT_DECLARING.has(names[0]))
419
+ return { kind: 'other' };
420
+ return names.includes('describe') ? this.describeCall(call, {}, context) : this.testCall(call, '', context);
421
+ });
382
422
  }
383
423
  testCall(call, key, context) {
384
424
  const args = call.arguments;
@@ -420,6 +460,7 @@ export class PlaywrightAdapter {
420
460
  problems.push(titleProblem);
421
461
  const line = this.file.source.line(call.start);
422
462
  const fn = isFunctionNode(last) ? last : null;
463
+ const hashed = fn ?? namedFunction(this.file, context.scopes, last);
423
464
  return {
424
465
  kind: 'test',
425
466
  title,
@@ -428,13 +469,23 @@ export class PlaywrightAdapter {
428
469
  mode: null,
429
470
  details: this.details(args.length === 3 ? args[1] : undefined, context, problems, title.text),
430
471
  fn,
431
- hashed: fn ?? namedFunction(this.file, context.scopes, last),
472
+ hashed,
432
473
  isParameterized: false,
433
474
  caseCount: null,
434
475
  testContext: null,
476
+ fixtures: destructuredNames(hashed, 0),
477
+ usesBench: false,
435
478
  problems,
436
479
  };
437
480
  }
481
+ /** `test.step(title, body)` in a test body; a title that can't be worked out is kept as source text. */
482
+ step(call, isSkipped, context) {
483
+ const { title } = readTitle(this.file, context.scopes, call.arguments[0], {
484
+ stripTags: false,
485
+ functionNames: false,
486
+ });
487
+ return { kind: 'step', title, isSkipped, fn: findFunction(call.arguments, 1) };
488
+ }
438
489
  modifier(call, state, context) {
439
490
  const [condition, description] = [call.arguments.at(0), call.arguments.at(1)];
440
491
  const problems = [];
@@ -479,6 +530,7 @@ export class PlaywrightAdapter {
479
530
  isOnly: kind.only === true,
480
531
  mode: kind.mode ?? null,
481
532
  isShuffled: false,
533
+ passesDown: null,
482
534
  details: this.details(!anonymous && args.length === 3 ? args[1] : undefined, context, problems, title?.text ?? ''),
483
535
  fn,
484
536
  hashed: fn,
@@ -510,10 +562,37 @@ export class PlaywrightAdapter {
510
562
  problems.push(unresolvedOption(this.file, 'lock', unresolved, importedFrom, 'the lock is missing'));
511
563
  }
512
564
  }
513
- if (!mode && locks.length === 0) {
565
+ const run = {};
566
+ const timeout = options?.values.get('timeout');
567
+ const timeoutValue = timeout ? readCount(this.file, 'timeout', timeout, problems) : null;
568
+ if (timeoutValue !== null)
569
+ run.timeout = timeoutValue;
570
+ const retries = options?.values.get('retries');
571
+ const count = retries ? readCount(this.file, 'retries', retries, problems) : null;
572
+ if (count !== null)
573
+ run.retry = { count, delay: null, condition: null };
574
+ if (!mode && locks.length === 0 && Object.keys(run).length === 0) {
514
575
  return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
515
576
  }
516
- return { kind: 'configure', mode, locks, problems };
577
+ return { kind: 'configure', mode, locks, run, problems };
578
+ }
579
+ /** `test.setTimeout(ms)` or `test.slow(condition?, description?)`, which apply where they are called. */
580
+ runSetting(call, key, context) {
581
+ const problems = [];
582
+ const settings = {};
583
+ const first = call.arguments.at(0);
584
+ if (key === 'slow') {
585
+ const condition = readCondition(this.file, context.scopes, first);
586
+ if (condition.applies !== 'never')
587
+ settings.slow = { conditions: condition.text === null ? null : [condition.text] };
588
+ }
589
+ else if (first && first.type !== 'SpreadElement') {
590
+ const option = { node: first, resolved: this.file.values.resolve(first, context.scopes) };
591
+ const timeout = readCount(this.file, 'timeout', option, problems);
592
+ if (timeout !== null)
593
+ settings.timeout = timeout;
594
+ }
595
+ return { kind: 'run', settings, argument: first ? snippet(text(this.file, first)) : null, problems };
517
596
  }
518
597
  /** `test.info().annotations.push(...)` inside a test body. */
519
598
  annotationsPush(call, links, context) {
@@ -529,7 +608,7 @@ export class PlaywrightAdapter {
529
608
  }
530
609
  /** Reads the `{ tag, annotation, lock }` details of a test or describe. */
531
610
  details(node, context, problems, title) {
532
- const details = { tags: titleTags(title), annotations: [], locks: [], meta: null };
611
+ const details = { tags: titleTags(title), annotations: [], locks: [], meta: null, run: {} };
533
612
  if (!node)
534
613
  return details;
535
614
  const options = readOptions(this.file, context.scopes, node);
@@ -26,7 +26,7 @@ export declare class VitestAdapter implements Adapter {
26
26
  private warnedAboutTags;
27
27
  constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
28
28
  classify(call: CallExpression, context: CallContext): ApiCall | null;
29
- testFunction(expression: Expression, scopes: readonly Node[]): TestFunction | null;
29
+ testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
30
30
  /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
31
31
  private benchCall;
32
32
  private chainCall;
@@ -62,6 +62,11 @@ export declare class VitestAdapter implements Adapter {
62
62
  private hookCall;
63
63
  private hook;
64
64
  private contextSkip;
65
+ /**
66
+ * `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
67
+ * second argument may also be an attachment.
68
+ */
69
+ private annotate;
65
70
  private declaration;
66
71
  /** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
67
72
  private isOptions;
@@ -69,6 +74,8 @@ export declare class VitestAdapter implements Adapter {
69
74
  /** Vitest's order: `only` wins over `skip`, which wins over `todo`. */
70
75
  private states;
71
76
  private readDeclarationOptions;
77
+ /** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`; the runner ignores an object before. */
78
+ private retry;
72
79
  private meta;
73
80
  private caseCount;
74
81
  private arrayLength;