test-inventory 0.4.1 → 0.6.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 +96 -0
- package/README.md +4 -1
- package/dist/ast.d.ts +3 -3
- package/dist/ast.js +4 -4
- package/dist/bin.js +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +27 -7
- package/dist/config-dependencies.d.ts +62 -0
- package/dist/config-dependencies.js +231 -0
- package/dist/config-runs.d.ts +113 -0
- package/dist/config-runs.js +807 -0
- package/dist/config.d.ts +90 -0
- package/dist/config.js +652 -0
- package/dist/dependency-hash.d.ts +242 -0
- package/dist/dependency-hash.js +1458 -0
- package/dist/diagnostics.js +6 -1
- package/dist/extract.d.ts +20 -0
- package/dist/extract.js +20 -4
- package/dist/filter.d.ts +6 -0
- package/dist/filter.js +18 -1
- package/dist/frameworks/bindings.d.ts +22 -0
- package/dist/frameworks/bindings.js +19 -9
- package/dist/frameworks/common.d.ts +5 -0
- package/dist/frameworks/common.js +11 -0
- package/dist/frameworks/playwright.d.ts +2 -1
- package/dist/frameworks/playwright.js +11 -2
- package/dist/frameworks/vitest.d.ts +2 -1
- package/dist/frameworks/vitest.js +9 -3
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/inventory.d.ts +2 -2
- package/dist/inventory.js +2 -1
- package/dist/json.js +2 -0
- package/dist/model.d.ts +9 -1
- package/dist/options.d.ts +2 -2
- package/dist/records.d.ts +58 -2
- package/dist/records.js +196 -28
- package/dist/scan-file.d.ts +13 -1
- package/dist/scan-file.js +16 -4
- package/dist/scan-source.js +1 -1
- package/dist/scan.d.ts +2 -1
- package/dist/scan.js +131 -18
- package/dist/types.d.ts +83 -7
- package/dist/values.d.ts +5 -0
- package/dist/values.js +5 -0
- package/dist/vitest-config.d.ts +6 -4
- package/dist/vitest-config.js +32 -28
- package/dist/vitest-defaults.d.ts +6 -0
- package/dist/vitest-defaults.js +16 -0
- package/package.json +10 -8
- package/schema/v1.json +200 -6
package/dist/diagnostics.js
CHANGED
|
@@ -22,6 +22,11 @@ export const LEVELS = {
|
|
|
22
22
|
'removed-api': 'error',
|
|
23
23
|
'only-in-skipped-describe': 'warning',
|
|
24
24
|
'config-may-change-state': 'warning',
|
|
25
|
+
'undefined-tag': 'error',
|
|
26
|
+
'config-value-unknown': 'warning',
|
|
27
|
+
'file-not-run': 'warning',
|
|
28
|
+
'forbidden-only': 'error',
|
|
29
|
+
'dependency-not-followed': 'warning',
|
|
25
30
|
'symlink-ignored': 'warning',
|
|
26
31
|
'invalid-import': 'error',
|
|
27
32
|
'globals-not-enabled': 'warning',
|
|
@@ -44,7 +49,7 @@ export const LEVELS = {
|
|
|
44
49
|
*/
|
|
45
50
|
export const REJECTS_FILE = {
|
|
46
51
|
playwright: new Set(['test-without-body', 'tag-without-at', 'invalid-chain', 'duplicate-title']),
|
|
47
|
-
vitest: new Set(['invalid-chain', 'removed-api']),
|
|
52
|
+
vitest: new Set(['invalid-chain', 'removed-api', 'undefined-tag']),
|
|
48
53
|
};
|
|
49
54
|
export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
|
|
50
55
|
const position = problem.node && source ? source.position(problem.node.start) : null;
|
package/dist/extract.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface Scope {
|
|
|
18
18
|
hookRun: RunSettings;
|
|
19
19
|
/** Fixtures of the hooks in the scope that run around each test. */
|
|
20
20
|
hookFixtures: string[];
|
|
21
|
+
/** Fixtures of every hook in the scope, `beforeAll` and `afterAll` too, which set up what its tests run. */
|
|
22
|
+
setupFixtures: string[];
|
|
23
|
+
/** A hook in the scope takes its fixtures whole, so it may use any. */
|
|
24
|
+
takesAllFixtures: boolean;
|
|
21
25
|
suite: PendingSuite | null;
|
|
22
26
|
}
|
|
23
27
|
/** Where a test or describe registered through an imported helper is written, in the helper's module. */
|
|
@@ -33,10 +37,17 @@ export interface PendingSuite {
|
|
|
33
37
|
/** For a describe declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
|
|
34
38
|
site: Span | null;
|
|
35
39
|
declaredAt: DeclaredAt | null;
|
|
40
|
+
/** The file the describe is written in. */
|
|
41
|
+
frame: Frame;
|
|
42
|
+
/** The outermost helper call in the scanned file that declares the describe; null when the file declares it. */
|
|
43
|
+
registeredBy: CallExpression | null;
|
|
36
44
|
/** Where the scanned file registers the describe: its own call, or the outermost helper call that declares it. */
|
|
37
45
|
order: number;
|
|
38
46
|
/** Statements of the tests and describes directly inside, left out of the describe's body hash. */
|
|
39
47
|
cuts: Span[];
|
|
48
|
+
/** Declared under a condition, or in a function: it may not run while the file loads. */
|
|
49
|
+
isInCondition: boolean;
|
|
50
|
+
isInFunction: boolean;
|
|
40
51
|
}
|
|
41
52
|
export interface PendingTest {
|
|
42
53
|
scope: Scope;
|
|
@@ -45,6 +56,10 @@ export interface PendingTest {
|
|
|
45
56
|
/** For a test declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
|
|
46
57
|
site: Span | null;
|
|
47
58
|
declaredAt: DeclaredAt | null;
|
|
59
|
+
/** The file the test is written in. */
|
|
60
|
+
frame: Frame;
|
|
61
|
+
/** The outermost helper call in the scanned file that declares the test; null when the file declares it. */
|
|
62
|
+
registeredBy: CallExpression | null;
|
|
48
63
|
/** Where the scanned file registers the test: its own call, or the outermost helper call that declares it. */
|
|
49
64
|
order: number;
|
|
50
65
|
/** States from the declaration, then from calls in the body such as `test.skip(condition)`. */
|
|
@@ -80,6 +95,11 @@ export interface Extraction {
|
|
|
80
95
|
/** Hooks at the top level of the file. */
|
|
81
96
|
hooks: HookCounts;
|
|
82
97
|
usesGlobals: boolean;
|
|
98
|
+
/** The file's `@module-tag` tags, where each is written. */
|
|
99
|
+
fileTags: {
|
|
100
|
+
name: string;
|
|
101
|
+
span: Span;
|
|
102
|
+
}[];
|
|
83
103
|
}
|
|
84
104
|
/**
|
|
85
105
|
* Applies settings written later in the same place: the last value of each wins, and a test is slow when any
|
package/dist/extract.js
CHANGED
|
@@ -140,6 +140,8 @@ class Walker {
|
|
|
140
140
|
hooks: noHooks(),
|
|
141
141
|
hookRun: {},
|
|
142
142
|
hookFixtures: [],
|
|
143
|
+
setupFixtures: [],
|
|
144
|
+
takesAllFixtures: false,
|
|
143
145
|
suite: null,
|
|
144
146
|
};
|
|
145
147
|
}
|
|
@@ -158,7 +160,7 @@ class Walker {
|
|
|
158
160
|
helper: null,
|
|
159
161
|
declaredIn: null,
|
|
160
162
|
quiet: false,
|
|
161
|
-
|
|
163
|
+
registeredBy: null,
|
|
162
164
|
});
|
|
163
165
|
// Functions defined inside these are deferred in turn; the loop reaches them too, as they are added to the end.
|
|
164
166
|
for (const entry of this.deferred)
|
|
@@ -175,6 +177,10 @@ class Walker {
|
|
|
175
177
|
problems: [...this.main.adapter.problems, ...this.problems],
|
|
176
178
|
hooks: this.fileScope.hooks,
|
|
177
179
|
usesGlobals: this.main.adapter.usesGlobals,
|
|
180
|
+
fileTags: this.main.adapter.fileTags.map((name, index) => ({
|
|
181
|
+
name,
|
|
182
|
+
span: this.main.adapter.fileTagSpans[index],
|
|
183
|
+
})),
|
|
178
184
|
};
|
|
179
185
|
}
|
|
180
186
|
visit(node, context) {
|
|
@@ -414,6 +420,8 @@ class Walker {
|
|
|
414
420
|
context.scope.hooks[api.name] += 1;
|
|
415
421
|
if (EACH_HOOKS.has(api.name))
|
|
416
422
|
context.scope.hookFixtures.push(...api.fixtures);
|
|
423
|
+
context.scope.setupFixtures.push(...api.fixtures);
|
|
424
|
+
context.scope.takesAllFixtures ||= api.takesAllFixtures;
|
|
417
425
|
}
|
|
418
426
|
if (api.fn) {
|
|
419
427
|
if (!call.arguments.includes(api.fn))
|
|
@@ -624,7 +632,7 @@ class Walker {
|
|
|
624
632
|
...context,
|
|
625
633
|
isInFunction: true,
|
|
626
634
|
declaredIn: null,
|
|
627
|
-
|
|
635
|
+
registeredBy: context.registeredBy ?? call,
|
|
628
636
|
});
|
|
629
637
|
});
|
|
630
638
|
this.callStack.pop();
|
|
@@ -922,7 +930,9 @@ class Walker {
|
|
|
922
930
|
call,
|
|
923
931
|
site: this.site,
|
|
924
932
|
declaredAt: this.declaredAt(call),
|
|
925
|
-
|
|
933
|
+
frame: this.frame,
|
|
934
|
+
registeredBy: context.registeredBy,
|
|
935
|
+
order: (context.registeredBy ?? call).start,
|
|
926
936
|
states: [...declaration.states],
|
|
927
937
|
annotations: [...declaration.details.annotations],
|
|
928
938
|
isInLoop: context.isInLoop,
|
|
@@ -972,6 +982,8 @@ class Walker {
|
|
|
972
982
|
hooks: noHooks(),
|
|
973
983
|
hookRun: {},
|
|
974
984
|
hookFixtures: [],
|
|
985
|
+
setupFixtures: [],
|
|
986
|
+
takesAllFixtures: false,
|
|
975
987
|
suite: null,
|
|
976
988
|
};
|
|
977
989
|
const suite = {
|
|
@@ -980,8 +992,12 @@ class Walker {
|
|
|
980
992
|
call,
|
|
981
993
|
site: this.site,
|
|
982
994
|
declaredAt: this.declaredAt(call),
|
|
983
|
-
|
|
995
|
+
frame: this.frame,
|
|
996
|
+
registeredBy: context.registeredBy,
|
|
997
|
+
order: (context.registeredBy ?? call).start,
|
|
984
998
|
cuts: [],
|
|
999
|
+
isInCondition: context.isInCondition,
|
|
1000
|
+
isInFunction: context.isInFunction,
|
|
985
1001
|
};
|
|
986
1002
|
scope.suite = suite;
|
|
987
1003
|
this.suites.push(suite);
|
package/dist/filter.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface InventoryFilter {
|
|
|
11
11
|
only?: boolean;
|
|
12
12
|
/** Keeps tests that don't run and give no reason. Tests in a file the runner refuses are left out. */
|
|
13
13
|
withoutReason?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Keeps the tests a change to one of these files can affect: those whose `dependencies` include it, and those that
|
|
16
|
+
* reach code the scan can't follow (`hasUnfollowedDependencies`). Paths are from the project root, as
|
|
17
|
+
* `dependencies` writes them. An empty list keeps only the tests the scan can't follow.
|
|
18
|
+
*/
|
|
19
|
+
related?: string[];
|
|
14
20
|
}
|
|
15
21
|
/** An inventory whose test records have only `id` and the keys picked with `pickTestFields()`. */
|
|
16
22
|
export type PickedInventory<K extends keyof TestRecord> = Omit<Inventory, 'tests'> & {
|
package/dist/filter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { checkObject, OptionsError } from "./options.js";
|
|
2
3
|
import { summarize } from "./summary.js";
|
|
3
4
|
const STATES = ['active', 'skip', 'fixme', 'todo', 'expectedToFail', 'notLoaded'];
|
|
@@ -7,11 +8,15 @@ const FILTER_KEYS = [
|
|
|
7
8
|
'grep',
|
|
8
9
|
'only',
|
|
9
10
|
'withoutReason',
|
|
11
|
+
'related',
|
|
10
12
|
];
|
|
11
13
|
// Typed as a record of every key, so a key added to TestRecord fails to compile until it is listed here.
|
|
12
14
|
const TEST_KEYS = Object.keys({
|
|
13
15
|
id: true,
|
|
14
16
|
bodyHash: true,
|
|
17
|
+
dependencyHash: true,
|
|
18
|
+
dependencies: true,
|
|
19
|
+
hasUnfollowedDependencies: true,
|
|
15
20
|
suiteId: true,
|
|
16
21
|
suitePath: true,
|
|
17
22
|
relativeFilePath: true,
|
|
@@ -55,6 +60,12 @@ const TEST_KEYS = Object.keys({
|
|
|
55
60
|
retryDelay: true,
|
|
56
61
|
retryCondition: true,
|
|
57
62
|
repeats: true,
|
|
63
|
+
timeoutSource: true,
|
|
64
|
+
retriesSource: true,
|
|
65
|
+
repeatsSource: true,
|
|
66
|
+
timeoutConfigText: true,
|
|
67
|
+
retriesConfigText: true,
|
|
68
|
+
repeatsConfigText: true,
|
|
58
69
|
fixtures: true,
|
|
59
70
|
expectCount: true,
|
|
60
71
|
expectedAssertions: true,
|
|
@@ -67,6 +78,7 @@ const TEST_KEYS = Object.keys({
|
|
|
67
78
|
isInLoop: true,
|
|
68
79
|
isInCondition: true,
|
|
69
80
|
isInFunction: true,
|
|
81
|
+
projects: true,
|
|
70
82
|
});
|
|
71
83
|
function list(value, option, allowed) {
|
|
72
84
|
if (value === undefined)
|
|
@@ -110,11 +122,16 @@ export function matcher(filter) {
|
|
|
110
122
|
const grep = pattern(checked.grep);
|
|
111
123
|
const only = flag(checked.only, 'filter.only');
|
|
112
124
|
const withoutReason = flag(checked.withoutReason, 'filter.withoutReason');
|
|
125
|
+
// Written as a shell or Windows may write them, as `./pages/order-ticket.ts` or `pages\\order-ticket.ts`.
|
|
126
|
+
const related = checked.related === undefined
|
|
127
|
+
? null
|
|
128
|
+
: new Set(list(checked.related, 'filter.related', null).map((file) => path.posix.normalize(file.replaceAll('\\', '/'))));
|
|
113
129
|
return (test) => (states.length === 0 || states.includes(test.state)) &&
|
|
114
130
|
(tags.length === 0 || test.tags.some((tag) => tags.includes(tag))) &&
|
|
115
131
|
(grep === null || grep.test(test.fullName)) &&
|
|
116
132
|
(!only || test.isOnly) &&
|
|
117
|
-
(!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null))
|
|
133
|
+
(!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null)) &&
|
|
134
|
+
(related === null || test.hasUnfollowedDependencies || test.dependencies.some((file) => related.has(file)));
|
|
118
135
|
}
|
|
119
136
|
/**
|
|
120
137
|
* Keeps the tests that pass every condition of the filter; a list keeps a test that has any of its values. The
|
|
@@ -54,10 +54,32 @@ export declare function allDeclarators(program: Program): VariableDeclarator[];
|
|
|
54
54
|
export declare function moduleImports(program: Program, isFramework: (source: string) => boolean): Map<string, ModuleImport>;
|
|
55
55
|
/** The module of `require('module')`, or null for any other expression. */
|
|
56
56
|
export declare function requiredModule(expression: Expression): string | null;
|
|
57
|
+
/** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
|
|
58
|
+
export declare function moduleExportsValue(statement: Node): Expression | null;
|
|
57
59
|
/** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
|
|
58
60
|
export declare function bindAll(declarators: VariableDeclarator[], bind: (declarator: VariableDeclarator) => boolean): void;
|
|
59
61
|
/** Whether a module exports `name` as a test object, or as a function that returns one. */
|
|
60
62
|
export declare function exportsTestObject(program: Program, name: string, objects: TestObjects, exported?: Exported): boolean;
|
|
63
|
+
/** How a module exports `name` itself, in `statement`: declared, or exported by name from a local or another module. */
|
|
64
|
+
export type OwnExport = {
|
|
65
|
+
statement: Node;
|
|
66
|
+
} & ({
|
|
67
|
+
kind: 'default';
|
|
68
|
+
value: Expression;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'variable' | 'function';
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'specifier';
|
|
73
|
+
local: string;
|
|
74
|
+
source: string | null;
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* The module's own export of `name`; null when it has none and only `export *` may pass the name on. An own export
|
|
78
|
+
* wins over those, as in JavaScript.
|
|
79
|
+
*/
|
|
80
|
+
export declare function ownExport(program: Program, name: string): OwnExport | null;
|
|
81
|
+
/** The module `export * from 'module'` or `module.exports = require('module')` passes every name on from; or null. */
|
|
82
|
+
export declare function starReexport(statement: Node): string | null;
|
|
61
83
|
/** Whether a module imports, re-exports or requires any of the given sources. */
|
|
62
84
|
export declare function importsFrom(program: Program, matches: (source: string) => boolean): boolean;
|
|
63
85
|
/** Whether a name a module imports from another module is a test object there, which follows that module in turn. */
|
|
@@ -135,8 +135,8 @@ export function requiredModule(expression) {
|
|
|
135
135
|
const argument = node.arguments.at(0);
|
|
136
136
|
return argument?.type === 'Literal' && typeof argument.value === 'string' ? argument.value : null;
|
|
137
137
|
}
|
|
138
|
-
/**
|
|
139
|
-
function
|
|
138
|
+
/** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
|
|
139
|
+
export function moduleExportsValue(statement) {
|
|
140
140
|
if (statement.type !== 'ExpressionStatement')
|
|
141
141
|
return null;
|
|
142
142
|
const expression = unwrap(statement.expression);
|
|
@@ -147,7 +147,12 @@ function commonJsReexport(statement) {
|
|
|
147
147
|
target.object.type === 'Identifier' &&
|
|
148
148
|
target.object.name === 'module' &&
|
|
149
149
|
propertyName(target) === 'exports';
|
|
150
|
-
return isModuleExports ?
|
|
150
|
+
return isModuleExports ? expression.right : null;
|
|
151
|
+
}
|
|
152
|
+
/** The module of `module.exports = require('module')`, which exports what that module does; null otherwise. */
|
|
153
|
+
function commonJsReexport(statement) {
|
|
154
|
+
const value = moduleExportsValue(statement);
|
|
155
|
+
return value ? requiredModule(value) : null;
|
|
151
156
|
}
|
|
152
157
|
/** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
|
|
153
158
|
export function bindAll(declarators, bind) {
|
|
@@ -191,10 +196,10 @@ export function exportsTestObject(program, name, objects, exported = 'test objec
|
|
|
191
196
|
* The module's own export of `name`; null when it has none and only `export *` may pass the name on. An own export
|
|
192
197
|
* wins over those, as in JavaScript.
|
|
193
198
|
*/
|
|
194
|
-
function ownExport(program, name) {
|
|
199
|
+
export function ownExport(program, name) {
|
|
195
200
|
for (const statement of program.body) {
|
|
196
201
|
if (statement.type === 'ExportDefaultDeclaration' && name === 'default') {
|
|
197
|
-
return { kind: 'default', value: statement.declaration };
|
|
202
|
+
return { statement, kind: 'default', value: statement.declaration };
|
|
198
203
|
}
|
|
199
204
|
if (statement.type !== 'ExportNamedDeclaration')
|
|
200
205
|
continue;
|
|
@@ -202,21 +207,26 @@ function ownExport(program, name) {
|
|
|
202
207
|
if (declaration?.type === 'VariableDeclaration') {
|
|
203
208
|
const declared = declaration.declarations.some((item) => item.id.type === 'Identifier' && item.id.name === name);
|
|
204
209
|
if (declared)
|
|
205
|
-
return { kind: 'variable' };
|
|
210
|
+
return { statement, kind: 'variable' };
|
|
206
211
|
}
|
|
207
212
|
const named = declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration';
|
|
208
213
|
if (named && declaration.id?.name === name)
|
|
209
|
-
return { kind: 'function' };
|
|
214
|
+
return { statement, kind: 'function' };
|
|
210
215
|
for (const specifier of statement.specifiers) {
|
|
211
216
|
if (exportName(specifier.exported) !== name)
|
|
212
217
|
continue;
|
|
213
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
statement,
|
|
220
|
+
kind: 'specifier',
|
|
221
|
+
local: exportName(specifier.local),
|
|
222
|
+
source: statement.source?.value ?? null,
|
|
223
|
+
};
|
|
214
224
|
}
|
|
215
225
|
}
|
|
216
226
|
return null;
|
|
217
227
|
}
|
|
218
228
|
/** The module `export * from 'module'` or `module.exports = require('module')` passes every name on from; or null. */
|
|
219
|
-
function starReexport(statement) {
|
|
229
|
+
export function starReexport(statement) {
|
|
220
230
|
if (statement.type === 'ExportAllDeclaration')
|
|
221
231
|
return statement.exported ? null : statement.source.value;
|
|
222
232
|
return commonJsReexport(statement);
|
|
@@ -116,6 +116,11 @@ export declare function readOptions(file: FileContext, scopes: readonly Node[],
|
|
|
116
116
|
export declare function unresolvedOptions(file: FileContext, options: Options, result: string): Problem[];
|
|
117
117
|
/** The names a function takes from the object pattern of one of its parameters, in source order. */
|
|
118
118
|
export declare function destructuredNames(fn: FunctionNode | null, index: number | null): string[];
|
|
119
|
+
/**
|
|
120
|
+
* Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
|
|
121
|
+
* pattern, so that any key of it may be used.
|
|
122
|
+
*/
|
|
123
|
+
export declare function takesWhole(fn: FunctionNode | null, index: number | null): boolean;
|
|
119
124
|
/** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
|
|
120
125
|
export declare function readCount(file: FileContext, name: string, option: OptionValue, problems: Problem[]): number | null;
|
|
121
126
|
/** A warning for one option whose value could not be worked out. */
|
|
@@ -287,6 +287,17 @@ export function destructuredNames(fn, index) {
|
|
|
287
287
|
const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
|
|
288
288
|
return names.filter((name) => name !== null);
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
|
|
292
|
+
* pattern, so that any key of it may be used.
|
|
293
|
+
*/
|
|
294
|
+
export function takesWhole(fn, index) {
|
|
295
|
+
const parameter = index === null ? undefined : fn?.params[index];
|
|
296
|
+
if (!parameter)
|
|
297
|
+
return false;
|
|
298
|
+
return (parameter.type !== 'ObjectPattern' ||
|
|
299
|
+
parameter.properties.some((property) => property.type === 'RestElement' || property.computed));
|
|
300
|
+
}
|
|
290
301
|
/** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
|
|
291
302
|
export function readCount(file, name, option, problems) {
|
|
292
303
|
const { node, resolved } = option;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { CallExpression, Expression, Node, Program } from 'oxc-parser';
|
|
2
|
-
import type { Adapter, ApiCall, CallContext, ModuleReader, Problem } from '../model.ts';
|
|
2
|
+
import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
|
|
3
3
|
import { type FileContext } from './common.ts';
|
|
4
4
|
export declare class PlaywrightAdapter implements Adapter {
|
|
5
5
|
readonly problems: Problem[];
|
|
6
6
|
readonly fileTags: string[];
|
|
7
|
+
readonly fileTagSpans: Span[];
|
|
7
8
|
readonly usesGlobals = false;
|
|
8
9
|
private readonly file;
|
|
9
10
|
private readonly readModule;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
3
|
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast, findFunction, hookFunction, 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";
|
|
4
|
+
import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, 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([
|
|
@@ -201,6 +201,7 @@ const PLAYWRIGHT = {
|
|
|
201
201
|
export class PlaywrightAdapter {
|
|
202
202
|
problems;
|
|
203
203
|
fileTags = [];
|
|
204
|
+
fileTagSpans = [];
|
|
204
205
|
usesGlobals = false;
|
|
205
206
|
file;
|
|
206
207
|
readModule;
|
|
@@ -258,7 +259,14 @@ export class PlaywrightAdapter {
|
|
|
258
259
|
return this.configure(call, context);
|
|
259
260
|
if (HOOKS.has(key)) {
|
|
260
261
|
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
261
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
kind: 'hook',
|
|
264
|
+
name: key,
|
|
265
|
+
fn,
|
|
266
|
+
testContext: null,
|
|
267
|
+
fixtures: destructuredNames(fn, 0),
|
|
268
|
+
takesAllFixtures: takesWhole(fn, 0),
|
|
269
|
+
};
|
|
262
270
|
}
|
|
263
271
|
if (RUN_MEMBERS.has(key))
|
|
264
272
|
return this.runSetting(call, key, context);
|
|
@@ -474,6 +482,7 @@ export class PlaywrightAdapter {
|
|
|
474
482
|
caseCount: null,
|
|
475
483
|
testContext: null,
|
|
476
484
|
fixtures: destructuredNames(hashed, 0),
|
|
485
|
+
takesAllFixtures: takesWhole(hashed, 0),
|
|
477
486
|
usesBench: false,
|
|
478
487
|
problems,
|
|
479
488
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
2
|
-
import type { Adapter, ApiCall, CallContext, ModuleReader, Problem } from '../model.ts';
|
|
2
|
+
import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
|
|
3
3
|
import type { ParsedFile } from '../parse.ts';
|
|
4
4
|
import { type FileContext, type Link } from './common.ts';
|
|
5
5
|
type Kind = 'test' | 'describe';
|
|
@@ -11,6 +11,7 @@ interface TestFunction {
|
|
|
11
11
|
export declare class VitestAdapter implements Adapter {
|
|
12
12
|
readonly problems: Problem[];
|
|
13
13
|
readonly fileTags: string[];
|
|
14
|
+
readonly fileTagSpans: Span[];
|
|
14
15
|
usesGlobals: boolean;
|
|
15
16
|
private readonly file;
|
|
16
17
|
private readonly readModule;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
4
|
+
import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
5
5
|
/** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
|
|
6
6
|
function shared(a, b) {
|
|
7
7
|
if (!a || a.kind !== b?.kind)
|
|
@@ -256,6 +256,7 @@ class VitestBindings {
|
|
|
256
256
|
export class VitestAdapter {
|
|
257
257
|
problems;
|
|
258
258
|
fileTags = [];
|
|
259
|
+
fileTagSpans = [];
|
|
259
260
|
usesGlobals = false;
|
|
260
261
|
file;
|
|
261
262
|
readModule;
|
|
@@ -293,8 +294,10 @@ export class VitestAdapter {
|
|
|
293
294
|
if (atLeast(file.version, 4, 1)) {
|
|
294
295
|
// Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
|
|
295
296
|
for (const match of parsed.source.text.matchAll(/(\/\/|\*)\s*@module-tag\s+([\w\-/]+)\b/g)) {
|
|
297
|
+
const span = { start: match.index, end: match.index + match[0].length };
|
|
296
298
|
this.fileTags.push(match[2]);
|
|
297
|
-
this.
|
|
299
|
+
this.fileTagSpans.push(span);
|
|
300
|
+
this.tagsUsed(span);
|
|
298
301
|
}
|
|
299
302
|
}
|
|
300
303
|
}
|
|
@@ -525,6 +528,7 @@ export class VitestAdapter {
|
|
|
525
528
|
fn,
|
|
526
529
|
testContext: index === null ? null : contextNames(fn, index),
|
|
527
530
|
fixtures: fixturesOf(fn, index, this.file.version),
|
|
531
|
+
takesAllFixtures: takesWhole(fn, index),
|
|
528
532
|
};
|
|
529
533
|
}
|
|
530
534
|
contextSkip(call, context) {
|
|
@@ -673,6 +677,7 @@ export class VitestAdapter {
|
|
|
673
677
|
states,
|
|
674
678
|
isOnly: flags.has('only'),
|
|
675
679
|
mode,
|
|
680
|
+
...(skips ? { skipPassedDown: passed.line } : {}),
|
|
676
681
|
details: read.details,
|
|
677
682
|
fn,
|
|
678
683
|
hashed: fn ?? namedFunction(this.file, context.scopes, body),
|
|
@@ -697,6 +702,7 @@ export class VitestAdapter {
|
|
|
697
702
|
...base,
|
|
698
703
|
testContext: contextNames(base.hashed, index),
|
|
699
704
|
fixtures: fixturesOf(base.hashed, index, this.file.version),
|
|
705
|
+
takesAllFixtures: takesWhole(base.hashed, index),
|
|
700
706
|
usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
|
|
701
707
|
};
|
|
702
708
|
}
|
|
@@ -871,7 +877,7 @@ export class VitestAdapter {
|
|
|
871
877
|
if (this.warnedAboutTags)
|
|
872
878
|
return;
|
|
873
879
|
this.warnedAboutTags = true;
|
|
874
|
-
this.problems.push(problem('config-may-change-state', node,
|
|
880
|
+
this.problems.push(problem('config-may-change-state', node, "The file uses Vitest tags, and tags defined in the Vitest config can skip tests or change how they run; the scan can't work out the config's tags."));
|
|
875
881
|
}
|
|
876
882
|
}
|
|
877
883
|
/** Rows in a tagged-template table such as ``each`a | b ${1} | ${2}` ``: values divided by header columns. */
|
package/dist/hash.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Node } from 'oxc-parser';
|
|
2
2
|
import type { Span } from './model.ts';
|
|
3
|
-
export declare function shortHash(input: string): string;
|
|
3
|
+
export declare function shortHash(input: string | Uint8Array): string;
|
|
4
4
|
/**
|
|
5
5
|
* Hashes code by its syntax tree, so formatting, comments, quote style, trailing commas and line endings don't change
|
|
6
6
|
* the hash. Nodes at the `cut` spans, such as the tests inside a describe, are left out. Returns null when no code is
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { scanSource } from './scan-source.ts';
|
|
|
3
3
|
export { filterInventory, pickTestFields } from './filter.ts';
|
|
4
4
|
export type { InventoryFilter, PickedInventory } from './filter.ts';
|
|
5
5
|
export type { CommentsMode, ScanOptions, ScanSourceOptions } from './options.ts';
|
|
6
|
-
export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, FileRecord, Framework, HookCounts, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
|
6
|
+
export type { Annotation, ConfigRecord, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, FileRecord, Framework, HookCounts, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
package/dist/inventory.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { FileRecords } from './records.ts';
|
|
2
|
-
import type { Diagnostic, Framework, Inventory } from './types.ts';
|
|
3
|
-
export declare function createInventory(framework: Framework, frameworkVersion: string | null, fileCount: number, files: readonly FileRecords[], projectDiagnostics: readonly Diagnostic[]): Inventory;
|
|
2
|
+
import type { ConfigRecord, Diagnostic, Framework, Inventory } from './types.ts';
|
|
3
|
+
export declare function createInventory(framework: Framework, frameworkVersion: string | null, fileCount: number, files: readonly FileRecords[], projectDiagnostics: readonly Diagnostic[], configs: ConfigRecord[]): Inventory;
|
package/dist/inventory.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { summarize } from "./summary.js";
|
|
2
2
|
import { toolVersion } from "./version.js";
|
|
3
|
-
export function createInventory(framework, frameworkVersion, fileCount, files, projectDiagnostics) {
|
|
3
|
+
export function createInventory(framework, frameworkVersion, fileCount, files, projectDiagnostics, configs) {
|
|
4
4
|
const suites = files.flatMap((file) => file.suites);
|
|
5
5
|
const tests = files.flatMap((file) => file.tests);
|
|
6
6
|
const diagnostics = [...projectDiagnostics, ...files.flatMap((file) => file.diagnostics)];
|
|
@@ -10,6 +10,7 @@ export function createInventory(framework, frameworkVersion, fileCount, files, p
|
|
|
10
10
|
framework,
|
|
11
11
|
frameworkVersion,
|
|
12
12
|
summary: summarize(fileCount, suites, tests, diagnostics),
|
|
13
|
+
configs,
|
|
13
14
|
files: files.map((file) => file.file),
|
|
14
15
|
suites,
|
|
15
16
|
tests,
|
package/dist/json.js
CHANGED
|
@@ -48,6 +48,8 @@ export async function writeInventoryLines(inventory, write) {
|
|
|
48
48
|
const line = (type, record) => emit(`${JSON.stringify({ type, ...record })}\n`);
|
|
49
49
|
const { schemaVersion, tool, framework, frameworkVersion } = inventory;
|
|
50
50
|
await line('inventory', { schemaVersion, tool, framework, frameworkVersion });
|
|
51
|
+
for (const config of inventory.configs)
|
|
52
|
+
await line('config', config);
|
|
51
53
|
for (const file of inventory.files)
|
|
52
54
|
await line('file', file);
|
|
53
55
|
for (const suite of inventory.suites)
|
package/dist/model.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface StateSetting {
|
|
|
11
11
|
reason: string | null;
|
|
12
12
|
/** The reason could not be worked out to a string, so `reason` holds its source text. */
|
|
13
13
|
hasDynamicReason: boolean;
|
|
14
|
-
line
|
|
14
|
+
/** The line in the file that sets the state; null for a state from the config. */
|
|
15
|
+
line: number | null;
|
|
15
16
|
/**
|
|
16
17
|
* Set by code in a test or hook body, which runs only when the test does: a state set while the file loads, on the
|
|
17
18
|
* test or a describe around it, keeps the test from getting there.
|
|
@@ -67,6 +68,8 @@ interface Declaration {
|
|
|
67
68
|
states: StateSetting[];
|
|
68
69
|
isOnly: boolean;
|
|
69
70
|
mode: Mode | null;
|
|
71
|
+
/** Vitest: the line of the describe whose `skip` option reaches this declaration, merged into its options. */
|
|
72
|
+
skipPassedDown?: number;
|
|
70
73
|
details: Details;
|
|
71
74
|
fn: FunctionNode | null;
|
|
72
75
|
/** The function whose code is hashed: `fn`, or a function declared in the file and passed by name. */
|
|
@@ -83,6 +86,8 @@ export interface TestDeclaration extends Declaration {
|
|
|
83
86
|
testContext: unknown;
|
|
84
87
|
/** Names the test function takes from its context, without the framework's own keys. */
|
|
85
88
|
fixtures: string[];
|
|
89
|
+
/** The test function takes its fixtures whole, or keeps the rest of them, so it may use any. */
|
|
90
|
+
takesAllFixtures: boolean;
|
|
86
91
|
/** Uses Vitest 5's `bench` fixture. */
|
|
87
92
|
usesBench: boolean;
|
|
88
93
|
}
|
|
@@ -125,6 +130,7 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
125
130
|
fn: FunctionNode | null;
|
|
126
131
|
testContext: unknown;
|
|
127
132
|
fixtures: string[];
|
|
133
|
+
takesAllFixtures: boolean;
|
|
128
134
|
}
|
|
129
135
|
/** Playwright `test.step()` or `test.step.skip()`. */
|
|
130
136
|
| {
|
|
@@ -193,6 +199,8 @@ export interface Adapter {
|
|
|
193
199
|
problems: Problem[];
|
|
194
200
|
/** Tags that apply to every test in the file. */
|
|
195
201
|
fileTags: string[];
|
|
202
|
+
/** Where each of `fileTags` is written. */
|
|
203
|
+
fileTagSpans: Span[];
|
|
196
204
|
usesGlobals: boolean;
|
|
197
205
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
198
206
|
/**
|
package/dist/options.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export type { CommentsMode };
|
|
|
5
5
|
export interface ScanOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Glob patterns for the test files, relative to `root`. As in Playwright and Vitest, they match names that start with
|
|
8
|
-
* a dot too.
|
|
8
|
+
* a dot too. Without them, the test files are those the configs in the root and the folders below it run.
|
|
9
9
|
*/
|
|
10
|
-
patterns
|
|
10
|
+
patterns?: string[];
|
|
11
11
|
framework: Framework;
|
|
12
12
|
/** The project root. Paths in the output are relative to it. Defaults to the current directory. */
|
|
13
13
|
root?: string;
|