test-inventory 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +180 -0
- package/README.md +16 -10
- package/dist/case-titles.d.ts +22 -0
- package/dist/case-titles.js +184 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +5 -5
- package/dist/config-dependencies.js +27 -3
- package/dist/config-runs.d.ts +117 -3
- package/dist/config-runs.js +1015 -19
- package/dist/config.d.ts +42 -1
- package/dist/config.js +287 -21
- package/dist/dependency-hash.d.ts +13 -5
- package/dist/dependency-hash.js +56 -12
- package/dist/diagnostics.js +16 -1
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +89 -29
- package/dist/files.js +4 -2
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +13 -2
- package/dist/framework-version.js +207 -7
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/bun.d.ts +42 -0
- package/dist/frameworks/bun.js +272 -0
- package/dist/frameworks/common.d.ts +3 -1
- package/dist/frameworks/common.js +18 -9
- package/dist/frameworks/cypress.d.ts +31 -0
- package/dist/frameworks/cypress.js +70 -0
- package/dist/frameworks/jasmine.d.ts +28 -0
- package/dist/frameworks/jasmine.js +218 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/mocha.d.ts +50 -0
- package/dist/frameworks/mocha.js +306 -0
- package/dist/frameworks/node.d.ts +30 -0
- package/dist/frameworks/node.js +268 -0
- package/dist/frameworks/playwright.d.ts +13 -0
- package/dist/frameworks/playwright.js +57 -0
- package/dist/frameworks/test-functions.d.ts +194 -0
- package/dist/frameworks/test-functions.js +534 -0
- package/dist/frameworks/vitest.d.ts +15 -62
- package/dist/frameworks/vitest.js +69 -389
- package/dist/frameworks/webdriverio.d.ts +27 -0
- package/dist/frameworks/webdriverio.js +37 -0
- package/dist/imported-values.js +15 -1
- package/dist/model.d.ts +9 -2
- package/dist/node-scripts.d.ts +6 -0
- package/dist/node-scripts.js +85 -0
- package/dist/options.d.ts +16 -3
- package/dist/options.js +22 -3
- package/dist/parse.js +17 -2
- package/dist/records.d.ts +8 -2
- package/dist/records.js +94 -22
- package/dist/resolve.d.ts +21 -4
- package/dist/resolve.js +91 -52
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +21 -3
- package/dist/scan.js +62 -12
- package/dist/toml.d.ts +13 -0
- package/dist/toml.js +172 -0
- package/dist/types.d.ts +27 -11
- package/dist/values.d.ts +15 -0
- package/dist/values.js +120 -2
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +148 -0
- package/package.json +7 -6
- package/schema/v1.json +30 -8
package/dist/dependency-hash.js
CHANGED
|
@@ -5,8 +5,9 @@ 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
7
|
import { parseFile } from "./parse.js";
|
|
8
|
-
import { snippet } from "./source.js";
|
|
8
|
+
import { snippet, SourceText } from "./source.js";
|
|
9
9
|
import { bindingNames, isScopeNode, Values } from "./values.js";
|
|
10
|
+
const NOTHING_MERGED = { files: [], byFile: new Map(), parts: new Map() };
|
|
10
11
|
/** Statements that only declare something, unless code in them runs while they are evaluated. */
|
|
11
12
|
const DECLARATIONS = new Set([
|
|
12
13
|
'ImportDeclaration',
|
|
@@ -386,11 +387,11 @@ export class DependencyGraph {
|
|
|
386
387
|
return { units, byFile, places, fixtures, asks, snapshots };
|
|
387
388
|
}
|
|
388
389
|
/**
|
|
389
|
-
* Everything a test's roots reach on top of `
|
|
390
|
-
* with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
|
|
391
|
-
* followed.
|
|
390
|
+
* Everything a test's roots reach on top of `bases`, such as the code its file runs when loaded, which all its tests
|
|
391
|
+
* share, with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
|
|
392
|
+
* followed. `shared` is what the bases cover, put together once for the many tests that share them.
|
|
392
393
|
*/
|
|
393
|
-
dependencies(roots, bases, needs, snapshots, report) {
|
|
394
|
+
dependencies(roots, bases, shared, needs, snapshots, report) {
|
|
394
395
|
const reached = [...bases, this.reach(roots, bases)];
|
|
395
396
|
// A fixture can ask for one an object it extends defines, or use another test object, so names are shared by
|
|
396
397
|
// every test object reached, and each name and object is read once.
|
|
@@ -476,11 +477,13 @@ export class DependencyGraph {
|
|
|
476
477
|
this.reported.add(key);
|
|
477
478
|
report(place);
|
|
478
479
|
}
|
|
479
|
-
const
|
|
480
|
+
const added = hashesByFile(reached.slice(bases.length));
|
|
481
|
+
const files = [...new Set([...shared.files, ...added.keys()])].sort();
|
|
480
482
|
const parts = files.map((file) => {
|
|
481
|
-
|
|
483
|
+
const own = added.get(file);
|
|
484
|
+
return own ? part(file, [...(shared.byFile.get(file) ?? []), ...own]) : shared.parts.get(file);
|
|
482
485
|
});
|
|
483
|
-
return { hash: shortHash(
|
|
486
|
+
return { hash: shortHash(`[${parts.join(',')}]`), files, isComplete: places.length === 0 };
|
|
484
487
|
}
|
|
485
488
|
newModule(relativeFilePath, parsed, text, values, readModule) {
|
|
486
489
|
return {
|
|
@@ -632,7 +635,8 @@ export class DependencyGraph {
|
|
|
632
635
|
}
|
|
633
636
|
/** A place in a module the scan can't follow, as a unit that marks the tests that reach it. */
|
|
634
637
|
placeUnit(module, at, message, isQuiet = false) {
|
|
635
|
-
|
|
638
|
+
// A JSON config, such as Jest's package.json, is taken by its text.
|
|
639
|
+
const source = module.parsed?.source ?? new SourceText(module.text);
|
|
636
640
|
const unit = this.newUnit(module, [], new Map(), false);
|
|
637
641
|
unit.edges = new Set();
|
|
638
642
|
const text = snippet(source.slice(at.start, at.end)) || null;
|
|
@@ -665,7 +669,11 @@ export class DependencyGraph {
|
|
|
665
669
|
if ('module' in gap) {
|
|
666
670
|
const module = this.moduleOf(gap.module);
|
|
667
671
|
// The path a setup key names, where the config writes it.
|
|
668
|
-
|
|
672
|
+
let at = null;
|
|
673
|
+
if (gap.text !== null && module.parsed)
|
|
674
|
+
at = literalIn(module.parsed.program, gap.text);
|
|
675
|
+
else if (gap.text !== null)
|
|
676
|
+
at = jsonStringIn(module.text, gap.text);
|
|
669
677
|
return this.placeUnit(module, at ?? { start: 0, end: 0 }, gap.message, gap.isQuiet);
|
|
670
678
|
}
|
|
671
679
|
const module = this.newModule(gap.relativeFilePath, null, null, new Values(), null);
|
|
@@ -1300,6 +1308,33 @@ export class DependencyGraph {
|
|
|
1300
1308
|
return edges;
|
|
1301
1309
|
}
|
|
1302
1310
|
}
|
|
1311
|
+
/** The hashes of several reaches by file, in the order the reaches come. */
|
|
1312
|
+
function hashesByFile(reaches) {
|
|
1313
|
+
const byFile = new Map();
|
|
1314
|
+
for (const item of reaches) {
|
|
1315
|
+
for (const [file, hashes] of item.byFile)
|
|
1316
|
+
byFile.set(file, [...(byFile.get(file) ?? []), ...hashes]);
|
|
1317
|
+
}
|
|
1318
|
+
return byFile;
|
|
1319
|
+
}
|
|
1320
|
+
/** A file and its hashes, sorted, as text: the hash is taken of these parts, in the order of the files. */
|
|
1321
|
+
function part(file, hashes) {
|
|
1322
|
+
return JSON.stringify([file, hashes.sort()]);
|
|
1323
|
+
}
|
|
1324
|
+
/** What `merged` covers with what one more reach adds, the parts of the files it adds hashes to written again. */
|
|
1325
|
+
function combine(merged, reach) {
|
|
1326
|
+
if (reach.byFile.size === 0)
|
|
1327
|
+
return merged;
|
|
1328
|
+
const byFile = new Map(merged.byFile);
|
|
1329
|
+
const parts = new Map(merged.parts);
|
|
1330
|
+
for (const [file, hashes] of reach.byFile) {
|
|
1331
|
+
const all = [...(merged.byFile.get(file) ?? []), ...hashes];
|
|
1332
|
+
parts.set(file, part(file, all));
|
|
1333
|
+
byFile.set(file, all);
|
|
1334
|
+
}
|
|
1335
|
+
const files = [...new Set([...merged.files, ...reach.byFile.keys()])].sort();
|
|
1336
|
+
return { files, byFile, parts };
|
|
1337
|
+
}
|
|
1303
1338
|
/** Whether an identifier refers to a binding, rather than naming a property, a label or a member. */
|
|
1304
1339
|
function isReference(key, parent) {
|
|
1305
1340
|
switch (parent?.type) {
|
|
@@ -1360,6 +1395,12 @@ function literalIn(node, value) {
|
|
|
1360
1395
|
}
|
|
1361
1396
|
return null;
|
|
1362
1397
|
}
|
|
1398
|
+
/** Where a JSON file writes a string, as `"./setup.js"`; null when it doesn't. */
|
|
1399
|
+
function jsonStringIn(text, value) {
|
|
1400
|
+
const written = JSON.stringify(value);
|
|
1401
|
+
const start = text.indexOf(written);
|
|
1402
|
+
return start === -1 ? null : { start, end: start + written.length };
|
|
1403
|
+
}
|
|
1363
1404
|
/**
|
|
1364
1405
|
* The snapshot files the matchers of a test compare against, from the project root, at the runners' default places:
|
|
1365
1406
|
* Vitest's `__snapshots__/<file>.snap`, paths given to `toMatchFileSnapshot()`, and browser screenshots in
|
|
@@ -1468,6 +1509,7 @@ export function fileDependencies(graph, main, extraction, types, report, runsWit
|
|
|
1468
1509
|
...runsWith.gaps.map((gap) => graph.gapUnit(gap)),
|
|
1469
1510
|
];
|
|
1470
1511
|
const base = graph.reach([load, ...config]);
|
|
1512
|
+
const baseMerged = combine(NOTHING_MERGED, base);
|
|
1471
1513
|
// The describes around a test and the helper call that registers it are shared by the tests there, often thousands
|
|
1472
1514
|
// for one helper call, so what they reach is walked once for all of them.
|
|
1473
1515
|
const contexts = new Map();
|
|
@@ -1481,7 +1523,8 @@ export function fileDependencies(graph, main, extraction, types, report, runsWit
|
|
|
1481
1523
|
if (around.suite)
|
|
1482
1524
|
roots.push(...suiteUnit(around.suite));
|
|
1483
1525
|
}
|
|
1484
|
-
|
|
1526
|
+
const reach = graph.reach(roots, [base]);
|
|
1527
|
+
context = { reach, merged: combine(baseMerged, reach) };
|
|
1485
1528
|
byCall.set(registeredBy, context);
|
|
1486
1529
|
}
|
|
1487
1530
|
return context;
|
|
@@ -1528,6 +1571,7 @@ export function fileDependencies(graph, main, extraction, types, report, runsWit
|
|
|
1528
1571
|
needs.names.push(...scope.setupFixtures);
|
|
1529
1572
|
needs.all ||= scope.takesAllFixtures;
|
|
1530
1573
|
}
|
|
1531
|
-
|
|
1574
|
+
const context = contextOf(test.scope, test.registeredBy);
|
|
1575
|
+
return graph.dependencies(roots, [base, context.reach], context.merged, needs, snapshots, report);
|
|
1532
1576
|
};
|
|
1533
1577
|
}
|
package/dist/diagnostics.js
CHANGED
|
@@ -18,9 +18,11 @@ export const LEVELS = {
|
|
|
18
18
|
'test-without-body': 'error',
|
|
19
19
|
'describe-not-called': 'error',
|
|
20
20
|
'async-describe': 'error',
|
|
21
|
+
'describe-throws': 'error',
|
|
21
22
|
'nested-test': 'warning',
|
|
22
23
|
'tag-without-at': 'error',
|
|
23
24
|
'removed-api': 'error',
|
|
25
|
+
'invalid-fixture-argument': 'error',
|
|
24
26
|
'only-in-skipped-describe': 'warning',
|
|
25
27
|
'config-may-change-state': 'warning',
|
|
26
28
|
'undefined-tag': 'error',
|
|
@@ -49,8 +51,21 @@ export const LEVELS = {
|
|
|
49
51
|
* calls that throw while the file is collected.
|
|
50
52
|
*/
|
|
51
53
|
export const REJECTS_FILE = {
|
|
52
|
-
playwright: new Set([
|
|
54
|
+
playwright: new Set([
|
|
55
|
+
'test-without-body',
|
|
56
|
+
'tag-without-at',
|
|
57
|
+
'invalid-chain',
|
|
58
|
+
'duplicate-title',
|
|
59
|
+
'invalid-fixture-argument',
|
|
60
|
+
]),
|
|
53
61
|
vitest: new Set(['invalid-chain', 'removed-api', 'undefined-tag']),
|
|
62
|
+
jest: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
|
|
63
|
+
mocha: new Set(['invalid-chain', 'test-without-body']),
|
|
64
|
+
jasmine: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
|
|
65
|
+
cypress: new Set(['invalid-chain', 'test-without-body', 'describe-throws']),
|
|
66
|
+
webdriverio: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
|
|
67
|
+
node: new Set(['invalid-chain']),
|
|
68
|
+
bun: new Set(['invalid-chain', 'test-without-body']),
|
|
54
69
|
};
|
|
55
70
|
export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
|
|
56
71
|
const position = problem.node && source ? source.position(problem.node.start) : null;
|
package/dist/extract.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface PendingSuite {
|
|
|
53
53
|
}
|
|
54
54
|
export interface PendingTest {
|
|
55
55
|
scope: Scope;
|
|
56
|
+
/** The test whose body declares this one as a subtest; null for a test declared in the file or a describe. */
|
|
57
|
+
parent: PendingTest | null;
|
|
56
58
|
declaration: TestDeclaration;
|
|
57
59
|
call: CallExpression;
|
|
58
60
|
/** For a test declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
|
package/dist/extract.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyName, returnValues, unwrap, } from "./ast.js";
|
|
2
|
-
import { exportName } from "./frameworks/bindings.js";
|
|
2
|
+
import { commonJsExport, exportName } from "./frameworks/bindings.js";
|
|
3
3
|
import { problem, readCount } from "./frameworks/common.js";
|
|
4
4
|
import { snippet, splitLines } from "./source.js";
|
|
5
|
-
import { vitestCaseTitle } from "./case-titles.js";
|
|
6
5
|
import { isScopeNode } from "./values.js";
|
|
7
6
|
/** How many helpers deep a call is followed into the functions it calls, far more than real suites nest. */
|
|
8
7
|
const MAX_DEPTH = 16;
|
|
@@ -363,14 +362,18 @@ class Walker {
|
|
|
363
362
|
return;
|
|
364
363
|
case 'ForInStatement':
|
|
365
364
|
case 'ForOfStatement': {
|
|
366
|
-
const
|
|
365
|
+
const leaves = leavesEarly(node.body);
|
|
366
|
+
const values = leaves ? null : this.loopValues(node);
|
|
367
367
|
const expands = values ? this.expands(node, values, context) : 'no';
|
|
368
368
|
if (values && expands === 'yes') {
|
|
369
369
|
this.expandLoop(node, values, context);
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
|
+
// For `for...in`, the keys are what `Object.keys()` lists.
|
|
373
|
+
const listed = node.type === 'ForOfStatement' && !leaves ? node.right : null;
|
|
374
|
+
const count = values?.length ?? (listed ? this.frame.values.lengthOf(listed, this.scopes.slice(0, -1)) : null);
|
|
372
375
|
this.walkUnexpanded(node, expands === 'limit', () => {
|
|
373
|
-
this.visitChildren(node, this.inLoop(context,
|
|
376
|
+
this.visitChildren(node, this.inLoop(context, count));
|
|
374
377
|
});
|
|
375
378
|
return;
|
|
376
379
|
}
|
|
@@ -571,7 +574,8 @@ class Walker {
|
|
|
571
574
|
const list = resolved.ok && Array.isArray(resolved.value) ? resolved.value : null;
|
|
572
575
|
for (const arg of call.arguments) {
|
|
573
576
|
if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
|
|
574
|
-
const
|
|
577
|
+
const leaves = leavesEarly(arg.body);
|
|
578
|
+
const values = leaves ? null : list;
|
|
575
579
|
const expands = values ? this.expands(null, values, context) : 'no';
|
|
576
580
|
if (values && expands === 'yes') {
|
|
577
581
|
// The callback gets each element, its index and the array.
|
|
@@ -583,8 +587,9 @@ class Walker {
|
|
|
583
587
|
}
|
|
584
588
|
}
|
|
585
589
|
else {
|
|
590
|
+
const count = values?.length ?? (leaves ? null : this.frame.values.lengthOf(callee.object, this.scopes));
|
|
586
591
|
this.walkUnexpanded(call, expands === 'limit', () => {
|
|
587
|
-
this.visitCallback(arg, this.inLoop(context,
|
|
592
|
+
this.visitCallback(arg, this.inLoop(context, count));
|
|
588
593
|
});
|
|
589
594
|
}
|
|
590
595
|
}
|
|
@@ -692,9 +697,11 @@ class Walker {
|
|
|
692
697
|
context.test?.problems.push(...api.problems.map((item) => this.here(item)));
|
|
693
698
|
return true;
|
|
694
699
|
case 'invalid': {
|
|
695
|
-
|
|
700
|
+
// Code that runs after the file loads, or only when a condition holds, can't be sure to stop it loading.
|
|
701
|
+
const afterLoad = context.test !== null || context.hookScope !== null || context.helper !== null || context.isInCondition;
|
|
696
702
|
this.report(afterLoad ? { ...api.problem, afterLoad } : api.problem);
|
|
697
|
-
|
|
703
|
+
// Under a condition, the call either never runs or fails the file, so it declares nothing either way.
|
|
704
|
+
return api.declaration && !context.isInCondition ? this.handle(call, api.declaration, context) : true;
|
|
698
705
|
}
|
|
699
706
|
case 'other':
|
|
700
707
|
return false;
|
|
@@ -710,11 +717,7 @@ class Walker {
|
|
|
710
717
|
return this.functionIn(this.frame, name, scopes);
|
|
711
718
|
}
|
|
712
719
|
functionIn(frame, name, scopes) {
|
|
713
|
-
|
|
714
|
-
if (declaration?.type === 'FunctionDeclaration')
|
|
715
|
-
return declaration;
|
|
716
|
-
const init = declaration?.type === 'VariableDeclarator' && declaration.init ? unwrap(declaration.init) : null;
|
|
717
|
-
return init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression' ? init : null;
|
|
720
|
+
return frame.values.functionOf(name, scopes);
|
|
718
721
|
}
|
|
719
722
|
/**
|
|
720
723
|
* Records a call to a function declared in the file. A call from the file or a describe returns the function, to be
|
|
@@ -793,11 +796,10 @@ class Walker {
|
|
|
793
796
|
}
|
|
794
797
|
/** The function a name imported from a local module stands for, in the file that writes it; null otherwise. */
|
|
795
798
|
importedFunction(name, scopes, from = this.frame) {
|
|
796
|
-
const declaration = from.values.declarationOf(name, scopes);
|
|
797
799
|
const source = from.values.importSource(name, scopes);
|
|
798
800
|
if (source === null)
|
|
799
801
|
return null;
|
|
800
|
-
const imported =
|
|
802
|
+
const imported = from.values.importedName(name, scopes);
|
|
801
803
|
const frame = this.openFrom(from, source);
|
|
802
804
|
return frame ? this.exportedFunction(frame, imported, new Set(), 0) : null;
|
|
803
805
|
}
|
|
@@ -821,14 +823,18 @@ class Walker {
|
|
|
821
823
|
return null;
|
|
822
824
|
searched.add(key);
|
|
823
825
|
const { program } = frame.parsed;
|
|
824
|
-
const local = (localName) => {
|
|
825
|
-
const fn = this.functionIn(frame, localName, [program]);
|
|
826
|
-
return fn ? { fn, frame } : null;
|
|
827
|
-
};
|
|
828
826
|
const from = (source, exported) => {
|
|
829
827
|
const module = this.openFrom(frame, source);
|
|
830
828
|
return module ? this.exportedFunction(module, exported, searched, depth + 1) : null;
|
|
831
829
|
};
|
|
830
|
+
const local = (localName) => {
|
|
831
|
+
const fn = this.functionIn(frame, localName, [program]);
|
|
832
|
+
if (fn)
|
|
833
|
+
return { fn, frame };
|
|
834
|
+
// A name the module imports and exports again, as `import helper from './a'; export { helper }`.
|
|
835
|
+
const source = frame.values.importSource(localName, [program]);
|
|
836
|
+
return source === null ? null : from(source, frame.values.importedName(localName, [program]));
|
|
837
|
+
};
|
|
832
838
|
for (const statement of program.body) {
|
|
833
839
|
if (statement.type === 'ExportDefaultDeclaration' && name === 'default') {
|
|
834
840
|
const declaration = statement.declaration;
|
|
@@ -857,7 +863,12 @@ class Walker {
|
|
|
857
863
|
return statement.source ? from(statement.source.value, localName) : local(localName);
|
|
858
864
|
}
|
|
859
865
|
}
|
|
860
|
-
|
|
866
|
+
// A CommonJS module: `module.exports = { registerSettlement }` or `exports.registerSettlement = function () {}`.
|
|
867
|
+
const commonJs = commonJsExport(program, name);
|
|
868
|
+
const value = commonJs ? unwrap(commonJs) : null;
|
|
869
|
+
if (value?.type === 'Identifier')
|
|
870
|
+
return local(value.name);
|
|
871
|
+
return isFunctionNode(value) ? { fn: value, frame } : null;
|
|
861
872
|
}
|
|
862
873
|
/**
|
|
863
874
|
* Walks a function at one of its calls from the file or a describe, as the runner runs it there: with the scopes
|
|
@@ -946,6 +957,18 @@ class Walker {
|
|
|
946
957
|
* called instead.
|
|
947
958
|
*/
|
|
948
959
|
runSetting(call, api, context) {
|
|
960
|
+
if (api.fileWide) {
|
|
961
|
+
for (const item of api.problems)
|
|
962
|
+
this.report(item);
|
|
963
|
+
// Called by a function or while tests run, it changes only the tests that run after it.
|
|
964
|
+
if (context.declaredIn !== null || context.test || context.hookScope)
|
|
965
|
+
return;
|
|
966
|
+
const { settings, problems } = this.guardedRun(call, api.settings, api.argument, context.guards);
|
|
967
|
+
for (const item of problems)
|
|
968
|
+
this.report(item);
|
|
969
|
+
mergeRun(this.fileScope.details.run, settings);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
949
972
|
if (context.declaredIn !== null && !context.test) {
|
|
950
973
|
for (const item of api.problems)
|
|
951
974
|
this.report(item);
|
|
@@ -984,11 +1007,18 @@ class Walker {
|
|
|
984
1007
|
conditions: own.map((condition) => joinConditions([...guards, ...(condition === null ? [] : [condition])])),
|
|
985
1008
|
};
|
|
986
1009
|
}
|
|
987
|
-
|
|
988
|
-
const message = `Option
|
|
1010
|
+
const unapplied = (key) => {
|
|
1011
|
+
const message = `Option ${key}: ${argument} is set only when ${guard}; it is not applied.`;
|
|
989
1012
|
problems.push(problem('unresolved-option', call, message, snippet(this.text(call))));
|
|
1013
|
+
};
|
|
1014
|
+
if (settings.timeout !== undefined && guard !== null) {
|
|
1015
|
+
unapplied('timeout');
|
|
990
1016
|
delete settings.timeout;
|
|
991
1017
|
}
|
|
1018
|
+
if (settings.retry !== undefined && guard !== null) {
|
|
1019
|
+
unapplied('retry');
|
|
1020
|
+
delete settings.retry;
|
|
1021
|
+
}
|
|
992
1022
|
return { settings, problems };
|
|
993
1023
|
}
|
|
994
1024
|
/** Run settings a helper sets when a test body or a `beforeEach` calls it, under the conditions around the call. */
|
|
@@ -1011,7 +1041,7 @@ class Walker {
|
|
|
1011
1041
|
expectCall(call, test) {
|
|
1012
1042
|
const callee = unwrap(call.callee);
|
|
1013
1043
|
const [root, member] = callee.type === 'MemberExpression' ? [unwrap(callee.object), propertyName(callee)] : [callee, 'call'];
|
|
1014
|
-
if (
|
|
1044
|
+
if (!this.isExpect(root))
|
|
1015
1045
|
return;
|
|
1016
1046
|
if (member === 'call' || member === 'soft' || member === 'poll' || member === 'element')
|
|
1017
1047
|
test.expectCount += 1;
|
|
@@ -1026,6 +1056,20 @@ class Walker {
|
|
|
1026
1056
|
test.problems.push(...problems.map((item) => this.here(item)));
|
|
1027
1057
|
}
|
|
1028
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Whether an expression is `expect`, or Playwright's `expect.configure(...)`, which gives an `expect` with other
|
|
1061
|
+
* defaults, written in place or kept in a variable.
|
|
1062
|
+
*/
|
|
1063
|
+
isExpect(expression) {
|
|
1064
|
+
const node = unwrap(expression);
|
|
1065
|
+
if (node.type === 'Identifier') {
|
|
1066
|
+
if (node.name === 'expect')
|
|
1067
|
+
return true;
|
|
1068
|
+
const declaration = this.frame.values.declarationOf(node.name, this.scopes);
|
|
1069
|
+
return declaration?.type === 'VariableDeclarator' && declaration.init !== null && isConfigured(declaration.init);
|
|
1070
|
+
}
|
|
1071
|
+
return isConfigured(node);
|
|
1072
|
+
}
|
|
1029
1073
|
/** Reports a problem not tied to a test, once, however many times a function with it is walked. */
|
|
1030
1074
|
report(found) {
|
|
1031
1075
|
const item = this.here(found);
|
|
@@ -1167,14 +1211,20 @@ class Walker {
|
|
|
1167
1211
|
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.`));
|
|
1168
1212
|
}
|
|
1169
1213
|
}
|
|
1170
|
-
nested(call, context) {
|
|
1214
|
+
nested(call, context, isTest) {
|
|
1171
1215
|
if (!context.test && !context.hookScope)
|
|
1172
1216
|
return false;
|
|
1173
|
-
|
|
1217
|
+
const inTest = context.test !== null && !context.hookScope && this.frame.adapter.nestsTests === true;
|
|
1218
|
+
if (isTest && inTest)
|
|
1219
|
+
return false;
|
|
1220
|
+
const message = inTest
|
|
1221
|
+
? 'A describe declared inside a test body runs as a nested describe of the test, but the scan does not list it or its tests.'
|
|
1222
|
+
: 'A test or describe declared inside a test or hook body is not collected by the runner, so it is not listed.';
|
|
1223
|
+
this.report(problem('nested-test', call, message, snippet(this.frame.parsed.source.slice(call.start, call.end))));
|
|
1174
1224
|
return true;
|
|
1175
1225
|
}
|
|
1176
1226
|
declareTest(call, declaration, context) {
|
|
1177
|
-
if (context.quiet || this.nested(call, context))
|
|
1227
|
+
if (context.quiet || this.nested(call, context, true))
|
|
1178
1228
|
return;
|
|
1179
1229
|
for (const item of this.cases(declaration, call, context))
|
|
1180
1230
|
this.declareCase(call, item, context);
|
|
@@ -1198,9 +1248,9 @@ class Walker {
|
|
|
1198
1248
|
const title = declaration.title;
|
|
1199
1249
|
if (!title || title.isDynamic)
|
|
1200
1250
|
return single("the scan can't work out the title");
|
|
1201
|
-
const titles = declaration.cases.map((item
|
|
1251
|
+
const titles = declaration.cases.map((item) => item.title);
|
|
1202
1252
|
if (titles.some((text) => text === null)) {
|
|
1203
|
-
return single("the title shows
|
|
1253
|
+
return single("the title shows a value the scan doesn't format the way the runner does");
|
|
1204
1254
|
}
|
|
1205
1255
|
if (declaration.kind === 'describe' && this.expands(null, declaration.cases, context) === 'limit') {
|
|
1206
1256
|
return single(`expanding it would take the file past ${MAX_EXPANDED.toLocaleString('en-US')} loop passes and describe cases`);
|
|
@@ -1220,6 +1270,7 @@ class Walker {
|
|
|
1220
1270
|
const empty = emptyTitle(declaration, call);
|
|
1221
1271
|
const test = {
|
|
1222
1272
|
scope: context.scope,
|
|
1273
|
+
parent: context.test,
|
|
1223
1274
|
declaration,
|
|
1224
1275
|
call,
|
|
1225
1276
|
site: this.site,
|
|
@@ -1275,7 +1326,7 @@ class Walker {
|
|
|
1275
1326
|
walk();
|
|
1276
1327
|
}
|
|
1277
1328
|
declareSuite(call, declaration, context) {
|
|
1278
|
-
if (context.quiet || this.nested(call, context))
|
|
1329
|
+
if (context.quiet || this.nested(call, context, false))
|
|
1279
1330
|
return;
|
|
1280
1331
|
for (const item of this.cases(declaration, call, context))
|
|
1281
1332
|
this.declareSuiteCase(call, item, context);
|
|
@@ -1370,3 +1421,12 @@ class Walker {
|
|
|
1370
1421
|
export function extract(main, openModule, options = { expandCases: false, version: null }) {
|
|
1371
1422
|
return new Walker(main, openModule, options).run();
|
|
1372
1423
|
}
|
|
1424
|
+
/** Whether an expression is a call of `expect.configure()`. */
|
|
1425
|
+
function isConfigured(expression) {
|
|
1426
|
+
const node = unwrap(expression);
|
|
1427
|
+
const callee = node.type === 'CallExpression' ? unwrap(node.callee) : null;
|
|
1428
|
+
if (callee?.type !== 'MemberExpression' || propertyName(callee) !== 'configure')
|
|
1429
|
+
return false;
|
|
1430
|
+
const object = unwrap(callee.object);
|
|
1431
|
+
return object.type === 'Identifier' && object.name === 'expect';
|
|
1432
|
+
}
|
package/dist/files.js
CHANGED
|
@@ -82,8 +82,10 @@ export async function findFiles(root, patterns, ignore, framework, platform = pr
|
|
|
82
82
|
const absolutePath = path.resolve(root, match);
|
|
83
83
|
const realPath = realpathSync(absolutePath);
|
|
84
84
|
const linked = realPath !== path.join(realRoot, match);
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Vitest and Mocha load a file through a link; Node.js loads it once, by its real path.
|
|
86
|
+
if (linked && framework !== 'vitest' && framework !== 'mocha') {
|
|
87
|
+
const name = framework === 'jest' ? 'Jest' : 'Playwright';
|
|
88
|
+
diagnostics.push(fileDiagnostic('symlink-ignored', match, `The file is reached through a symbolic link, which ${name} does not follow.`));
|
|
87
89
|
continue;
|
|
88
90
|
}
|
|
89
91
|
candidates.push({ absolutePath, relativeFilePath: match, realPath, linked });
|
package/dist/filter.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Framework } from './types.ts';
|
|
2
|
-
/**
|
|
3
|
-
export declare const
|
|
2
|
+
/** Each framework's name in messages. */
|
|
3
|
+
export declare const FRAMEWORK_NAMES: Record<Framework, string>;
|
|
4
|
+
/** Oldest supported version of each framework, as major, minor and for Bun, whose 1.2.23 changed how it runs tests, patch. */
|
|
5
|
+
export declare const OLDEST: Record<Framework, number[]>;
|
|
4
6
|
/** Where the framework version came from. */
|
|
5
7
|
export interface DetectedVersion {
|
|
6
8
|
version: string;
|
|
@@ -9,6 +11,8 @@ export interface DetectedVersion {
|
|
|
9
11
|
/** Only a range is known, such as `^4.0.0`; `version` is its lowest version. */
|
|
10
12
|
range: string | null;
|
|
11
13
|
}
|
|
14
|
+
/** The root, then each folder above it, as a monorepo package's dependencies may be installed or locked higher up. */
|
|
15
|
+
export declare function folders(root: string): string[];
|
|
12
16
|
/**
|
|
13
17
|
* The framework version from the installed package, then a lockfile, then package.json, each looked for in the root
|
|
14
18
|
* and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found. When none of
|
|
@@ -16,3 +20,10 @@ export interface DetectedVersion {
|
|
|
16
20
|
* below the root that declares it and holds one of the scanned `files` is read the same way, from its own folder up.
|
|
17
21
|
*/
|
|
18
22
|
export declare function detectFrameworkVersion(root: string, framework: Framework, files: readonly string[]): DetectedVersion | null;
|
|
23
|
+
/**
|
|
24
|
+
* The runner a project uses, for a scan without `framework`: the one whose package a package.json in the root or a
|
|
25
|
+
* folder below it, outside `ignore`, lists, or whose config is there. Only when none is found there do the package.json files above the
|
|
26
|
+
* root count, as a monorepo may declare its runner once at its top. Throws an `OptionsError` when the project uses
|
|
27
|
+
* none of them or several, naming what it found.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectFramework(root: string, ignore: readonly string[]): Framework;
|