test-inventory 0.8.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 +129 -0
- package/README.md +16 -11
- package/dist/case-titles.d.ts +9 -0
- package/dist/case-titles.js +82 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -2
- package/dist/config-dependencies.js +19 -0
- package/dist/config-runs.d.ts +95 -3
- package/dist/config-runs.js +752 -16
- package/dist/config.d.ts +24 -0
- package/dist/config.js +139 -19
- package/dist/diagnostics.js +14 -1
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +36 -10
- package/dist/files.js +2 -1
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +4 -2
- package/dist/framework-version.js +170 -15
- 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 +11 -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.js +1 -1
- 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 +26 -1
- package/dist/frameworks/test-functions.js +64 -22
- package/dist/frameworks/vitest.d.ts +9 -1
- package/dist/frameworks/vitest.js +36 -5
- package/dist/frameworks/webdriverio.d.ts +27 -0
- package/dist/frameworks/webdriverio.js +37 -0
- package/dist/model.d.ts +4 -0
- package/dist/node-scripts.d.ts +6 -0
- package/dist/node-scripts.js +85 -0
- package/dist/options.d.ts +13 -4
- package/dist/options.js +22 -3
- package/dist/records.d.ts +8 -2
- package/dist/records.js +50 -13
- package/dist/resolve.d.ts +21 -4
- package/dist/resolve.js +52 -16
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +16 -2
- package/dist/scan.js +42 -7
- package/dist/toml.d.ts +13 -0
- package/dist/toml.js +172 -0
- package/dist/types.d.ts +19 -9
- package/dist/values.d.ts +5 -0
- package/dist/values.js +13 -0
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +148 -0
- package/package.json +7 -6
- package/schema/v1.json +26 -6
package/dist/options.d.ts
CHANGED
|
@@ -4,13 +4,16 @@ export type { CommentsMode };
|
|
|
4
4
|
/** @inline */
|
|
5
5
|
export interface ScanOptions {
|
|
6
6
|
/**
|
|
7
|
-
* Glob patterns for the test files, relative to `root`. As in
|
|
8
|
-
*
|
|
7
|
+
* Glob patterns for the test files, relative to `root`. As in the runners, they match names that start with a dot
|
|
8
|
+
* too. Without them, the test files are those the configs in the root and the folders below it run, or for node:test
|
|
9
|
+
* and Bun those `node --test` and `bun test` run.
|
|
9
10
|
*/
|
|
10
11
|
patterns?: string[];
|
|
11
12
|
/**
|
|
12
|
-
* The runner whose tests are listed. Without it, the one the project uses: whose package a package.json
|
|
13
|
-
* whose config is
|
|
13
|
+
* The runner whose tests are listed. Without it, the one the project uses: whose package a package.json in the root
|
|
14
|
+
* or below lists, whose config is there, or for node:test and Bun, run by a package.json script, else one a
|
|
15
|
+
* package.json above the root lists. With none of them or
|
|
16
|
+
* several, the scan throws a `TypeError` that names them.
|
|
14
17
|
*/
|
|
15
18
|
framework?: Framework;
|
|
16
19
|
/** The project root. Paths in the output are relative to it. Defaults to the current directory. */
|
|
@@ -26,6 +29,11 @@ export interface ScanOptions {
|
|
|
26
29
|
* out, each with the title the runner gives it. Defaults to `false`: one record per declaration.
|
|
27
30
|
*/
|
|
28
31
|
expandCases?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Stops the scan when it aborts: the scan checks it before it starts and between files, and then rejects with the
|
|
34
|
+
* signal's `reason`, as `fetch` does.
|
|
35
|
+
*/
|
|
36
|
+
signal?: AbortSignal;
|
|
29
37
|
}
|
|
30
38
|
/** @inline */
|
|
31
39
|
export interface ScanSourceOptions {
|
|
@@ -51,6 +59,7 @@ export declare function checkObject(value: unknown, name: string): Record<string
|
|
|
51
59
|
export declare function checkFramework(value: unknown, option: string): Framework;
|
|
52
60
|
export declare function checkComments(value: unknown, option: string): CommentsMode;
|
|
53
61
|
export declare function checkFlag(value: unknown, option: string): boolean;
|
|
62
|
+
export declare function checkSignal(value: unknown, option: string): AbortSignal | null;
|
|
54
63
|
export declare function checkVersion(value: unknown, option: string): string | null;
|
|
55
64
|
export declare function checkString(value: unknown, option: string): string;
|
|
56
65
|
export declare function checkStrings(value: unknown, option: string, required: boolean): string[];
|
package/dist/options.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
const FRAMEWORKS = [
|
|
1
|
+
const FRAMEWORKS = [
|
|
2
|
+
'playwright',
|
|
3
|
+
'vitest',
|
|
4
|
+
'jest',
|
|
5
|
+
'mocha',
|
|
6
|
+
'jasmine',
|
|
7
|
+
'cypress',
|
|
8
|
+
'webdriverio',
|
|
9
|
+
'node',
|
|
10
|
+
'bun',
|
|
11
|
+
];
|
|
2
12
|
const COMMENTS = ['all', 'non-active', 'none'];
|
|
3
13
|
// A prerelease and build metadata each appear once, so a long input can't make the match backtrack for long.
|
|
4
14
|
const VERSION = /^v?(\d+)\.(\d+)(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
@@ -14,8 +24,10 @@ export function checkObject(value, name) {
|
|
|
14
24
|
return value;
|
|
15
25
|
}
|
|
16
26
|
export function checkFramework(value, option) {
|
|
17
|
-
if (!FRAMEWORKS.includes(value))
|
|
18
|
-
|
|
27
|
+
if (!FRAMEWORKS.includes(value)) {
|
|
28
|
+
const names = FRAMEWORKS.map((name) => `"${name}"`);
|
|
29
|
+
throw invalid(option, `${names.slice(0, -1).join(', ')} or ${names.at(-1)}`);
|
|
30
|
+
}
|
|
19
31
|
return value;
|
|
20
32
|
}
|
|
21
33
|
export function checkComments(value, option) {
|
|
@@ -32,6 +44,13 @@ export function checkFlag(value, option) {
|
|
|
32
44
|
throw invalid(option, 'true or false');
|
|
33
45
|
return value;
|
|
34
46
|
}
|
|
47
|
+
export function checkSignal(value, option) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
if (!(value instanceof AbortSignal))
|
|
51
|
+
throw invalid(option, 'an AbortSignal');
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
35
54
|
export function checkVersion(value, option) {
|
|
36
55
|
if (value === undefined || value === null)
|
|
37
56
|
return null;
|
package/dist/records.d.ts
CHANGED
|
@@ -52,8 +52,14 @@ export interface VitestTags {
|
|
|
52
52
|
}
|
|
53
53
|
/** What the config the file uses gives it. */
|
|
54
54
|
export interface FileConfig {
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Why a `.only` fails, from Playwright's `forbidOnly` or Vitest's `allowOnly`, and whether it surely does; null when
|
|
57
|
+
* it doesn't.
|
|
58
|
+
*/
|
|
59
|
+
forbidOnly: {
|
|
60
|
+
message: string;
|
|
61
|
+
isSure: boolean;
|
|
62
|
+
} | null;
|
|
57
63
|
/** The names of the projects that run a test, null when unknown, and what they set for it. */
|
|
58
64
|
forTest(test: TestRecord, suites: SuiteRecord[]): {
|
|
59
65
|
projects: string[] | null;
|
package/dist/records.js
CHANGED
|
@@ -184,11 +184,16 @@ class RecordBuilder {
|
|
|
184
184
|
fileName;
|
|
185
185
|
ordinals = new Map();
|
|
186
186
|
suiteIds = new Map();
|
|
187
|
+
testIds = new Map();
|
|
187
188
|
diagnostics = [];
|
|
188
189
|
/** The line of the first problem for which the runner refuses the file; null when it loads. */
|
|
189
190
|
rejectedAt = null;
|
|
191
|
+
/** Whether the tests declared before the line that stops the file loading still run. */
|
|
192
|
+
keepsEarlierTests = false;
|
|
190
193
|
/** Jest: the describes with a test of their own that uses `.only`, which then keep their mode from their others. */
|
|
191
194
|
focused = new Set();
|
|
195
|
+
/** Jasmine: scopes with a focused spec or describe below them, whose own focus Jasmine gives up for the inner one. */
|
|
196
|
+
focusedBelow = new Set();
|
|
192
197
|
constructor(parsed, options) {
|
|
193
198
|
this.parsed = parsed;
|
|
194
199
|
this.options = options;
|
|
@@ -215,7 +220,14 @@ class RecordBuilder {
|
|
|
215
220
|
const lines = [...rejecting, ...(firstTag ? [firstTag] : [])].map((item) => this.parsed.source.line(item.node.start));
|
|
216
221
|
if (lines.length > 0)
|
|
217
222
|
this.rejectedAt = Math.min(...lines);
|
|
223
|
+
this.keepsEarlierTests = rejecting.length > 0 && rejecting.every((item) => item.keepsEarlierTests === true);
|
|
218
224
|
this.focused = new Set(extraction.tests.filter((test) => test.declaration.isOnly).map((test) => test.scope));
|
|
225
|
+
const focusedAt = [
|
|
226
|
+
...extraction.tests.filter((test) => test.declaration.isOnly).map((test) => test.scope),
|
|
227
|
+
// A describe's scope is inside the file's, so it has a parent.
|
|
228
|
+
...extraction.suites.filter((suite) => suite.declaration.isOnly).map((suite) => suite.scope.parent),
|
|
229
|
+
];
|
|
230
|
+
this.focusedBelow = new Set(focusedAt.flatMap((scope) => outward(scope)));
|
|
219
231
|
const suites = extraction.suites.map((suite) => this.suite(suite));
|
|
220
232
|
const tests = extraction.tests.map((test) => this.test(test, suites));
|
|
221
233
|
for (const item of problems) {
|
|
@@ -265,15 +277,12 @@ class RecordBuilder {
|
|
|
265
277
|
* warning. `call` is the `.only` call when it is in this file, whose callee the diagnostic shows.
|
|
266
278
|
*/
|
|
267
279
|
forbidOnly(located, call) {
|
|
268
|
-
const forbid = this.options.config?.forbidOnly ??
|
|
269
|
-
if (forbid ===
|
|
280
|
+
const forbid = this.options.config?.forbidOnly ?? null;
|
|
281
|
+
if (forbid === null)
|
|
270
282
|
return;
|
|
271
|
-
const message = forbid === true
|
|
272
|
-
? 'The config sets forbidOnly, so this .only fails the run before any test runs.'
|
|
273
|
-
: `The config sets forbidOnly when ${forbid}, so then this .only fails the run before any test runs.`;
|
|
274
283
|
const callee = call && snippet(this.parsed.source.text.slice(call.callee.start, call.callee.end));
|
|
275
|
-
const found = problem('forbidden-only', located, message, callee);
|
|
276
|
-
this.diagnostics.push(this.diagnostic(forbid
|
|
284
|
+
const found = problem('forbidden-only', located, forbid.message, callee);
|
|
285
|
+
this.diagnostics.push(this.diagnostic(forbid.isSure ? found : { ...found, level: 'warning' }));
|
|
277
286
|
}
|
|
278
287
|
suite(pending) {
|
|
279
288
|
const { scope, declaration, call } = pending;
|
|
@@ -366,6 +375,16 @@ class RecordBuilder {
|
|
|
366
375
|
const setting = own('skip') ?? own('todo') ?? own('expectedToFail');
|
|
367
376
|
return { chosen: setting ? { setting, source: 'test' } : null, isOnly: declaration.isOnly || passesOnly };
|
|
368
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Whether Jasmine runs a spec as focused: a `fit`, or a spec in an `fdescribe` with no focused spec or describe
|
|
380
|
+
* below it, as Jasmine runs only the innermost focus (checked with Jasmine 4.6.0 and 7.0.0).
|
|
381
|
+
*/
|
|
382
|
+
jasmineFocus(pending, scopes) {
|
|
383
|
+
if (pending.declaration.isOnly)
|
|
384
|
+
return true;
|
|
385
|
+
const nearest = scopes.find((scope) => scope.isOnly);
|
|
386
|
+
return nearest !== undefined && !this.focusedBelow.has(nearest);
|
|
387
|
+
}
|
|
369
388
|
test(pending, suites) {
|
|
370
389
|
const { declaration } = pending;
|
|
371
390
|
const dependencies = this.options.dependencies(pending);
|
|
@@ -374,7 +393,14 @@ class RecordBuilder {
|
|
|
374
393
|
const scopes = outward(pending.scope);
|
|
375
394
|
const describes = scopes.filter((scope) => scope.kind === 'describe').reverse();
|
|
376
395
|
const title = declaration.title;
|
|
377
|
-
const
|
|
396
|
+
const parents = [];
|
|
397
|
+
for (let parent = pending.parent; parent; parent = parent.parent)
|
|
398
|
+
parents.unshift(parent);
|
|
399
|
+
const parentTitles = parents.map((parent) => parent.declaration.title.withoutTags);
|
|
400
|
+
const id = this.id(parents.length > 0
|
|
401
|
+
? [this.describeTitles(describes), parentTitles, title.withoutTags]
|
|
402
|
+
: [this.describeTitles(describes), title.withoutTags]);
|
|
403
|
+
this.testIds.set(pending, id);
|
|
378
404
|
const fileScope = scopes[scopes.length - 1];
|
|
379
405
|
const tags = unique([
|
|
380
406
|
...(this.options.config?.configTags ?? []),
|
|
@@ -387,21 +413,31 @@ class RecordBuilder {
|
|
|
387
413
|
const tagsFirst = this.options.config?.tags?.beatsDescribes ?? true;
|
|
388
414
|
const tagged = (own, tag, around) => tagsFirst ? [...own, ...tag, ...around] : [...own, ...around, ...tag];
|
|
389
415
|
const jest = this.options.framework === 'jest' ? this.jestState(pending, scopes) : null;
|
|
416
|
+
const focused = this.options.framework === 'jasmine' ? this.jasmineFocus(pending, scopes) : null;
|
|
417
|
+
// A focused Jasmine spec runs inside an `xdescribe`, whose own skip doesn't reach it; `pending()` still does.
|
|
418
|
+
const declaredSkip = (scope, setting) => focused === true && scope.kind === 'describe' && suiteOf(scope).declaration.states.includes(setting);
|
|
390
419
|
const chosen = jest
|
|
391
420
|
? jest.chosen
|
|
392
421
|
: chooseState([
|
|
393
422
|
...declaredStates(pending.states.slice(0, declaration.states.length), fromTags, declaration),
|
|
394
423
|
...pending.states.slice(declaration.states.length).map((setting) => ({ setting, source: 'test' })),
|
|
395
|
-
|
|
424
|
+
// A test the declaration skips never runs its body, so its subtests never run; one its body skips still runs it.
|
|
425
|
+
...parents.flatMap((parent) => parent.declaration.states.map((setting) => ({ setting, source: 'suite' }))),
|
|
426
|
+
...scopes.flatMap((scope) => scope.states
|
|
427
|
+
.filter((setting) => !declaredSkip(scope, setting))
|
|
428
|
+
.map((setting) => ({
|
|
396
429
|
setting,
|
|
397
430
|
source: scope.kind === 'file' ? 'file' : 'suite',
|
|
398
431
|
}))),
|
|
399
432
|
]);
|
|
400
|
-
// When the runner refuses the file, no test in it runs, whatever else is written.
|
|
401
|
-
const rejected = this.rejectedAt !== null;
|
|
433
|
+
// When the runner refuses the file, no test in it runs, whatever else is written, or none after the refused call.
|
|
434
|
+
const rejected = this.rejectedAt !== null && !(this.keepsEarlierTests && this.parsed.source.line(located.start) < this.rejectedAt);
|
|
402
435
|
const state = rejected ? 'notLoaded' : (chosen?.setting.state ?? 'active');
|
|
403
436
|
const setting = rejected ? null : (chosen?.setting ?? null);
|
|
404
|
-
const suitePath =
|
|
437
|
+
const suitePath = [
|
|
438
|
+
...describes.flatMap((scope) => suiteOf(scope).declaration.title?.text ?? []),
|
|
439
|
+
...parents.map((parent) => parent.declaration.title.text),
|
|
440
|
+
];
|
|
405
441
|
const tagConcurrent = this.options.config?.tags?.concurrentBeatsTags ? undefined : fromTags?.concurrent;
|
|
406
442
|
const tagMode = tagConcurrent === undefined ? null : tagConcurrent ? 'parallel' : 'default';
|
|
407
443
|
const [mode = null] = tagged([declaration.mode], [tagMode], scopes.map((scope) => scope.mode)).filter((item) => item !== null);
|
|
@@ -411,7 +447,7 @@ class RecordBuilder {
|
|
|
411
447
|
}
|
|
412
448
|
const isOnly = jest
|
|
413
449
|
? jest.isOnly
|
|
414
|
-
: declaration.isOnly || scopes.some((scope) => scope.isOnly) || fromTags?.only === true;
|
|
450
|
+
: (focused ?? (declaration.isOnly || scopes.some((scope) => scope.isOnly) || fromTags?.only === true));
|
|
415
451
|
const comments = this.options.comments === 'all' || (this.options.comments === 'non-active' && state !== 'active')
|
|
416
452
|
? pending.comments
|
|
417
453
|
: [];
|
|
@@ -442,6 +478,7 @@ class RecordBuilder {
|
|
|
442
478
|
dependencies: dependencies.files,
|
|
443
479
|
hasUnfollowedDependencies: !dependencies.isComplete,
|
|
444
480
|
suiteId: describes.length > 0 ? this.suiteIds.get(describes[describes.length - 1]) : null,
|
|
481
|
+
parentTestId: pending.parent ? this.testIds.get(pending.parent) : null,
|
|
445
482
|
suitePath,
|
|
446
483
|
relativeFilePath: this.options.relativeFilePath,
|
|
447
484
|
fileName: this.fileName,
|
package/dist/resolve.d.ts
CHANGED
|
@@ -2,8 +2,25 @@ import type { ModuleReader } from './model.ts';
|
|
|
2
2
|
/** Parses JSON written with comments and trailing commas, as tsconfig files often are; null when it isn't JSON. */
|
|
3
3
|
export declare function parseJsonc(text: string): unknown;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* How a runner's config maps imports, for every module a test imports: aliases, tried first, as Vite's
|
|
6
|
+
* `resolve.alias` and Jest's `moduleNameMapper`, and folders a bare name is looked for in, as Jest's `modulePaths` and
|
|
7
|
+
* `moduleDirectories`.
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
9
|
+
export interface ImportMap {
|
|
10
|
+
/** A string `find` maps the name itself and the names below it, as `@` maps `@/x`; a regex maps what it matches. */
|
|
11
|
+
aliases: {
|
|
12
|
+
find: string | RegExp;
|
|
13
|
+
replacements: string[];
|
|
14
|
+
}[];
|
|
15
|
+
/** Folders a bare name is looked for in. */
|
|
16
|
+
paths: string[];
|
|
17
|
+
/** Names of folders a bare name is looked for in, in the importing file's folder and each folder above it. */
|
|
18
|
+
directories: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns a reader for the modules a test file imports. It follows the import map of the config the file runs with,
|
|
22
|
+
* relative paths, tsconfig `paths` and `baseUrl` from the tsconfig nearest the importing file, package.json `imports`
|
|
23
|
+
* (`#name`), and workspace packages whose entry is TypeScript source. Each module it reads carries a reader for the
|
|
24
|
+
* modules that module imports in turn, with the same import map.
|
|
25
|
+
*/
|
|
26
|
+
export declare function moduleResolver(root: string): (fromFile: string, map?: ImportMap | null) => ModuleReader;
|
package/dist/resolve.js
CHANGED
|
@@ -323,13 +323,34 @@ function workspaceSource(specifier, directory, packagesOf) {
|
|
|
323
323
|
const local = packagesOf(directory).get(name);
|
|
324
324
|
return local ? packageEntry(local, subpath) : null;
|
|
325
325
|
}
|
|
326
|
+
/** A file a bare name stands for in the folders an import map adds; null when none has it. */
|
|
327
|
+
function searched(specifier, directory, map) {
|
|
328
|
+
const folders = [...map.paths];
|
|
329
|
+
for (let folder = directory;; folder = path.dirname(folder)) {
|
|
330
|
+
folders.push(...map.directories.map((name) => path.join(folder, name)));
|
|
331
|
+
if (path.dirname(folder) === folder)
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
return folders.map((folder) => findFile(path.join(folder, specifier))).find((file) => file !== null) ?? null;
|
|
335
|
+
}
|
|
336
|
+
/** The names an alias maps a specifier to; null when it doesn't match. */
|
|
337
|
+
function mapped(specifier, alias) {
|
|
338
|
+
const { find, replacements } = alias;
|
|
339
|
+
if (typeof find === 'string') {
|
|
340
|
+
if (specifier !== find && !specifier.startsWith(`${find}/`))
|
|
341
|
+
return null;
|
|
342
|
+
return replacements.map((replacement) => replacement + specifier.slice(find.length));
|
|
343
|
+
}
|
|
344
|
+
return find.test(specifier) ? replacements.map((replacement) => specifier.replace(find, replacement)) : null;
|
|
345
|
+
}
|
|
326
346
|
/**
|
|
327
|
-
* Returns a reader for the modules a test file imports. It follows
|
|
328
|
-
* the tsconfig nearest the importing file, package.json `imports`
|
|
329
|
-
* TypeScript source. Each module it reads carries a reader for the
|
|
347
|
+
* Returns a reader for the modules a test file imports. It follows the import map of the config the file runs with,
|
|
348
|
+
* relative paths, tsconfig `paths` and `baseUrl` from the tsconfig nearest the importing file, package.json `imports`
|
|
349
|
+
* (`#name`), and workspace packages whose entry is TypeScript source. Each module it reads carries a reader for the
|
|
350
|
+
* modules that module imports in turn, with the same import map.
|
|
330
351
|
*/
|
|
331
352
|
export function moduleResolver(root) {
|
|
332
|
-
// Many test files import the same fixtures file and share a tsconfig; each is read once.
|
|
353
|
+
// Many test files import the same fixtures file and share a tsconfig; each is read once per import map.
|
|
333
354
|
const modules = new Map();
|
|
334
355
|
const tsconfigs = new Map();
|
|
335
356
|
const pathOptions = new Map();
|
|
@@ -362,11 +383,11 @@ export function moduleResolver(root) {
|
|
|
362
383
|
workspaces.set(folder, packages);
|
|
363
384
|
return packages;
|
|
364
385
|
};
|
|
365
|
-
|
|
366
|
-
|
|
386
|
+
/** The file a name stands for from a folder, or why it can't be followed. */
|
|
387
|
+
const locate = (specifier, directory, map) => {
|
|
367
388
|
let candidates;
|
|
368
389
|
// `.` and `..` are paths too: the index of the file's folder, or of the folder above.
|
|
369
|
-
if (/^\.\.?(?:\/|$)/.test(specifier)) {
|
|
390
|
+
if (/^\.\.?(?:\/|$)/.test(specifier) || path.isAbsolute(specifier)) {
|
|
370
391
|
candidates = [path.resolve(directory, specifier)];
|
|
371
392
|
}
|
|
372
393
|
else if (specifier.startsWith('#')) {
|
|
@@ -388,29 +409,44 @@ export function moduleResolver(root) {
|
|
|
388
409
|
const workspace = fromBaseUrl ? null : workspaceSource(specifier, directory, packagesOf);
|
|
389
410
|
if (workspace && 'reason' in workspace)
|
|
390
411
|
return workspace;
|
|
391
|
-
const file = fromBaseUrl ?? workspace?.file;
|
|
412
|
+
const file = fromBaseUrl ?? workspace?.file ?? (map && searched(specifier, directory, map));
|
|
392
413
|
// No package name starts with `@/` or `~/`, so these are aliases, as Vite's `resolve.alias` sets them; any
|
|
393
414
|
// other name with no file may still be a package or a built-in module, as TypeScript takes it to be.
|
|
394
415
|
if (!file && /^[@~]\//.test(specifier)) {
|
|
395
|
-
return { reason: targets ? 'could not be read' : 'is a path alias that no tsconfig paths define' };
|
|
416
|
+
return { reason: targets ? 'could not be read' : 'is a path alias that no tsconfig paths or config define' };
|
|
396
417
|
}
|
|
397
418
|
if (!file)
|
|
398
419
|
return { reason: 'is not a local file, a path alias or a workspace package' };
|
|
399
420
|
candidates = [file];
|
|
400
421
|
}
|
|
401
422
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
423
|
+
return candidates.map(findFile).find((file) => file !== null) ?? { reason: 'could not be read' };
|
|
424
|
+
};
|
|
425
|
+
const reader = (fromFile, map = null) => (specifier) => {
|
|
426
|
+
const directory = path.dirname(fromFile);
|
|
427
|
+
const aliased = map?.aliases.map((alias) => mapped(specifier, alias)).find((names) => names !== null);
|
|
428
|
+
// The config maps the name to others, each a path or a name looked up as written, the first that is found.
|
|
429
|
+
const located = (aliased ?? [specifier]).map((name) => locate(name, directory, map));
|
|
430
|
+
const found = located.find((item) => typeof item === 'string');
|
|
431
|
+
if (found === undefined) {
|
|
432
|
+
return aliased
|
|
433
|
+
? { reason: `is mapped by the config to ${aliased.join(' or ')}, which could not be read` }
|
|
434
|
+
: located[0];
|
|
435
|
+
}
|
|
436
|
+
let known = modules.get(map);
|
|
437
|
+
if (!known) {
|
|
438
|
+
known = new Map();
|
|
439
|
+
modules.set(map, known);
|
|
440
|
+
}
|
|
441
|
+
if (!known.has(found)) {
|
|
406
442
|
const content = readContent(found);
|
|
407
|
-
|
|
443
|
+
known.set(found, content && {
|
|
408
444
|
...content,
|
|
409
445
|
relativeFilePath: path.relative(root, found).replaceAll('\\', '/'),
|
|
410
|
-
readModule: reader(found),
|
|
446
|
+
readModule: reader(found, map),
|
|
411
447
|
});
|
|
412
448
|
}
|
|
413
|
-
return
|
|
449
|
+
return known.get(found) ?? { reason: 'could not be read' };
|
|
414
450
|
};
|
|
415
451
|
return reader;
|
|
416
452
|
}
|
package/dist/scan-file.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RunsWith } from './config-dependencies.ts';
|
|
2
2
|
import { DependencyGraph, type SnapshotFiles } from './dependency-hash.ts';
|
|
3
3
|
import { ExportedValues } from './imported-values.ts';
|
|
4
|
+
import { type MochaInterface } from './frameworks/mocha.ts';
|
|
5
|
+
import { type WebdriverIOFramework } from './frameworks/webdriverio.ts';
|
|
4
6
|
import type { ModuleReader } from './model.ts';
|
|
5
7
|
import { type CommentsMode, type FileConfig, type FileRecords } from './records.ts';
|
|
6
8
|
import type { Framework } from './types.ts';
|
|
@@ -18,6 +20,12 @@ export interface FileSettings {
|
|
|
18
20
|
isBenchmarkFile?: boolean | null;
|
|
19
21
|
/** What the config the file uses sets for its tests; null without a config. */
|
|
20
22
|
config?: FileConfig | null;
|
|
23
|
+
/** The interface Mocha's config sets for the tests: `describe` and `it`, or `suite` and `test`. */
|
|
24
|
+
mochaInterface?: MochaInterface;
|
|
25
|
+
/** The length Vitest's config shortens a value in a case title to; Vitest's default, 40, when unset. */
|
|
26
|
+
titleTruncate?: number;
|
|
27
|
+
/** The framework a WebdriverIO config runs the specs with. */
|
|
28
|
+
webdriverioFramework?: WebdriverIOFramework;
|
|
21
29
|
/** Shared by the files of one scan, so that code many tests reach is read once. */
|
|
22
30
|
graph?: DependencyGraph;
|
|
23
31
|
/** Shared by the files of one scan, so that each module's exported values are worked out once. */
|
package/dist/scan-file.js
CHANGED
|
@@ -7,6 +7,12 @@ import { atLeast, problem } from "./frameworks/common.js";
|
|
|
7
7
|
import { PlaywrightAdapter } from "./frameworks/playwright.js";
|
|
8
8
|
import { VitestAdapter } from "./frameworks/vitest.js";
|
|
9
9
|
import { JestAdapter } from "./frameworks/jest.js";
|
|
10
|
+
import { MochaAdapter } from "./frameworks/mocha.js";
|
|
11
|
+
import { BunAdapter } from "./frameworks/bun.js";
|
|
12
|
+
import { CypressAdapter } from "./frameworks/cypress.js";
|
|
13
|
+
import { NodeAdapter } from "./frameworks/node.js";
|
|
14
|
+
import { WebdriverIOJasmineAdapter, WebdriverIOMochaAdapter, } from "./frameworks/webdriverio.js";
|
|
15
|
+
import { JasmineAdapter } from "./frameworks/jasmine.js";
|
|
10
16
|
import { parseFile } from "./parse.js";
|
|
11
17
|
import { buildRecords, fileRecord } from "./records.js";
|
|
12
18
|
import { snippet } from "./source.js";
|
|
@@ -15,14 +21,22 @@ const TYPE_TEST = /\.(test|spec)-d\.[cm]?[jt]sx?$/;
|
|
|
15
21
|
const BENCHMARK = /\.(bench|benchmark)\.[cm]?[jt]sx?$/;
|
|
16
22
|
const ADAPTERS = {
|
|
17
23
|
playwright: (parsed, file, readModule) => new PlaywrightAdapter(parsed.program, file, readModule),
|
|
18
|
-
vitest: (parsed, file, readModule) => new VitestAdapter(parsed, file, readModule),
|
|
24
|
+
vitest: (parsed, file, readModule, settings) => new VitestAdapter(parsed, file, readModule, settings.titleTruncate),
|
|
19
25
|
jest: (parsed, file, readModule) => new JestAdapter(parsed, file, readModule),
|
|
26
|
+
mocha: (parsed, file, readModule, settings) => new MochaAdapter(parsed, file, readModule, settings.mochaInterface),
|
|
27
|
+
jasmine: (parsed, file, readModule) => new JasmineAdapter(parsed, file, readModule),
|
|
28
|
+
cypress: (parsed, file, readModule) => new CypressAdapter(parsed, file, readModule),
|
|
29
|
+
webdriverio: (parsed, file, readModule, settings) => settings.webdriverioFramework === 'jasmine'
|
|
30
|
+
? new WebdriverIOJasmineAdapter(parsed, file, readModule)
|
|
31
|
+
: new WebdriverIOMochaAdapter(parsed, file, readModule, settings.mochaInterface),
|
|
32
|
+
node: (parsed, file, readModule) => new NodeAdapter(parsed, file, readModule),
|
|
33
|
+
bun: (parsed, file, readModule) => new BunAdapter(parsed, file, readModule),
|
|
20
34
|
};
|
|
21
35
|
/** A file read for the scan: the scanned file, or a module a helper function is imported from. */
|
|
22
36
|
function frame(parsed, relativeFilePath, settings, readModule, exports) {
|
|
23
37
|
const values = new Values(exports.reader(readModule));
|
|
24
38
|
const file = { source: parsed.source, values, version: settings.version };
|
|
25
|
-
const adapter = ADAPTERS[settings.framework](parsed, file, readModule);
|
|
39
|
+
const adapter = ADAPTERS[settings.framework](parsed, file, readModule, settings);
|
|
26
40
|
return { parsed, adapter, values: file.values, readModule, relativeFilePath };
|
|
27
41
|
}
|
|
28
42
|
/** The records of a file the scan read no tests from. */
|
package/dist/scan.js
CHANGED
|
@@ -3,15 +3,16 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { glob } from 'tinyglobby';
|
|
5
5
|
import { ConfigDependencies } from "./config-dependencies.js";
|
|
6
|
-
import { ConfigRuns, fileConfig } from "./config-runs.js";
|
|
6
|
+
import { ConfigRuns, fileConfig, importMap, mochaInterface, webdriverioFramework, webdriverioInterface, } from "./config-runs.js";
|
|
7
7
|
import { Configs, settingAt } from "./config.js";
|
|
8
|
+
import { atLeast } from "./frameworks/common.js";
|
|
8
9
|
import { DependencyGraph } from "./dependency-hash.js";
|
|
9
10
|
import { ExportedValues } from "./imported-values.js";
|
|
10
11
|
import { fileDiagnostic } from "./diagnostics.js";
|
|
11
12
|
import { findFiles, normalizePattern, readFailure } from "./files.js";
|
|
12
13
|
import { detectFramework, detectFrameworkVersion, FRAMEWORK_NAMES, OLDEST } from "./framework-version.js";
|
|
13
14
|
import { createInventory } from "./inventory.js";
|
|
14
|
-
import { checkComments, checkFlag, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
15
|
+
import { checkComments, checkFlag, checkFramework, checkObject, checkSignal, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
15
16
|
import { moduleResolver } from "./resolve.js";
|
|
16
17
|
import { noRecords, scanFile } from "./scan-file.js";
|
|
17
18
|
import { globalsCheck } from "./vitest-config.js";
|
|
@@ -23,12 +24,17 @@ function versionDiagnostics(framework, version, source, range) {
|
|
|
23
24
|
];
|
|
24
25
|
}
|
|
25
26
|
const [major, minor] = majorMinor(version);
|
|
26
|
-
const
|
|
27
|
+
const patch = Number(/^v?\d+\.\d+\.(\d+)/.exec(version)?.[1] ?? 0);
|
|
28
|
+
const [oldestMajor, oldestMinor, oldestPatch = 0] = OLDEST[framework];
|
|
29
|
+
const oldest = OLDEST[framework].join('.');
|
|
27
30
|
// A range gets the newest rules of its major, so only its major has to be supported.
|
|
28
|
-
const tooOld = major < oldestMajor ||
|
|
31
|
+
const tooOld = major < oldestMajor ||
|
|
32
|
+
(major === oldestMajor &&
|
|
33
|
+
range === null &&
|
|
34
|
+
(minor < oldestMinor || (minor === oldestMinor && patch < oldestPatch)));
|
|
29
35
|
if (tooOld) {
|
|
30
36
|
return [
|
|
31
|
-
fileDiagnostic('unsupported-framework-version', source, `${name} ${version} is older than ${
|
|
37
|
+
fileDiagnostic('unsupported-framework-version', source, `${name} ${version} is older than ${oldest}, the oldest supported version; the results follow ${oldest}.`),
|
|
32
38
|
];
|
|
33
39
|
}
|
|
34
40
|
if (range === null)
|
|
@@ -37,6 +43,18 @@ function versionDiagnostics(framework, version, source, range) {
|
|
|
37
43
|
fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
|
|
38
44
|
];
|
|
39
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The length Vitest shortens a value in a case title to: `test.taskTitleValueFormatTruncate` from Vitest 5,
|
|
48
|
+
* `test.chaiConfig.truncateThreshold` before, from the project that runs the file; 40, Vitest's default, when it sets
|
|
49
|
+
* none or the scan can't read it.
|
|
50
|
+
*/
|
|
51
|
+
function titleTruncate(placement, version) {
|
|
52
|
+
const key = atLeast(version, 5, 0)
|
|
53
|
+
? ['test', 'taskTitleValueFormatTruncate']
|
|
54
|
+
: ['test', 'chaiConfig', 'truncateThreshold'];
|
|
55
|
+
const setting = placement.settings && settingAt(placement.settings, key);
|
|
56
|
+
return setting?.kind === 'value' && typeof setting.value === 'number' ? setting.value : 40;
|
|
57
|
+
}
|
|
40
58
|
/** Why Jest gives a file that uses its globals none: a project that runs it sets `injectGlobals` to false. */
|
|
41
59
|
function jestGlobals(placement) {
|
|
42
60
|
const off = placement.projects?.find(({ settings }) => {
|
|
@@ -111,6 +129,9 @@ function noFiles(root, patterns, framework, placements) {
|
|
|
111
129
|
if ([...placements.values()].some((placement) => placement.config)) {
|
|
112
130
|
return 'No test files were found from the configs in the root.';
|
|
113
131
|
}
|
|
132
|
+
if (framework === 'node') {
|
|
133
|
+
return "No patterns were given, and no file in the root matches what node --test runs: Node.js's default patterns, or the files a package.json script gives it.";
|
|
134
|
+
}
|
|
114
135
|
const name = FRAMEWORK_NAMES[framework];
|
|
115
136
|
return `No patterns were given, and no ${name} config is in the root or a folder below it, so there are no test files to find.`;
|
|
116
137
|
}
|
|
@@ -127,6 +148,8 @@ export async function scan(options) {
|
|
|
127
148
|
const comments = checkComments(checked.comments, 'options.comments');
|
|
128
149
|
const expandCases = checkFlag(checked.expandCases, 'options.expandCases');
|
|
129
150
|
const override = checkVersion(checked.frameworkVersion, 'options.frameworkVersion');
|
|
151
|
+
const signal = checkSignal(checked.signal, 'options.signal');
|
|
152
|
+
signal?.throwIfAborted();
|
|
130
153
|
if (checked.root !== undefined && (typeof checked.root !== 'string' || checked.root === '')) {
|
|
131
154
|
throw invalid('options.root', 'a non-empty string');
|
|
132
155
|
}
|
|
@@ -138,6 +161,7 @@ export async function scan(options) {
|
|
|
138
161
|
: checkFramework(checked.framework, 'options.framework');
|
|
139
162
|
// Without patterns, the configs choose among all JavaScript and TypeScript files.
|
|
140
163
|
const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
|
|
164
|
+
signal?.throwIfAborted();
|
|
141
165
|
const scanned = found.files.map((file) => file.relativeFilePath);
|
|
142
166
|
const detected = override === null ? detectFrameworkVersion(root, framework, scanned) : null;
|
|
143
167
|
const frameworkVersion = override ?? detected?.version ?? null;
|
|
@@ -186,6 +210,7 @@ export async function scan(options) {
|
|
|
186
210
|
const results = [];
|
|
187
211
|
const globalsFiles = [];
|
|
188
212
|
for (const { absolutePath, relativeFilePath } of files) {
|
|
213
|
+
signal?.throwIfAborted();
|
|
189
214
|
const placement = placements.get(absolutePath);
|
|
190
215
|
if (placement.config)
|
|
191
216
|
configs.use(placement.config);
|
|
@@ -211,10 +236,17 @@ export async function scan(options) {
|
|
|
211
236
|
version: rules,
|
|
212
237
|
comments,
|
|
213
238
|
expandCases,
|
|
214
|
-
readModule: readerFor(absolutePath),
|
|
239
|
+
readModule: readerFor(absolutePath, importMap(placement, framework)),
|
|
215
240
|
root,
|
|
216
241
|
isBenchmarkFile: placement.runs === null ? null : placement.runs === 'benchmark',
|
|
217
242
|
config: fileConfig(placement, framework, rules, absolutePath),
|
|
243
|
+
mochaInterface: framework === 'mocha'
|
|
244
|
+
? mochaInterface(placement, configs)
|
|
245
|
+
: framework === 'webdriverio'
|
|
246
|
+
? webdriverioInterface(placement, configs)
|
|
247
|
+
: undefined,
|
|
248
|
+
webdriverioFramework: framework === 'webdriverio' ? webdriverioFramework(placement, configs) : undefined,
|
|
249
|
+
titleTruncate: framework === 'vitest' ? titleTruncate(placement, rules) : undefined,
|
|
218
250
|
graph,
|
|
219
251
|
exports,
|
|
220
252
|
runsWith,
|
|
@@ -229,9 +261,12 @@ export async function scan(options) {
|
|
|
229
261
|
}
|
|
230
262
|
results.push({ ...result, file: { ...result.file, config } });
|
|
231
263
|
}
|
|
264
|
+
// Mocha, Jasmine, Cypress, WebdriverIO and Bun always set their globals, and Playwright and node:test have none.
|
|
232
265
|
const missingGlobals = framework === 'jest'
|
|
233
266
|
? (file) => jestGlobals(placements.get(file))
|
|
234
|
-
:
|
|
267
|
+
: ['mocha', 'jasmine', 'cypress', 'webdriverio', 'node', 'bun'].includes(framework)
|
|
268
|
+
? () => null
|
|
269
|
+
: globalsCheck(root, configs, (file) => placements.get(file));
|
|
235
270
|
let globals = null;
|
|
236
271
|
for (const { absolutePath, relativeFilePath } of globalsFiles) {
|
|
237
272
|
const message = missingGlobals(absolutePath);
|
package/dist/toml.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Value } from './values.ts';
|
|
2
|
+
/** A line of TOML the reader doesn't take, with its line number. */
|
|
3
|
+
export declare class TomlError extends Error {
|
|
4
|
+
readonly line: number;
|
|
5
|
+
constructor(message: string, line: number);
|
|
6
|
+
}
|
|
7
|
+
type Table = Record<string, Value>;
|
|
8
|
+
/**
|
|
9
|
+
* The part of TOML a `bunfig.toml` uses: tables and dotted keys, strings, numbers, booleans, arrays over several lines,
|
|
10
|
+
* inline tables and comments. Dates and arrays of tables are refused with a `TomlError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readToml(text: string): Table;
|
|
13
|
+
export {};
|