test-inventory 0.6.0 → 0.6.1
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 +77 -0
- package/dist/config-runs.d.ts +31 -5
- package/dist/config-runs.js +181 -41
- package/dist/config.js +65 -3
- package/dist/dependency-hash.d.ts +12 -4
- package/dist/dependency-hash.js +85 -10
- package/dist/extract.js +5 -2
- package/dist/frameworks/vitest.d.ts +1 -1
- package/dist/frameworks/vitest.js +33 -5
- package/dist/model.d.ts +11 -2
- package/dist/parse.d.ts +6 -0
- package/dist/parse.js +204 -0
- package/dist/records.d.ts +2 -0
- package/dist/records.js +6 -2
- package/dist/resolve.js +28 -8
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -140,6 +140,68 @@ function spread(base, over, at) {
|
|
|
140
140
|
return { kind: 'object', keys: new Map(over.keys), rest: over.rest };
|
|
141
141
|
return { kind: 'object', keys: new Map([...base.keys, ...over.keys]), rest: base.rest };
|
|
142
142
|
}
|
|
143
|
+
/** `{ ...a, ...b }` of two objects either of which may be unset, as Playwright merges `use` and `expect`. */
|
|
144
|
+
function assign(a, b, at) {
|
|
145
|
+
return [a, b].reduce((merged, over) => (over ? spread(merged, over, at) : merged), emptyObject());
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Playwright's `defineConfig(base, over)`: `over`'s keys replace `base`'s, but `expect`, `use` and `build` merge key by
|
|
149
|
+
* key, and `projects` by name. `webServer`, whose lists Playwright joins, isn't read.
|
|
150
|
+
*/
|
|
151
|
+
function playwrightMerge(base, over, at) {
|
|
152
|
+
const merged = spread(base, over, at);
|
|
153
|
+
if (over.kind !== 'object' || over.rest)
|
|
154
|
+
return merged;
|
|
155
|
+
for (const key of ['expect', 'use', 'build']) {
|
|
156
|
+
if (merged.keys.has(key))
|
|
157
|
+
merged.keys.set(key, assign(child(base, key), child(over, key), at));
|
|
158
|
+
}
|
|
159
|
+
const overrides = child(over, 'projects');
|
|
160
|
+
// Without projects of its own, `over` keeps those of `base`; with `projects: undefined`, it drops them.
|
|
161
|
+
if (overrides === null || isNullish(overrides))
|
|
162
|
+
return merged;
|
|
163
|
+
const projects = child(base, 'projects');
|
|
164
|
+
const items = projects === null || isNullish(projects) ? [] : projects.kind === 'list' ? projects.items : null;
|
|
165
|
+
const isKnown = items !== null && overrides.kind === 'list';
|
|
166
|
+
merged.keys.set('projects', isKnown ? mergeProjects(items, overrides.items, at) : unknown(at));
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Playwright's projects of a config with those of a config over it merged in by name, with their `use` key by key; the
|
|
171
|
+
* others are added after, the last of each name. Unknown when the scan can't tell a project's name.
|
|
172
|
+
*/
|
|
173
|
+
function mergeProjects(base, overrides, at) {
|
|
174
|
+
const byName = new Map();
|
|
175
|
+
for (const project of overrides) {
|
|
176
|
+
const name = projectName(project);
|
|
177
|
+
if (name === null)
|
|
178
|
+
return unknown(at);
|
|
179
|
+
byName.set(name, project);
|
|
180
|
+
}
|
|
181
|
+
const projects = [];
|
|
182
|
+
for (const project of base) {
|
|
183
|
+
const name = projectName(project);
|
|
184
|
+
if (name === null)
|
|
185
|
+
return unknown(at);
|
|
186
|
+
const override = byName.get(name);
|
|
187
|
+
byName.delete(name);
|
|
188
|
+
if (!override) {
|
|
189
|
+
projects.push(project);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const merged = spread(spread(emptyObject(), project, at), override, at);
|
|
193
|
+
merged.keys.set('use', assign(child(project, 'use'), child(override, 'use'), at));
|
|
194
|
+
projects.push(merged);
|
|
195
|
+
}
|
|
196
|
+
return { kind: 'list', items: [...projects, ...byName.values()] };
|
|
197
|
+
}
|
|
198
|
+
/** The name Playwright merges a project by: undefined when unset, null when the scan can't tell. */
|
|
199
|
+
function projectName(project) {
|
|
200
|
+
const name = child(project, 'name');
|
|
201
|
+
if (name === null || (name.kind === 'value' && name.value === undefined))
|
|
202
|
+
return undefined;
|
|
203
|
+
return name.kind === 'value' && typeof name.value === 'string' ? name.value : null;
|
|
204
|
+
}
|
|
143
205
|
/**
|
|
144
206
|
* One of two settings, as the two sides of a condition: what both agree on is known, and the rest is unknown at `at`,
|
|
145
207
|
* where the condition is written. Null is a setting neither side sets.
|
|
@@ -479,9 +541,9 @@ class ConfigCode {
|
|
|
479
541
|
// Vitest's defineConfig() and defineProject() return the config they are given, or the one a function returns.
|
|
480
542
|
return isFunctionNode(args[0]) ? this.returned(args[0], frame, scopes) : this.evaluate(args[0], frame, scopes);
|
|
481
543
|
}
|
|
482
|
-
// Playwright's defineConfig() merges the configs it is given, each over the ones before it.
|
|
483
|
-
|
|
484
|
-
return
|
|
544
|
+
// Playwright's defineConfig() merges the configs it is given, each over the ones before it.
|
|
545
|
+
const [first, ...others] = args;
|
|
546
|
+
return others.reduce((merged, arg) => playwrightMerge(merged, this.evaluate(arg, frame, scopes), { frame, node: arg }), spread(emptyObject(), this.evaluate(first, frame, scopes), { frame, node: first }));
|
|
485
547
|
}
|
|
486
548
|
/**
|
|
487
549
|
* Where a function a config calls comes from: `name` of `import { name } from 'module'` or
|
|
@@ -2,17 +2,20 @@ import { type Node } from 'oxc-parser';
|
|
|
2
2
|
import type { ConfigGap, RunsWith } from './config-dependencies.ts';
|
|
3
3
|
import type { Extraction, Frame, PendingTest } from './extract.ts';
|
|
4
4
|
import type { ModuleReader, ModuleSource, Problem, Span } from './model.ts';
|
|
5
|
-
import type
|
|
5
|
+
import { type ParsedFile } from './parse.ts';
|
|
6
6
|
import { type SourceText } from './source.ts';
|
|
7
7
|
import type { Framework } from './types.ts';
|
|
8
8
|
import { Values } from './values.ts';
|
|
9
9
|
/** A file whose code a test can run: the scanned file, a module it imports, or a data file such as JSON. */
|
|
10
10
|
interface Module {
|
|
11
11
|
relativeFilePath: string;
|
|
12
|
-
/** Null for a file that isn't JavaScript or TypeScript, which is taken by its text. */
|
|
12
|
+
/** Null for a file that isn't JavaScript or TypeScript, which is taken by its text; for a component, its scripts. */
|
|
13
13
|
parsed: ParsedFile | null;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The file's text with line endings read as `\n`, for a file that isn't code or a component; its bytes when it isn't
|
|
16
|
+
* UTF-8 text, such as an image.
|
|
17
|
+
*/
|
|
18
|
+
text: string | Uint8Array | null;
|
|
16
19
|
values: Values;
|
|
17
20
|
readModule: ModuleReader | null;
|
|
18
21
|
/** Its top-level code that runs when it is imported, and the modules it imports, which run then too. */
|
|
@@ -191,6 +194,11 @@ export declare class DependencyGraph {
|
|
|
191
194
|
* takes the whole module, so that nothing it may run is left out.
|
|
192
195
|
*/
|
|
193
196
|
private exported;
|
|
197
|
+
/**
|
|
198
|
+
* A name a module passes on from another, as `export { quote } from './nasdaq'` does, with the re-export itself: which
|
|
199
|
+
* module the name comes from is code too, so the barrel is among the files a change to which affects the test.
|
|
200
|
+
*/
|
|
201
|
+
private passedOn;
|
|
194
202
|
/** What `export * from source` gives for a name; null when the module behind it doesn't export it. */
|
|
195
203
|
private reexported;
|
|
196
204
|
/**
|
package/dist/dependency-hash.js
CHANGED
|
@@ -4,6 +4,7 @@ import { childNodes, isFunctionNode, propertyKey, unwrap } from "./ast.js";
|
|
|
4
4
|
import { exportName, parseModule } from "./frameworks/bindings.js";
|
|
5
5
|
import { destructuredNames, problem, takesWhole } from "./frameworks/common.js";
|
|
6
6
|
import { shortHash, hashNodes } from "./hash.js";
|
|
7
|
+
import { parseFile } from "./parse.js";
|
|
7
8
|
import { snippet } from "./source.js";
|
|
8
9
|
import { bindingNames, isScopeNode, Values } from "./values.js";
|
|
9
10
|
/** Statements that only declare something, unless code in them runs while they are evaluated. */
|
|
@@ -157,6 +158,50 @@ function isAutoOption(property) {
|
|
|
157
158
|
property.value.value === true);
|
|
158
159
|
}
|
|
159
160
|
const CODE_FILE = /\.[cm]?[jt]sx?$/;
|
|
161
|
+
/** A Vue or Svelte single-file component: markup with its code in `<script>` blocks. */
|
|
162
|
+
const COMPONENT_FILE = /\.(?:vue|svelte)$/;
|
|
163
|
+
/**
|
|
164
|
+
* A comment, which may hold a `<script>` that isn't one, or a script block with its attributes and code. An attribute
|
|
165
|
+
* may hold `>`, as Svelte's `generics="T extends Record<string, number>"` does. Both end where HTML ends them: a
|
|
166
|
+
* comment also at `--!>`, a script at `</script` with anything before its `>`.
|
|
167
|
+
*/
|
|
168
|
+
const COMPONENT_BLOCK = /<!--[\s\S]*?--!?>|<script\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/script(?:[\s/][^>]*)?>/gi;
|
|
169
|
+
/**
|
|
170
|
+
* A file's bytes with line endings as `\n`, as git may check out a text file with either; one git takes for binary, with
|
|
171
|
+
* a NUL byte in its first 8,000, stays as it is.
|
|
172
|
+
*/
|
|
173
|
+
function withLineFeeds(bytes) {
|
|
174
|
+
if (bytes.subarray(0, 8000).includes(0))
|
|
175
|
+
return bytes;
|
|
176
|
+
// Latin-1 maps each byte to one character and back, so only the line endings change.
|
|
177
|
+
return Buffer.from(Buffer.from(bytes).toString('latin1').replace(/\r\n?/g, '\n'), 'latin1');
|
|
178
|
+
}
|
|
179
|
+
/** Whether a script block holds code: one with a `type` other than a JavaScript one, as JSON-LD, holds data. */
|
|
180
|
+
function isScriptCode(attributes) {
|
|
181
|
+
const type = /(?:^|\s)type\s*=\s*["']?([^"'\s>]*)/i.exec(attributes)?.[1] ?? '';
|
|
182
|
+
return /^(?:module|(?:text|application)\/(?:java|ecma)script)?$/i.test(type);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A component's script blocks as one module, the rest of the file blanked so that offsets stay those of the file, and
|
|
186
|
+
* a file name for the language they are in: `lang="ts"` is TypeScript, no `lang` JavaScript.
|
|
187
|
+
*/
|
|
188
|
+
function componentScript(code) {
|
|
189
|
+
// A byte order mark stays, for the parser to leave out as it does from any file.
|
|
190
|
+
const blank = (text) => text.replace(/[^\r\n\uFEFF]/g, ' ');
|
|
191
|
+
let script = '';
|
|
192
|
+
let end = 0;
|
|
193
|
+
let lang = 'js';
|
|
194
|
+
for (const match of code.matchAll(COMPONENT_BLOCK)) {
|
|
195
|
+
const [whole, attributes, body] = match;
|
|
196
|
+
if (whole.startsWith('<!--') || !isScriptCode(attributes))
|
|
197
|
+
continue;
|
|
198
|
+
const start = match.index + '<script'.length + attributes.length + 1;
|
|
199
|
+
script += blank(code.slice(end, start)) + body;
|
|
200
|
+
end = start + body.length;
|
|
201
|
+
lang = /(?:^|\s)lang\s*=\s*["']?(\w+)/.exec(attributes)?.[1] ?? lang;
|
|
202
|
+
}
|
|
203
|
+
return { fileName: `component.${lang}`, code: script + blank(code.slice(end)) };
|
|
204
|
+
}
|
|
160
205
|
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
161
206
|
/**
|
|
162
207
|
* What an import being looked up stands for while the lookup leads back to it, through re-exports: nothing found
|
|
@@ -469,7 +514,8 @@ export class DependencyGraph {
|
|
|
469
514
|
hashOf(unit) {
|
|
470
515
|
if (unit.hash === undefined) {
|
|
471
516
|
if (unit.module.text !== null) {
|
|
472
|
-
unit
|
|
517
|
+
// Counted once, through the unit of all of it, however many of a component's units a test reaches.
|
|
518
|
+
unit.hash = unit.module.whole.includes(unit) ? shortHash(unit.module.text) : null;
|
|
473
519
|
}
|
|
474
520
|
else {
|
|
475
521
|
const cut = [...unit.cut].map(([start, end]) => ({ start, end }));
|
|
@@ -490,7 +536,12 @@ export class DependencyGraph {
|
|
|
490
536
|
this.modules.set(parsed, module);
|
|
491
537
|
}
|
|
492
538
|
else {
|
|
493
|
-
|
|
539
|
+
const text = source.bytes ? withLineFeeds(source.bytes) : source.code.replace(/\r\n?/g, '\n');
|
|
540
|
+
// A component's markup and styles shape what it renders, so it is taken by its text, and its scripts are
|
|
541
|
+
// followed for the modules they import.
|
|
542
|
+
const script = COMPONENT_FILE.test(source.relativeFilePath) ? componentScript(source.code) : null;
|
|
543
|
+
const parsed = script && parseFile(script.fileName, script.code);
|
|
544
|
+
module = this.newModule(source.relativeFilePath, parsed, text, new Values(), parsed && source.readModule);
|
|
494
545
|
}
|
|
495
546
|
this.modules.set(source, module);
|
|
496
547
|
}
|
|
@@ -572,7 +623,7 @@ export class DependencyGraph {
|
|
|
572
623
|
importUnit(module, statement, specifier) {
|
|
573
624
|
let unit = this.imports.get(specifier);
|
|
574
625
|
if (!unit) {
|
|
575
|
-
const roots = [statement.source, specifier].map((node) => ({ node, scopes: [] }));
|
|
626
|
+
const roots = [...new Set([statement.source, specifier])].map((node) => ({ node, scopes: [] }));
|
|
576
627
|
unit = this.newUnit(module, roots, new Map(), false);
|
|
577
628
|
unit.edges = new Set();
|
|
578
629
|
this.imports.set(specifier, unit);
|
|
@@ -682,8 +733,12 @@ export class DependencyGraph {
|
|
|
682
733
|
];
|
|
683
734
|
}
|
|
684
735
|
if (module.parsed.error) {
|
|
685
|
-
|
|
736
|
+
const place = this.placeUnit(from, at, `'${specifier}' could not be parsed, so dependencyHash doesn't cover it.`);
|
|
737
|
+
return module.text === null ? [place] : [this.textUnit(module), place];
|
|
686
738
|
}
|
|
739
|
+
// A component's markup may use anything its scripts declare or import, its types included.
|
|
740
|
+
if (module.text !== null)
|
|
741
|
+
return [this.wholeUnit(module, false)];
|
|
687
742
|
return name === '' ? [this.loadUnit(module, types)] : this.exported(module, name, types, new Set());
|
|
688
743
|
}
|
|
689
744
|
/**
|
|
@@ -711,7 +766,7 @@ export class DependencyGraph {
|
|
|
711
766
|
: [this.declarationUnit(module, declaration, types)];
|
|
712
767
|
}
|
|
713
768
|
if (statement.type === 'ExportAllDeclaration' && statement.exported && exportName(statement.exported) === name) {
|
|
714
|
-
return this.
|
|
769
|
+
return this.passedOn(module, statement, statement.source, statement.exported, '*', types);
|
|
715
770
|
}
|
|
716
771
|
if (statement.type !== 'ExportNamedDeclaration')
|
|
717
772
|
continue;
|
|
@@ -735,7 +790,7 @@ export class DependencyGraph {
|
|
|
735
790
|
continue;
|
|
736
791
|
const localName = exportName(specifier.local);
|
|
737
792
|
return statement.source
|
|
738
|
-
? this.
|
|
793
|
+
? this.passedOn(module, statement, statement.source, specifier, localName, types)
|
|
739
794
|
: local(localName);
|
|
740
795
|
}
|
|
741
796
|
// `export *` passes on every name but `default`, and only when the module doesn't export the name itself.
|
|
@@ -745,11 +800,19 @@ export class DependencyGraph {
|
|
|
745
800
|
continue;
|
|
746
801
|
const found = this.reexported(module, statement.source.value, name, types, searched);
|
|
747
802
|
if (found)
|
|
748
|
-
return found;
|
|
803
|
+
return [...found, this.importUnit(module, statement, statement.source)];
|
|
749
804
|
}
|
|
750
805
|
}
|
|
751
806
|
return [this.wholeUnit(module, types)];
|
|
752
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* A name a module passes on from another, as `export { quote } from './nasdaq'` does, with the re-export itself: which
|
|
810
|
+
* module the name comes from is code too, so the barrel is among the files a change to which affects the test.
|
|
811
|
+
*/
|
|
812
|
+
passedOn(module, statement, source, specifier, name, types) {
|
|
813
|
+
const units = this.imported(module, source.value, name, types, statement);
|
|
814
|
+
return units === IN_PROGRESS ? units : [...units, this.importUnit(module, { source }, specifier)];
|
|
815
|
+
}
|
|
753
816
|
/** What `export * from source` gives for a name; null when the module behind it doesn't export it. */
|
|
754
817
|
reexported(from, source, name, types, searched) {
|
|
755
818
|
const read = this.read(from, source);
|
|
@@ -1161,10 +1224,22 @@ export class DependencyGraph {
|
|
|
1161
1224
|
}
|
|
1162
1225
|
break;
|
|
1163
1226
|
}
|
|
1164
|
-
case 'ImportDeclaration':
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1227
|
+
case 'ImportDeclaration': {
|
|
1228
|
+
if (module.text === null) {
|
|
1229
|
+
if (node.importKind !== 'type' || types)
|
|
1230
|
+
add(this.imported(module, node.source.value, '', types, node));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
// A component's markup uses what its scripts import, where the scan doesn't look, and Vue builds the props
|
|
1234
|
+
// of `defineProps<Props>()` from the types, so every import counts, types with their types.
|
|
1235
|
+
const isType = node.importKind === 'type';
|
|
1236
|
+
add(this.imported(module, node.source.value, '', isType, node));
|
|
1237
|
+
for (const specifier of node.specifiers) {
|
|
1238
|
+
const isTypeName = isType || (specifier.type === 'ImportSpecifier' && specifier.importKind === 'type');
|
|
1239
|
+
add(this.toDeclaration(module, specifier, specifier.local.name, null, isTypeName));
|
|
1240
|
+
}
|
|
1167
1241
|
return;
|
|
1242
|
+
}
|
|
1168
1243
|
case 'TSImportEqualsDeclaration':
|
|
1169
1244
|
if (node.moduleReference.type !== 'TSExternalModuleReference')
|
|
1170
1245
|
break;
|
package/dist/extract.js
CHANGED
|
@@ -753,13 +753,16 @@ class Walker {
|
|
|
753
753
|
mergeRun(call.target.hookRun, settings);
|
|
754
754
|
}
|
|
755
755
|
}
|
|
756
|
-
/**
|
|
756
|
+
/**
|
|
757
|
+
* Counts `expect()`, `expect.soft()`, `expect.poll()` and `expect.element()`, and reads `expect.assertions(n)` and
|
|
758
|
+
* `expect.hasAssertions()`.
|
|
759
|
+
*/
|
|
757
760
|
expectCall(call, test) {
|
|
758
761
|
const callee = unwrap(call.callee);
|
|
759
762
|
const [root, member] = callee.type === 'MemberExpression' ? [unwrap(callee.object), propertyName(callee)] : [callee, 'call'];
|
|
760
763
|
if (root.type !== 'Identifier' || root.name !== 'expect')
|
|
761
764
|
return;
|
|
762
|
-
if (member === 'call' || member === 'soft' || member === 'poll')
|
|
765
|
+
if (member === 'call' || member === 'soft' || member === 'poll' || member === 'element')
|
|
763
766
|
test.expectCount += 1;
|
|
764
767
|
if (member === 'hasAssertions')
|
|
765
768
|
test.expectsAssertions = true;
|
|
@@ -75,7 +75,7 @@ export declare class VitestAdapter implements Adapter {
|
|
|
75
75
|
/** Vitest's order: `only` wins over `skip`, which wins over `todo`. */
|
|
76
76
|
private states;
|
|
77
77
|
private readDeclarationOptions;
|
|
78
|
-
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }
|
|
78
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
|
|
79
79
|
private retry;
|
|
80
80
|
private meta;
|
|
81
81
|
private caseCount;
|
|
@@ -27,6 +27,7 @@ const MODIFIERS = {
|
|
|
27
27
|
};
|
|
28
28
|
const TABLES = new Set(['each', 'for']);
|
|
29
29
|
const CONDITIONS = new Set(['skipIf', 'runIf']);
|
|
30
|
+
const STATE_FLAGS = ['only', 'todo', 'fails'];
|
|
30
31
|
const DERIVATIONS = new Set(['extend', 'override', 'scoped']);
|
|
31
32
|
/** Hooks, with the position of the test context among their callback's parameters. */
|
|
32
33
|
const HOOKS = new Map([
|
|
@@ -634,6 +635,17 @@ export class VitestAdapter {
|
|
|
634
635
|
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
635
636
|
const flags = new Set(chain);
|
|
636
637
|
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
638
|
+
// From Vitest 5.0.5 a test's `skip`, `skipIf` and `runIf` each set its `skip`, so the last of them decides, and a
|
|
639
|
+
// `skipIf` or `runIf` replaces the skip of a tag or of a describe around. The scan applies that to all of Vitest 5.
|
|
640
|
+
const lastSkip = links.filter((link) => link.name === 'skip' || CONDITIONS.has(link.name)).at(-1);
|
|
641
|
+
const skipsByChain = kind === 'test' && lastSkip !== undefined && atLeast(this.file.version, 5, 0);
|
|
642
|
+
const conditionSetsSkip = skipsByChain && CONDITIONS.has(lastSkip.name);
|
|
643
|
+
if (skipsByChain) {
|
|
644
|
+
flags.delete('skip');
|
|
645
|
+
conditions = conditionSetsSkip ? [this.linkCondition(lastSkip, context)] : [];
|
|
646
|
+
if (!conditionSetsSkip)
|
|
647
|
+
flags.add('skip');
|
|
648
|
+
}
|
|
637
649
|
// A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
|
|
638
650
|
// test's mode, and for everything on a describe.
|
|
639
651
|
for (const [flag, value] of read.flags) {
|
|
@@ -653,7 +665,7 @@ export class VitestAdapter {
|
|
|
653
665
|
// win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
|
|
654
666
|
const line = this.file.source.line(call.start);
|
|
655
667
|
const around = context.passedDown;
|
|
656
|
-
const passed = read.skip ? null : around;
|
|
668
|
+
const passed = read.skip || conditionSetsSkip ? null : around;
|
|
657
669
|
const skips = passed?.condition.applies === 'always' && !flags.has('only');
|
|
658
670
|
const table = links.find((link) => TABLES.has(link.name)) ?? null;
|
|
659
671
|
const isTodo = flags.has('todo') || body === undefined;
|
|
@@ -704,6 +716,10 @@ export class VitestAdapter {
|
|
|
704
716
|
fixtures: fixturesOf(base.hashed, index, this.file.version),
|
|
705
717
|
takesAllFixtures: takesWhole(base.hashed, index),
|
|
706
718
|
usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
|
|
719
|
+
ownStateOptions: [
|
|
720
|
+
...(read.skip || conditionSetsSkip ? ['skip'] : []),
|
|
721
|
+
...STATE_FLAGS.filter((flag) => read.flags.has(flag)),
|
|
722
|
+
],
|
|
707
723
|
};
|
|
708
724
|
}
|
|
709
725
|
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
@@ -763,8 +779,14 @@ export class VitestAdapter {
|
|
|
763
779
|
return result;
|
|
764
780
|
const options = readOptions(this.file, context.scopes, node);
|
|
765
781
|
problems.push(...unresolvedOptions(this.file, options, 'its flags, tags and meta are missing'));
|
|
782
|
+
// From Vitest 5.0.5 an option set to `undefined` replaces nothing; the scan applies that to all of Vitest 5.
|
|
783
|
+
const given = (key) => {
|
|
784
|
+
const option = options.values.get(key);
|
|
785
|
+
const isUndefined = option?.resolved.ok === true && option.resolved.value === undefined;
|
|
786
|
+
return isUndefined && atLeast(this.file.version, 5, 0) ? undefined : option;
|
|
787
|
+
};
|
|
766
788
|
for (const flag of ['only', 'todo', 'fails', 'concurrent', 'sequential', 'shuffle']) {
|
|
767
|
-
const option =
|
|
789
|
+
const option = given(flag);
|
|
768
790
|
if (option && !option.resolved.ok) {
|
|
769
791
|
const { importedFrom } = option.resolved;
|
|
770
792
|
problems.push(unresolvedOption(this.file, flag, option.node, importedFrom, 'it is not applied'));
|
|
@@ -776,7 +798,7 @@ export class VitestAdapter {
|
|
|
776
798
|
result.flags.set(flag, Boolean(option.resolved.value));
|
|
777
799
|
}
|
|
778
800
|
}
|
|
779
|
-
const skip =
|
|
801
|
+
const skip = given('skip');
|
|
780
802
|
if (skip) {
|
|
781
803
|
result.skip = skip.resolved.ok
|
|
782
804
|
? { applies: skip.resolved.value ? 'always' : 'never', text: null }
|
|
@@ -806,7 +828,7 @@ export class VitestAdapter {
|
|
|
806
828
|
this.retry(retry, details, context, problems);
|
|
807
829
|
return result;
|
|
808
830
|
}
|
|
809
|
-
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }
|
|
831
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
|
|
810
832
|
retry(option, details, context, problems) {
|
|
811
833
|
const node = unwrap(option.node);
|
|
812
834
|
if (node.type !== 'ObjectExpression') {
|
|
@@ -815,8 +837,14 @@ export class VitestAdapter {
|
|
|
815
837
|
details.run.retry = { count, delay: null, condition: null };
|
|
816
838
|
return;
|
|
817
839
|
}
|
|
818
|
-
if (!atLeast(this.file.version, 4, 1))
|
|
840
|
+
if (!atLeast(this.file.version, 4, 1)) {
|
|
841
|
+
// Before 4.1 the runner compares the attempt number with the object, so it never runs the test.
|
|
842
|
+
const source = snippet(text(this.file, node));
|
|
843
|
+
const message = `Option retry: ${source} is an object, which Vitest reads from 4.1; before, it never runs the test.`;
|
|
844
|
+
problems.push(problem('unresolved-option', node, message, source));
|
|
845
|
+
details.run.retry = null;
|
|
819
846
|
return;
|
|
847
|
+
}
|
|
820
848
|
const values = readOptions(this.file, context.scopes, node);
|
|
821
849
|
problems.push(...unresolvedOptions(this.file, values, 'the retry is not applied'));
|
|
822
850
|
const read = (key) => {
|
package/dist/model.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
|
2
2
|
import type { FunctionNode } from './ast.ts';
|
|
3
3
|
import type { Annotation, DiagnosticCode, DiagnosticLevel, HookCounts, MetaValue } from './types.ts';
|
|
4
4
|
export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
|
|
5
|
+
/** The Vitest options that set a test's state. */
|
|
6
|
+
export type StateOption = 'skip' | 'only' | 'todo' | 'fails';
|
|
5
7
|
export type Mode = 'parallel' | 'serial' | 'default';
|
|
6
8
|
/** A state set by a declaration, a describe or a call such as `test.skip(condition)`. */
|
|
7
9
|
export interface StateSetting {
|
|
@@ -51,12 +53,15 @@ export interface Details {
|
|
|
51
53
|
/** Run settings written in one place; a key that is missing is not set there. */
|
|
52
54
|
export interface RunSettings {
|
|
53
55
|
timeout?: number;
|
|
54
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* A retry setting replaces the whole `retry` of the describes around, delay and condition included; null when it is
|
|
58
|
+
* set to a value the runner can't use.
|
|
59
|
+
*/
|
|
55
60
|
retry?: {
|
|
56
61
|
count: number;
|
|
57
62
|
delay: number | null;
|
|
58
63
|
condition: string | null;
|
|
59
|
-
};
|
|
64
|
+
} | null;
|
|
60
65
|
repeats?: number;
|
|
61
66
|
/** Playwright `test.slow()`: the conditions of the calls, any of which makes the test slow; null when one always does. */
|
|
62
67
|
slow?: {
|
|
@@ -90,6 +95,8 @@ export interface TestDeclaration extends Declaration {
|
|
|
90
95
|
takesAllFixtures: boolean;
|
|
91
96
|
/** Uses Vitest 5's `bench` fixture. */
|
|
92
97
|
usesBench: boolean;
|
|
98
|
+
/** Vitest: the state options the test sets itself, to `false` too, which replace those of its tags. */
|
|
99
|
+
ownStateOptions?: StateOption[];
|
|
93
100
|
}
|
|
94
101
|
export interface SuiteDeclaration extends Declaration {
|
|
95
102
|
kind: 'describe';
|
|
@@ -186,6 +193,8 @@ export interface Returns {
|
|
|
186
193
|
/** A module read to see whether it exports a custom `test` object. */
|
|
187
194
|
export interface ModuleSource {
|
|
188
195
|
code: string;
|
|
196
|
+
/** The file's bytes when it isn't UTF-8 text, such as an image. */
|
|
197
|
+
bytes?: Uint8Array;
|
|
189
198
|
relativeFilePath: string;
|
|
190
199
|
/** Reads the modules this module imports in turn. */
|
|
191
200
|
readModule: ModuleReader;
|
package/dist/parse.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface ParsedFile {
|
|
|
15
15
|
end: number;
|
|
16
16
|
} | null;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The offset of the first bracket nested more than `limit` deep, or null. Parentheses, brackets, braces and template
|
|
20
|
+
* substitutions count; strings, comments, regular expressions and template text are skipped. A closing bracket closes
|
|
21
|
+
* up to the one it matches, so a literal read wrongly unbalances only the brackets around it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function tooDeeplyNested(text: string, limit: number): number | null;
|
|
18
24
|
/** The first parser error, located at its first label, or at the start of the file when it has none. */
|
|
19
25
|
export declare function firstError(errors: readonly OxcError[]): ParsedFile['error'];
|
|
20
26
|
export declare function parseFile(fileName: string, code: string): ParsedFile;
|