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/config.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface ObjectSetting {
|
|
|
39
39
|
kind: 'object';
|
|
40
40
|
keys: Map<string, Setting>;
|
|
41
41
|
rest: Unknown | null;
|
|
42
|
+
/** Where the object is written; absent for one the scan builds, as by merging two. */
|
|
43
|
+
where?: Site;
|
|
42
44
|
}
|
|
43
45
|
/** The source text of a setting the scan can't work out; null for one of a config it couldn't read at all. */
|
|
44
46
|
export declare function unknownText(setting: Unknown): string | null;
|
|
@@ -59,11 +61,13 @@ export interface ConfigFile {
|
|
|
59
61
|
/** Finds and reads the configs of one scan, and collects what it can't work out in them as diagnostics. */
|
|
60
62
|
export declare class Configs {
|
|
61
63
|
readonly diagnostics: Diagnostic[];
|
|
62
|
-
|
|
64
|
+
/** The project root, where the runner is taken to be started. */
|
|
65
|
+
readonly root: string;
|
|
63
66
|
private readonly framework;
|
|
64
67
|
private readonly readerFor;
|
|
65
68
|
private readonly inFolders;
|
|
66
69
|
private readonly files;
|
|
70
|
+
private mocha;
|
|
67
71
|
private readonly used;
|
|
68
72
|
private readonly reported;
|
|
69
73
|
private readonly version;
|
|
@@ -83,8 +87,45 @@ export declare class Configs {
|
|
|
83
87
|
records(projectsOf: (config: ConfigFile) => ProjectRecord[] | null): ConfigRecord[];
|
|
84
88
|
/** Reports a setting the scan reads but can't work out, once; `key` names it, and `effect` says what the scan does. */
|
|
85
89
|
reportUnknown(setting: Unknown, key: string, effect: string): void;
|
|
90
|
+
/**
|
|
91
|
+
* Reports, once, a setting the runner mishandles, where it is written, or on its config when the scan built it from
|
|
92
|
+
* several places; `key` names it.
|
|
93
|
+
*/
|
|
94
|
+
reportSetting(setting: ObjectSetting, config: ConfigFile, key: string, message: string): void;
|
|
95
|
+
private reportAt;
|
|
96
|
+
/** Reports, once, a config the runner reads in a way the scan doesn't follow, or refuses. */
|
|
97
|
+
reportConfig(config: ConfigFile, message: string): void;
|
|
98
|
+
/**
|
|
99
|
+
* The config Mocha runs with, started in the root: the first `.mocharc` file from the root up, joined with the `mocha`
|
|
100
|
+
* key of the nearest package.json. Null without either.
|
|
101
|
+
*/
|
|
102
|
+
mochaConfig(): ConfigFile | null;
|
|
103
|
+
/**
|
|
104
|
+
* The config the jasmine command reads, started in the root: the first of `spec/support/jasmine.mjs` (from Jasmine
|
|
105
|
+
* 5), `.json` and `.js` there; null without one.
|
|
106
|
+
*/
|
|
107
|
+
jasmineConfig(): ConfigFile | null;
|
|
108
|
+
/**
|
|
109
|
+
* The config `cypress run` or `wdio` reads, started in the root without a config named: the first of its config
|
|
110
|
+
* files there; null without one.
|
|
111
|
+
*/
|
|
112
|
+
rootConfig(): ConfigFile | null;
|
|
86
113
|
/** The config the runner started in a folder would use; null when the folder has none. */
|
|
87
114
|
inFolder(folder: string): ConfigFile | null;
|
|
115
|
+
/** Config file names, in the order the runner looks for them; Jest reads `.cts` from 30.0 and `.mts` from 30.4. */
|
|
116
|
+
private names;
|
|
117
|
+
/**
|
|
118
|
+
* The config Jest takes in a folder: a `jest.config` file, else the `package.json` with a `jest` key, which from 30.0
|
|
119
|
+
* may be the path to the config. A `package.json` without one is a config only in the root, as Jest started there
|
|
120
|
+
* reads it; below the root it is a package, whose tests the root's config runs. Jest refuses to start with more than
|
|
121
|
+
* one config in the folder, which is reported.
|
|
122
|
+
*/
|
|
123
|
+
private jestConfigIn;
|
|
124
|
+
/**
|
|
125
|
+
* The config Jest finds for a folder its `projects` names: from the folder up to the root, the first `jest.config`
|
|
126
|
+
* file or `package.json`, which Jest reads there also without a `jest` key.
|
|
127
|
+
*/
|
|
128
|
+
projectConfig(folder: string): ConfigFile | null;
|
|
88
129
|
private read;
|
|
89
130
|
}
|
|
90
131
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -4,16 +4,28 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { childNodes, isFunctionNode, propertyKey, propertyName, returnValues, unwrap, } from "./ast.js";
|
|
5
5
|
import { fileDiagnostic, toDiagnostic } from "./diagnostics.js";
|
|
6
6
|
import { readFailure } from "./files.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { folders } from "./framework-version.js";
|
|
8
|
+
import { commonJsExport, exportName, moduleExportsValue, ownExport, parseModule, requiredModule, starReexport, } from "./frameworks/bindings.js";
|
|
9
|
+
import { atLeast, problem } from "./frameworks/common.js";
|
|
9
10
|
import { parseFile } from "./parse.js";
|
|
10
11
|
import { snippet } from "./source.js";
|
|
11
12
|
import { isKey, isScopeNode, Values } from "./values.js";
|
|
12
13
|
import { VITEST_INCLUDE, VITEST_TYPECHECK_INCLUDE, vitestExclude } from "./vitest-defaults.js";
|
|
14
|
+
import { readToml, TomlError } from "./toml.js";
|
|
15
|
+
import { readYaml, YamlError } from "./yaml.js";
|
|
13
16
|
/** Config file names in the order each runner looks for them in a folder. */
|
|
14
17
|
const CONFIG_FILES = {
|
|
15
18
|
playwright: ['.ts', '.js', '.mts', '.mjs', '.cts', '.cjs'].map((extension) => `playwright.config${extension}`),
|
|
16
19
|
vitest: ['vitest.config', 'vite.config'].flatMap((name) => ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'].map((extension) => name + extension)),
|
|
20
|
+
jest: ['.js', '.ts', '.mjs', '.mts', '.cjs', '.cts', '.json'].map((extension) => `jest.config${extension}`),
|
|
21
|
+
mocha: ['.cjs', '.js', '.mjs', '.yaml', '.yml', '.jsonc', '.json'].map((extension) => `.mocharc${extension}`),
|
|
22
|
+
// From the root, which the jasmine command runs in; `.mjs` from Jasmine 5.
|
|
23
|
+
jasmine: ['.mjs', '.json', '.js'].map((extension) => `spec/support/jasmine${extension}`),
|
|
24
|
+
cypress: ['.ts', '.js', '.mjs', '.cjs', '.mts', '.cts'].map((extension) => `cypress.config${extension}`),
|
|
25
|
+
// The files `wdio run` takes without a config named, in its order.
|
|
26
|
+
webdriverio: ['.js', '.ts', '.mjs', '.mts', '.cjs', '.cts'].map((extension) => `wdio.conf${extension}`),
|
|
27
|
+
node: [],
|
|
28
|
+
bun: ['bunfig.toml'],
|
|
17
29
|
};
|
|
18
30
|
/** Methods that change the object or list they are called on. */
|
|
19
31
|
const CHANGING_METHODS = new Set([
|
|
@@ -266,7 +278,7 @@ class ConfigCode {
|
|
|
266
278
|
const { program } = frame.parsed;
|
|
267
279
|
const own = ownExport(program, name);
|
|
268
280
|
if (own?.kind === 'default')
|
|
269
|
-
return this.
|
|
281
|
+
return this.config(own.value, frame, [program]);
|
|
270
282
|
if (own) {
|
|
271
283
|
const at = { frame, node: own.statement };
|
|
272
284
|
const found = own.kind === 'specifier' && own.source !== null
|
|
@@ -277,8 +289,12 @@ class ConfigCode {
|
|
|
277
289
|
if (name === 'default') {
|
|
278
290
|
// `export *` passes on every name but the default.
|
|
279
291
|
const commonJs = program.body.map(moduleExportsValue).find((value) => value !== null);
|
|
280
|
-
return commonJs ? this.
|
|
292
|
+
return commonJs ? this.config(commonJs, frame, [program]) : null;
|
|
281
293
|
}
|
|
294
|
+
// A name a CommonJS module exports, as WebdriverIO's `exports.config = { ... }`.
|
|
295
|
+
const commonJs = commonJsExport(program, name);
|
|
296
|
+
if (commonJs)
|
|
297
|
+
return this.config(commonJs, frame, [program]);
|
|
282
298
|
for (const statement of program.body) {
|
|
283
299
|
const source = starReexport(statement);
|
|
284
300
|
const found = source === null ? null : this.fromModule(frame, source, name, site);
|
|
@@ -287,6 +303,21 @@ class ConfigCode {
|
|
|
287
303
|
}
|
|
288
304
|
return null;
|
|
289
305
|
}
|
|
306
|
+
/** A module's default export, which Jest calls when it is a function, and awaits. */
|
|
307
|
+
config(node, frame, scopes) {
|
|
308
|
+
if (this.framework !== 'jest')
|
|
309
|
+
return this.evaluate(node, frame, scopes);
|
|
310
|
+
let fn = unwrap(node);
|
|
311
|
+
let around = scopes;
|
|
312
|
+
if (fn.type === 'Identifier') {
|
|
313
|
+
const constant = frame.values.constantOf(fn.name, scopes);
|
|
314
|
+
if (constant)
|
|
315
|
+
[fn, around] = [unwrap(constant.init), constant.scopes];
|
|
316
|
+
}
|
|
317
|
+
return fn.type === 'FunctionDeclaration' || isFunctionNode(fn)
|
|
318
|
+
? this.returned(fn, frame, around)
|
|
319
|
+
: this.evaluate(node, frame, scopes);
|
|
320
|
+
}
|
|
290
321
|
/** Works out an expression; `scopes` are the scope nodes around it, outermost first. */
|
|
291
322
|
evaluate(node, frame, scopes) {
|
|
292
323
|
const expression = unwrap(node);
|
|
@@ -344,10 +375,11 @@ class ConfigCode {
|
|
|
344
375
|
const setting = this.once(constant.init, site, () => this.evaluate(constant.init, frame, constant.scopes));
|
|
345
376
|
return this.changed(setting, frame, declaration);
|
|
346
377
|
}
|
|
347
|
-
|
|
348
|
-
return null;
|
|
349
|
-
const imported = declaration.type === 'ImportSpecifier' ? exportName(declaration.imported) : 'default';
|
|
378
|
+
// An import, or a local file loaded with `require()`, which reads as one.
|
|
350
379
|
const source = frame.values.importSource(name, scopes);
|
|
380
|
+
const imported = frame.values.importedName(name, scopes);
|
|
381
|
+
if (source === null || imported === null)
|
|
382
|
+
return null;
|
|
351
383
|
if (source === 'vitest/config' || source === 'vitest/node') {
|
|
352
384
|
const defaults = this.vitestDefaults(imported, site);
|
|
353
385
|
if (defaults)
|
|
@@ -398,6 +430,19 @@ class ConfigCode {
|
|
|
398
430
|
}
|
|
399
431
|
/** What a module an import names exports under a name; null when the module doesn't export it. */
|
|
400
432
|
fromModule(frame, specifier, name, site) {
|
|
433
|
+
if (specifier.endsWith('.json')) {
|
|
434
|
+
// A JSON file, required or imported with `with { type: 'json' }`, is its value.
|
|
435
|
+
const module = frame.readModule(specifier);
|
|
436
|
+
if ('reason' in module)
|
|
437
|
+
return unknown(site);
|
|
438
|
+
const value = attemptJson(module.code);
|
|
439
|
+
if (value === null)
|
|
440
|
+
return unknown(site);
|
|
441
|
+
if (!this.followed.includes(module.relativeFilePath))
|
|
442
|
+
this.followed.push(module.relativeFilePath);
|
|
443
|
+
const setting = fromValue(value.json);
|
|
444
|
+
return name === 'default' ? setting : child(setting, name);
|
|
445
|
+
}
|
|
401
446
|
const module = this.open(frame, specifier);
|
|
402
447
|
if (!module)
|
|
403
448
|
return unknown(site);
|
|
@@ -455,7 +500,7 @@ class ConfigCode {
|
|
|
455
500
|
// A method or getter is a function, which the scan can't work out.
|
|
456
501
|
object.keys.set(String(key.value), this.evaluate(property.value, frame, scopes));
|
|
457
502
|
}
|
|
458
|
-
return object;
|
|
503
|
+
return { ...object, where: { frame, node } };
|
|
459
504
|
}
|
|
460
505
|
/**
|
|
461
506
|
* `{ ...object, ...argument }`, also for an argument that picks between two objects, as in
|
|
@@ -483,10 +528,12 @@ class ConfigCode {
|
|
|
483
528
|
items.push(this.evaluate(element, frame, scopes));
|
|
484
529
|
continue;
|
|
485
530
|
}
|
|
531
|
+
// A spread the scan can't work out is one unknown item, so the items it can are kept.
|
|
486
532
|
const spreadList = this.evaluate(element.argument, frame, scopes);
|
|
487
|
-
if (spreadList.kind
|
|
488
|
-
|
|
489
|
-
|
|
533
|
+
if (spreadList.kind === 'list')
|
|
534
|
+
items.push(...spreadList.items);
|
|
535
|
+
else
|
|
536
|
+
items.push(unknown({ frame, node: element }));
|
|
490
537
|
}
|
|
491
538
|
return { kind: 'list', items };
|
|
492
539
|
}
|
|
@@ -518,6 +565,12 @@ class ConfigCode {
|
|
|
518
565
|
*/
|
|
519
566
|
call(call, frame, scopes) {
|
|
520
567
|
const site = { frame, node: call };
|
|
568
|
+
// `require('./jest.base.js')` of a local file, as CommonJS configs build on a shared one, is its default export.
|
|
569
|
+
const required = requiredModule(call);
|
|
570
|
+
const isGlobal = frame.values.declarationOf('require', scopes) === null;
|
|
571
|
+
if (required !== null && isGlobal && /^\.\.?\//.test(required)) {
|
|
572
|
+
return this.fromModule(frame, required, 'default', site) ?? unknown(site);
|
|
573
|
+
}
|
|
521
574
|
const origin = this.origin(unwrap(call.callee), frame, scopes);
|
|
522
575
|
const pathFunction = origin && PATH_FUNCTIONS[`${origin.source.replace(/^node:/, '')}.${origin.name}`];
|
|
523
576
|
if (pathFunction) {
|
|
@@ -529,7 +582,15 @@ class ConfigCode {
|
|
|
529
582
|
const name = isPackage && CONFIG_FUNCTIONS.has(origin.name) ? origin.name : null;
|
|
530
583
|
const { arguments: given } = call;
|
|
531
584
|
const needed = name === 'mergeConfig' ? 2 : 1;
|
|
532
|
-
|
|
585
|
+
// Jest exports defineConfig() and mergeConfig() from 30.3; neither takes a project.
|
|
586
|
+
const isJest = origin !== null &&
|
|
587
|
+
['jest', 'jest-config'].includes(origin.source) &&
|
|
588
|
+
name !== 'defineProject' &&
|
|
589
|
+
atLeast(this.version, 30, 3);
|
|
590
|
+
if (name === null ||
|
|
591
|
+
(this.framework === 'jest' && !isJest) ||
|
|
592
|
+
given.length < needed ||
|
|
593
|
+
given.some((arg) => arg.type === 'SpreadElement')) {
|
|
533
594
|
return unknown(site);
|
|
534
595
|
}
|
|
535
596
|
const args = given;
|
|
@@ -537,8 +598,8 @@ class ConfigCode {
|
|
|
537
598
|
const [base, over] = args.map((arg) => this.evaluate(arg, frame, scopes));
|
|
538
599
|
return mergeDeep(base, over);
|
|
539
600
|
}
|
|
540
|
-
if (this.framework
|
|
541
|
-
// Vitest's
|
|
601
|
+
if (this.framework !== 'playwright') {
|
|
602
|
+
// Vitest's and Jest's defineConfig() return the config they are given, or the one a function returns.
|
|
542
603
|
return isFunctionNode(args[0]) ? this.returned(args[0], frame, scopes) : this.evaluate(args[0], frame, scopes);
|
|
543
604
|
}
|
|
544
605
|
// Playwright's defineConfig() merges the configs it is given, each over the ones before it.
|
|
@@ -593,11 +654,13 @@ class ConfigCode {
|
|
|
593
654
|
/** Finds and reads the configs of one scan, and collects what it can't work out in them as diagnostics. */
|
|
594
655
|
export class Configs {
|
|
595
656
|
diagnostics = [];
|
|
657
|
+
/** The project root, where the runner is taken to be started. */
|
|
596
658
|
root;
|
|
597
659
|
framework;
|
|
598
660
|
readerFor;
|
|
599
661
|
inFolders = new Map();
|
|
600
662
|
files = new Map();
|
|
663
|
+
mocha;
|
|
601
664
|
used = new Set();
|
|
602
665
|
reported = new Set();
|
|
603
666
|
version;
|
|
@@ -649,29 +712,142 @@ export class Configs {
|
|
|
649
712
|
}
|
|
650
713
|
/** Reports a setting the scan reads but can't work out, once; `key` names it, and `effect` says what the scan does. */
|
|
651
714
|
reportUnknown(setting, key, effect) {
|
|
652
|
-
|
|
715
|
+
const message = `The config's ${key} could not be worked out without running the config, so ${effect}.`;
|
|
716
|
+
this.reportAt(setting.where, key, message);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Reports, once, a setting the runner mishandles, where it is written, or on its config when the scan built it from
|
|
720
|
+
* several places; `key` names it.
|
|
721
|
+
*/
|
|
722
|
+
reportSetting(setting, config, key, message) {
|
|
723
|
+
if (setting.where)
|
|
724
|
+
this.reportAt(setting.where, key, message);
|
|
725
|
+
else
|
|
726
|
+
this.reportConfig(config, message);
|
|
727
|
+
}
|
|
728
|
+
reportAt(where, key, message) {
|
|
729
|
+
if (where === null)
|
|
653
730
|
return;
|
|
654
|
-
const { frame, node } =
|
|
731
|
+
const { frame, node } = where;
|
|
655
732
|
const id = `${frame.relativeFilePath}\0${node.start}\0${key}`;
|
|
656
733
|
if (this.reported.has(id))
|
|
657
734
|
return;
|
|
658
735
|
this.reported.add(id);
|
|
659
|
-
const message = `The config's ${key} could not be worked out without running the config, so ${effect}.`;
|
|
660
736
|
const code = snippet(frame.parsed.source.text.slice(node.start, node.end));
|
|
661
737
|
const found = problem('config-value-unknown', node, message, code);
|
|
662
738
|
this.diagnostics.push(toDiagnostic(found, frame.relativeFilePath, frame.parsed.source));
|
|
663
739
|
}
|
|
740
|
+
/** Reports, once, a config the runner reads in a way the scan doesn't follow, or refuses. */
|
|
741
|
+
reportConfig(config, message) {
|
|
742
|
+
const id = `${config.record.relativeFilePath}\0${message}`;
|
|
743
|
+
if (this.reported.has(id))
|
|
744
|
+
return;
|
|
745
|
+
this.reported.add(id);
|
|
746
|
+
this.diagnostics.push(fileDiagnostic('config-value-unknown', config.record.relativeFilePath, message));
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* The config Mocha runs with, started in the root: the first `.mocharc` file from the root up, joined with the `mocha`
|
|
750
|
+
* key of the nearest package.json. Null without either.
|
|
751
|
+
*/
|
|
752
|
+
mochaConfig() {
|
|
753
|
+
if (this.mocha !== undefined)
|
|
754
|
+
return this.mocha;
|
|
755
|
+
let rc = null;
|
|
756
|
+
let manifest = null;
|
|
757
|
+
for (const folder of folders(this.root)) {
|
|
758
|
+
const name = rc ? undefined : CONFIG_FILES.mocha.find((item) => existsSync(path.join(folder, item)));
|
|
759
|
+
if (name)
|
|
760
|
+
rc = this.at(path.join(folder, name));
|
|
761
|
+
if (manifest === null && existsSync(path.join(folder, 'package.json')))
|
|
762
|
+
manifest = path.join(folder, 'package.json');
|
|
763
|
+
if (rc && manifest)
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
const key = manifest && manifestKey(manifest, 'mocha') !== undefined ? this.at(manifest) : null;
|
|
767
|
+
this.mocha = rc && key ? joinMochaConfigs(rc, key) : (rc ?? key);
|
|
768
|
+
return this.mocha;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* The config the jasmine command reads, started in the root: the first of `spec/support/jasmine.mjs` (from Jasmine
|
|
772
|
+
* 5), `.json` and `.js` there; null without one.
|
|
773
|
+
*/
|
|
774
|
+
jasmineConfig() {
|
|
775
|
+
const names = CONFIG_FILES.jasmine.filter((name) => !name.endsWith('.mjs') || atLeast(this.version, 5, 0));
|
|
776
|
+
const name = names.find((item) => existsSync(path.join(this.root, item)));
|
|
777
|
+
return name ? this.at(path.join(this.root, name)) : null;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* The config `cypress run` or `wdio` reads, started in the root without a config named: the first of its config
|
|
781
|
+
* files there; null without one.
|
|
782
|
+
*/
|
|
783
|
+
rootConfig() {
|
|
784
|
+
const name = CONFIG_FILES[this.framework].find((item) => existsSync(path.join(this.root, item)));
|
|
785
|
+
return name ? this.at(path.join(this.root, name)) : null;
|
|
786
|
+
}
|
|
664
787
|
/** The config the runner started in a folder would use; null when the folder has none. */
|
|
665
788
|
inFolder(folder) {
|
|
666
789
|
let config = this.inFolders.get(folder);
|
|
667
790
|
if (config === undefined) {
|
|
668
791
|
// The runners take a config that exists, without checking that it is a file they can read.
|
|
669
|
-
|
|
670
|
-
|
|
792
|
+
if (this.framework === 'jest') {
|
|
793
|
+
config = this.jestConfigIn(folder);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
const name = this.names().find((item) => existsSync(path.join(folder, item)));
|
|
797
|
+
config = name ? this.at(path.join(folder, name)) : null;
|
|
798
|
+
}
|
|
671
799
|
this.inFolders.set(folder, config);
|
|
672
800
|
}
|
|
673
801
|
return config;
|
|
674
802
|
}
|
|
803
|
+
/** Config file names, in the order the runner looks for them; Jest reads `.cts` from 30.0 and `.mts` from 30.4. */
|
|
804
|
+
names() {
|
|
805
|
+
if (this.framework !== 'jest')
|
|
806
|
+
return CONFIG_FILES[this.framework];
|
|
807
|
+
return CONFIG_FILES.jest.filter((name) => (!name.endsWith('.cts') || atLeast(this.version, 30, 0)) &&
|
|
808
|
+
(!name.endsWith('.mts') || atLeast(this.version, 30, 4)));
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* The config Jest takes in a folder: a `jest.config` file, else the `package.json` with a `jest` key, which from 30.0
|
|
812
|
+
* may be the path to the config. A `package.json` without one is a config only in the root, as Jest started there
|
|
813
|
+
* reads it; below the root it is a package, whose tests the root's config runs. Jest refuses to start with more than
|
|
814
|
+
* one config in the folder, which is reported.
|
|
815
|
+
*/
|
|
816
|
+
jestConfigIn(folder, anyManifest = false) {
|
|
817
|
+
const names = this.names().filter((item) => existsSync(path.join(folder, item)));
|
|
818
|
+
const manifest = path.join(folder, 'package.json');
|
|
819
|
+
const key = existsSync(manifest) ? manifestKey(manifest) : undefined;
|
|
820
|
+
const KEY = 'the jest key in package.json';
|
|
821
|
+
if (key !== undefined)
|
|
822
|
+
names.push(KEY);
|
|
823
|
+
if (names.length > 1) {
|
|
824
|
+
const listed = `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
825
|
+
const message = `Jest finds more than one config in this folder, ${listed}, and refuses to start; the scan reads ${names[0]}.`;
|
|
826
|
+
const relativeFilePath = this.relative(path.join(folder, names[0]));
|
|
827
|
+
if (!this.reported.has(`${relativeFilePath}\0${message}`)) {
|
|
828
|
+
this.reported.add(`${relativeFilePath}\0${message}`);
|
|
829
|
+
this.diagnostics.push(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (names.length > 0 && names[0] !== KEY)
|
|
833
|
+
return this.at(path.join(folder, names[0]));
|
|
834
|
+
if (typeof key === 'string' && atLeast(this.version, 30, 0))
|
|
835
|
+
return this.at(path.resolve(folder, key));
|
|
836
|
+
return key !== undefined || ((anyManifest || folder === this.root) && existsSync(manifest))
|
|
837
|
+
? this.at(manifest)
|
|
838
|
+
: null;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* The config Jest finds for a folder its `projects` names: from the folder up to the root, the first `jest.config`
|
|
842
|
+
* file or `package.json`, which Jest reads there also without a `jest` key.
|
|
843
|
+
*/
|
|
844
|
+
projectConfig(folder) {
|
|
845
|
+
for (let current = folder;; current = path.dirname(current)) {
|
|
846
|
+
const config = this.jestConfigIn(current, true);
|
|
847
|
+
if (config || current === this.root || path.dirname(current) === current)
|
|
848
|
+
return config;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
675
851
|
read(file) {
|
|
676
852
|
const relativeFilePath = path.relative(this.root, file).replaceAll('\\', '/');
|
|
677
853
|
const unreadable = (diagnostic) => {
|
|
@@ -687,6 +863,26 @@ export class Configs {
|
|
|
687
863
|
const message = `The config could not be read: ${readFailure(error)}, ${none}`;
|
|
688
864
|
return unreadable(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
689
865
|
}
|
|
866
|
+
if (/\.(jsonc?|ya?ml|toml)$/.test(file)) {
|
|
867
|
+
let value;
|
|
868
|
+
try {
|
|
869
|
+
// Mocha reads `.mocharc.json` with comments too.
|
|
870
|
+
const comments = file.endsWith('.jsonc') || path.basename(file).startsWith('.mocharc');
|
|
871
|
+
value = /\.ya?ml$/.test(file)
|
|
872
|
+
? readYaml(code)
|
|
873
|
+
: file.endsWith('.toml')
|
|
874
|
+
? readToml(code)
|
|
875
|
+
: JSON.parse(comments ? withoutJsonComments(code) : code);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
const at = error instanceof YamlError || error instanceof TomlError ? ` on line ${error.line}` : '';
|
|
879
|
+
const message = `The config could not be parsed${at}: ${error.message}, ${none}`;
|
|
880
|
+
return unreadable(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
881
|
+
}
|
|
882
|
+
// Jest reads a package.json without a `jest` key as an empty config, and Mocha one without a `mocha` key.
|
|
883
|
+
const settings = path.basename(file) === 'package.json' ? manifestKeyOf(value, this.framework) : value;
|
|
884
|
+
return { file, record: { relativeFilePath, extends: [] }, settings: fromValue(settings ?? {}) };
|
|
885
|
+
}
|
|
690
886
|
const parsed = parseFile(relativeFilePath, code);
|
|
691
887
|
if (parsed.error) {
|
|
692
888
|
const { message, start, end } = parsed.error;
|
|
@@ -697,7 +893,9 @@ export class Configs {
|
|
|
697
893
|
const config = new ConfigCode(frame, this.framework, this.root, this.version);
|
|
698
894
|
let settings;
|
|
699
895
|
try {
|
|
700
|
-
|
|
896
|
+
// WebdriverIO's config is the module's `config` export.
|
|
897
|
+
const name = this.framework === 'webdriverio' ? 'config' : 'default';
|
|
898
|
+
settings = config.exported(frame, name, { frame, node: parsed.program });
|
|
701
899
|
}
|
|
702
900
|
catch (error) {
|
|
703
901
|
// The scan runs none of the project's code, so what fails here is its own and throws an Error.
|
|
@@ -706,9 +904,77 @@ export class Configs {
|
|
|
706
904
|
return unreadable(fileDiagnostic('internal-error', relativeFilePath, message));
|
|
707
905
|
}
|
|
708
906
|
if (settings === null) {
|
|
709
|
-
const message = `The config has no default export the scan can find, ${none}`;
|
|
907
|
+
const message = `The config has no ${this.framework === 'webdriverio' ? '`config`' : 'default'} export the scan can find, ${none}`;
|
|
710
908
|
return unreadable(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
711
909
|
}
|
|
712
910
|
return { file, record: { relativeFilePath, extends: config.followed }, settings };
|
|
713
911
|
}
|
|
714
912
|
}
|
|
913
|
+
/** The `jest` or `mocha` key of a package.json; undefined when it has none, or can't be read. */
|
|
914
|
+
function manifestKey(file, framework = 'jest') {
|
|
915
|
+
try {
|
|
916
|
+
return manifestKeyOf(JSON.parse(readFileSync(file, 'utf8')), framework);
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/** Mocha's options that take a list, which it joins across a `.mocharc` file and package.json. */
|
|
923
|
+
const MOCHA_LISTS = new Set([
|
|
924
|
+
'extension',
|
|
925
|
+
'file',
|
|
926
|
+
'global',
|
|
927
|
+
'ignore',
|
|
928
|
+
'exclude',
|
|
929
|
+
'node-option',
|
|
930
|
+
'reporter-option',
|
|
931
|
+
'require',
|
|
932
|
+
'spec',
|
|
933
|
+
'watch-files',
|
|
934
|
+
'watch-ignore',
|
|
935
|
+
]);
|
|
936
|
+
/**
|
|
937
|
+
* A `.mocharc` file and the `mocha` key of a package.json as Mocha joins them: a list option is the file's items and
|
|
938
|
+
* then the package.json's, a single value counting as a list of one; for any other key the file's value wins (checked
|
|
939
|
+
* with Mocha 10.8.2, 11.7.5 and 12.0.3).
|
|
940
|
+
*/
|
|
941
|
+
function joinMochaConfigs(rc, key) {
|
|
942
|
+
const record = { ...rc.record, extends: [...rc.record.extends, key.record.relativeFilePath] };
|
|
943
|
+
if (rc.settings.kind !== 'object' || key.settings.kind !== 'object')
|
|
944
|
+
return { ...rc, record };
|
|
945
|
+
const keys = new Map([...key.settings.keys, ...rc.settings.keys]);
|
|
946
|
+
for (const name of MOCHA_LISTS) {
|
|
947
|
+
const [first, second] = [rc.settings.keys.get(name), key.settings.keys.get(name)];
|
|
948
|
+
if (!first || !second)
|
|
949
|
+
continue;
|
|
950
|
+
const items = (setting) => (setting.kind === 'list' ? setting.items : [setting]);
|
|
951
|
+
keys.set(name, { kind: 'list', items: [...items(first), ...items(second)] });
|
|
952
|
+
}
|
|
953
|
+
return { ...rc, record, settings: { kind: 'object', keys, rest: rc.settings.rest } };
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* The `jest` or `mocha` key of a parsed package.json, by the framework; undefined for a value the runner reads as no
|
|
957
|
+
* config, as `false` or `""` are.
|
|
958
|
+
*/
|
|
959
|
+
function manifestKeyOf(packageJson, framework = 'jest') {
|
|
960
|
+
const key = packageJson?.[framework === 'mocha' ? 'mocha' : 'jest'];
|
|
961
|
+
if (!key)
|
|
962
|
+
return undefined;
|
|
963
|
+
return key;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* JSON text without `//` and `/* *\/` comments outside strings, as Mocha reads `.mocharc.jsonc`; a comment becomes
|
|
967
|
+
* spaces, so the parser's positions stay right.
|
|
968
|
+
*/
|
|
969
|
+
function withoutJsonComments(text) {
|
|
970
|
+
return text.replaceAll(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (match, string) => string ?? match.replaceAll(/[^\n]/g, ' '));
|
|
971
|
+
}
|
|
972
|
+
/** A JSON text's value; null when it isn't JSON. */
|
|
973
|
+
function attemptJson(text) {
|
|
974
|
+
try {
|
|
975
|
+
return { json: JSON.parse(text) };
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { ConfigGap, RunsWith } from './config-dependencies.ts';
|
|
|
3
3
|
import type { Extraction, Frame, PendingTest } from './extract.ts';
|
|
4
4
|
import type { ModuleReader, ModuleSource, Problem, Span } from './model.ts';
|
|
5
5
|
import { type ParsedFile } from './parse.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { SourceText } from './source.ts';
|
|
7
7
|
import type { Framework } from './types.ts';
|
|
8
8
|
import { Values } from './values.ts';
|
|
9
9
|
/** A file whose code a test can run: the scanned file, a module it imports, or a data file such as JSON. */
|
|
@@ -96,6 +96,14 @@ interface Reach {
|
|
|
96
96
|
asks: string[] | null;
|
|
97
97
|
snapshots: SnapshotUse[];
|
|
98
98
|
}
|
|
99
|
+
/** What several reaches cover together, by file. */
|
|
100
|
+
interface Merged {
|
|
101
|
+
files: string[];
|
|
102
|
+
/** Each file's hashes, sorted. */
|
|
103
|
+
byFile: Map<string, string[]>;
|
|
104
|
+
/** Each file's part of the text the dependency hash is taken of. */
|
|
105
|
+
parts: Map<string, string>;
|
|
106
|
+
}
|
|
99
107
|
/** A fixture one `extend()` call defines, and the fixtures it asks for; null when it takes them whole. */
|
|
100
108
|
interface Fixture {
|
|
101
109
|
unit: Unit;
|
|
@@ -145,11 +153,11 @@ export declare class DependencyGraph {
|
|
|
145
153
|
/** The units a set of roots reaches, leaving out those `skip` reached, with their hashes by file. */
|
|
146
154
|
reach(roots: Unit[], skip?: Reach[]): Reach;
|
|
147
155
|
/**
|
|
148
|
-
* Everything a test's roots reach on top of `
|
|
149
|
-
* with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
|
|
150
|
-
* followed.
|
|
156
|
+
* Everything a test's roots reach on top of `bases`, such as the code its file runs when loaded, which all its tests
|
|
157
|
+
* share, with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
|
|
158
|
+
* followed. `shared` is what the bases cover, put together once for the many tests that share them.
|
|
151
159
|
*/
|
|
152
|
-
dependencies(roots: Unit[], bases: Reach[], needs: FixtureNeeds, snapshots: (uses: SnapshotUse[]) => Unit[], report: (place: Place) => void): Dependencies;
|
|
160
|
+
dependencies(roots: Unit[], bases: Reach[], shared: Merged, needs: FixtureNeeds, snapshots: (uses: SnapshotUse[]) => Unit[], report: (place: Place) => void): Dependencies;
|
|
153
161
|
private newModule;
|
|
154
162
|
private newUnit;
|
|
155
163
|
private hashOf;
|