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.
- package/CHANGELOG.md +129 -0
- package/README.md +16 -11
- package/dist/case-titles.d.ts +9 -0
- package/dist/case-titles.js +82 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -2
- package/dist/config-dependencies.js +19 -0
- package/dist/config-runs.d.ts +95 -3
- package/dist/config-runs.js +752 -16
- package/dist/config.d.ts +24 -0
- package/dist/config.js +139 -19
- package/dist/diagnostics.js +14 -1
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +36 -10
- package/dist/files.js +2 -1
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +4 -2
- package/dist/framework-version.js +170 -15
- package/dist/frameworks/bun.d.ts +42 -0
- package/dist/frameworks/bun.js +272 -0
- package/dist/frameworks/common.d.ts +3 -1
- package/dist/frameworks/common.js +11 -9
- package/dist/frameworks/cypress.d.ts +31 -0
- package/dist/frameworks/cypress.js +70 -0
- package/dist/frameworks/jasmine.d.ts +28 -0
- package/dist/frameworks/jasmine.js +218 -0
- package/dist/frameworks/jest.js +1 -1
- package/dist/frameworks/mocha.d.ts +50 -0
- package/dist/frameworks/mocha.js +306 -0
- package/dist/frameworks/node.d.ts +30 -0
- package/dist/frameworks/node.js +268 -0
- package/dist/frameworks/playwright.d.ts +13 -0
- package/dist/frameworks/playwright.js +57 -0
- package/dist/frameworks/test-functions.d.ts +26 -1
- package/dist/frameworks/test-functions.js +64 -22
- package/dist/frameworks/vitest.d.ts +9 -1
- package/dist/frameworks/vitest.js +36 -5
- package/dist/frameworks/webdriverio.d.ts +27 -0
- package/dist/frameworks/webdriverio.js +37 -0
- package/dist/model.d.ts +4 -0
- package/dist/node-scripts.d.ts +6 -0
- package/dist/node-scripts.js +85 -0
- package/dist/options.d.ts +13 -4
- package/dist/options.js +22 -3
- package/dist/records.d.ts +8 -2
- package/dist/records.js +50 -13
- package/dist/resolve.d.ts +21 -4
- package/dist/resolve.js +52 -16
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +16 -2
- package/dist/scan.js +42 -7
- package/dist/toml.d.ts +13 -0
- package/dist/toml.js +172 -0
- package/dist/types.d.ts +19 -9
- package/dist/values.d.ts +5 -0
- package/dist/values.js +13 -0
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +148 -0
- package/package.json +7 -6
- package/schema/v1.json +26 -6
|
@@ -209,6 +209,7 @@ export class PlaywrightAdapter {
|
|
|
209
209
|
imports;
|
|
210
210
|
followed = new Map();
|
|
211
211
|
reported = new Set();
|
|
212
|
+
refusedRests = new Set();
|
|
212
213
|
/** Variables and names of functions read as test objects on trust, each reported once. */
|
|
213
214
|
assumed = new Set();
|
|
214
215
|
aliases = new Map();
|
|
@@ -259,6 +260,7 @@ export class PlaywrightAdapter {
|
|
|
259
260
|
return this.configure(call, context);
|
|
260
261
|
if (HOOKS.has(key)) {
|
|
261
262
|
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
263
|
+
this.checkFixtureArguments(fn);
|
|
262
264
|
return {
|
|
263
265
|
kind: 'hook',
|
|
264
266
|
name: key,
|
|
@@ -270,6 +272,8 @@ export class PlaywrightAdapter {
|
|
|
270
272
|
}
|
|
271
273
|
if (RUN_MEMBERS.has(key))
|
|
272
274
|
return this.runSetting(call, key, context);
|
|
275
|
+
if (key === 'extend')
|
|
276
|
+
this.checkExtendFixtures(call, context);
|
|
273
277
|
if (OTHER_MEMBERS.has(key))
|
|
274
278
|
return { kind: 'other' };
|
|
275
279
|
return this.invalid(call, names, "is not part of Playwright's API", context);
|
|
@@ -476,6 +480,7 @@ export class PlaywrightAdapter {
|
|
|
476
480
|
const line = this.file.source.line(call.start);
|
|
477
481
|
const fn = isFunctionNode(last) ? last : null;
|
|
478
482
|
const hashed = fn ?? namedFunction(this.file, context.scopes, last);
|
|
483
|
+
this.checkFixtureArguments(hashed);
|
|
479
484
|
return {
|
|
480
485
|
kind: 'test',
|
|
481
486
|
title,
|
|
@@ -495,6 +500,56 @@ export class PlaywrightAdapter {
|
|
|
495
500
|
problems,
|
|
496
501
|
};
|
|
497
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Playwright reads which fixtures a function takes from its source, and refuses a rest property among them, as
|
|
505
|
+
* `({ page, ...fixtures })`, failing the whole run. A nested one, as `({ page: { mouse, ...rest } })`, is allowed.
|
|
506
|
+
*/
|
|
507
|
+
checkFixtureArguments(fn) {
|
|
508
|
+
const parameter = fn?.params[0];
|
|
509
|
+
const rest = parameter?.type === 'ObjectPattern' ? parameter.properties.find((item) => item.type === 'RestElement') : null;
|
|
510
|
+
if (!rest || this.refusedRests.has(rest))
|
|
511
|
+
return;
|
|
512
|
+
this.refusedRests.add(rest);
|
|
513
|
+
const source = snippet(text(this.file, rest));
|
|
514
|
+
const message = `Playwright refuses the rest property ${source} in fixture arguments, so it fails to load the file and runs no tests at all.`;
|
|
515
|
+
this.problems.push(problem('invalid-fixture-argument', rest, message, source));
|
|
516
|
+
}
|
|
517
|
+
/** The fixture functions of `test.extend({ ... })`, each given as a function or as `[function, options]`. */
|
|
518
|
+
checkExtendFixtures(call, context) {
|
|
519
|
+
const argument = call.arguments.at(0);
|
|
520
|
+
if (argument && argument.type !== 'SpreadElement')
|
|
521
|
+
this.checkFixtureObject(argument, context.scopes, new Set());
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* An object of fixtures, written in place or held in a constant, with the objects it spreads; `seen` ends a constant
|
|
525
|
+
* that spreads itself, which throws at run time.
|
|
526
|
+
*/
|
|
527
|
+
checkFixtureObject(expression, scopes, seen) {
|
|
528
|
+
const node = unwrap(expression);
|
|
529
|
+
if (seen.has(node))
|
|
530
|
+
return;
|
|
531
|
+
seen.add(node);
|
|
532
|
+
if (node.type === 'Identifier') {
|
|
533
|
+
const constant = this.file.values.constantOf(node.name, scopes);
|
|
534
|
+
if (constant)
|
|
535
|
+
this.checkFixtureObject(constant.init, constant.scopes, seen);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (node.type !== 'ObjectExpression')
|
|
539
|
+
return;
|
|
540
|
+
for (const property of node.properties) {
|
|
541
|
+
if (property.type === 'SpreadElement') {
|
|
542
|
+
this.checkFixtureObject(property.argument, scopes, seen);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const value = unwrap(property.value);
|
|
546
|
+
const fn = value.type === 'ArrayExpression' ? value.elements[0] : value;
|
|
547
|
+
if (isFunctionNode(fn))
|
|
548
|
+
this.checkFixtureArguments(fn);
|
|
549
|
+
else if (fn?.type === 'Identifier')
|
|
550
|
+
this.checkFixtureArguments(this.file.values.functionOf(fn.name, scopes));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
498
553
|
/** `test.step(title, body)` in a test body; a title that can't be worked out is kept as source text. */
|
|
499
554
|
step(call, isSkipped, context) {
|
|
500
555
|
const { title } = readTitle(this.file, context.scopes, call.arguments[0], {
|
|
@@ -505,6 +560,8 @@ export class PlaywrightAdapter {
|
|
|
505
560
|
}
|
|
506
561
|
modifier(call, state, context) {
|
|
507
562
|
const [condition, description] = [call.arguments.at(0), call.arguments.at(1)];
|
|
563
|
+
if (isFunctionNode(condition))
|
|
564
|
+
this.checkFixtureArguments(condition);
|
|
508
565
|
const problems = [];
|
|
509
566
|
if (isTitleLiteral(condition)) {
|
|
510
567
|
const callee = snippet(text(this.file, call.callee));
|
|
@@ -27,7 +27,8 @@ export interface Dialect {
|
|
|
27
27
|
mismatched: ReadonlySet<string>;
|
|
28
28
|
/** The names that start a chain, as exported and as globals; a map, so `constructor` matches nothing inherited. */
|
|
29
29
|
starts: ReadonlyMap<string, Start>;
|
|
30
|
-
|
|
30
|
+
/** Each hook's name in code, and the name it is counted under, as Mocha's `before` is counted as `beforeAll`. */
|
|
31
|
+
hooks: ReadonlyMap<string, keyof HookCounts>;
|
|
31
32
|
/** Members that build a test object from another, such as Vitest's `extend`. */
|
|
32
33
|
derivations: ReadonlySet<string>;
|
|
33
34
|
/** Members that give a test function a mode, as `skip` and `only` do, which a variable can hold it with. */
|
|
@@ -40,6 +41,14 @@ export interface Dialect {
|
|
|
40
41
|
defaultImport: string | null;
|
|
41
42
|
/** Why `require()` of the module fails; null when the module can be required. */
|
|
42
43
|
required: string | null;
|
|
44
|
+
/** Other modules the runner reads as its own, as Bun rewrites `@jest/globals` and `vitest` to `bun:test`. */
|
|
45
|
+
aliases?: ReadonlySet<string>;
|
|
46
|
+
/** What the module is when called, as `node:test`'s default export and `require()` are its `test`. */
|
|
47
|
+
callable?: Start;
|
|
48
|
+
/** The module's other members, which a chain on the callable module may start with and is then no test. */
|
|
49
|
+
others?: ReadonlySet<string>;
|
|
50
|
+
/** Whether the functions must be imported, as the runner sets no globals; `starts` are then exports only. */
|
|
51
|
+
importOnly?: boolean;
|
|
43
52
|
}
|
|
44
53
|
/** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
|
|
45
54
|
export declare function shared(a: TestFunction | null, b: TestFunction | null): TestFunction | null;
|
|
@@ -83,6 +92,8 @@ export declare class SuiteBindings implements TestObjects {
|
|
|
83
92
|
* its module, as the walk reads globals and chains itself; in a followed module, also globals and chains.
|
|
84
93
|
*/
|
|
85
94
|
private boundStart;
|
|
95
|
+
/** A name the module is bound to: its namespace, and its `test` too when the module can be called. */
|
|
96
|
+
private bindModule;
|
|
86
97
|
exportedStart(name: string): Start | null;
|
|
87
98
|
private holdsTestObject;
|
|
88
99
|
reexports(source: string, imported: string, exported: Exported): boolean;
|
|
@@ -130,6 +141,20 @@ export declare abstract class SuiteAdapter implements Adapter {
|
|
|
130
141
|
*/
|
|
131
142
|
protected resolve(chain: Chain, args: readonly Argument[], lookup: Lookup): TestFunction | null;
|
|
132
143
|
private start;
|
|
144
|
+
/**
|
|
145
|
+
* What a chain on the framework's module starts with: the member it names, or for a module that can be called, as
|
|
146
|
+
* `node:test`'s, the module itself, as in `test.skip(...)`.
|
|
147
|
+
*/
|
|
148
|
+
private member;
|
|
149
|
+
/**
|
|
150
|
+
* The member a name is destructured from a test or describe function as, with that function's kind and the
|
|
151
|
+
* expression it is taken from: `each`, `test` and `test` in `const { each } = test`; null for any other name.
|
|
152
|
+
*/
|
|
153
|
+
protected destructuredMember(name: string, lookup: Lookup): {
|
|
154
|
+
kind: Kind;
|
|
155
|
+
member: string;
|
|
156
|
+
from: Expression;
|
|
157
|
+
} | null;
|
|
133
158
|
/**
|
|
134
159
|
* The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
|
|
135
160
|
* takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
|
|
@@ -43,7 +43,7 @@ export class SuiteBindings {
|
|
|
43
43
|
this.program = program;
|
|
44
44
|
this.isFollowed = isFollowed;
|
|
45
45
|
this.declared = topLevelNames(program);
|
|
46
|
-
this.imported = moduleImports(program, (source) =>
|
|
46
|
+
this.imported = moduleImports(program, (source) => isOwnModule(dialect, source) || dialect.mismatched.has(source));
|
|
47
47
|
this.follow = follow;
|
|
48
48
|
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
49
49
|
for (const statement of program.body) {
|
|
@@ -53,7 +53,7 @@ export class SuiteBindings {
|
|
|
53
53
|
if (dialect.mismatched.has(source)) {
|
|
54
54
|
this.problems.push(problem('framework-mismatch', statement, `The file imports '${source}', but the scan is for ${dialect.name}.`));
|
|
55
55
|
}
|
|
56
|
-
if (
|
|
56
|
+
if (!isOwnModule(dialect, source))
|
|
57
57
|
continue;
|
|
58
58
|
for (const specifier of statement.specifiers) {
|
|
59
59
|
this.declarations.add(specifier);
|
|
@@ -62,7 +62,7 @@ export class SuiteBindings {
|
|
|
62
62
|
}
|
|
63
63
|
else if (specifier.type === 'ImportDefaultSpecifier') {
|
|
64
64
|
if (dialect.defaultImport === null)
|
|
65
|
-
this.
|
|
65
|
+
this.bindModule(specifier.local.name);
|
|
66
66
|
else
|
|
67
67
|
this.problems.push(problem('invalid-import', specifier, dialect.defaultImport));
|
|
68
68
|
}
|
|
@@ -74,7 +74,7 @@ export class SuiteBindings {
|
|
|
74
74
|
const declarators = allDeclarators(program);
|
|
75
75
|
if (dialect.required !== null) {
|
|
76
76
|
for (const declarator of declarators) {
|
|
77
|
-
if (declarator.init && requiredModule(declarator.init)
|
|
77
|
+
if (declarator.init && isOwnModule(dialect, requiredModule(declarator.init))) {
|
|
78
78
|
this.problems.push(problem('invalid-import', declarator, dialect.required));
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -129,7 +129,7 @@ export class SuiteBindings {
|
|
|
129
129
|
startOf(expression) {
|
|
130
130
|
const node = unwrap(expression);
|
|
131
131
|
if (node.type === 'Identifier') {
|
|
132
|
-
const global = this.declared.has(node.name) ? undefined : this.dialect.starts.get(node.name);
|
|
132
|
+
const global = this.declared.has(node.name) || this.dialect.importOnly ? undefined : this.dialect.starts.get(node.name);
|
|
133
133
|
return this.starts.get(node.name) ?? global ?? null;
|
|
134
134
|
}
|
|
135
135
|
if (node.type !== 'MemberExpression')
|
|
@@ -154,7 +154,14 @@ export class SuiteBindings {
|
|
|
154
154
|
return this.starts.get(node.name) ?? null;
|
|
155
155
|
if (node.type !== 'MemberExpression' || !this.isNamespace(node.object))
|
|
156
156
|
return null;
|
|
157
|
-
|
|
157
|
+
const name = propertyName(node) ?? '';
|
|
158
|
+
return this.dialect.starts.get(name) ?? null;
|
|
159
|
+
}
|
|
160
|
+
/** A name the module is bound to: its namespace, and its `test` too when the module can be called. */
|
|
161
|
+
bindModule(name) {
|
|
162
|
+
this.namespaces.add(name);
|
|
163
|
+
if (this.dialect.callable)
|
|
164
|
+
this.starts.set(name, this.dialect.callable);
|
|
158
165
|
}
|
|
159
166
|
exportedStart(name) {
|
|
160
167
|
const own = ownExport(this.program, name);
|
|
@@ -167,7 +174,7 @@ export class SuiteBindings {
|
|
|
167
174
|
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
168
175
|
}
|
|
169
176
|
reexports(source, imported, exported) {
|
|
170
|
-
if (
|
|
177
|
+
if (!isOwnModule(this.dialect, source))
|
|
171
178
|
return this.follow(source, imported, exported);
|
|
172
179
|
return exported === 'test object' && this.dialect.starts.has(imported);
|
|
173
180
|
}
|
|
@@ -183,10 +190,11 @@ export class SuiteBindings {
|
|
|
183
190
|
}
|
|
184
191
|
bindMember(imported, local) {
|
|
185
192
|
const start = this.dialect.starts.get(imported);
|
|
193
|
+
const hook = this.dialect.hooks.get(imported);
|
|
186
194
|
if (start)
|
|
187
195
|
this.starts.set(local, start);
|
|
188
|
-
else if (
|
|
189
|
-
this.hooks.set(local,
|
|
196
|
+
else if (hook)
|
|
197
|
+
this.hooks.set(local, hook);
|
|
190
198
|
else
|
|
191
199
|
this.members.set(local, imported);
|
|
192
200
|
}
|
|
@@ -202,7 +210,7 @@ export class SuiteBindings {
|
|
|
202
210
|
if (!this.dialect.isModule(init))
|
|
203
211
|
return false;
|
|
204
212
|
if (id.type === 'Identifier') {
|
|
205
|
-
this.
|
|
213
|
+
this.bindModule(id.name);
|
|
206
214
|
return true;
|
|
207
215
|
}
|
|
208
216
|
if (id.type !== 'ObjectPattern')
|
|
@@ -218,6 +226,10 @@ export class SuiteBindings {
|
|
|
218
226
|
return true;
|
|
219
227
|
}
|
|
220
228
|
}
|
|
229
|
+
/** Whether a module is the framework's own, or one it reads as its own. */
|
|
230
|
+
function isOwnModule(dialect, source) {
|
|
231
|
+
return source !== null && (source === dialect.source || (dialect.aliases?.has(source) ?? false));
|
|
232
|
+
}
|
|
221
233
|
/**
|
|
222
234
|
* What Vitest's and Jest's adapters share: how a call's chain is followed to the test or describe function it starts
|
|
223
235
|
* from, through imports, variables, helper functions and globals.
|
|
@@ -257,7 +269,7 @@ export class SuiteAdapter {
|
|
|
257
269
|
return followed.get(key) === true;
|
|
258
270
|
});
|
|
259
271
|
this.problems = [...this.bindings.problems];
|
|
260
|
-
this.imports = moduleImports(parsed.program, (source) =>
|
|
272
|
+
this.imports = moduleImports(parsed.program, (source) => isOwnModule(dialect, source) || dialect.mismatched.has(source));
|
|
261
273
|
}
|
|
262
274
|
testFunction(expression, scopes, returnsOf) {
|
|
263
275
|
return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
|
|
@@ -288,10 +300,8 @@ export class SuiteAdapter {
|
|
|
288
300
|
const { base, links: chainLinks } = chain;
|
|
289
301
|
const { scopes, inBody } = lookup;
|
|
290
302
|
// `require('@jest/globals').test(...)`: the module itself, called in place.
|
|
291
|
-
if (base.type === 'CallExpression' && this.dialect.isModule(base))
|
|
292
|
-
|
|
293
|
-
return start ? { kind: start.kind, index: 1, modifiers: links(start.links) } : null;
|
|
294
|
-
}
|
|
303
|
+
if (base.type === 'CallExpression' && this.dialect.isModule(base))
|
|
304
|
+
return this.member(chainLinks);
|
|
295
305
|
if (base.type === 'CallExpression') {
|
|
296
306
|
const returned = this.returned(base, chainLinks, args, lookup);
|
|
297
307
|
return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
|
|
@@ -302,7 +312,7 @@ export class SuiteAdapter {
|
|
|
302
312
|
// name, such as `(test) => test.result()`, is not the framework's.
|
|
303
313
|
const declaration = this.file.values.declarationOf(base.name, scopes);
|
|
304
314
|
if (declaration === null) {
|
|
305
|
-
const global = this.dialect.starts.get(base.name);
|
|
315
|
+
const global = this.dialect.importOnly ? undefined : this.dialect.starts.get(base.name);
|
|
306
316
|
if (!global)
|
|
307
317
|
return null;
|
|
308
318
|
this.usesGlobals = true;
|
|
@@ -322,13 +332,40 @@ export class SuiteAdapter {
|
|
|
322
332
|
? { kind: this.dialect.starts.get(base.name)?.kind ?? 'test', index: 0 }
|
|
323
333
|
: null;
|
|
324
334
|
}
|
|
325
|
-
if (this.bindings.namespaces.has(base.name))
|
|
326
|
-
|
|
327
|
-
return start ? { kind: start.kind, index: 1, modifiers: links(start.links) } : null;
|
|
328
|
-
}
|
|
335
|
+
if (this.bindings.namespaces.has(base.name))
|
|
336
|
+
return this.member(chainLinks);
|
|
329
337
|
const start = this.bindings.starts.get(base.name);
|
|
330
338
|
return start ? { kind: start.kind, index: 0, modifiers: links(start.links) } : null;
|
|
331
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* What a chain on the framework's module starts with: the member it names, or for a module that can be called, as
|
|
342
|
+
* `node:test`'s, the module itself, as in `test.skip(...)`.
|
|
343
|
+
*/
|
|
344
|
+
member(chainLinks) {
|
|
345
|
+
const start = this.dialect.starts.get(chainLinks.at(0)?.name ?? '');
|
|
346
|
+
if (start)
|
|
347
|
+
return { kind: start.kind, index: 1, modifiers: links(start.links) };
|
|
348
|
+
const { callable } = this.dialect;
|
|
349
|
+
// The module's other members, as `test.mock.fn()`, are not tests; one it doesn't have is refused as a chain.
|
|
350
|
+
if (!callable || this.dialect.others?.has(chainLinks.at(0)?.name ?? ''))
|
|
351
|
+
return null;
|
|
352
|
+
return { kind: callable.kind, index: 0, modifiers: links(callable.links) };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The member a name is destructured from a test or describe function as, with that function's kind and the
|
|
356
|
+
* expression it is taken from: `each`, `test` and `test` in `const { each } = test`; null for any other name.
|
|
357
|
+
*/
|
|
358
|
+
destructuredMember(name, lookup) {
|
|
359
|
+
const declaration = this.file.values.declarationOf(name, lookup.scopes);
|
|
360
|
+
if (declaration?.type !== 'VariableDeclarator')
|
|
361
|
+
return null;
|
|
362
|
+
const member = boundMembers(declaration.id, name)?.at(-1);
|
|
363
|
+
if (member === undefined)
|
|
364
|
+
return null;
|
|
365
|
+
const alias = this.alias(declaration, name, lookup);
|
|
366
|
+
// A variable holds a test function only through its value, so there is one.
|
|
367
|
+
return alias && { kind: alias.kind, member, from: declaration.init };
|
|
368
|
+
}
|
|
332
369
|
/**
|
|
333
370
|
* The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
|
|
334
371
|
* takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
|
|
@@ -432,7 +469,7 @@ export class SuiteAdapter {
|
|
|
432
469
|
const { dialect } = this;
|
|
433
470
|
return {
|
|
434
471
|
name: dialect.name,
|
|
435
|
-
isSource: (source) =>
|
|
472
|
+
isSource: (source) => isOwnModule(dialect, source),
|
|
436
473
|
testObjects: (program, follow) => new SuiteBindings(program, dialect, follow, true),
|
|
437
474
|
};
|
|
438
475
|
}
|
|
@@ -458,11 +495,16 @@ export class SuiteAdapter {
|
|
|
458
495
|
/** Hooks called directly, such as `beforeEach(...)` imported from the framework or used as a global. */
|
|
459
496
|
hookCall(call, context) {
|
|
460
497
|
const callee = unwrap(call.callee);
|
|
498
|
+
// A hook on the module, as `test.beforeEach()` on `node:test`'s default export.
|
|
499
|
+
if (callee.type === 'MemberExpression' && this.bindings.isNamespace(callee.object)) {
|
|
500
|
+
const hook = this.dialect.hooks.get(propertyName(callee) ?? '');
|
|
501
|
+
return hook ? this.hook(call, hook, context) : null;
|
|
502
|
+
}
|
|
461
503
|
if (callee.type !== 'Identifier')
|
|
462
504
|
return null;
|
|
463
505
|
const declaration = this.file.values.declarationOf(callee.name, context.scopes);
|
|
464
506
|
const hook = declaration === null
|
|
465
|
-
? this.dialect.hooks.
|
|
507
|
+
? this.dialect.hooks.get(callee.name)
|
|
466
508
|
: this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
|
|
467
509
|
return hook ? this.hook(call, hook, context) : null;
|
|
468
510
|
}
|
|
@@ -6,11 +6,19 @@ import { type FileContext, type Link } from './common.ts';
|
|
|
6
6
|
import { SuiteAdapter, type Kind } from './test-functions.ts';
|
|
7
7
|
export declare class VitestAdapter extends SuiteAdapter {
|
|
8
8
|
private warnedAboutTags;
|
|
9
|
-
|
|
9
|
+
/** The length a value in a case title is shortened to, as the config sets it. */
|
|
10
|
+
private readonly titleTruncate;
|
|
11
|
+
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null, titleTruncate?: number);
|
|
10
12
|
protected isAliasLink(kind: Kind, link: Link): boolean;
|
|
11
13
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
12
14
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
13
15
|
private benchCall;
|
|
16
|
+
/**
|
|
17
|
+
* A call of a member Vitest calls on its test or describe function, destructured from it, as `each` in
|
|
18
|
+
* `const { each } = test`: the member reads the function it is called on, which a plain call doesn't give it, so the
|
|
19
|
+
* call throws while the file loads. Which members do that depends on the version.
|
|
20
|
+
*/
|
|
21
|
+
private unboundCall;
|
|
14
22
|
private chainCall;
|
|
15
23
|
protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
|
|
16
24
|
private contextSkip;
|
|
@@ -37,7 +37,7 @@ const VITEST = {
|
|
|
37
37
|
['describe', { kind: 'describe', links: [] }],
|
|
38
38
|
['suite', { kind: 'describe', links: [] }],
|
|
39
39
|
]),
|
|
40
|
-
hooks: new
|
|
40
|
+
hooks: new Map([...HOOKS.keys()].map((name) => [name, name])),
|
|
41
41
|
derivations: DERIVATIONS,
|
|
42
42
|
modifiers: new Set([...MODIFIERS.test, ...MODIFIERS.describe]),
|
|
43
43
|
carriesKinds: true,
|
|
@@ -112,10 +112,24 @@ function fixturesOf(fn, index, version) {
|
|
|
112
112
|
return !since || !atLeast(version, ...since);
|
|
113
113
|
});
|
|
114
114
|
}
|
|
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))
|
|
123
|
+
return true;
|
|
124
|
+
return ['extend', 'scoped', 'override'].includes(member) && atLeast(version, 4, 1);
|
|
125
|
+
}
|
|
115
126
|
export class VitestAdapter extends SuiteAdapter {
|
|
116
127
|
warnedAboutTags = false;
|
|
117
|
-
|
|
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) {
|
|
118
131
|
super(parsed, file, readModule, VITEST, TEST_MEMBERS);
|
|
132
|
+
this.titleTruncate = titleTruncate;
|
|
119
133
|
if (atLeast(file.version, 5, 0)) {
|
|
120
134
|
for (const call of nestedHoistedCalls(parsed.program)) {
|
|
121
135
|
const callee = snippet(text(this.file, call.callee));
|
|
@@ -139,6 +153,7 @@ export class VitestAdapter extends SuiteAdapter {
|
|
|
139
153
|
return (this.benchCall(call, context) ??
|
|
140
154
|
this.hookCall(call, context) ??
|
|
141
155
|
this.contextSkip(call, context) ??
|
|
156
|
+
this.unboundCall(call, context) ??
|
|
142
157
|
this.chainCall(call, context));
|
|
143
158
|
}
|
|
144
159
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
@@ -157,6 +172,22 @@ export class VitestAdapter extends SuiteAdapter {
|
|
|
157
172
|
problem: problem('removed-api', call, `Vitest 5 no longer exports bench from 'vitest'; ${callee}(...) fails. Benchmarks are not tests.`, callee),
|
|
158
173
|
};
|
|
159
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
|
+
}
|
|
160
191
|
chainCall(call, context) {
|
|
161
192
|
const chain = readChain(call.callee);
|
|
162
193
|
const resolved = chain ? this.resolve(chain, call.arguments, context) : null;
|
|
@@ -489,12 +520,12 @@ export class VitestAdapter extends SuiteAdapter {
|
|
|
489
520
|
if (meta)
|
|
490
521
|
details.meta = this.meta(meta, context, problems);
|
|
491
522
|
for (const key of ['timeout', 'repeats']) {
|
|
492
|
-
const option =
|
|
523
|
+
const option = given(key);
|
|
493
524
|
const value = option ? readCount(this.file, key, option, problems) : null;
|
|
494
525
|
if (value !== null)
|
|
495
526
|
details.run[key] = value;
|
|
496
527
|
}
|
|
497
|
-
const retry =
|
|
528
|
+
const retry = given('retry');
|
|
498
529
|
if (retry)
|
|
499
530
|
this.retry(retry, details, context, problems);
|
|
500
531
|
return result;
|
|
@@ -583,7 +614,7 @@ export class VitestAdapter extends SuiteAdapter {
|
|
|
583
614
|
return null;
|
|
584
615
|
cases = resolved.value;
|
|
585
616
|
}
|
|
586
|
-
const caseTitle = (items, index) => title.isDynamic ? null : vitestCaseTitle(title.text, items, index, this.file.version);
|
|
617
|
+
const caseTitle = (items, index) => title.isDynamic ? null : vitestCaseTitle(title.text, items, index, this.file.version, this.titleTruncate);
|
|
587
618
|
if (table.name === 'for') {
|
|
588
619
|
return cases.map((item, index) => ({
|
|
589
620
|
title: caseTitle(item === null || item === undefined ? [] : Array.isArray(item) ? item : [item], index),
|
|
@@ -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
|
+
}
|
package/dist/model.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface Problem {
|
|
|
37
37
|
level?: DiagnosticLevel;
|
|
38
38
|
/** Raised by code in a test, hook or helper body, which runs after the file is loaded and so can't stop it loading. */
|
|
39
39
|
afterLoad?: boolean;
|
|
40
|
+
/** The runner still runs the tests the file declared before the call that stops it loading, as WebdriverIO's Mocha. */
|
|
41
|
+
keepsEarlierTests?: boolean;
|
|
40
42
|
}
|
|
41
43
|
export interface Title {
|
|
42
44
|
text: string;
|
|
@@ -222,6 +224,8 @@ export interface Adapter {
|
|
|
222
224
|
/** Where each of `fileTags` is written. */
|
|
223
225
|
fileTagSpans: Span[];
|
|
224
226
|
usesGlobals: boolean;
|
|
227
|
+
/** Whether a test declared in a test's body is a subtest the runner runs as the test does, as in `node:test`. */
|
|
228
|
+
nestsTests?: boolean;
|
|
225
229
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
226
230
|
/**
|
|
227
231
|
* The test or describe function a value holds, in the adapter's own terms, as `it.skip` in `return it.skip`; null
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `node --test` commands of a package.json script, each as the files and globs it gives; empty for a command
|
|
3
|
+
* that gives none. A command runs node:test when `--test` comes before any file, as `node build.js --test` passes it on
|
|
4
|
+
* to the script.
|
|
5
|
+
*/
|
|
6
|
+
export declare function nodeTestCommands(script: string): string[][];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Node.js options that take their value as the next argument, so it is not a file to test. */
|
|
2
|
+
const VALUE_OPTIONS = new Set([
|
|
3
|
+
'--allow-fs-read',
|
|
4
|
+
'--allow-fs-write',
|
|
5
|
+
'--conditions',
|
|
6
|
+
'-C',
|
|
7
|
+
'--config-file',
|
|
8
|
+
'--cpu-prof-dir',
|
|
9
|
+
'--cpu-prof-name',
|
|
10
|
+
'--diagnostic-dir',
|
|
11
|
+
'--disable-warning',
|
|
12
|
+
'--dns-result-order',
|
|
13
|
+
'--env-file',
|
|
14
|
+
'--env-file-if-exists',
|
|
15
|
+
'--experimental-config-file',
|
|
16
|
+
'--experimental-loader',
|
|
17
|
+
'--heap-prof-dir',
|
|
18
|
+
'--heap-prof-name',
|
|
19
|
+
'--heapsnapshot-signal',
|
|
20
|
+
'--icu-data-dir',
|
|
21
|
+
'--import',
|
|
22
|
+
'--input-type',
|
|
23
|
+
'--loader',
|
|
24
|
+
'--localstorage-file',
|
|
25
|
+
'--max-http-header-size',
|
|
26
|
+
'--openssl-config',
|
|
27
|
+
'--redirect-warnings',
|
|
28
|
+
'--report-dir',
|
|
29
|
+
'--report-filename',
|
|
30
|
+
'--require',
|
|
31
|
+
'-r',
|
|
32
|
+
'--secure-heap',
|
|
33
|
+
'--secure-heap-min',
|
|
34
|
+
'--test-concurrency',
|
|
35
|
+
'--test-coverage-branches',
|
|
36
|
+
'--test-coverage-exclude',
|
|
37
|
+
'--test-coverage-functions',
|
|
38
|
+
'--test-coverage-include',
|
|
39
|
+
'--test-coverage-lines',
|
|
40
|
+
'--test-global-setup',
|
|
41
|
+
'--test-isolation',
|
|
42
|
+
'--test-name-pattern',
|
|
43
|
+
'--test-random-seed',
|
|
44
|
+
'--test-reporter',
|
|
45
|
+
'--test-reporter-destination',
|
|
46
|
+
'--test-rerun-failures',
|
|
47
|
+
'--test-shard',
|
|
48
|
+
'--test-skip-pattern',
|
|
49
|
+
'--test-timeout',
|
|
50
|
+
'--title',
|
|
51
|
+
'--unhandled-rejections',
|
|
52
|
+
'--watch-kill-signal',
|
|
53
|
+
'--watch-path',
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* The `node --test` commands of a package.json script, each as the files and globs it gives; empty for a command
|
|
57
|
+
* that gives none. A command runs node:test when `--test` comes before any file, as `node build.js --test` passes it on
|
|
58
|
+
* to the script.
|
|
59
|
+
*/
|
|
60
|
+
export function nodeTestCommands(script) {
|
|
61
|
+
const commands = [];
|
|
62
|
+
for (const command of script.split(/&&|\|\||[;|]/)) {
|
|
63
|
+
const words = [...command.matchAll(/'([^']*)'|"([^"]*)"|(\S+)/g)].map((match) => match[1] ?? match[2] ?? match[3]);
|
|
64
|
+
const node = words.findIndex((word) => /(^|[/\\])node(\.exe)?$/.test(word));
|
|
65
|
+
if (node === -1)
|
|
66
|
+
continue;
|
|
67
|
+
let runsTests = false;
|
|
68
|
+
const files = [];
|
|
69
|
+
for (let index = node + 1; index < words.length; index++) {
|
|
70
|
+
const word = words[index];
|
|
71
|
+
if (word === '--test')
|
|
72
|
+
runsTests = true;
|
|
73
|
+
else if (VALUE_OPTIONS.has(word))
|
|
74
|
+
index++;
|
|
75
|
+
else if (!word.startsWith('-')) {
|
|
76
|
+
if (!runsTests)
|
|
77
|
+
break;
|
|
78
|
+
files.push(word);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (runsTests)
|
|
82
|
+
commands.push(files);
|
|
83
|
+
}
|
|
84
|
+
return commands;
|
|
85
|
+
}
|