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/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;
|
|
@@ -65,6 +67,7 @@ export declare class Configs {
|
|
|
65
67
|
private readonly readerFor;
|
|
66
68
|
private readonly inFolders;
|
|
67
69
|
private readonly files;
|
|
70
|
+
private mocha;
|
|
68
71
|
private readonly used;
|
|
69
72
|
private readonly reported;
|
|
70
73
|
private readonly version;
|
|
@@ -84,8 +87,29 @@ export declare class Configs {
|
|
|
84
87
|
records(projectsOf: (config: ConfigFile) => ProjectRecord[] | null): ConfigRecord[];
|
|
85
88
|
/** Reports a setting the scan reads but can't work out, once; `key` names it, and `effect` says what the scan does. */
|
|
86
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;
|
|
87
96
|
/** Reports, once, a config the runner reads in a way the scan doesn't follow, or refuses. */
|
|
88
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;
|
|
89
113
|
/** The config the runner started in a folder would use; null when the folder has none. */
|
|
90
114
|
inFolder(folder: string): ConfigFile | null;
|
|
91
115
|
/** Config file names, in the order the runner looks for them; Jest reads `.cts` from 30.0 and `.mts` from 30.4. */
|
package/dist/config.js
CHANGED
|
@@ -4,17 +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 {
|
|
7
|
+
import { folders } from "./framework-version.js";
|
|
8
|
+
import { commonJsExport, exportName, moduleExportsValue, ownExport, parseModule, requiredModule, starReexport, } from "./frameworks/bindings.js";
|
|
8
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)),
|
|
17
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'],
|
|
18
29
|
};
|
|
19
30
|
/** Methods that change the object or list they are called on. */
|
|
20
31
|
const CHANGING_METHODS = new Set([
|
|
@@ -280,6 +291,10 @@ class ConfigCode {
|
|
|
280
291
|
const commonJs = program.body.map(moduleExportsValue).find((value) => value !== null);
|
|
281
292
|
return commonJs ? this.config(commonJs, frame, [program]) : null;
|
|
282
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]);
|
|
283
298
|
for (const statement of program.body) {
|
|
284
299
|
const source = starReexport(statement);
|
|
285
300
|
const found = source === null ? null : this.fromModule(frame, source, name, site);
|
|
@@ -485,7 +500,7 @@ class ConfigCode {
|
|
|
485
500
|
// A method or getter is a function, which the scan can't work out.
|
|
486
501
|
object.keys.set(String(key.value), this.evaluate(property.value, frame, scopes));
|
|
487
502
|
}
|
|
488
|
-
return object;
|
|
503
|
+
return { ...object, where: { frame, node } };
|
|
489
504
|
}
|
|
490
505
|
/**
|
|
491
506
|
* `{ ...object, ...argument }`, also for an argument that picks between two objects, as in
|
|
@@ -645,6 +660,7 @@ export class Configs {
|
|
|
645
660
|
readerFor;
|
|
646
661
|
inFolders = new Map();
|
|
647
662
|
files = new Map();
|
|
663
|
+
mocha;
|
|
648
664
|
used = new Set();
|
|
649
665
|
reported = new Set();
|
|
650
666
|
version;
|
|
@@ -696,14 +712,27 @@ export class Configs {
|
|
|
696
712
|
}
|
|
697
713
|
/** Reports a setting the scan reads but can't work out, once; `key` names it, and `effect` says what the scan does. */
|
|
698
714
|
reportUnknown(setting, key, effect) {
|
|
699
|
-
|
|
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)
|
|
700
730
|
return;
|
|
701
|
-
const { frame, node } =
|
|
731
|
+
const { frame, node } = where;
|
|
702
732
|
const id = `${frame.relativeFilePath}\0${node.start}\0${key}`;
|
|
703
733
|
if (this.reported.has(id))
|
|
704
734
|
return;
|
|
705
735
|
this.reported.add(id);
|
|
706
|
-
const message = `The config's ${key} could not be worked out without running the config, so ${effect}.`;
|
|
707
736
|
const code = snippet(frame.parsed.source.text.slice(node.start, node.end));
|
|
708
737
|
const found = problem('config-value-unknown', node, message, code);
|
|
709
738
|
this.diagnostics.push(toDiagnostic(found, frame.relativeFilePath, frame.parsed.source));
|
|
@@ -716,6 +745,45 @@ export class Configs {
|
|
|
716
745
|
this.reported.add(id);
|
|
717
746
|
this.diagnostics.push(fileDiagnostic('config-value-unknown', config.record.relativeFilePath, message));
|
|
718
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
|
+
}
|
|
719
787
|
/** The config the runner started in a folder would use; null when the folder has none. */
|
|
720
788
|
inFolder(folder) {
|
|
721
789
|
let config = this.inFolders.get(folder);
|
|
@@ -748,7 +816,7 @@ export class Configs {
|
|
|
748
816
|
jestConfigIn(folder, anyManifest = false) {
|
|
749
817
|
const names = this.names().filter((item) => existsSync(path.join(folder, item)));
|
|
750
818
|
const manifest = path.join(folder, 'package.json');
|
|
751
|
-
const key = existsSync(manifest) ?
|
|
819
|
+
const key = existsSync(manifest) ? manifestKey(manifest) : undefined;
|
|
752
820
|
const KEY = 'the jest key in package.json';
|
|
753
821
|
if (key !== undefined)
|
|
754
822
|
names.push(KEY);
|
|
@@ -795,17 +863,24 @@ export class Configs {
|
|
|
795
863
|
const message = `The config could not be read: ${readFailure(error)}, ${none}`;
|
|
796
864
|
return unreadable(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
797
865
|
}
|
|
798
|
-
if (
|
|
866
|
+
if (/\.(jsonc?|ya?ml|toml)$/.test(file)) {
|
|
799
867
|
let value;
|
|
800
868
|
try {
|
|
801
|
-
|
|
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);
|
|
802
876
|
}
|
|
803
877
|
catch (error) {
|
|
804
|
-
const
|
|
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}`;
|
|
805
880
|
return unreadable(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
806
881
|
}
|
|
807
|
-
// Jest reads a package.json without a `jest` key as an empty config.
|
|
808
|
-
const settings = path.basename(file) === 'package.json' ?
|
|
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;
|
|
809
884
|
return { file, record: { relativeFilePath, extends: [] }, settings: fromValue(settings ?? {}) };
|
|
810
885
|
}
|
|
811
886
|
const parsed = parseFile(relativeFilePath, code);
|
|
@@ -818,7 +893,9 @@ export class Configs {
|
|
|
818
893
|
const config = new ConfigCode(frame, this.framework, this.root, this.version);
|
|
819
894
|
let settings;
|
|
820
895
|
try {
|
|
821
|
-
|
|
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 });
|
|
822
899
|
}
|
|
823
900
|
catch (error) {
|
|
824
901
|
// The scan runs none of the project's code, so what fails here is its own and throws an Error.
|
|
@@ -827,28 +904,71 @@ export class Configs {
|
|
|
827
904
|
return unreadable(fileDiagnostic('internal-error', relativeFilePath, message));
|
|
828
905
|
}
|
|
829
906
|
if (settings === null) {
|
|
830
|
-
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}`;
|
|
831
908
|
return unreadable(fileDiagnostic('config-value-unknown', relativeFilePath, message));
|
|
832
909
|
}
|
|
833
910
|
return { file, record: { relativeFilePath, extends: config.followed }, settings };
|
|
834
911
|
}
|
|
835
912
|
}
|
|
836
|
-
/** The `jest` key of a package.json; undefined when it has none, or can't be read. */
|
|
837
|
-
function
|
|
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') {
|
|
838
915
|
try {
|
|
839
|
-
return
|
|
916
|
+
return manifestKeyOf(JSON.parse(readFileSync(file, 'utf8')), framework);
|
|
840
917
|
}
|
|
841
918
|
catch {
|
|
842
919
|
return undefined;
|
|
843
920
|
}
|
|
844
921
|
}
|
|
845
|
-
/**
|
|
846
|
-
|
|
847
|
-
|
|
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'];
|
|
848
961
|
if (!key)
|
|
849
962
|
return undefined;
|
|
850
963
|
return key;
|
|
851
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
|
+
}
|
|
852
972
|
/** A JSON text's value; null when it isn't JSON. */
|
|
853
973
|
function attemptJson(text) {
|
|
854
974
|
try {
|
package/dist/diagnostics.js
CHANGED
|
@@ -22,6 +22,7 @@ export const LEVELS = {
|
|
|
22
22
|
'nested-test': 'warning',
|
|
23
23
|
'tag-without-at': 'error',
|
|
24
24
|
'removed-api': 'error',
|
|
25
|
+
'invalid-fixture-argument': 'error',
|
|
25
26
|
'only-in-skipped-describe': 'warning',
|
|
26
27
|
'config-may-change-state': 'warning',
|
|
27
28
|
'undefined-tag': 'error',
|
|
@@ -50,9 +51,21 @@ export const LEVELS = {
|
|
|
50
51
|
* calls that throw while the file is collected.
|
|
51
52
|
*/
|
|
52
53
|
export const REJECTS_FILE = {
|
|
53
|
-
playwright: new Set([
|
|
54
|
+
playwright: new Set([
|
|
55
|
+
'test-without-body',
|
|
56
|
+
'tag-without-at',
|
|
57
|
+
'invalid-chain',
|
|
58
|
+
'duplicate-title',
|
|
59
|
+
'invalid-fixture-argument',
|
|
60
|
+
]),
|
|
54
61
|
vitest: new Set(['invalid-chain', 'removed-api', 'undefined-tag']),
|
|
55
62
|
jest: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
|
|
63
|
+
mocha: new Set(['invalid-chain', 'test-without-body']),
|
|
64
|
+
jasmine: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
|
|
65
|
+
cypress: new Set(['invalid-chain', 'test-without-body', 'describe-throws']),
|
|
66
|
+
webdriverio: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
|
|
67
|
+
node: new Set(['invalid-chain']),
|
|
68
|
+
bun: new Set(['invalid-chain', 'test-without-body']),
|
|
56
69
|
};
|
|
57
70
|
export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
|
|
58
71
|
const position = problem.node && source ? source.position(problem.node.start) : null;
|
package/dist/extract.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface PendingSuite {
|
|
|
53
53
|
}
|
|
54
54
|
export interface PendingTest {
|
|
55
55
|
scope: Scope;
|
|
56
|
+
/** The test whose body declares this one as a subtest; null for a test declared in the file or a describe. */
|
|
57
|
+
parent: PendingTest | null;
|
|
56
58
|
declaration: TestDeclaration;
|
|
57
59
|
call: CallExpression;
|
|
58
60
|
/** For a test declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
|
package/dist/extract.js
CHANGED
|
@@ -717,11 +717,7 @@ class Walker {
|
|
|
717
717
|
return this.functionIn(this.frame, name, scopes);
|
|
718
718
|
}
|
|
719
719
|
functionIn(frame, name, scopes) {
|
|
720
|
-
|
|
721
|
-
if (declaration?.type === 'FunctionDeclaration')
|
|
722
|
-
return declaration;
|
|
723
|
-
const init = declaration?.type === 'VariableDeclarator' && declaration.init ? unwrap(declaration.init) : null;
|
|
724
|
-
return init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression' ? init : null;
|
|
720
|
+
return frame.values.functionOf(name, scopes);
|
|
725
721
|
}
|
|
726
722
|
/**
|
|
727
723
|
* Records a call to a function declared in the file. A call from the file or a describe returns the function, to be
|
|
@@ -1045,7 +1041,7 @@ class Walker {
|
|
|
1045
1041
|
expectCall(call, test) {
|
|
1046
1042
|
const callee = unwrap(call.callee);
|
|
1047
1043
|
const [root, member] = callee.type === 'MemberExpression' ? [unwrap(callee.object), propertyName(callee)] : [callee, 'call'];
|
|
1048
|
-
if (
|
|
1044
|
+
if (!this.isExpect(root))
|
|
1049
1045
|
return;
|
|
1050
1046
|
if (member === 'call' || member === 'soft' || member === 'poll' || member === 'element')
|
|
1051
1047
|
test.expectCount += 1;
|
|
@@ -1060,6 +1056,20 @@ class Walker {
|
|
|
1060
1056
|
test.problems.push(...problems.map((item) => this.here(item)));
|
|
1061
1057
|
}
|
|
1062
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Whether an expression is `expect`, or Playwright's `expect.configure(...)`, which gives an `expect` with other
|
|
1061
|
+
* defaults, written in place or kept in a variable.
|
|
1062
|
+
*/
|
|
1063
|
+
isExpect(expression) {
|
|
1064
|
+
const node = unwrap(expression);
|
|
1065
|
+
if (node.type === 'Identifier') {
|
|
1066
|
+
if (node.name === 'expect')
|
|
1067
|
+
return true;
|
|
1068
|
+
const declaration = this.frame.values.declarationOf(node.name, this.scopes);
|
|
1069
|
+
return declaration?.type === 'VariableDeclarator' && declaration.init !== null && isConfigured(declaration.init);
|
|
1070
|
+
}
|
|
1071
|
+
return isConfigured(node);
|
|
1072
|
+
}
|
|
1063
1073
|
/** Reports a problem not tied to a test, once, however many times a function with it is walked. */
|
|
1064
1074
|
report(found) {
|
|
1065
1075
|
const item = this.here(found);
|
|
@@ -1201,14 +1211,20 @@ class Walker {
|
|
|
1201
1211
|
test.problems.push(problem('test-in-function', test.call, `The test is declared in a function, and ${why}. The runner registers the test at each call of the function, in the describe around the call; it is listed once, where it is written.`));
|
|
1202
1212
|
}
|
|
1203
1213
|
}
|
|
1204
|
-
nested(call, context) {
|
|
1214
|
+
nested(call, context, isTest) {
|
|
1205
1215
|
if (!context.test && !context.hookScope)
|
|
1206
1216
|
return false;
|
|
1207
|
-
|
|
1217
|
+
const inTest = context.test !== null && !context.hookScope && this.frame.adapter.nestsTests === true;
|
|
1218
|
+
if (isTest && inTest)
|
|
1219
|
+
return false;
|
|
1220
|
+
const message = inTest
|
|
1221
|
+
? 'A describe declared inside a test body runs as a nested describe of the test, but the scan does not list it or its tests.'
|
|
1222
|
+
: 'A test or describe declared inside a test or hook body is not collected by the runner, so it is not listed.';
|
|
1223
|
+
this.report(problem('nested-test', call, message, snippet(this.frame.parsed.source.slice(call.start, call.end))));
|
|
1208
1224
|
return true;
|
|
1209
1225
|
}
|
|
1210
1226
|
declareTest(call, declaration, context) {
|
|
1211
|
-
if (context.quiet || this.nested(call, context))
|
|
1227
|
+
if (context.quiet || this.nested(call, context, true))
|
|
1212
1228
|
return;
|
|
1213
1229
|
for (const item of this.cases(declaration, call, context))
|
|
1214
1230
|
this.declareCase(call, item, context);
|
|
@@ -1254,6 +1270,7 @@ class Walker {
|
|
|
1254
1270
|
const empty = emptyTitle(declaration, call);
|
|
1255
1271
|
const test = {
|
|
1256
1272
|
scope: context.scope,
|
|
1273
|
+
parent: context.test,
|
|
1257
1274
|
declaration,
|
|
1258
1275
|
call,
|
|
1259
1276
|
site: this.site,
|
|
@@ -1309,7 +1326,7 @@ class Walker {
|
|
|
1309
1326
|
walk();
|
|
1310
1327
|
}
|
|
1311
1328
|
declareSuite(call, declaration, context) {
|
|
1312
|
-
if (context.quiet || this.nested(call, context))
|
|
1329
|
+
if (context.quiet || this.nested(call, context, false))
|
|
1313
1330
|
return;
|
|
1314
1331
|
for (const item of this.cases(declaration, call, context))
|
|
1315
1332
|
this.declareSuiteCase(call, item, context);
|
|
@@ -1404,3 +1421,12 @@ class Walker {
|
|
|
1404
1421
|
export function extract(main, openModule, options = { expandCases: false, version: null }) {
|
|
1405
1422
|
return new Walker(main, openModule, options).run();
|
|
1406
1423
|
}
|
|
1424
|
+
/** Whether an expression is a call of `expect.configure()`. */
|
|
1425
|
+
function isConfigured(expression) {
|
|
1426
|
+
const node = unwrap(expression);
|
|
1427
|
+
const callee = node.type === 'CallExpression' ? unwrap(node.callee) : null;
|
|
1428
|
+
if (callee?.type !== 'MemberExpression' || propertyName(callee) !== 'configure')
|
|
1429
|
+
return false;
|
|
1430
|
+
const object = unwrap(callee.object);
|
|
1431
|
+
return object.type === 'Identifier' && object.name === 'expect';
|
|
1432
|
+
}
|
package/dist/files.js
CHANGED
|
@@ -82,7 +82,8 @@ export async function findFiles(root, patterns, ignore, framework, platform = pr
|
|
|
82
82
|
const absolutePath = path.resolve(root, match);
|
|
83
83
|
const realPath = realpathSync(absolutePath);
|
|
84
84
|
const linked = realPath !== path.join(realRoot, match);
|
|
85
|
-
|
|
85
|
+
// Vitest and Mocha load a file through a link; Node.js loads it once, by its real path.
|
|
86
|
+
if (linked && framework !== 'vitest' && framework !== 'mocha') {
|
|
86
87
|
const name = framework === 'jest' ? 'Jest' : 'Playwright';
|
|
87
88
|
diagnostics.push(fileDiagnostic('symlink-ignored', match, `The file is reached through a symbolic link, which ${name} does not follow.`));
|
|
88
89
|
continue;
|
package/dist/filter.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Framework } from './types.ts';
|
|
2
2
|
/** Each framework's name in messages. */
|
|
3
3
|
export declare const FRAMEWORK_NAMES: Record<Framework, string>;
|
|
4
|
-
/** Oldest supported version of each framework. */
|
|
5
|
-
export declare const OLDEST: Record<Framework, [
|
|
4
|
+
/** Oldest supported version of each framework, as major, minor and for Bun, whose 1.2.23 changed how it runs tests, patch. */
|
|
5
|
+
export declare const OLDEST: Record<Framework, number[]>;
|
|
6
6
|
/** Where the framework version came from. */
|
|
7
7
|
export interface DetectedVersion {
|
|
8
8
|
version: string;
|
|
@@ -11,6 +11,8 @@ export interface DetectedVersion {
|
|
|
11
11
|
/** Only a range is known, such as `^4.0.0`; `version` is its lowest version. */
|
|
12
12
|
range: string | null;
|
|
13
13
|
}
|
|
14
|
+
/** The root, then each folder above it, as a monorepo package's dependencies may be installed or locked higher up. */
|
|
15
|
+
export declare function folders(root: string): string[];
|
|
14
16
|
/**
|
|
15
17
|
* The framework version from the installed package, then a lockfile, then package.json, each looked for in the root
|
|
16
18
|
* and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found. When none of
|