test-inventory 0.6.1 → 0.8.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 +100 -0
- package/README.md +4 -3
- package/dist/case-titles.d.ts +27 -0
- package/dist/case-titles.js +329 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -5
- package/dist/config-dependencies.js +8 -3
- package/dist/config-runs.d.ts +22 -0
- package/dist/config-runs.js +263 -3
- package/dist/config.d.ts +18 -1
- package/dist/config.js +160 -14
- package/dist/dependency-hash.d.ts +13 -5
- package/dist/dependency-hash.js +56 -12
- package/dist/diagnostics.js +3 -0
- package/dist/extract.d.ts +19 -1
- package/dist/extract.js +391 -41
- package/dist/files.js +3 -2
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +13 -2
- package/dist/framework-version.js +64 -5
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/common.d.ts +6 -0
- package/dist/frameworks/common.js +27 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/playwright.d.ts +3 -1
- package/dist/frameworks/playwright.js +14 -5
- package/dist/frameworks/test-functions.d.ts +169 -0
- package/dist/frameworks/test-functions.js +492 -0
- package/dist/frameworks/vitest.d.ts +13 -58
- package/dist/frameworks/vitest.js +84 -378
- package/dist/imported-values.d.ts +22 -0
- package/dist/imported-values.js +149 -0
- package/dist/model.d.ts +11 -0
- package/dist/options.d.ts +16 -1
- package/dist/options.js +9 -2
- package/dist/parse.js +17 -2
- package/dist/records.js +57 -15
- package/dist/resolve.js +39 -36
- package/dist/scan-file.d.ts +5 -0
- package/dist/scan-file.js +15 -8
- package/dist/scan-source.js +3 -1
- package/dist/scan.js +31 -10
- package/dist/types.d.ts +24 -6
- package/dist/values.d.ts +49 -1
- package/dist/values.js +531 -19
- package/package.json +1 -1
- package/schema/v1.json +33 -4
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
import { globSync } from 'tinyglobby';
|
|
4
|
+
import { OptionsError } from "./options.js";
|
|
5
|
+
const PACKAGES = { playwright: '@playwright/test', vitest: 'vitest', jest: 'jest' };
|
|
6
|
+
/** Each framework's name in messages. */
|
|
7
|
+
export const FRAMEWORK_NAMES = { playwright: 'Playwright', vitest: 'Vitest', jest: 'Jest' };
|
|
4
8
|
/** Oldest supported version of each framework. */
|
|
5
|
-
export const OLDEST = { playwright: [1, 42], vitest: [3, 0] };
|
|
9
|
+
export const OLDEST = { playwright: [1, 42], vitest: [3, 0], jest: [29, 0] };
|
|
6
10
|
/** A file's text, or null when it is missing or can't be read. */
|
|
7
11
|
function readText(file) {
|
|
8
12
|
try {
|
|
@@ -71,12 +75,25 @@ function declaredVersion(folder, name) {
|
|
|
71
75
|
}
|
|
72
76
|
/**
|
|
73
77
|
* The framework version from the installed package, then a lockfile, then package.json, each looked for in the root
|
|
74
|
-
* and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found.
|
|
78
|
+
* and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found. When none of
|
|
79
|
+
* those has the framework, as in a monorepo whose root leaves it to a package such as `code/`, the shallowest package
|
|
80
|
+
* below the root that declares it and holds one of the scanned `files` is read the same way, from its own folder up.
|
|
75
81
|
*/
|
|
76
|
-
export function detectFrameworkVersion(root, framework) {
|
|
82
|
+
export function detectFrameworkVersion(root, framework, files) {
|
|
77
83
|
const name = PACKAGES[framework];
|
|
84
|
+
const found = detectIn(root, folders(root), name);
|
|
85
|
+
if (found)
|
|
86
|
+
return found;
|
|
87
|
+
// In path order, then shallowest first; the sort is stable, so packages as deep stay in path order.
|
|
88
|
+
const nested = globSync('**/package.json', { cwd: root, ignore: ['**/node_modules/**'] })
|
|
89
|
+
.sort()
|
|
90
|
+
.sort((a, b) => a.split('/').length - b.split('/').length)
|
|
91
|
+
.map((file) => path.dirname(file))
|
|
92
|
+
.find((folder) => files.some((file) => file.startsWith(`${folder}/`)) && declaredVersion(path.join(root, folder), name) !== null);
|
|
93
|
+
return nested === undefined ? null : detectIn(root, folders(path.join(root, nested)), name);
|
|
94
|
+
}
|
|
95
|
+
function detectIn(root, all, name) {
|
|
78
96
|
const source = (folder, file) => path.relative(root, path.join(folder, file)).replaceAll('\\', '/');
|
|
79
|
-
const all = folders(root);
|
|
80
97
|
for (const folder of all) {
|
|
81
98
|
const version = installedVersion(folder, name);
|
|
82
99
|
if (version)
|
|
@@ -94,3 +111,45 @@ export function detectFrameworkVersion(root, framework) {
|
|
|
94
111
|
}
|
|
95
112
|
return null;
|
|
96
113
|
}
|
|
114
|
+
/** Config file names of each runner. A Vite config alone doesn't count, as Vite apps without Vitest have one. */
|
|
115
|
+
const CONFIG_NAMES = {
|
|
116
|
+
playwright: /^playwright\.config\.[cm]?[jt]s$/,
|
|
117
|
+
vitest: /^vitest\.config\.[cm]?[jt]s$/,
|
|
118
|
+
jest: /^jest\.config\.([cm]?[jt]s|json)$/,
|
|
119
|
+
};
|
|
120
|
+
/** Whether a package.json lists a runner, or for Jest has a `jest` key, its config. */
|
|
121
|
+
function declares(manifest, framework) {
|
|
122
|
+
const json = readJson(manifest);
|
|
123
|
+
const fields = ['devDependencies', 'dependencies', 'peerDependencies', 'optionalDependencies'];
|
|
124
|
+
return (fields.some((field) => json?.[field]?.[PACKAGES[framework]] !== undefined) ||
|
|
125
|
+
(framework === 'jest' && json?.jest !== undefined));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The runner a project uses, for a scan without `framework`: the one whose package a package.json in the root or a
|
|
129
|
+
* folder below it, outside `ignore`, lists, or whose config is there. Only when none is found there do the package.json files above the
|
|
130
|
+
* root count, as a monorepo may declare its runner once at its top. Throws an `OptionsError` when the project uses
|
|
131
|
+
* none of them or several, naming what it found.
|
|
132
|
+
*/
|
|
133
|
+
export function detectFramework(root, ignore) {
|
|
134
|
+
const below = globSync(['**/package.json', '**/*.config.*'], {
|
|
135
|
+
cwd: root,
|
|
136
|
+
ignore: ['**/node_modules/**', '**/.git/**', ...ignore],
|
|
137
|
+
});
|
|
138
|
+
const manifests = below.filter((file) => path.basename(file) === 'package.json').map((file) => path.join(root, file));
|
|
139
|
+
const above = folders(root)
|
|
140
|
+
.slice(1)
|
|
141
|
+
.map((folder) => path.join(folder, 'package.json'));
|
|
142
|
+
const all = ['playwright', 'vitest', 'jest'];
|
|
143
|
+
let found = all.filter((framework) => below.some((file) => CONFIG_NAMES[framework].test(path.basename(file))) ||
|
|
144
|
+
manifests.some((manifest) => declares(manifest, framework)));
|
|
145
|
+
if (found.length === 0)
|
|
146
|
+
found = all.filter((framework) => above.some((manifest) => declares(manifest, framework)));
|
|
147
|
+
if (found.length === 1)
|
|
148
|
+
return found[0];
|
|
149
|
+
if (found.length === 0) {
|
|
150
|
+
throw new OptionsError('options.framework is needed: no package.json in the root, below it or above it lists @playwright/test, vitest or jest, and no config of theirs is in the root or below.');
|
|
151
|
+
}
|
|
152
|
+
const names = found.map((framework) => FRAMEWORK_NAMES[framework]);
|
|
153
|
+
const values = found.map((framework) => `"${framework}"`);
|
|
154
|
+
throw new OptionsError(`options.framework is needed, as the project uses ${names.slice(0, -1).join(', ')} and ${names.at(-1)}: scan it once with each of ${values.join(', ')}.`);
|
|
155
|
+
}
|
|
@@ -19,6 +19,8 @@ export interface TestObjects {
|
|
|
19
19
|
returnsTestObject(name: string): boolean;
|
|
20
20
|
/** Whether re-exporting `imported` from `source` exports a test object, or a function that returns one. */
|
|
21
21
|
reexports(source: string, imported: string, exported: Exported): boolean;
|
|
22
|
+
/** What the test object the module exports as `name` stands for, as `it.skip` stands for `it` with `skip`. */
|
|
23
|
+
exportedStart?(name: string): unknown;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* The functions a module declares at its top level, by name, to tell whether one returns a test object: every value it
|
|
@@ -54,6 +56,13 @@ export declare function allDeclarators(program: Program): VariableDeclarator[];
|
|
|
54
56
|
export declare function moduleImports(program: Program, isFramework: (source: string) => boolean): Map<string, ModuleImport>;
|
|
55
57
|
/** The module of `require('module')`, or null for any other expression. */
|
|
56
58
|
export declare function requiredModule(expression: Expression): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* What a CommonJS module exports under `name`: `exports.name = value`, `module.exports.name = value` or a key of
|
|
61
|
+
* `module.exports = { ... }`, and for `default`, `module.exports` itself; the last one written wins; null otherwise.
|
|
62
|
+
*/
|
|
63
|
+
export declare function commonJsExport(program: Program, name: string): Expression | null;
|
|
64
|
+
/** The names a CommonJS module exports one by one, as `exports.name = value` and `module.exports.name = value` do. */
|
|
65
|
+
export declare function commonJsNames(program: Program): string[];
|
|
57
66
|
/** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
|
|
58
67
|
export declare function moduleExportsValue(statement: Node): Expression | null;
|
|
59
68
|
/** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
|
|
@@ -103,10 +112,15 @@ export declare function followModule(source: string, name: string, readModule: M
|
|
|
103
112
|
interface Followed {
|
|
104
113
|
exported: boolean;
|
|
105
114
|
reason: string | null;
|
|
115
|
+
/** What the exported test object stands for, when the framework's test objects say. */
|
|
116
|
+
start?: unknown;
|
|
117
|
+
/** The module writes a function under the name, a helper that registers tests itself, which the walk follows. */
|
|
118
|
+
isHelper?: boolean;
|
|
106
119
|
}
|
|
107
120
|
/** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
|
|
108
121
|
export declare function followImport(name: string, imported: ModuleImport, readModule: ModuleReader | null, file: FileContext, framework: Framework): {
|
|
109
122
|
exported: boolean;
|
|
110
123
|
problem: Problem | null;
|
|
124
|
+
start: unknown;
|
|
111
125
|
};
|
|
112
126
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { childNodes, isFunctionNode, propertyName, returnValues, unwrap } from "../ast.js";
|
|
1
|
+
import { childNodes, isFunctionNode, propertyKey, propertyName, returnValues, unwrap, } from "../ast.js";
|
|
2
2
|
import { parseFile } from "../parse.js";
|
|
3
3
|
import { snippet } from "../source.js";
|
|
4
4
|
import { problem, text } from "./common.js";
|
|
@@ -125,6 +125,18 @@ export function moduleImports(program, isFramework) {
|
|
|
125
125
|
imports.set(specifier.local.name, { source: statement.source.value, imported, node: specifier });
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
// `const { test } = require('./fixtures')` imports as `import { test } from './fixtures'` does.
|
|
129
|
+
for (const declarator of topLevelDeclarators(program)) {
|
|
130
|
+
const source = declarator.init ? requiredModule(declarator.init) : null;
|
|
131
|
+
if (source === null || !/^\.\.?\//.test(source) || declarator.id.type !== 'ObjectPattern')
|
|
132
|
+
continue;
|
|
133
|
+
for (const property of declarator.id.properties) {
|
|
134
|
+
const key = property.type === 'Property' ? propertyKey(property) : null;
|
|
135
|
+
if (key !== null && property.type === 'Property' && property.value.type === 'Identifier') {
|
|
136
|
+
imports.set(property.value.name, { source, imported: key, node: declarator });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
128
140
|
return imports;
|
|
129
141
|
}
|
|
130
142
|
/** The module of `require('module')`, or null for any other expression. */
|
|
@@ -135,6 +147,59 @@ export function requiredModule(expression) {
|
|
|
135
147
|
const argument = node.arguments.at(0);
|
|
136
148
|
return argument?.type === 'Literal' && typeof argument.value === 'string' ? argument.value : null;
|
|
137
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* What a CommonJS module exports under `name`: `exports.name = value`, `module.exports.name = value` or a key of
|
|
152
|
+
* `module.exports = { ... }`, and for `default`, `module.exports` itself; the last one written wins; null otherwise.
|
|
153
|
+
*/
|
|
154
|
+
export function commonJsExport(program, name) {
|
|
155
|
+
let found = null;
|
|
156
|
+
for (const statement of program.body) {
|
|
157
|
+
const whole = moduleExportsValue(statement);
|
|
158
|
+
if (whole) {
|
|
159
|
+
const object = unwrap(whole);
|
|
160
|
+
if (name === 'default')
|
|
161
|
+
found = whole;
|
|
162
|
+
else if (object.type === 'ObjectExpression') {
|
|
163
|
+
for (const property of object.properties) {
|
|
164
|
+
if (property.type === 'Property' && propertyKey(property) === name)
|
|
165
|
+
found = property.value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (statement.type !== 'ExpressionStatement')
|
|
171
|
+
continue;
|
|
172
|
+
const expression = unwrap(statement.expression);
|
|
173
|
+
if (expression.type !== 'AssignmentExpression' || expression.left.type !== 'MemberExpression')
|
|
174
|
+
continue;
|
|
175
|
+
if (propertyName(expression.left) !== name)
|
|
176
|
+
continue;
|
|
177
|
+
const target = unwrap(expression.left.object);
|
|
178
|
+
const isExports = target.type === 'Identifier' && target.name === 'exports';
|
|
179
|
+
const isModuleExports = target.type === 'MemberExpression' &&
|
|
180
|
+
target.object.type === 'Identifier' &&
|
|
181
|
+
target.object.name === 'module' &&
|
|
182
|
+
propertyName(target) === 'exports';
|
|
183
|
+
if (isExports || isModuleExports)
|
|
184
|
+
found = expression.right;
|
|
185
|
+
}
|
|
186
|
+
return found;
|
|
187
|
+
}
|
|
188
|
+
/** The names a CommonJS module exports one by one, as `exports.name = value` and `module.exports.name = value` do. */
|
|
189
|
+
export function commonJsNames(program) {
|
|
190
|
+
const names = new Set();
|
|
191
|
+
for (const statement of program.body) {
|
|
192
|
+
if (statement.type !== 'ExpressionStatement')
|
|
193
|
+
continue;
|
|
194
|
+
const expression = unwrap(statement.expression);
|
|
195
|
+
if (expression.type !== 'AssignmentExpression' || expression.left.type !== 'MemberExpression')
|
|
196
|
+
continue;
|
|
197
|
+
const name = propertyName(expression.left);
|
|
198
|
+
if (name !== null && commonJsExport(program, name) === expression.right)
|
|
199
|
+
names.add(name);
|
|
200
|
+
}
|
|
201
|
+
return [...names];
|
|
202
|
+
}
|
|
138
203
|
/** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
|
|
139
204
|
export function moduleExportsValue(statement) {
|
|
140
205
|
if (statement.type !== 'ExpressionStatement')
|
|
@@ -187,6 +252,9 @@ export function exportsTestObject(program, name, objects, exported = 'test objec
|
|
|
187
252
|
case 'specifier':
|
|
188
253
|
return own.source === null ? local(own.local) : objects.reexports(own.source, own.local, exported);
|
|
189
254
|
}
|
|
255
|
+
const commonJs = exported === 'test object' && name !== 'default' ? commonJsExport(program, name) : null;
|
|
256
|
+
if (commonJs)
|
|
257
|
+
return objects.isTestObject(commonJs);
|
|
190
258
|
return program.body.some((statement) => {
|
|
191
259
|
const source = starReexport(statement);
|
|
192
260
|
return source !== null && objects.reexports(source, name, exported);
|
|
@@ -382,12 +450,21 @@ function followExports(module, name, framework, exported, within, followed) {
|
|
|
382
450
|
return { exported: false, reason: 'could not be parsed' };
|
|
383
451
|
const followFrom = (from, imported, kind) => followModule(from, imported, module.readModule, framework, kind, within, followed);
|
|
384
452
|
const objects = framework.testObjects(program, (from, imported, kind = 'test object') => followFrom(from, imported, kind).exported);
|
|
385
|
-
if (exportsTestObject(program, name, objects, exported))
|
|
386
|
-
return { exported: true, reason: null };
|
|
453
|
+
if (exportsTestObject(program, name, objects, exported)) {
|
|
454
|
+
return { exported: true, reason: null, start: objects.exportedStart?.(name) };
|
|
455
|
+
}
|
|
456
|
+
// A function the module writes and exports is a helper that registers tests itself, which the walk follows.
|
|
457
|
+
if (exported === 'test object' && exportsFunction(program, name)) {
|
|
458
|
+
return { exported: false, reason: null, isHelper: true };
|
|
459
|
+
}
|
|
387
460
|
const unrecognized = (kind) => `does not export ${kind === 'function' ? 'a function that returns a test object' : `a ${framework.name} test object`} that could be recognized`;
|
|
388
461
|
const origin = importOrigin(program, name, framework.isSource, exported);
|
|
389
462
|
if (origin) {
|
|
390
|
-
const
|
|
463
|
+
const followed = followFrom(origin.source, origin.imported, origin.exported);
|
|
464
|
+
// A helper function re-exported, as `import helper from './a'; export { helper }`, is no broken test object.
|
|
465
|
+
if (followed.isHelper && origin.how === 're-exports')
|
|
466
|
+
return followed;
|
|
467
|
+
const end = followed.reason ?? unrecognized(origin.exported);
|
|
391
468
|
return { exported: false, reason: `${origin.how} it from '${origin.source}', which ${end}` };
|
|
392
469
|
}
|
|
393
470
|
if (importsFrom(program, framework.isSource))
|
|
@@ -397,6 +474,22 @@ function followExports(module, name, framework, exported, within, followed) {
|
|
|
397
474
|
const call = exported === 'test object' ? creatingCall(program, name, source) : null;
|
|
398
475
|
return { exported: false, reason: call ? `builds it with ${call}, which is not followed` : null };
|
|
399
476
|
}
|
|
477
|
+
/** Whether a module writes the function it exports as `name`: declared, written in place, or held in a variable. */
|
|
478
|
+
function exportsFunction(program, name) {
|
|
479
|
+
const own = ownExport(program, name);
|
|
480
|
+
if (own?.kind === 'function')
|
|
481
|
+
return true;
|
|
482
|
+
if (own?.kind === 'default') {
|
|
483
|
+
const value = unwrap(own.value);
|
|
484
|
+
return value.type === 'FunctionDeclaration' || isFunctionNode(value);
|
|
485
|
+
}
|
|
486
|
+
const local = own?.kind === 'variable' ? name : own?.kind === 'specifier' && own.source === null ? own.local : null;
|
|
487
|
+
if (local === null)
|
|
488
|
+
return false;
|
|
489
|
+
const declared = program.body.some((statement) => statement.type === 'FunctionDeclaration' && statement.id?.name === local);
|
|
490
|
+
const declarator = topLevelDeclarators(program).find((item) => item.id.type === 'Identifier' && item.id.name === local);
|
|
491
|
+
return declared || isFunctionNode(declarator?.init ? unwrap(declarator.init) : null);
|
|
492
|
+
}
|
|
400
493
|
/**
|
|
401
494
|
* The call that creates what the module exports as `name`, a variable or its default export, as `defineBrokerTest()`;
|
|
402
495
|
* null for other values.
|
|
@@ -412,9 +505,9 @@ function creatingCall(program, name, source) {
|
|
|
412
505
|
}
|
|
413
506
|
/** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
|
|
414
507
|
export function followImport(name, imported, readModule, file, framework) {
|
|
415
|
-
const { exported, reason } = followModule(imported.source, imported.imported, readModule, framework);
|
|
508
|
+
const { exported, reason, start } = followModule(imported.source, imported.imported, readModule, framework);
|
|
416
509
|
const problemFound = reason
|
|
417
510
|
? problem('unresolved-test-import', imported.node, `'${name}' is imported from '${imported.source}', which ${reason}; tests that use it are missing.`, snippet(text(file, imported.node)))
|
|
418
511
|
: null;
|
|
419
|
-
return { exported, problem: problemFound };
|
|
512
|
+
return { exported, problem: problemFound, start };
|
|
420
513
|
}
|
|
@@ -116,6 +116,12 @@ 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
|
+
* The members of a declared value that `name` gets: none in `const name = test`, `['describe', 'serial']` in
|
|
121
|
+
* `const { describe: { serial: name } } = test`, and none for the rest of an object, which keeps the other members.
|
|
122
|
+
* Null when it gets something else: a default, an array element, or a member under a computed key.
|
|
123
|
+
*/
|
|
124
|
+
export declare function boundMembers(pattern: Node, name: string): string[] | null;
|
|
119
125
|
/**
|
|
120
126
|
* Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
|
|
121
127
|
* pattern, so that any key of it may be used.
|
|
@@ -185,6 +185,13 @@ export function readTitle(file, scopes, node, options) {
|
|
|
185
185
|
if (value === null && options.functionNames && expression?.type === 'Identifier') {
|
|
186
186
|
value = file.values.isFunction(expression.name, scopes) ? expression.name : null;
|
|
187
187
|
}
|
|
188
|
+
// A function or class written in place gives its name, as `describe(function OrderBook() {}, ...)` does.
|
|
189
|
+
const written = expression && unwrap(expression);
|
|
190
|
+
if (value === null &&
|
|
191
|
+
options.functionNames &&
|
|
192
|
+
(written?.type === 'FunctionExpression' || written?.type === 'ClassExpression')) {
|
|
193
|
+
value = written.id?.name ?? '';
|
|
194
|
+
}
|
|
188
195
|
if (value !== null) {
|
|
189
196
|
const withoutTags = options.stripTags ? value.replace(/@\S+/g, ' ').replace(/\s+/g, ' ').trim() : value;
|
|
190
197
|
return { title: { text: value, withoutTags, isDynamic: false }, problem: null };
|
|
@@ -287,6 +294,26 @@ export function destructuredNames(fn, index) {
|
|
|
287
294
|
const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
|
|
288
295
|
return names.filter((name) => name !== null);
|
|
289
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* The members of a declared value that `name` gets: none in `const name = test`, `['describe', 'serial']` in
|
|
299
|
+
* `const { describe: { serial: name } } = test`, and none for the rest of an object, which keeps the other members.
|
|
300
|
+
* Null when it gets something else: a default, an array element, or a member under a computed key.
|
|
301
|
+
*/
|
|
302
|
+
export function boundMembers(pattern, name) {
|
|
303
|
+
if (pattern.type === 'Identifier')
|
|
304
|
+
return pattern.name === name ? [] : null;
|
|
305
|
+
if (pattern.type !== 'ObjectPattern')
|
|
306
|
+
return null;
|
|
307
|
+
for (const property of pattern.properties) {
|
|
308
|
+
if (property.type === 'RestElement')
|
|
309
|
+
return boundMembers(property.argument, name);
|
|
310
|
+
const members = boundMembers(property.value, name);
|
|
311
|
+
const key = propertyKey(property);
|
|
312
|
+
if (members)
|
|
313
|
+
return key === null ? null : [key, ...members];
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
290
317
|
/**
|
|
291
318
|
* Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
|
|
292
319
|
* pattern, so that any key of it may be used.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CallExpression } from 'oxc-parser';
|
|
2
|
+
import type { ApiCall, CallContext, ModuleReader } from '../model.ts';
|
|
3
|
+
import type { ParsedFile } from '../parse.ts';
|
|
4
|
+
import type { HookCounts } from '../types.ts';
|
|
5
|
+
import { type FileContext, type Link } from './common.ts';
|
|
6
|
+
import { SuiteAdapter, type Kind } from './test-functions.ts';
|
|
7
|
+
export declare class JestAdapter extends SuiteAdapter {
|
|
8
|
+
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
9
|
+
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
10
|
+
protected isAliasLink(_kind: Kind, link: Link): boolean;
|
|
11
|
+
protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
|
|
12
|
+
/**
|
|
13
|
+
* `jest.retryTimes(n, options)`. Jest keeps one count for the file and reads it as each test runs, so the last call
|
|
14
|
+
* made while the file loads applies to every test, wherever it is written. From 30.5, `entireDescribe` keeps the
|
|
15
|
+
* count for the describe it is called in instead, and retries the describe as a whole.
|
|
16
|
+
*/
|
|
17
|
+
private retryCall;
|
|
18
|
+
/** Whether an expression is Jest's `jest` object: the global, a name imported from Jest, or a member of the module. */
|
|
19
|
+
private isJest;
|
|
20
|
+
private chainCall;
|
|
21
|
+
private declaration;
|
|
22
|
+
/** Whether a table makes `describe.each` throw, as jest-each does on an empty array or a value that isn't a table. */
|
|
23
|
+
private isInvalidTable;
|
|
24
|
+
/** Jest's order: `todo` and `skip` set the test's mode, `only` runs it, and `failing` applies when it runs. */
|
|
25
|
+
private states;
|
|
26
|
+
/**
|
|
27
|
+
* How many tests a table registers. No table, an empty array, or a tagged template whose first line isn't a line of names or
|
|
28
|
+
* whose values don't fill its rows, makes Jest register one test with the title as written, which fails.
|
|
29
|
+
*/
|
|
30
|
+
private caseCount;
|
|
31
|
+
/**
|
|
32
|
+
* The cases of a table whose values the scan can work out, as Jest passes them: a case that is an array is spread
|
|
33
|
+
* into the callback's arguments when every case is one; otherwise each case is passed whole. A tagged template gives
|
|
34
|
+
* one object per row, keyed by the names in its first line.
|
|
35
|
+
*/
|
|
36
|
+
private tableCases;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What jest-each's heading pattern, `/^\n\s*[^\s]+\s*(\|\s*[^\s]+\s*)*\n/`, matches at the start of a table's first
|
|
40
|
+
* string. The pattern backtracks exponentially on a long run of `|` it can't match, which a scanned file could hang the
|
|
41
|
+
* scan with, so its tries are worked out once per position, from the end. `heading[i]` is where the match ends when a
|
|
42
|
+
* heading, `\s*[^\s]+\s*`, starts at `i`, and `rest[i]` where it ends when the heading before ends at `i`: on a `|`,
|
|
43
|
+
* another heading first, else the line break. The pattern tries the whole word first, with its spaces given back one by
|
|
44
|
+
* one, then shorter words, of which only one that ends before a `|` can go on.
|
|
45
|
+
*/
|
|
46
|
+
export declare function headingsMatch(text: string): string | null;
|