test-inventory 0.1.0 → 0.1.2
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 +26 -0
- package/README.md +2 -2
- package/dist/cli.js +3 -2
- package/dist/diagnostics.d.ts +4 -1
- package/dist/diagnostics.js +6 -2
- package/dist/extract.js +88 -10
- package/dist/files.js +3 -0
- package/dist/frameworks/playwright.js +26 -12
- package/dist/frameworks/vitest.d.ts +2 -0
- package/dist/frameworks/vitest.js +80 -5
- package/dist/model.d.ts +7 -3
- package/dist/options.d.ts +2 -0
- package/dist/records.d.ts +1 -0
- package/dist/records.js +6 -2
- package/dist/resolve.js +2 -1
- package/dist/scan.js +17 -3
- package/dist/types.d.ts +17 -4
- package/package.json +2 -1
- package/schema/v1.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# test-inventory
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#13](https://github.com/Huldoser/test-inventory/pull/13) [`89a029f`](https://github.com/Huldoser/test-inventory/commit/89a029f9e45c2aaa57b5f907717bb8f00829e335) - Locks set with `test.describe.configure({ lock })`, new in Playwright 1.64, are added to every test in the file or describe where the call is made. They were dropped before.
|
|
8
|
+
|
|
9
|
+
- [#13](https://github.com/Huldoser/test-inventory/pull/13) [`89a029f`](https://github.com/Huldoser/test-inventory/commit/89a029f9e45c2aaa57b5f907717bb8f00829e335) - From Vitest 5, a file that calls `vi.mock`, `vi.unmock` or `vi.hoisted` anywhere but its top level gets a `removed-api` error at the call, and its tests are `notLoaded`, as Vitest refuses the file. Files with in-source tests are not checked, as in Vitest.
|
|
10
|
+
|
|
11
|
+
## 0.1.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A custom test object imported from `'.'` or `'..'` is followed like other relative imports.
|
|
16
|
+
|
|
17
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - When a pattern names a folder, the `no-files-matched` message suggests a pattern for the test files in it, and the command line prints that message.
|
|
18
|
+
|
|
19
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A pattern whose folder doesn't exist now gives only the `no-files-matched` warning, not a `read-error`.
|
|
20
|
+
|
|
21
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - Messages for problems that make Playwright refuse a file now say that Playwright runs no tests at all, in any file.
|
|
22
|
+
|
|
23
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A new `test-in-function` warning marks a test declared in a function that is called more than once, from another describe, never, or from other files. The runner registers such a test in the describe around each call, while the record stays where the test is written.
|
|
24
|
+
|
|
25
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A version range that starts below the oldest supported version, such as `^1.40.0`, now gets the `approximate-framework-version` warning, since the newest rules of its major apply.
|
|
26
|
+
|
|
27
|
+
- [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - Every test in a Vitest file that fails to load is now `notLoaded`, as Vitest runs none of them: a chain Vitest doesn't have, `sequential` or `bench` from Vitest 5, or options as the third argument from Vitest 4. A call to `bench` is reported where it is made, not where it is imported.
|
|
28
|
+
|
|
3
29
|
## 0.1.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# test-inventory
|
|
1
|
+
# <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Huldoser/test-inventory/main/docs/public/logo-dark.svg"><img src="https://raw.githubusercontent.com/Huldoser/test-inventory/main/docs/public/logo-light.svg" alt="test-inventory" height="48"></picture>
|
|
2
2
|
|
|
3
3
|
Test metadata from source code, not from test results.
|
|
4
4
|
|
|
5
5
|
test-inventory reads Playwright and Vitest test files and lists every test declaration as JSON: its title, file and
|
|
6
6
|
lines, the describes around it, its tags and annotations, and whether it is active, skipped, fixme, todo, expected
|
|
7
|
-
to fail or in a file
|
|
7
|
+
to fail or in a file the runner refuses to load, with the condition when the state only applies sometimes. It parses
|
|
8
8
|
the code and runs nothing, so it needs no browsers, no build and no runner configuration.
|
|
9
9
|
|
|
10
10
|
Documentation: [huldoser.github.io/test-inventory](https://huldoser.github.io/test-inventory/)
|
package/dist/cli.js
CHANGED
|
@@ -130,8 +130,9 @@ export async function run(argv, io) {
|
|
|
130
130
|
io.stderr.write(`test-inventory: unexpected error: ${error instanceof Error ? String(error.stack) : String(error)}\n`);
|
|
131
131
|
return FAILED;
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const unmatched = inventory.diagnostics.find((diagnostic) => diagnostic.code === 'no-files-matched');
|
|
134
|
+
if (unmatched) {
|
|
135
|
+
io.stderr.write(`test-inventory: ${unmatched.message[0].toLowerCase()}${unmatched.message.slice(1)}\n`);
|
|
135
136
|
}
|
|
136
137
|
// A reader that stops early, as `| head` does, closes the pipe: an EPIPE error, which ends the output quietly.
|
|
137
138
|
const quiet = () => undefined;
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ import type { SourceText } from './source.ts';
|
|
|
3
3
|
import type { Diagnostic, DiagnosticCode, DiagnosticLevel, Framework } from './types.ts';
|
|
4
4
|
/** `error` when tests may be missing from the output; `warning` when a value is missing or approximate. */
|
|
5
5
|
export declare const LEVELS: Record<DiagnosticCode, DiagnosticLevel>;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Problems for which the runner refuses to load the whole file, so that none of its tests run. In Vitest, these are
|
|
8
|
+
* calls that throw while the file is collected.
|
|
9
|
+
*/
|
|
7
10
|
export declare const REJECTS_FILE: Record<Framework, ReadonlySet<DiagnosticCode>>;
|
|
8
11
|
export declare function toDiagnostic(problem: Problem, relativeFilePath: string, source: SourceText | null, testId?: string | null): Diagnostic;
|
|
9
12
|
export declare function fileDiagnostic(code: DiagnosticCode, relativeFilePath: string, message: string): Diagnostic;
|
package/dist/diagnostics.js
CHANGED
|
@@ -34,11 +34,15 @@ export const LEVELS = {
|
|
|
34
34
|
'no-files-matched': 'warning',
|
|
35
35
|
'unsupported-file': 'warning',
|
|
36
36
|
'approximate-framework-version': 'warning',
|
|
37
|
+
'test-in-function': 'warning',
|
|
37
38
|
};
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Problems for which the runner refuses to load the whole file, so that none of its tests run. In Vitest, these are
|
|
41
|
+
* calls that throw while the file is collected.
|
|
42
|
+
*/
|
|
39
43
|
export const REJECTS_FILE = {
|
|
40
44
|
playwright: new Set(['test-without-body', 'tag-without-at', 'invalid-chain', 'duplicate-title']),
|
|
41
|
-
vitest: new Set(),
|
|
45
|
+
vitest: new Set(['invalid-chain', 'removed-api']),
|
|
42
46
|
};
|
|
43
47
|
export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
|
|
44
48
|
const position = problem.node && source ? source.position(problem.node.start) : null;
|
package/dist/extract.js
CHANGED
|
@@ -3,7 +3,21 @@ import { exportName } from "./frameworks/bindings.js";
|
|
|
3
3
|
import { problem } from "./frameworks/common.js";
|
|
4
4
|
import { snippet, splitLines } from "./source.js";
|
|
5
5
|
import { isScopeNode } from "./values.js";
|
|
6
|
+
/** Whether a function is called where it is written, so that its tests are registered there. */
|
|
7
|
+
function calledWhereWritten(call, written) {
|
|
8
|
+
return (call.scope === written.scope &&
|
|
9
|
+
call.helper === written.helper &&
|
|
10
|
+
call.guards === written.guards &&
|
|
11
|
+
call.isInLoop === written.isInLoop &&
|
|
12
|
+
!call.inBody);
|
|
13
|
+
}
|
|
6
14
|
const LOOP_METHODS = new Set(['forEach', 'map']);
|
|
15
|
+
/** What `test.describe.configure()` sets on the describe or file it is called in: the last mode wins, locks add up. */
|
|
16
|
+
function configure(scope, settings) {
|
|
17
|
+
if (settings.mode)
|
|
18
|
+
scope.mode = settings.mode;
|
|
19
|
+
scope.details.locks.push(...settings.locks);
|
|
20
|
+
}
|
|
7
21
|
/** Joins conditions with `&&`, adding parentheses where a part could otherwise be read differently. */
|
|
8
22
|
function joinConditions(parts) {
|
|
9
23
|
if (parts.length <= 1)
|
|
@@ -30,6 +44,12 @@ class Walker {
|
|
|
30
44
|
values;
|
|
31
45
|
effects = new Map();
|
|
32
46
|
helperCalls = [];
|
|
47
|
+
/** Where each function with a name is written, outside test and hook bodies. */
|
|
48
|
+
written = new Map();
|
|
49
|
+
/** Every call by name to a function declared in the file. */
|
|
50
|
+
callSites = new Map();
|
|
51
|
+
/** The function with a name each test is declared in, which registers the test when it is called. */
|
|
52
|
+
declaredIn = new Map();
|
|
33
53
|
constructor(parsed, adapter, values) {
|
|
34
54
|
this.parsed = parsed;
|
|
35
55
|
this.adapter = adapter;
|
|
@@ -55,8 +75,11 @@ class Walker {
|
|
|
55
75
|
isInCondition: false,
|
|
56
76
|
isInFunction: false,
|
|
57
77
|
helper: null,
|
|
78
|
+
declaredIn: null,
|
|
58
79
|
});
|
|
59
|
-
this.
|
|
80
|
+
const exported = this.exportedFunctions(this.parsed.program);
|
|
81
|
+
this.applyHelpers(exported);
|
|
82
|
+
this.checkFunctionTests(exported);
|
|
60
83
|
return {
|
|
61
84
|
suites: this.suites,
|
|
62
85
|
tests: this.tests,
|
|
@@ -134,17 +157,36 @@ class Walker {
|
|
|
134
157
|
case 'ArrowFunctionExpression': {
|
|
135
158
|
// A function outside any test or hook runs when it is called; what it sets applies where it is called.
|
|
136
159
|
const helper = context.test || context.hookScope ? null : node;
|
|
160
|
+
const named = helper !== null && this.isNamed(node);
|
|
161
|
+
if (named)
|
|
162
|
+
this.written.set(node, { ...this.placement(context), declaredIn: context.declaredIn });
|
|
137
163
|
this.visitChildren(node, {
|
|
138
164
|
...context,
|
|
139
165
|
isInFunction: true,
|
|
140
166
|
helper: helper ?? context.helper,
|
|
141
167
|
guards: helper ? [] : context.guards,
|
|
168
|
+
declaredIn: named ? node : context.declaredIn,
|
|
142
169
|
});
|
|
143
170
|
return;
|
|
144
171
|
}
|
|
145
172
|
}
|
|
146
173
|
this.visitChildren(node, context);
|
|
147
174
|
}
|
|
175
|
+
/** Whether a function has a name it can be called by: `function name() {}` or `const name = () => {}`. */
|
|
176
|
+
isNamed(fn) {
|
|
177
|
+
const parent = this.parents.at(-2);
|
|
178
|
+
return ((fn.type === 'FunctionDeclaration' && fn.id !== null) ||
|
|
179
|
+
(parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.init === fn));
|
|
180
|
+
}
|
|
181
|
+
placement(context) {
|
|
182
|
+
return {
|
|
183
|
+
scope: context.scope,
|
|
184
|
+
helper: context.helper,
|
|
185
|
+
guards: joinConditions(context.guards),
|
|
186
|
+
isInLoop: context.isInLoop,
|
|
187
|
+
inBody: context.test !== null || context.hookScope !== null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
148
190
|
guarded(context, guard) {
|
|
149
191
|
return { ...context, isInCondition: true, guards: guard === null ? context.guards : [...context.guards, guard] };
|
|
150
192
|
}
|
|
@@ -215,11 +257,14 @@ class Walker {
|
|
|
215
257
|
(context.test ?? context.hookScope ?? context.scope).states.push(setting);
|
|
216
258
|
return true;
|
|
217
259
|
}
|
|
218
|
-
case '
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
case 'configure':
|
|
261
|
+
this.problems.push(...api.problems);
|
|
262
|
+
if (context.helper) {
|
|
263
|
+
this.effectsOf(context.helper).push({ kind: 'configure', mode: api.mode, locks: api.locks, call });
|
|
264
|
+
}
|
|
265
|
+
else if (!context.test && !context.hookScope) {
|
|
266
|
+
configure(context.scope, api);
|
|
267
|
+
}
|
|
223
268
|
return true;
|
|
224
269
|
case 'hook':
|
|
225
270
|
if (context.test)
|
|
@@ -266,6 +311,9 @@ class Walker {
|
|
|
266
311
|
const fn = callee.type === 'Identifier' ? this.functionNamed(callee.name, this.scopes) : null;
|
|
267
312
|
if (!fn)
|
|
268
313
|
return;
|
|
314
|
+
const sites = this.callSites.get(fn) ?? [];
|
|
315
|
+
sites.push(this.placement(context));
|
|
316
|
+
this.callSites.set(fn, sites);
|
|
269
317
|
if (context.helper) {
|
|
270
318
|
this.effectsOf(context.helper).push({ kind: 'call', fn, guards: context.guards });
|
|
271
319
|
}
|
|
@@ -307,8 +355,7 @@ class Walker {
|
|
|
307
355
|
* Applies what helper functions set to the tests, hooks and describes that call them, through nested helpers too,
|
|
308
356
|
* with the conditions around each call. A helper no test or hook reaches, or that the file exports, is reported.
|
|
309
357
|
*/
|
|
310
|
-
applyHelpers() {
|
|
311
|
-
const exported = this.exportedFunctions(this.parsed.program);
|
|
358
|
+
applyHelpers(exported) {
|
|
312
359
|
const reached = new Set();
|
|
313
360
|
const expand = (fn, guards, path) => {
|
|
314
361
|
if (path.has(fn) || exported.has(fn))
|
|
@@ -317,7 +364,7 @@ class Walker {
|
|
|
317
364
|
return (this.effects.get(fn) ?? []).flatMap((effect) => {
|
|
318
365
|
if (effect.kind === 'call')
|
|
319
366
|
return expand(effect.fn, [...guards, ...effect.guards], new Set([...path, fn]));
|
|
320
|
-
if (effect.kind === '
|
|
367
|
+
if (effect.kind === 'configure')
|
|
321
368
|
return [effect];
|
|
322
369
|
const condition = joinConditions([...guards, ...(effect.setting.condition ? [effect.setting.condition] : [])]);
|
|
323
370
|
return [{ ...effect, setting: { ...effect.setting, condition } }];
|
|
@@ -328,7 +375,7 @@ class Walker {
|
|
|
328
375
|
if (effect.kind === 'state')
|
|
329
376
|
call.target.states.push(effect.setting);
|
|
330
377
|
else if (call.scope)
|
|
331
|
-
call.scope
|
|
378
|
+
configure(call.scope, effect);
|
|
332
379
|
}
|
|
333
380
|
}
|
|
334
381
|
for (const [fn, effects] of this.effects) {
|
|
@@ -340,6 +387,35 @@ class Walker {
|
|
|
340
387
|
this.problems.push(problem('state-in-helper', own.call, `${callee}() is in a function that ${why}; it applies to the tests that call the function, so it is not applied here.`, callee));
|
|
341
388
|
}
|
|
342
389
|
}
|
|
390
|
+
/** Why calling a function with a name doesn't register its tests where they are written; null when it does. */
|
|
391
|
+
misplaced(fn, exported) {
|
|
392
|
+
if (exported.has(fn))
|
|
393
|
+
return 'the file exports the function';
|
|
394
|
+
const sites = this.callSites.get(fn) ?? [];
|
|
395
|
+
if (sites.length === 0)
|
|
396
|
+
return 'nothing in the file calls the function by name';
|
|
397
|
+
if (sites.length > 1)
|
|
398
|
+
return `the function is called ${sites.length} times`;
|
|
399
|
+
const written = this.written.get(fn);
|
|
400
|
+
return calledWhereWritten(sites[0], written)
|
|
401
|
+
? null
|
|
402
|
+
: 'the function is called in another place than where it is written';
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Warns about tests declared in a function with a name, or in one around it, whose calls register them somewhere
|
|
406
|
+
* else than where they are written: the runner adds a test to the describe around each call.
|
|
407
|
+
*/
|
|
408
|
+
checkFunctionTests(exported) {
|
|
409
|
+
for (const [test, innermost] of this.declaredIn) {
|
|
410
|
+
let why = null;
|
|
411
|
+
for (let fn = innermost; fn && !why; fn = this.written.get(fn).declaredIn) {
|
|
412
|
+
why = this.misplaced(fn, exported);
|
|
413
|
+
}
|
|
414
|
+
if (!why)
|
|
415
|
+
continue;
|
|
416
|
+
test.problems.push(problem('test-in-function', test.call, `The test is declared in a function, and ${why}. The runner registers the test at each call of the function, in the describe around the call; it is listed once, where it is written.`));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
343
419
|
nested(call, context) {
|
|
344
420
|
if (!context.test && !context.hookScope)
|
|
345
421
|
return false;
|
|
@@ -363,6 +439,8 @@ class Walker {
|
|
|
363
439
|
problems: [...declaration.problems],
|
|
364
440
|
};
|
|
365
441
|
this.tests.push(test);
|
|
442
|
+
if (context.declaredIn)
|
|
443
|
+
this.declaredIn.set(test, context.declaredIn);
|
|
366
444
|
context.scope.suite?.cuts.push(statement);
|
|
367
445
|
if (declaration.fn) {
|
|
368
446
|
this.visitCallback(declaration.fn, {
|
package/dist/files.js
CHANGED
|
@@ -59,6 +59,9 @@ export async function findFiles(root, patterns, ignore, framework) {
|
|
|
59
59
|
for (const failure of failures) {
|
|
60
60
|
const where = relative(failure.path);
|
|
61
61
|
if (failure.kind === 'folder') {
|
|
62
|
+
// A folder a pattern names that isn't there holds no tests; `no-files-matched` reports a scan that found none.
|
|
63
|
+
if (failure.error.code === 'ENOENT' || failure.error.code === 'ENOTDIR')
|
|
64
|
+
continue;
|
|
62
65
|
const message = `The folder could not be read: ${readFailure(failure.error)}; test files in it are missing.`;
|
|
63
66
|
diagnostics.push(fileDiagnostic('read-error', `${where}/`, message));
|
|
64
67
|
}
|
|
@@ -345,7 +345,7 @@ export class PlaywrightAdapter {
|
|
|
345
345
|
const callee = snippet(text(this.file, call.callee));
|
|
346
346
|
return {
|
|
347
347
|
kind: 'invalid',
|
|
348
|
-
problem: problem('test-without-body', call, `${callee}() has no test body, so Playwright fails to load the file.`),
|
|
348
|
+
problem: problem('test-without-body', call, `${callee}() has no test body, so Playwright fails to load the file and runs no tests at all.`),
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
351
|
const state = STATES.get(key) ?? null;
|
|
@@ -429,17 +429,31 @@ export class PlaywrightAdapter {
|
|
|
429
429
|
}
|
|
430
430
|
configure(call, context) {
|
|
431
431
|
const options = call.arguments[0] ? readOptions(this.file, context.scopes, call.arguments[0]) : null;
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
432
|
+
const option = options?.values.get('mode');
|
|
433
|
+
const mode = option?.resolved.ok && ['parallel', 'serial', 'default'].includes(option.resolved.value)
|
|
434
|
+
? option.resolved.value
|
|
435
|
+
: null;
|
|
436
|
+
const problems = mode
|
|
437
|
+
? []
|
|
438
|
+
: [
|
|
439
|
+
...(options ? unresolvedOptions(this.file, options, 'the mode is unknown') : []),
|
|
440
|
+
...(option && !option.resolved.ok
|
|
441
|
+
? [unresolvedOption(this.file, 'mode', option.node, option.resolved.importedFrom, 'the mode is unknown')]
|
|
442
|
+
: []),
|
|
443
|
+
];
|
|
444
|
+
const locks = [];
|
|
445
|
+
const lock = options?.values.get('lock');
|
|
446
|
+
if (lock && atLeast(this.file.version, 1, 64)) {
|
|
447
|
+
const strings = readStrings(this.file, context.scopes, lock);
|
|
448
|
+
locks.push(...strings.values);
|
|
449
|
+
for (const { node: unresolved, importedFrom } of strings.unresolved) {
|
|
450
|
+
problems.push(unresolvedOption(this.file, 'lock', unresolved, importedFrom, 'the lock is missing'));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!mode && locks.length === 0) {
|
|
454
|
+
return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
|
|
435
455
|
}
|
|
436
|
-
|
|
437
|
-
...(options ? unresolvedOptions(this.file, options, 'the mode is unknown') : []),
|
|
438
|
-
...(mode && !mode.resolved.ok
|
|
439
|
-
? [unresolvedOption(this.file, 'mode', mode.node, mode.resolved.importedFrom, 'the mode is unknown')]
|
|
440
|
-
: []),
|
|
441
|
-
];
|
|
442
|
-
return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
|
|
456
|
+
return { kind: 'configure', mode, locks, problems };
|
|
443
457
|
}
|
|
444
458
|
/** `test.info().annotations.push(...)` inside a test body. */
|
|
445
459
|
annotationsPush(call, links, context) {
|
|
@@ -468,7 +482,7 @@ export class PlaywrightAdapter {
|
|
|
468
482
|
details.tags.push(value);
|
|
469
483
|
}
|
|
470
484
|
else {
|
|
471
|
-
problems.push(problem('tag-without-at', tag.node, `Tag "${value}" does not start with "@", so Playwright fails to load the file.`, value));
|
|
485
|
+
problems.push(problem('tag-without-at', tag.node, `Tag "${value}" does not start with "@", so Playwright fails to load the file and runs no tests at all.`, value));
|
|
472
486
|
}
|
|
473
487
|
}
|
|
474
488
|
for (const { node: unresolved, importedFrom } of tags.unresolved) {
|
|
@@ -19,6 +19,8 @@ export declare class VitestAdapter implements Adapter {
|
|
|
19
19
|
private warnedAboutTags;
|
|
20
20
|
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
21
21
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
22
|
+
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
23
|
+
private benchCall;
|
|
22
24
|
private chainCall;
|
|
23
25
|
/**
|
|
24
26
|
* 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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap } from "../ast.js";
|
|
1
|
+
import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
3
|
import { allDeclarators, bindAll, exportName, followImport, moduleImports, requiredModule, } from "./bindings.js";
|
|
4
4
|
import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, text, } from "./common.js";
|
|
@@ -28,11 +28,63 @@ const HOOKS = new Map([
|
|
|
28
28
|
]);
|
|
29
29
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
30
30
|
const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
|
|
31
|
+
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
32
|
+
const HOISTED_OBJECTS = new Set(['vi', 'vitest']);
|
|
33
|
+
const HOISTED_METHODS = new Set(['mock', 'unmock', 'hoisted']);
|
|
34
|
+
/**
|
|
35
|
+
* The hoisted calls that are not written at the top level of the file, where Vitest moves them. Vitest 5 fails such a
|
|
36
|
+
* file while it loads it. As in Vitest, `const value = vi.hoisted(...)` and `await vi.hoisted(...)` count as top
|
|
37
|
+
* level, and a file with in-source tests is never checked.
|
|
38
|
+
*/
|
|
39
|
+
function nestedHoistedCalls(program) {
|
|
40
|
+
if (hasInSourceTests(program))
|
|
41
|
+
return [];
|
|
42
|
+
const topLevel = new Set(program.body);
|
|
43
|
+
for (const statement of program.body) {
|
|
44
|
+
if (statement.type === 'ExpressionStatement')
|
|
45
|
+
topLevel.add(statement.expression);
|
|
46
|
+
}
|
|
47
|
+
const found = [];
|
|
48
|
+
const parents = [];
|
|
49
|
+
const visit = (node) => {
|
|
50
|
+
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
|
|
51
|
+
const { object, property } = node.callee;
|
|
52
|
+
const method = !node.callee.computed && property.type === 'Identifier' ? property.name : '';
|
|
53
|
+
if (object.type === 'Identifier' && HOISTED_OBJECTS.has(object.name) && HOISTED_METHODS.has(method)) {
|
|
54
|
+
if (!topLevel.has(method === 'hoisted' ? hoistedValue(node, parents) : node))
|
|
55
|
+
found.push(node);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
parents.push(node);
|
|
59
|
+
for (const child of childNodes(node))
|
|
60
|
+
visit(child);
|
|
61
|
+
parents.pop();
|
|
62
|
+
};
|
|
63
|
+
visit(program);
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
/** Whether the file has in-source tests, which read `import.meta.vitest`. */
|
|
67
|
+
function hasInSourceTests(node) {
|
|
68
|
+
return (node.type === 'MemberExpression' && isImportMetaVitest(node)) || childNodes(node).some(hasInSourceTests);
|
|
69
|
+
}
|
|
70
|
+
/** What Vitest moves for a `vi.hoisted` call: the declaration it initializes, the `await` around it, or the call. */
|
|
71
|
+
function hoistedValue(call, parents) {
|
|
72
|
+
const declaration = parents.findLast((node) => node.type === 'VariableDeclaration');
|
|
73
|
+
const init = declaration?.type === 'VariableDeclaration' ? declaration.declarations[0]?.init : null;
|
|
74
|
+
const value = init ? unwrap(init) : null;
|
|
75
|
+
if (declaration && (value === call || (value?.type === 'AwaitExpression' && unwrap(value.argument) === call))) {
|
|
76
|
+
return declaration;
|
|
77
|
+
}
|
|
78
|
+
const parent = parents.at(-1);
|
|
79
|
+
return parent?.type === 'AwaitExpression' ? parent : call;
|
|
80
|
+
}
|
|
31
81
|
/** The names a file binds to Vitest's `test`, `describe`, hooks and namespace. */
|
|
32
82
|
class VitestBindings {
|
|
33
83
|
kinds = new Map();
|
|
34
84
|
hooks = new Map();
|
|
35
85
|
namespaces = new Set();
|
|
86
|
+
/** Names `bench` is imported as from Vitest 5, which no longer exports it. */
|
|
87
|
+
benches = new Set();
|
|
36
88
|
/** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
|
|
37
89
|
declarations = new Set();
|
|
38
90
|
problems = [];
|
|
@@ -55,7 +107,7 @@ class VitestBindings {
|
|
|
55
107
|
this.problems.push(problem('invalid-import', specifier, "Vitest has no default export; import { test, describe } from 'vitest' instead."));
|
|
56
108
|
}
|
|
57
109
|
else {
|
|
58
|
-
this.bindImported(exportName(specifier.imported), specifier.local.name,
|
|
110
|
+
this.bindImported(exportName(specifier.imported), specifier.local.name, version);
|
|
59
111
|
}
|
|
60
112
|
}
|
|
61
113
|
}
|
|
@@ -100,7 +152,7 @@ class VitestBindings {
|
|
|
100
152
|
const node = unwrap(expression);
|
|
101
153
|
return node.type === 'Identifier' && this.namespaces.has(node.name);
|
|
102
154
|
}
|
|
103
|
-
bindImported(imported, local,
|
|
155
|
+
bindImported(imported, local, version) {
|
|
104
156
|
const kind = KINDS.get(imported);
|
|
105
157
|
if (kind) {
|
|
106
158
|
this.kinds.set(local, kind);
|
|
@@ -109,7 +161,7 @@ class VitestBindings {
|
|
|
109
161
|
this.hooks.set(local, imported);
|
|
110
162
|
}
|
|
111
163
|
else if (imported === 'bench' && atLeast(version, 5, 0)) {
|
|
112
|
-
this.
|
|
164
|
+
this.benches.add(local);
|
|
113
165
|
}
|
|
114
166
|
}
|
|
115
167
|
bind(declarator) {
|
|
@@ -156,6 +208,12 @@ export class VitestAdapter {
|
|
|
156
208
|
this.readModule = readModule;
|
|
157
209
|
this.bindings = new VitestBindings(parsed.program, file.version);
|
|
158
210
|
this.problems = [...this.bindings.problems];
|
|
211
|
+
if (atLeast(file.version, 5, 0)) {
|
|
212
|
+
for (const call of nestedHoistedCalls(parsed.program)) {
|
|
213
|
+
const callee = snippet(text(this.file, call.callee));
|
|
214
|
+
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));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
159
217
|
this.imports = moduleImports(parsed.program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
160
218
|
if (atLeast(file.version, 4, 1)) {
|
|
161
219
|
// Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
|
|
@@ -166,7 +224,24 @@ export class VitestAdapter {
|
|
|
166
224
|
}
|
|
167
225
|
}
|
|
168
226
|
classify(call, context) {
|
|
169
|
-
return
|
|
227
|
+
return (this.benchCall(call, context) ??
|
|
228
|
+
this.hookCall(call, context) ??
|
|
229
|
+
this.contextSkip(call, context) ??
|
|
230
|
+
this.chainCall(call, context));
|
|
231
|
+
}
|
|
232
|
+
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
233
|
+
benchCall(call, context) {
|
|
234
|
+
const base = readChain(call.callee)?.base;
|
|
235
|
+
if (base?.type !== 'Identifier' || !this.bindings.benches.has(base.name))
|
|
236
|
+
return null;
|
|
237
|
+
const declaration = this.file.values.declarationOf(base.name, context.scopes);
|
|
238
|
+
if (declaration === null || !this.bindings.declarations.has(declaration))
|
|
239
|
+
return null;
|
|
240
|
+
const callee = snippet(text(this.file, call.callee));
|
|
241
|
+
return {
|
|
242
|
+
kind: 'invalid',
|
|
243
|
+
problem: problem('removed-api', call, `Vitest 5 no longer exports bench from 'vitest'; ${callee}(...) fails. Benchmarks are not tests.`, callee),
|
|
244
|
+
};
|
|
170
245
|
}
|
|
171
246
|
chainCall(call, context) {
|
|
172
247
|
const chain = readChain(call.callee);
|
package/dist/model.d.ts
CHANGED
|
@@ -77,9 +77,13 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
77
77
|
reason: string | null;
|
|
78
78
|
hasDynamicReason: boolean;
|
|
79
79
|
problems: Problem[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
}
|
|
81
|
+
/** `test.describe.configure()`: a mode, locks (Playwright 1.64), or both. */
|
|
82
|
+
| {
|
|
83
|
+
kind: 'configure';
|
|
84
|
+
mode: Mode | null;
|
|
85
|
+
locks: string[];
|
|
86
|
+
problems: Problem[];
|
|
83
87
|
} | {
|
|
84
88
|
kind: 'hook';
|
|
85
89
|
fn: FunctionNode | null;
|
package/dist/options.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommentsMode } from './records.ts';
|
|
2
2
|
import type { Framework } from './types.ts';
|
|
3
3
|
export type { CommentsMode };
|
|
4
|
+
/** @inline */
|
|
4
5
|
export interface ScanOptions {
|
|
5
6
|
/** Glob patterns for the test files, relative to `root`. */
|
|
6
7
|
patterns: string[];
|
|
@@ -14,6 +15,7 @@ export interface ScanOptions {
|
|
|
14
15
|
/** Which tests get the comments above them. Defaults to `non-active`. */
|
|
15
16
|
comments?: CommentsMode;
|
|
16
17
|
}
|
|
18
|
+
/** @inline */
|
|
17
19
|
export interface ScanSourceOptions {
|
|
18
20
|
code: string;
|
|
19
21
|
/** Path of the file from the project root, used in the output and in ids. */
|
package/dist/records.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Extraction } from './extract.ts';
|
|
2
2
|
import type { ParsedFile } from './parse.ts';
|
|
3
3
|
import type { Diagnostic, Framework, SuiteRecord, TestRecord } from './types.ts';
|
|
4
|
+
/** @inline */
|
|
4
5
|
export type CommentsMode = 'all' | 'non-active' | 'none';
|
|
5
6
|
export interface FileRecords {
|
|
6
7
|
suites: SuiteRecord[];
|
package/dist/records.js
CHANGED
|
@@ -203,7 +203,11 @@ class RecordBuilder {
|
|
|
203
203
|
]),
|
|
204
204
|
annotations: [...describes.flatMap((scope) => scope.details.annotations), ...pending.annotations],
|
|
205
205
|
comments,
|
|
206
|
-
locks: unique([
|
|
206
|
+
locks: unique([
|
|
207
|
+
...fileScope.details.locks,
|
|
208
|
+
...describes.flatMap((scope) => scope.details.locks),
|
|
209
|
+
...declaration.details.locks,
|
|
210
|
+
]),
|
|
207
211
|
meta: Object.keys(meta).length > 0 ? meta : null,
|
|
208
212
|
isParameterized: tables.length > 0,
|
|
209
213
|
caseCount: tables.length > 0 ? repetitions(tables.map((item) => item.caseCount)) : null,
|
|
@@ -231,7 +235,7 @@ class RecordBuilder {
|
|
|
231
235
|
seen.set(group, keys);
|
|
232
236
|
if (keys.has(key)) {
|
|
233
237
|
const message = playwright
|
|
234
|
-
? 'Another test in the file has the same title path, so Playwright fails to load the file.'
|
|
238
|
+
? 'Another test in the file has the same title path, so Playwright fails to load the file and runs no tests at all.'
|
|
235
239
|
: 'Another test in the same describe has the same title.';
|
|
236
240
|
const item = problem('duplicate-title', test.call, message, test.declaration.title.text);
|
|
237
241
|
found.push({ index, problem: playwright ? { ...item, level: 'error' } : item });
|
package/dist/resolve.js
CHANGED
|
@@ -179,7 +179,8 @@ export function moduleResolver(root) {
|
|
|
179
179
|
return (fromFile) => (specifier) => {
|
|
180
180
|
const directory = path.dirname(fromFile);
|
|
181
181
|
let candidates;
|
|
182
|
-
|
|
182
|
+
// `.` and `..` are paths too: the index of the file's folder, or of the folder above.
|
|
183
|
+
if (/^\.\.?(?:\/|$)/.test(specifier)) {
|
|
183
184
|
candidates = [path.resolve(directory, specifier)];
|
|
184
185
|
}
|
|
185
186
|
else if (specifier.startsWith('#')) {
|
package/dist/scan.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileDiagnostic } from "./diagnostics.js";
|
|
5
|
-
import { findFiles, readFailure } from "./files.js";
|
|
5
|
+
import { findFiles, normalizePattern, readFailure } from "./files.js";
|
|
6
6
|
import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
|
|
7
7
|
import { createInventory } from "./inventory.js";
|
|
8
8
|
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, } from "./options.js";
|
|
@@ -18,7 +18,9 @@ function versionDiagnostics(framework, version, source, range) {
|
|
|
18
18
|
}
|
|
19
19
|
const [major, minor] = majorMinor(version);
|
|
20
20
|
const [oldestMajor, oldestMinor] = OLDEST[framework];
|
|
21
|
-
|
|
21
|
+
// A range gets the newest rules of its major, so only its major has to be supported.
|
|
22
|
+
const tooOld = major < oldestMajor || (major === oldestMajor && minor < oldestMinor && range === null);
|
|
23
|
+
if (tooOld) {
|
|
22
24
|
return [
|
|
23
25
|
fileDiagnostic('unsupported-framework-version', source, `${name} ${version} is older than ${oldestMajor}.${oldestMinor}, the oldest supported version; the results follow ${oldestMajor}.${oldestMinor}.`),
|
|
24
26
|
];
|
|
@@ -29,6 +31,17 @@ function versionDiagnostics(framework, version, source, range) {
|
|
|
29
31
|
fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
|
|
30
32
|
];
|
|
31
33
|
}
|
|
34
|
+
/** For a pattern that names a folder, which matches no file, a pattern that matches the test files in it. */
|
|
35
|
+
function folderHint(root, patterns, framework) {
|
|
36
|
+
const folder = patterns.find((pattern) => {
|
|
37
|
+
const target = path.resolve(root, normalizePattern(pattern));
|
|
38
|
+
return existsSync(target) && statSync(target).isDirectory();
|
|
39
|
+
});
|
|
40
|
+
if (folder === undefined)
|
|
41
|
+
return '';
|
|
42
|
+
const example = `${folder.replace(/[\\/]+$/, '')}/**/*.${framework === 'playwright' ? 'spec' : 'test'}.ts`;
|
|
43
|
+
return ` "${folder}" is a folder; to scan the test files in it, use a pattern such as "${example}".`;
|
|
44
|
+
}
|
|
32
45
|
/**
|
|
33
46
|
* Scans the test files that match `patterns` under `root`, one file at a time.
|
|
34
47
|
*
|
|
@@ -60,7 +73,8 @@ export async function scan(options) {
|
|
|
60
73
|
projectDiagnostics.push(...found.diagnostics);
|
|
61
74
|
if (found.files.length === 0) {
|
|
62
75
|
const list = patterns.map((pattern) => `"${pattern}"`).join(', ');
|
|
63
|
-
|
|
76
|
+
const message = `No test files match ${list} in the root.${folderHint(root, patterns, framework)}`;
|
|
77
|
+
projectDiagnostics.push(fileDiagnostic('no-files-matched', '.', message));
|
|
64
78
|
}
|
|
65
79
|
const readerFor = moduleResolver(root);
|
|
66
80
|
const results = [];
|
package/dist/types.d.ts
CHANGED
|
@@ -10,17 +10,28 @@ type Count = number;
|
|
|
10
10
|
type Position = number;
|
|
11
11
|
/** @pattern ^[0-9a-f]{16}$ */
|
|
12
12
|
type Hash = string;
|
|
13
|
+
/** @inline */
|
|
13
14
|
export type Framework = 'playwright' | 'vitest';
|
|
14
15
|
/**
|
|
15
16
|
* The state the runner applies to a test, after describes and file-level calls are taken into account. `notLoaded`:
|
|
16
17
|
* the runner refuses to load the file, so none of its tests run.
|
|
18
|
+
*
|
|
19
|
+
* @inline
|
|
17
20
|
*/
|
|
18
21
|
export type TestState = 'active' | 'skip' | 'fixme' | 'todo' | 'expectedToFail' | 'notLoaded';
|
|
19
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Where a test's state is set: on the test, on a describe around it, or at file level.
|
|
24
|
+
*
|
|
25
|
+
* @inline
|
|
26
|
+
*/
|
|
20
27
|
export type StateSource = 'test' | 'suite' | 'file';
|
|
21
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* `error`: tests may be missing from the output. `warning`: a value is missing or approximate.
|
|
30
|
+
*
|
|
31
|
+
* @inline
|
|
32
|
+
*/
|
|
22
33
|
export type DiagnosticLevel = 'error' | 'warning';
|
|
23
|
-
export type DiagnosticCode = 'unresolved-test-import' | 'parse-error' | 'read-error' | 'framework-mismatch' | 'unresolved-tag' | 'unresolved-annotation' | 'unresolved-cases' | 'test-in-loop' | 'dynamic-title' | 'duplicate-title' | 'duplicate-id' | 'internal-error' | 'invalid-chain' | 'skip-without-body' | 'test-without-body' | 'describe-not-called' | 'async-describe' | 'nested-test' | 'tag-without-at' | 'removed-api' | 'only-in-skipped-describe' | 'config-may-change-state' | 'symlink-ignored' | 'invalid-import' | 'globals-not-enabled' | 'unknown-framework-version' | 'unsupported-framework-version' | 'local-test-object' | 'state-in-helper' | 'unresolved-option' | 'dynamic-reason' | 'no-files-matched' | 'unsupported-file' | 'approximate-framework-version';
|
|
34
|
+
export type DiagnosticCode = 'unresolved-test-import' | 'parse-error' | 'read-error' | 'framework-mismatch' | 'unresolved-tag' | 'unresolved-annotation' | 'unresolved-cases' | 'test-in-loop' | 'dynamic-title' | 'duplicate-title' | 'duplicate-id' | 'internal-error' | 'invalid-chain' | 'skip-without-body' | 'test-without-body' | 'describe-not-called' | 'async-describe' | 'nested-test' | 'tag-without-at' | 'removed-api' | 'only-in-skipped-describe' | 'config-may-change-state' | 'symlink-ignored' | 'invalid-import' | 'globals-not-enabled' | 'unknown-framework-version' | 'unsupported-framework-version' | 'local-test-object' | 'state-in-helper' | 'unresolved-option' | 'dynamic-reason' | 'no-files-matched' | 'unsupported-file' | 'approximate-framework-version' | 'test-in-function';
|
|
24
35
|
export type MetaValue = string | number | boolean | null | MetaValue[] | {
|
|
25
36
|
[key: string]: MetaValue;
|
|
26
37
|
};
|
|
@@ -38,6 +49,7 @@ export interface Inventory {
|
|
|
38
49
|
tests: TestRecord[];
|
|
39
50
|
diagnostics: Diagnostic[];
|
|
40
51
|
}
|
|
52
|
+
/** @inline */
|
|
41
53
|
export interface Tool {
|
|
42
54
|
name: 'test-inventory';
|
|
43
55
|
/** Version of test-inventory that produced the output. */
|
|
@@ -184,7 +196,7 @@ export interface TestRecord {
|
|
|
184
196
|
annotations: Annotation[];
|
|
185
197
|
/** Comment lines directly above the test. By default only filled for tests that are not active. */
|
|
186
198
|
comments: string[];
|
|
187
|
-
/** From the test and
|
|
199
|
+
/** From the test, its describes and the `test.describe.configure()` calls around it. Playwright only. */
|
|
188
200
|
locks: string[];
|
|
189
201
|
/** Merged from the describes and the test. Vitest only; literal values only. */
|
|
190
202
|
meta: Record<string, MetaValue> | null;
|
|
@@ -202,6 +214,7 @@ export interface TestRecord {
|
|
|
202
214
|
/** Declared inside a plain function. */
|
|
203
215
|
isInFunction: boolean;
|
|
204
216
|
}
|
|
217
|
+
/** @inline */
|
|
205
218
|
export interface Annotation {
|
|
206
219
|
type: string;
|
|
207
220
|
description: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-inventory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Test metadata from source code, not from test results.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"licenses": "node scripts/check-licenses.ts",
|
|
66
66
|
"compare": "node scripts/compare-runners.ts",
|
|
67
67
|
"corpus": "node scripts/scan-corpus.ts",
|
|
68
|
+
"release": "node scripts/release.ts",
|
|
68
69
|
"lint": "eslint .",
|
|
69
70
|
"lint:fix": "eslint . --fix",
|
|
70
71
|
"format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .git/info/exclude",
|
package/schema/v1.json
CHANGED
|
@@ -646,7 +646,7 @@
|
|
|
646
646
|
"items": {
|
|
647
647
|
"type": "string"
|
|
648
648
|
},
|
|
649
|
-
"description": "From the test and
|
|
649
|
+
"description": "From the test, its describes and the `test.describe.configure()` calls around it. Playwright only."
|
|
650
650
|
},
|
|
651
651
|
"meta": {
|
|
652
652
|
"anyOf": [
|
|
@@ -880,7 +880,8 @@
|
|
|
880
880
|
"dynamic-reason",
|
|
881
881
|
"no-files-matched",
|
|
882
882
|
"unsupported-file",
|
|
883
|
-
"approximate-framework-version"
|
|
883
|
+
"approximate-framework-version",
|
|
884
|
+
"test-in-function"
|
|
884
885
|
]
|
|
885
886
|
},
|
|
886
887
|
{
|