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-runs.js
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import picomatch from 'picomatch';
|
|
4
5
|
import { globSync } from 'tinyglobby';
|
|
5
6
|
import { mergeDeep, settingAt, unknownText, } from "./config.js";
|
|
6
7
|
import { atLeast } from "./frameworks/common.js";
|
|
8
|
+
import { nodeTestCommands } from "./node-scripts.js";
|
|
9
|
+
import { durationOf } from "./frameworks/mocha.js";
|
|
7
10
|
import { VITEST_BENCHMARK_INCLUDE, VITEST_INCLUDE, VITEST_TYPECHECK_INCLUDE, vitestExclude, } from "./vitest-defaults.js";
|
|
8
11
|
const PLAYWRIGHT_TEST_MATCH = '**/*.@(spec|test).?(c|m)[jt]s?(x)';
|
|
9
12
|
/** A file Vitest takes as a project config, by name. */
|
|
10
13
|
const VITEST_CONFIG = /^(vitest|vite)(\..*)?\.config\./;
|
|
14
|
+
/** Jest's default `testMatch` before 30.0, and from 30.0, which also matches `.mjs`, `.cjs`, `.mts` and `.cts`. */
|
|
15
|
+
const JEST_TEST_MATCH = {
|
|
16
|
+
29: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
|
|
17
|
+
30: ['**/__tests__/**/*.?([mc])[jt]s?(x)', '**/?(*.)+(spec|test).?([mc])[jt]s?(x)'],
|
|
18
|
+
};
|
|
19
|
+
/** The extensions a file a Jest `projects` glob matches needs to be read as a config. */
|
|
20
|
+
const JEST_CONFIG_EXTENSIONS = ['.js', '.ts', '.mjs', '.mts', '.cjs', '.cts', '.json'];
|
|
21
|
+
/** Jest's default `moduleFileExtensions` before 30.0; 30.0 adds `mts` and `cts`. */
|
|
22
|
+
const JEST_EXTENSIONS = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'];
|
|
23
|
+
/** Presets that leave Jest's choice of test files as it is, so the scan needs nothing from them. */
|
|
24
|
+
const NEUTRAL_PRESETS = /^(ts-jest(\/.*)?|jest-puppeteer)$/;
|
|
11
25
|
/** Vitest 3's workspace files, which list its projects in place of `test.projects`. */
|
|
12
26
|
const VITEST_WORKSPACES = ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json'].map((extension) => `vitest.workspace.${extension}`);
|
|
13
27
|
const NO_DEFAULTS = {
|
|
@@ -101,6 +115,11 @@ export class ConfigRuns {
|
|
|
101
115
|
projects = new Map();
|
|
102
116
|
ignoreRules = new Map();
|
|
103
117
|
folderStates = new Map();
|
|
118
|
+
/** The project Jest runs with when no config is found up to the root. */
|
|
119
|
+
jestDefaults = null;
|
|
120
|
+
mochaDefaults = null;
|
|
121
|
+
nodeRuns = null;
|
|
122
|
+
bunFiles = null;
|
|
104
123
|
/** The Vitest configs whose projects are being found, with their names, to stop at one whose projects name itself. */
|
|
105
124
|
expanding = new Map();
|
|
106
125
|
constructor(configs, framework, version, findsFiles) {
|
|
@@ -119,7 +138,21 @@ export class ConfigRuns {
|
|
|
119
138
|
return { ...placement, tags: this.configTags(placement.config) };
|
|
120
139
|
}
|
|
121
140
|
place(file) {
|
|
122
|
-
|
|
141
|
+
if (this.framework === 'mocha')
|
|
142
|
+
return this.placeMocha(file);
|
|
143
|
+
if (this.framework === 'jasmine')
|
|
144
|
+
return this.placeJasmine(file);
|
|
145
|
+
if (this.framework === 'cypress')
|
|
146
|
+
return this.placeCypress(file);
|
|
147
|
+
if (this.framework === 'webdriverio')
|
|
148
|
+
return this.placeWebdriverIO(file);
|
|
149
|
+
if (this.framework === 'node')
|
|
150
|
+
return this.placeNode(file);
|
|
151
|
+
if (this.framework === 'bun')
|
|
152
|
+
return this.placeBun(file);
|
|
153
|
+
// Jest started at the root reads the config there only, and the others through its `projects`.
|
|
154
|
+
const rootConfig = this.framework === 'jest' ? this.configs.inFolder(this.configs.root) : null;
|
|
155
|
+
const around = this.framework !== 'jest' ? this.configs.around(file) : rootConfig ? [rootConfig] : [];
|
|
123
156
|
for (const config of around) {
|
|
124
157
|
const running = this.projectsOf(config).flatMap((project) => {
|
|
125
158
|
const runs = project.runs?.(file);
|
|
@@ -137,6 +170,12 @@ export class ConfigRuns {
|
|
|
137
170
|
return { config: unknown.config, settings: unknown.settings, runs: null, projects: null };
|
|
138
171
|
}
|
|
139
172
|
const nearest = around.at(-1) ?? null;
|
|
173
|
+
if (nearest === null && this.framework === 'jest') {
|
|
174
|
+
// Jest then reads a package.json above the root, taken to have no `jest` key: its defaults apply from the root.
|
|
175
|
+
this.jestDefaults ??= this.jestProject(this.noConfig(), emptySettings(), this.configs.root, emptySettings());
|
|
176
|
+
const runs = this.jestDefaults.runs;
|
|
177
|
+
return { config: null, settings: null, runs: runs(file), projects: null };
|
|
178
|
+
}
|
|
140
179
|
return {
|
|
141
180
|
config: nearest,
|
|
142
181
|
settings: nearest?.settings ?? null,
|
|
@@ -186,17 +225,498 @@ export class ConfigRuns {
|
|
|
186
225
|
const project = this.projectsOf(config).find((item) => item.runs && item.about.name === name);
|
|
187
226
|
return project ? { about: project.about, runs: project.runs } : null;
|
|
188
227
|
}
|
|
228
|
+
/** Mocha runs one set of files, from the config it starts with, or its defaults without one. */
|
|
229
|
+
placeMocha(file) {
|
|
230
|
+
const config = this.configs.mochaConfig();
|
|
231
|
+
const project = config
|
|
232
|
+
? this.projectsOf(config)[0]
|
|
233
|
+
: (this.mochaDefaults ??= this.mochaProject(this.noConfig(), emptySettings()));
|
|
234
|
+
if (project.unknown) {
|
|
235
|
+
this.configs.reportUnknown(project.unknown.setting, project.unknown.key, this.findsFiles ? NOT_FOUND : ASSUMED);
|
|
236
|
+
return { config, settings: project.settings, runs: null, projects: null };
|
|
237
|
+
}
|
|
238
|
+
const runs = project.runs(file);
|
|
239
|
+
if (!config)
|
|
240
|
+
return { config: null, settings: null, runs, projects: null };
|
|
241
|
+
return { config, settings: project.settings, runs, projects: runs ? [project.about] : [] };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The files Mocha runs, as it looks them up from the root, the folder it starts in, wherever its config is: each
|
|
245
|
+
* `spec` entry a glob, a file, a file without its extension, or a folder whose files with one of the `extension`s it
|
|
246
|
+
* takes, also in folders below with `recursive`, less those `ignore` matches; and the `file` entries, loaded first.
|
|
247
|
+
* With globs, every file they match, less `ignore`. A timeout may be written as `'2s'`.
|
|
248
|
+
*/
|
|
249
|
+
mochaProject(config, settings) {
|
|
250
|
+
const folder = this.configs.root;
|
|
251
|
+
const lists = {};
|
|
252
|
+
for (const [key, fallback] of [
|
|
253
|
+
['spec', ['./test']],
|
|
254
|
+
['extension', ['js', 'cjs', 'mjs']],
|
|
255
|
+
['ignore', []],
|
|
256
|
+
['file', []],
|
|
257
|
+
]) {
|
|
258
|
+
const setting = settingAt(settings, [key]) ?? (key === 'ignore' ? settingAt(settings, ['exclude']) : null);
|
|
259
|
+
const found = setting === null || isUnset(setting) ? [...fallback] : patterns(setting, false);
|
|
260
|
+
if (!Array.isArray(found))
|
|
261
|
+
return this.unknownProject(config, found, key);
|
|
262
|
+
lists[key] = found.map((item) => (key === 'extension' ? item.replace(/^\./, '') : item));
|
|
263
|
+
}
|
|
264
|
+
const recursiveSetting = settingAt(settings, ['recursive']);
|
|
265
|
+
if (recursiveSetting?.kind === 'unknown')
|
|
266
|
+
return this.unknownProject(config, recursiveSetting, 'recursive');
|
|
267
|
+
const recursive = recursiveSetting?.kind === 'value' && recursiveSetting.value === true;
|
|
268
|
+
const extensions = lists.extension;
|
|
269
|
+
// Mocha matches `ignore` against the path as its lookup finds it, without `./`, so a pattern starting with `./`
|
|
270
|
+
// matches nothing; picomatch would drop the `./` and match.
|
|
271
|
+
const ignored = lists.ignore.map((glob) => (glob.startsWith('./') ? () => false : picomatch(posix(glob))));
|
|
272
|
+
const specs = lists.spec.map((spec) => mochaSpec(folder, spec, extensions, recursive));
|
|
273
|
+
// A `file` entry is loaded as written, before the specs, and `ignore` doesn't apply to it.
|
|
274
|
+
const loaded = new Set(lists.file.map((entry) => path.resolve(folder, entry)));
|
|
275
|
+
const duration = (key) => {
|
|
276
|
+
const setting = settingAt(settings, [key]);
|
|
277
|
+
const value = setting?.kind === 'value' ? durationOf(setting.value, key) : null;
|
|
278
|
+
return value === null ? setting : { kind: 'value', value };
|
|
279
|
+
};
|
|
280
|
+
const about = {
|
|
281
|
+
name: '',
|
|
282
|
+
testDir: folder,
|
|
283
|
+
dependencies: [],
|
|
284
|
+
settings,
|
|
285
|
+
root: folder,
|
|
286
|
+
container: null,
|
|
287
|
+
config,
|
|
288
|
+
grep: null,
|
|
289
|
+
grepRoot: folder,
|
|
290
|
+
runsByDefault: true,
|
|
291
|
+
defaults: { ...NO_DEFAULTS, timeout: duration('timeout'), retry: duration('retries') },
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
config,
|
|
295
|
+
settings,
|
|
296
|
+
about,
|
|
297
|
+
unknown: null,
|
|
298
|
+
runs: (file) => {
|
|
299
|
+
const relative = posix(path.relative(folder, file));
|
|
300
|
+
if (loaded.has(file))
|
|
301
|
+
return 'test';
|
|
302
|
+
// A project's scripts often give Mocha the spec on its command line, which no config shows, so with globs the
|
|
303
|
+
// scan takes each file they match as run, less `ignore`.
|
|
304
|
+
if (this.findsFiles && !specs.some((spec) => spec(file)))
|
|
305
|
+
return false;
|
|
306
|
+
return ignored.some((isMatch) => isMatch(relative)) ? false : 'test';
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Jasmine runs one set of files, from its config. Without one it runs none, unless it is given files: Karma and
|
|
312
|
+
* Angular projects give it theirs from their own configs, which the scan doesn't read, so their globs decide.
|
|
313
|
+
*/
|
|
314
|
+
placeJasmine(file) {
|
|
315
|
+
const config = this.configs.jasmineConfig();
|
|
316
|
+
if (!config)
|
|
317
|
+
return { config: null, settings: null, runs: this.findsFiles ? false : 'test', projects: null };
|
|
318
|
+
const [project] = this.projectsOf(config);
|
|
319
|
+
if (project.unknown) {
|
|
320
|
+
this.configs.reportUnknown(project.unknown.setting, project.unknown.key, this.findsFiles ? NOT_FOUND : ASSUMED);
|
|
321
|
+
return { config, settings: project.settings, runs: null, projects: null };
|
|
322
|
+
}
|
|
323
|
+
const runs = project.runs(file);
|
|
324
|
+
return { config, settings: project.settings, runs, projects: runs ? [project.about] : [] };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The files Jasmine runs: those under `spec_dir`, from the root, that a `spec_files` glob matches, less those a glob
|
|
328
|
+
* starting with `!` matches. With globs, every file they match, as files given to Jasmine replace `spec_files`.
|
|
329
|
+
*/
|
|
330
|
+
jasmineProject(config) {
|
|
331
|
+
const folder = this.configs.root;
|
|
332
|
+
const settings = config.settings;
|
|
333
|
+
const dirSetting = settingAt(settings, ['spec_dir']);
|
|
334
|
+
if (dirSetting?.kind === 'unknown')
|
|
335
|
+
return this.unknownProject(config, dirSetting, 'spec_dir');
|
|
336
|
+
const specDir = dirSetting?.kind === 'value' && typeof dirSetting.value === 'string' ? dirSetting.value : '';
|
|
337
|
+
const filesSetting = settingAt(settings, ['spec_files']);
|
|
338
|
+
const found = filesSetting === null || isUnset(filesSetting) ? [] : patterns(filesSetting, false);
|
|
339
|
+
if (!Array.isArray(found))
|
|
340
|
+
return this.unknownProject(config, found, 'spec_files');
|
|
341
|
+
const globs = found.map((glob) => ({ exclude: glob.startsWith('!'), glob: glob.replace(/^!/, '') }));
|
|
342
|
+
const matcher = (exclude) => {
|
|
343
|
+
const list = globs.filter((item) => item.exclude === exclude).map((item) => posix(path.join(specDir, item.glob)));
|
|
344
|
+
return list.length > 0 ? picomatch(list) : () => false;
|
|
345
|
+
};
|
|
346
|
+
const [included, excluded] = [matcher(false), matcher(true)];
|
|
347
|
+
const about = {
|
|
348
|
+
name: '',
|
|
349
|
+
testDir: path.resolve(folder, specDir),
|
|
350
|
+
dependencies: [],
|
|
351
|
+
settings,
|
|
352
|
+
root: folder,
|
|
353
|
+
container: null,
|
|
354
|
+
config,
|
|
355
|
+
grep: null,
|
|
356
|
+
grepRoot: folder,
|
|
357
|
+
runsByDefault: true,
|
|
358
|
+
defaults: NO_DEFAULTS,
|
|
359
|
+
};
|
|
360
|
+
return {
|
|
361
|
+
config,
|
|
362
|
+
settings,
|
|
363
|
+
about,
|
|
364
|
+
unknown: null,
|
|
365
|
+
runs: (file) => {
|
|
366
|
+
// Files given to Jasmine replace `spec_files`, its `!` globs included.
|
|
367
|
+
if (!this.findsFiles)
|
|
368
|
+
return 'test';
|
|
369
|
+
const relative = posix(path.relative(folder, file));
|
|
370
|
+
return included(relative) && !excluded(relative) ? 'test' : false;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Cypress runs a file in its e2e project, or in its component project when the config sets up component testing.
|
|
376
|
+
* Without a config it runs nothing, unless it is given files.
|
|
377
|
+
*/
|
|
378
|
+
placeCypress(file) {
|
|
379
|
+
const config = this.configs.rootConfig();
|
|
380
|
+
if (!config)
|
|
381
|
+
return { config: null, settings: null, runs: this.findsFiles ? false : 'test', projects: null };
|
|
382
|
+
const projects = this.projectsOf(config);
|
|
383
|
+
const unknown = projects.find((project) => project.unknown);
|
|
384
|
+
if (unknown?.unknown) {
|
|
385
|
+
this.configs.reportUnknown(unknown.unknown.setting, unknown.unknown.key, this.findsFiles ? NOT_FOUND : ASSUMED);
|
|
386
|
+
return { config, settings: unknown.settings, runs: null, projects: null };
|
|
387
|
+
}
|
|
388
|
+
// Cypress runs a file given to it only when its testing type's `specPattern` takes the file too.
|
|
389
|
+
const running = projects.filter((project) => project.runs?.(file));
|
|
390
|
+
return {
|
|
391
|
+
config,
|
|
392
|
+
settings: config.settings,
|
|
393
|
+
runs: running.length > 0 ? 'test' : false,
|
|
394
|
+
projects: running.map((project) => project.about),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Cypress's testing types as projects: `e2e`, and `component` when the config has it. Each runs the files under the
|
|
399
|
+
* config's folder that its `specPattern` matches, dot files left out, without those its `excludeSpecPattern` matches,
|
|
400
|
+
* dot files included, and those in `node_modules` unless a `specPattern` names it; component testing also leaves out
|
|
401
|
+
* the files e2e's `specPattern` matches. `cypress run` matches each pattern against the whole path, as globby does.
|
|
402
|
+
*/
|
|
403
|
+
cypressProjects(config) {
|
|
404
|
+
const folder = path.dirname(config.file);
|
|
405
|
+
const types = [
|
|
406
|
+
['e2e', ['cypress/e2e/**/*.cy.{js,jsx,ts,tsx}'], ['*.hot-update.js']],
|
|
407
|
+
];
|
|
408
|
+
if (settingAt(config.settings, ['component']) !== null) {
|
|
409
|
+
types.push(['component', ['**/*.cy.{js,jsx,ts,tsx}'], ['**/__snapshots__/*', '**/__image_snapshots__/*']]);
|
|
410
|
+
}
|
|
411
|
+
// Cypress takes a pattern written from the config's folder as one relative to it.
|
|
412
|
+
const relative = (glob) => posix(glob.startsWith(folder + path.sep) ? path.relative(folder, glob) : glob);
|
|
413
|
+
const projects = [];
|
|
414
|
+
let e2eSpecs = [];
|
|
415
|
+
for (const [type, defaultSpecs, defaultExcludes] of types) {
|
|
416
|
+
const lists = [];
|
|
417
|
+
for (const [key, fallback] of [
|
|
418
|
+
['specPattern', defaultSpecs],
|
|
419
|
+
['excludeSpecPattern', defaultExcludes],
|
|
420
|
+
]) {
|
|
421
|
+
const setting = settingAt(config.settings, [type, key]);
|
|
422
|
+
const found = setting === null || isUnset(setting) ? fallback : patterns(setting, false);
|
|
423
|
+
if (!Array.isArray(found))
|
|
424
|
+
return [this.unknownProject(config, found, `${type}.${key}`)];
|
|
425
|
+
lists.push(found);
|
|
426
|
+
}
|
|
427
|
+
const [specs, excludes] = lists;
|
|
428
|
+
if (type === 'e2e')
|
|
429
|
+
e2eSpecs = specs;
|
|
430
|
+
const isSpec = picomatch(specs.map(relative));
|
|
431
|
+
const excluded = [...excludes, ...(type === 'component' ? e2eSpecs : [])].map(relative);
|
|
432
|
+
if (!specs.some((glob) => glob.includes('node_modules')))
|
|
433
|
+
excluded.push('**/node_modules/**');
|
|
434
|
+
const isExcluded = picomatch(excluded, { dot: true });
|
|
435
|
+
const retry = settingAt(config.settings, [type, 'retries']) ?? settingAt(config.settings, ['retries']);
|
|
436
|
+
const written = retry?.kind === 'object' ? settingAt(retry, ['runMode']) : retry;
|
|
437
|
+
// Cypress takes `null` as no retries set.
|
|
438
|
+
const runMode = written?.kind === 'value' && written.value === null ? null : written;
|
|
439
|
+
const about = {
|
|
440
|
+
name: type,
|
|
441
|
+
testDir: folder,
|
|
442
|
+
dependencies: [],
|
|
443
|
+
settings: config.settings,
|
|
444
|
+
root: folder,
|
|
445
|
+
container: null,
|
|
446
|
+
config,
|
|
447
|
+
grep: null,
|
|
448
|
+
grepRoot: folder,
|
|
449
|
+
runsByDefault: true,
|
|
450
|
+
defaults: { ...NO_DEFAULTS, retry: runMode },
|
|
451
|
+
};
|
|
452
|
+
projects.push({
|
|
453
|
+
config,
|
|
454
|
+
settings: config.settings,
|
|
455
|
+
about,
|
|
456
|
+
unknown: null,
|
|
457
|
+
runs: (file) => {
|
|
458
|
+
const from = posix(path.relative(folder, file));
|
|
459
|
+
return isSpec(from) && !isExcluded(from) ? 'test' : false;
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return projects;
|
|
464
|
+
}
|
|
465
|
+
/** `bun test` runs the files of `bunFilesOf`, with the settings of the `bunfig.toml` in the root, when there is one. */
|
|
466
|
+
placeBun(file) {
|
|
467
|
+
const config = this.configs.rootConfig();
|
|
468
|
+
if (!this.bunFiles) {
|
|
469
|
+
this.bunFiles = this.bunFilesOf(config);
|
|
470
|
+
if (this.bunFiles.refused)
|
|
471
|
+
this.configs.reportConfig(config, this.bunFiles.refused);
|
|
472
|
+
}
|
|
473
|
+
const found = this.bunFiles;
|
|
474
|
+
const runs = found.refused === null && (!this.findsFiles || found.runs(file)) ? 'test' : false;
|
|
475
|
+
if (!config)
|
|
476
|
+
return { config: null, settings: null, runs, projects: null };
|
|
477
|
+
const projects = runs ? [found.isConcurrent(file) ? found.concurrent : found.about] : [];
|
|
478
|
+
return { config, settings: config.settings, runs, projects };
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* The files `bun test` runs: those under `[test] root`, the root by default, whose name without its extension ends
|
|
482
|
+
* with `.test`, `_test`, `.spec` or `_spec`, with a JavaScript or TypeScript extension, outside dot folders and
|
|
483
|
+
* `node_modules`, and from Bun 1.3.11 outside the folders and files a `pathIgnorePatterns` glob matches. `[test] retry`
|
|
484
|
+
* applies to every test, and the tests of the files a `concurrentTestGlob` glob matches run at once.
|
|
485
|
+
*/
|
|
486
|
+
bunFilesOf(config) {
|
|
487
|
+
const folder = this.configs.root;
|
|
488
|
+
const settings = config?.settings ?? emptySettings();
|
|
489
|
+
// Bun reads a string or a list of them, and ignores a `root` of another type.
|
|
490
|
+
const read = (key) => {
|
|
491
|
+
const setting = settingAt(settings, ['test', key]);
|
|
492
|
+
if (setting === null)
|
|
493
|
+
return [];
|
|
494
|
+
const found = patterns(setting, false);
|
|
495
|
+
return Array.isArray(found) ? found : null;
|
|
496
|
+
};
|
|
497
|
+
const roots = read('root') ?? [];
|
|
498
|
+
// Bun 1.2 reads neither `pathIgnorePatterns` nor `retry`. It reads the first from 1.3.11, and the version holds no
|
|
499
|
+
// patch to tell 1.3.10 from it, so all of 1.3 reads it.
|
|
500
|
+
const readsFrom13 = atLeast(this.version, 1, 3);
|
|
501
|
+
const ignored = readsFrom13 ? read('pathIgnorePatterns') : [];
|
|
502
|
+
const concurrent = read('concurrentTestGlob');
|
|
503
|
+
const wrong = ignored === null ? 'pathIgnorePatterns' : concurrent === null ? 'concurrentTestGlob' : null;
|
|
504
|
+
const refused = wrong &&
|
|
505
|
+
`The config's test.${wrong} is not a string or a list of strings, so Bun refuses the config and runs no test.`;
|
|
506
|
+
const testRoot = path.resolve(folder, roots[0] ?? '.');
|
|
507
|
+
const isIgnored = picomatch(ignored ?? []);
|
|
508
|
+
const isConcurrent = picomatch(concurrent ?? []);
|
|
509
|
+
const about = (parallel) => ({
|
|
510
|
+
name: '',
|
|
511
|
+
testDir: testRoot,
|
|
512
|
+
dependencies: [],
|
|
513
|
+
settings,
|
|
514
|
+
root: folder,
|
|
515
|
+
container: null,
|
|
516
|
+
config: config,
|
|
517
|
+
grep: null,
|
|
518
|
+
grepRoot: folder,
|
|
519
|
+
runsByDefault: true,
|
|
520
|
+
defaults: {
|
|
521
|
+
...NO_DEFAULTS,
|
|
522
|
+
retry: readsFrom13 ? settingAt(settings, ['test', 'retry']) : null,
|
|
523
|
+
parallel: parallel ? { kind: 'value', value: true } : null,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
refused,
|
|
528
|
+
about: about(false),
|
|
529
|
+
concurrent: about(true),
|
|
530
|
+
// Bun 1.4.4 matches the glob against the file's whole path.
|
|
531
|
+
isConcurrent: (file) => isConcurrent(posix(file)),
|
|
532
|
+
runs: (file) => {
|
|
533
|
+
if (!isInside(testRoot, file))
|
|
534
|
+
return false;
|
|
535
|
+
const relative = posix(path.relative(folder, file));
|
|
536
|
+
const parts = relative.split('/');
|
|
537
|
+
if (parts.some((part) => part.startsWith('.') || part === 'node_modules'))
|
|
538
|
+
return false;
|
|
539
|
+
if (parts.some((_, index) => isIgnored(parts.slice(0, index + 1).join('/'))))
|
|
540
|
+
return false;
|
|
541
|
+
return /(\.test|_test|\.spec|_spec)\.(js|jsx|ts|tsx|mjs|cjs|mts|cts)$/i.test(relative);
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/** `node --test` runs the files its arguments name, or without any, those its default patterns match; see `nodeFiles`. */
|
|
546
|
+
placeNode(file) {
|
|
547
|
+
if (!this.findsFiles)
|
|
548
|
+
return { config: null, settings: null, runs: 'test', projects: null };
|
|
549
|
+
this.nodeRuns ??= this.nodeFiles();
|
|
550
|
+
const runs = this.nodeRuns(posix(path.relative(this.configs.root, file)));
|
|
551
|
+
return { config: null, settings: null, runs: runs ? 'test' : false, projects: null };
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The files `node --test` runs from the root, outside dot folders and `node_modules`: those the arguments of the
|
|
555
|
+
* package.json script that runs it give, the `test` script first, each a glob or a path, and from Node.js 26.8 a folder
|
|
556
|
+
* for the default patterns inside it; without arguments, the default patterns. They take TypeScript files too from
|
|
557
|
+
* Node.js 22.18, which strips types by default.
|
|
558
|
+
*/
|
|
559
|
+
nodeFiles() {
|
|
560
|
+
const extensions = stripsTypes(this.version) ? 'cjs,mjs,js,cts,mts,ts' : 'cjs,mjs,js';
|
|
561
|
+
const defaults = ['**/*.test', '**/*-test', '**/*_test', '**/test-*', '**/test', '**/test/**/*'].map((glob) => `${glob}.{${extensions}}`);
|
|
562
|
+
const searchesFolders = atLeast(this.version, 26, 8);
|
|
563
|
+
const commands = nodeTestArguments(path.join(this.configs.root, 'package.json'));
|
|
564
|
+
// Each command runs the files it gives, or without any, those the default patterns match.
|
|
565
|
+
const globs = (commands.length > 0 ? commands : [[]]).flatMap((files) => files.length === 0
|
|
566
|
+
? defaults
|
|
567
|
+
: files.flatMap((argument) => {
|
|
568
|
+
const folder = path.resolve(this.configs.root, argument);
|
|
569
|
+
if (!searchesFolders || !existsSync(folder) || !statSync(folder).isDirectory())
|
|
570
|
+
return [argument];
|
|
571
|
+
return defaults.map((glob) => `${argument.replace(/\/+$/, '')}/${glob}`);
|
|
572
|
+
}));
|
|
573
|
+
const isMatch = picomatch(globs.map((glob) => path.posix.normalize(posix(glob))));
|
|
574
|
+
return (relative) => isMatch(relative) && !relative.split('/').includes('node_modules');
|
|
575
|
+
}
|
|
576
|
+
/** WebdriverIO runs the files of the config in the root; without one, it runs nothing, unless it is given files. */
|
|
577
|
+
placeWebdriverIO(file) {
|
|
578
|
+
const config = this.configs.rootConfig();
|
|
579
|
+
if (!config)
|
|
580
|
+
return { config: null, settings: null, runs: this.findsFiles ? false : 'test', projects: null };
|
|
581
|
+
const [project] = this.projectsOf(config);
|
|
582
|
+
if (project.unknown) {
|
|
583
|
+
this.configs.reportUnknown(project.unknown.setting, project.unknown.key, this.findsFiles ? NOT_FOUND : ASSUMED);
|
|
584
|
+
return { config, settings: project.settings, runs: null, projects: null };
|
|
585
|
+
}
|
|
586
|
+
const runs = project.runs(file);
|
|
587
|
+
return { config, settings: project.settings, runs, projects: runs ? [project.about] : [] };
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* The files `wdio` runs: those under `rootDir`, the config's folder by default, that a `specs` glob matches, also in
|
|
591
|
+
* the nested lists that group files into one worker, less those `exclude` leaves out. A capability's own `specs` replace the config's for it, and its `exclude` adds to the config's. Globs leave out
|
|
592
|
+
* dot files and folders, and one without a `/` matches a file's name at any depth. When every `exclude` entry holds a
|
|
593
|
+
* `/`, `\` or `*`, they are globs too; otherwise each one leaves out every file whose URL contains it. Files given
|
|
594
|
+
* with `--spec` replace `specs`, so scanned with globs, every file they match counts as run, less the excludes. The
|
|
595
|
+
* config's `mochaOpts.timeout` and `retries`, or `jasmineOpts.defaultTimeoutInterval`, apply to every test.
|
|
596
|
+
*/
|
|
597
|
+
webdriverioProject(config) {
|
|
598
|
+
const { settings } = config;
|
|
599
|
+
const writtenRoot = settingAt(settings, ['rootDir']);
|
|
600
|
+
if (writtenRoot !== null &&
|
|
601
|
+
!isUnset(writtenRoot) &&
|
|
602
|
+
!(writtenRoot.kind === 'value' && typeof writtenRoot.value === 'string')) {
|
|
603
|
+
return this.unknownProject(config, unknownAt(writtenRoot), 'rootDir');
|
|
604
|
+
}
|
|
605
|
+
// The config's `rootDir`, from the folder `wdio` starts in, replaces the config's folder.
|
|
606
|
+
const folder = writtenRoot?.kind === 'value' && typeof writtenRoot.value === 'string'
|
|
607
|
+
? path.resolve(this.configs.root, writtenRoot.value)
|
|
608
|
+
: path.dirname(config.file);
|
|
609
|
+
const globs = (owner, keys) => {
|
|
610
|
+
const setting = keys.map((key) => settingAt(owner, [key])).find((item) => item !== null && !isUnset(item));
|
|
611
|
+
if (!setting)
|
|
612
|
+
return null;
|
|
613
|
+
// A single glob is taken as a list of one; a nested list groups files into one worker.
|
|
614
|
+
const items = setting.kind === 'list'
|
|
615
|
+
? setting.items.flatMap((item) => (item.kind === 'list' ? item.items : [item]))
|
|
616
|
+
: [setting];
|
|
617
|
+
const found = patterns({ kind: 'list', items }, false);
|
|
618
|
+
return Array.isArray(found) ? found : found;
|
|
619
|
+
};
|
|
620
|
+
// WebdriverIO globs each pattern with `/` for `\`, from `rootDir`.
|
|
621
|
+
const fromRoot = (glob) => {
|
|
622
|
+
const slashed = glob.replaceAll('\\', '/');
|
|
623
|
+
return path.posix.normalize(path.isAbsolute(slashed) ? posix(path.relative(folder, slashed)) : slashed);
|
|
624
|
+
};
|
|
625
|
+
// As glob's matchBase does, a pattern without a slash matches at any depth, outside dot folders too.
|
|
626
|
+
const matcher = (list) => picomatch(list.map(fromRoot).map((glob) => (glob.includes('/') ? glob : `**/${glob}`)));
|
|
627
|
+
const specs = globs(settings, ['specs']) ?? [];
|
|
628
|
+
if (!Array.isArray(specs))
|
|
629
|
+
return this.unknownProject(config, specs, 'specs');
|
|
630
|
+
const written = globs(settings, ['exclude']) ?? [];
|
|
631
|
+
if (!Array.isArray(written))
|
|
632
|
+
return this.unknownProject(config, written, 'exclude');
|
|
633
|
+
const excludes = written.filter((item) => !isSuiteName(settings, item));
|
|
634
|
+
const isGlob = excludes.every((item) => /[/\\*]/.test(item));
|
|
635
|
+
const isPathExcluded = matcher(excludes);
|
|
636
|
+
const isExcluded = (file, relative) => isGlob ? isPathExcluded(relative) : excludes.some((item) => pathToFileURL(file).href.includes(item));
|
|
637
|
+
// Each capability may set its own specs, which replace the config's, and excludes, added to the config's.
|
|
638
|
+
const capabilities = settingAt(settings, ['capabilities']);
|
|
639
|
+
const runs = [];
|
|
640
|
+
for (const capability of capabilities?.kind === 'list' && capabilities.items.length > 0
|
|
641
|
+
? capabilities.items
|
|
642
|
+
: [emptySettings()]) {
|
|
643
|
+
const own = globs(capability, ['specs', 'wdio:specs']);
|
|
644
|
+
if (own !== null && !Array.isArray(own))
|
|
645
|
+
return this.unknownProject(config, own, 'capabilities.specs');
|
|
646
|
+
const exclude = globs(capability, ['exclude', 'wdio:exclude']) ?? [];
|
|
647
|
+
if (!Array.isArray(exclude))
|
|
648
|
+
return this.unknownProject(config, exclude, 'capabilities.exclude');
|
|
649
|
+
runs.push({ isSpec: matcher(own ?? specs), isExcluded: matcher(exclude) });
|
|
650
|
+
}
|
|
651
|
+
const framework = settingAt(settings, ['framework']);
|
|
652
|
+
const isJasmine = framework?.kind === 'value' && framework.value === 'jasmine';
|
|
653
|
+
const options = settingAt(settings, [isJasmine ? 'jasmineOpts' : 'mochaOpts']) ?? emptySettings();
|
|
654
|
+
const duration = (key) => {
|
|
655
|
+
const setting = settingAt(options, [key]);
|
|
656
|
+
const value = setting?.kind === 'value' ? durationOf(setting.value, key) : null;
|
|
657
|
+
return value === null ? setting : { kind: 'value', value };
|
|
658
|
+
};
|
|
659
|
+
// A `defaultTimeoutInterval` of 0 replaces WebdriverIO's default, and its Jasmine adapter then takes 60 seconds.
|
|
660
|
+
const interval = settingAt(options, ['defaultTimeoutInterval']);
|
|
661
|
+
const jasmineTimeout = interval?.kind === 'value' && interval.value === 0 ? { kind: 'value', value: 60_000 } : interval;
|
|
662
|
+
const about = {
|
|
663
|
+
name: '',
|
|
664
|
+
testDir: folder,
|
|
665
|
+
dependencies: [],
|
|
666
|
+
settings,
|
|
667
|
+
root: folder,
|
|
668
|
+
container: null,
|
|
669
|
+
config,
|
|
670
|
+
grep: null,
|
|
671
|
+
grepRoot: folder,
|
|
672
|
+
runsByDefault: true,
|
|
673
|
+
defaults: isJasmine
|
|
674
|
+
? { ...NO_DEFAULTS, timeout: jasmineTimeout }
|
|
675
|
+
: { ...NO_DEFAULTS, timeout: duration('timeout'), retry: duration('retries') },
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
config,
|
|
679
|
+
settings,
|
|
680
|
+
about,
|
|
681
|
+
unknown: null,
|
|
682
|
+
runs: (file) => {
|
|
683
|
+
const relative = posix(path.relative(folder, file));
|
|
684
|
+
if (isExcluded(file, relative))
|
|
685
|
+
return false;
|
|
686
|
+
// Files given with `--spec` replace every capability's specs.
|
|
687
|
+
const running = runs.some((run) => (!this.findsFiles || run.isSpec(relative)) && !run.isExcluded(relative));
|
|
688
|
+
return running ? 'test' : false;
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
189
692
|
projectsOf(config) {
|
|
190
693
|
let projects = this.projects.get(config);
|
|
191
694
|
if (!projects) {
|
|
192
|
-
|
|
695
|
+
if (this.framework === 'playwright')
|
|
696
|
+
projects = this.playwrightProjects(config);
|
|
697
|
+
else if (this.framework === 'jest')
|
|
698
|
+
projects = this.jestProjects(config);
|
|
699
|
+
else if (this.framework === 'mocha')
|
|
700
|
+
projects = [this.mochaProject(config, config.settings)];
|
|
701
|
+
else if (this.framework === 'jasmine')
|
|
702
|
+
projects = [this.jasmineProject(config)];
|
|
703
|
+
else if (this.framework === 'cypress')
|
|
704
|
+
projects = this.cypressProjects(config);
|
|
705
|
+
else if (this.framework === 'webdriverio')
|
|
706
|
+
projects = [this.webdriverioProject(config)];
|
|
707
|
+
else
|
|
708
|
+
projects = this.vitestProjects(config);
|
|
193
709
|
this.projects.set(config, projects);
|
|
194
710
|
}
|
|
195
711
|
return projects;
|
|
196
712
|
}
|
|
197
713
|
/** Vitest's `retry`: before 4.1, which reads only a count, an object is one the scan can't work out. */
|
|
198
|
-
vitestRetry(setting) {
|
|
199
|
-
|
|
714
|
+
vitestRetry(config, setting) {
|
|
715
|
+
if (setting?.kind !== 'object' || atLeast(this.version, 4, 1))
|
|
716
|
+
return setting;
|
|
717
|
+
const message = "The config's test.retry is an object, which Vitest reads from 4.1; before, it runs no test.";
|
|
718
|
+
this.configs.reportSetting(setting, config, 'test.retry', message);
|
|
719
|
+
return unknownAt(setting);
|
|
200
720
|
}
|
|
201
721
|
/** A project whose files can't be worked out, because of `setting` under `key`: it may run any file. */
|
|
202
722
|
unknownProject(config, setting, key) {
|
|
@@ -429,10 +949,196 @@ export class ConfigRuns {
|
|
|
429
949
|
}
|
|
430
950
|
return rules;
|
|
431
951
|
}
|
|
952
|
+
/** A config for the defaults of a runner started where no config is found. */
|
|
953
|
+
noConfig() {
|
|
954
|
+
const file = path.join(this.configs.root, 'package.json');
|
|
955
|
+
return { file, record: { relativeFilePath: 'package.json', extends: [] }, settings: emptySettings() };
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Jest's projects: the config itself, or the entries of its `projects`. A path or glob there names a folder, whose
|
|
959
|
+
* config is the one Jest finds from it, or a config file; an object is a project whose `rootDir` defaults to the
|
|
960
|
+
* config's folder. The config's own test files then count only when an entry names its folder.
|
|
961
|
+
*/
|
|
962
|
+
jestProjects(config) {
|
|
963
|
+
const folder = path.dirname(config.file);
|
|
964
|
+
const list = settingAt(config.settings, ['projects']);
|
|
965
|
+
const isEmpty = list?.kind === 'list' && list.items.length === 0;
|
|
966
|
+
if (list === null || isUnset(list) || isEmpty) {
|
|
967
|
+
return [this.jestProject(config, config.settings, folder, config.settings)];
|
|
968
|
+
}
|
|
969
|
+
if (list.kind !== 'list')
|
|
970
|
+
return [this.unknownProject(config, unknownAt(list), 'projects')];
|
|
971
|
+
const rootDir = jestRootDir(config.settings, folder);
|
|
972
|
+
if (typeof rootDir !== 'string')
|
|
973
|
+
return [this.unknownProject(config, rootDir, 'rootDir')];
|
|
974
|
+
return list.items.flatMap((item) => {
|
|
975
|
+
if (item.kind === 'object')
|
|
976
|
+
return [this.jestProject(config, item, folder, config.settings)];
|
|
977
|
+
if (item.kind !== 'value' || typeof item.value !== 'string') {
|
|
978
|
+
return [this.unknownProject(config, unknownAt(item), 'projects')];
|
|
979
|
+
}
|
|
980
|
+
const entry = replaceRootDir(rootDir, item.value);
|
|
981
|
+
// Jest globs from where it is started, with `\` as a separator on Windows, and keeps an entry that matches nothing
|
|
982
|
+
// as a path.
|
|
983
|
+
const pattern = posix(path.isAbsolute(entry) ? path.relative(this.configs.root, entry) : entry);
|
|
984
|
+
const matches = picomatch.scan(pattern).isGlob
|
|
985
|
+
? globSync(pattern, { cwd: this.configs.root, absolute: true, onlyFiles: false, expandDirectories: false })
|
|
986
|
+
: [];
|
|
987
|
+
// Jest leaves out a matched file it can't require, such as a README.
|
|
988
|
+
const requirable = matches.filter((match) => statSync(match).isDirectory() || JEST_CONFIG_EXTENSIONS.some((extension) => match.endsWith(extension)));
|
|
989
|
+
const targets = matches.length > 0 ? requirable : [path.resolve(this.configs.root, entry)];
|
|
990
|
+
// Jest fails on an entry that names nothing, so it adds no project here.
|
|
991
|
+
return targets.sort().flatMap((target) => {
|
|
992
|
+
if (!existsSync(target))
|
|
993
|
+
return [];
|
|
994
|
+
const isFile = statSync(target).isFile();
|
|
995
|
+
const nested = isFile ? this.configs.at(target) : this.configs.projectConfig(target);
|
|
996
|
+
if (!nested || nested === config)
|
|
997
|
+
return [this.jestProject(config, config.settings, folder, config.settings)];
|
|
998
|
+
return [this.jestProject(nested, nested.settings, path.dirname(nested.file), config.settings)];
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* The files a Jest project runs, as Jest's SearchSource picks them: under one of `roots`, with an extension in
|
|
1004
|
+
* `moduleFileExtensions`, not left out by `modulePathIgnorePatterns`, matching `testMatch` or `testRegex`, and not
|
|
1005
|
+
* `testPathIgnorePatterns`. Paths and patterns are matched with `/`, and `<rootDir>` stands for `rootDir`. `global`
|
|
1006
|
+
* is the config Jest is started with, whose `testTimeout` applies to every project in Jest 29.
|
|
1007
|
+
*/
|
|
1008
|
+
jestProject(config, written, folder, global) {
|
|
1009
|
+
const rootDir = jestRootDir(written, folder);
|
|
1010
|
+
if (typeof rootDir !== 'string')
|
|
1011
|
+
return this.unknownProject(config, rootDir, 'rootDir');
|
|
1012
|
+
const settings = this.withPreset(config, written, rootDir);
|
|
1013
|
+
const isJest30 = this.version === null || this.version[0] >= 30;
|
|
1014
|
+
const lists = {};
|
|
1015
|
+
const defaults = [
|
|
1016
|
+
['roots', ['<rootDir>']],
|
|
1017
|
+
['testMatch', isJest30 ? JEST_TEST_MATCH[30] : JEST_TEST_MATCH[29]],
|
|
1018
|
+
['testRegex', []],
|
|
1019
|
+
['testPathIgnorePatterns', ['/node_modules/']],
|
|
1020
|
+
['modulePathIgnorePatterns', []],
|
|
1021
|
+
['moduleFileExtensions', isJest30 ? [...JEST_EXTENSIONS, 'mts', 'cts'] : JEST_EXTENSIONS],
|
|
1022
|
+
];
|
|
1023
|
+
for (const [key, fallback] of defaults) {
|
|
1024
|
+
const setting = settingAt(settings, [key]);
|
|
1025
|
+
const found = isJestSet(setting) ? patterns(setting, key === 'testRegex') : fallback;
|
|
1026
|
+
// A list of paths left out only takes files away, so the entries the scan can read are still a good guess.
|
|
1027
|
+
if (!Array.isArray(found) && key.endsWith('IgnorePatterns')) {
|
|
1028
|
+
this.configs.reportUnknown(found, key, 'the scan may list test files that Jest leaves out');
|
|
1029
|
+
const items = setting?.kind === 'list' ? setting.items : [];
|
|
1030
|
+
lists[key] = items.flatMap((item) => item.kind === 'value' && typeof item.value === 'string' ? [item.value] : []);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (!Array.isArray(found))
|
|
1034
|
+
return this.unknownProject(config, found, key);
|
|
1035
|
+
lists[key] = found;
|
|
1036
|
+
}
|
|
1037
|
+
const testRegex = lists.testRegex.map((item) => typeof item === 'string' ? regExp(item, '') : item);
|
|
1038
|
+
const invalid = lists.testRegex.find((_, index) => testRegex[index] === null);
|
|
1039
|
+
const testMatchSetting = settingAt(settings, ['testMatch']);
|
|
1040
|
+
let refusal = null;
|
|
1041
|
+
if (invalid !== undefined) {
|
|
1042
|
+
refusal = `The config's testRegex '${String(invalid)}' is not a regular expression, so Jest fails to start.`;
|
|
1043
|
+
}
|
|
1044
|
+
else if (testRegex.length > 0 && isJestSet(testMatchSetting)) {
|
|
1045
|
+
refusal = 'The config sets both testMatch and testRegex, which Jest refuses, so it runs no tests.';
|
|
1046
|
+
}
|
|
1047
|
+
const roots = lists.roots
|
|
1048
|
+
.map((item) => replaceRootDir(rootDir, item))
|
|
1049
|
+
.map((root) => path.resolve(rootDir, root));
|
|
1050
|
+
const escaped = posix(rootDir).replaceAll(/([!()*?[\\\]{}])/g, '\\$1');
|
|
1051
|
+
const testMatch = testRegex.length > 0 ? [] : lists.testMatch.map((glob) => posix(replaceRootDir(escaped, glob)));
|
|
1052
|
+
const isTestMatch = jestGlobs(testMatch);
|
|
1053
|
+
// A pattern is a regular expression, in which Jest replaces every `<rootDir>`; escapes stay as written.
|
|
1054
|
+
const joined = (key) => {
|
|
1055
|
+
const items = lists[key].map((item) => item.replaceAll('<rootDir>', posix(rootDir)));
|
|
1056
|
+
if (items.length === 0)
|
|
1057
|
+
return null;
|
|
1058
|
+
const expression = regExp(items.join('|'), '');
|
|
1059
|
+
refusal ??= expression ? null : `The config's ${key} is not a regular expression, so Jest fails to start.`;
|
|
1060
|
+
return expression;
|
|
1061
|
+
};
|
|
1062
|
+
const ignored = joined('testPathIgnorePatterns');
|
|
1063
|
+
const moduleIgnored = joined('modulePathIgnorePatterns');
|
|
1064
|
+
if (refusal)
|
|
1065
|
+
this.configs.reportConfig(config, refusal);
|
|
1066
|
+
const extensions = new Set(lists.moduleFileExtensions);
|
|
1067
|
+
const preset = settingAt(written, ['preset']);
|
|
1068
|
+
const isPackage = preset?.kind === 'value' && typeof preset.value === 'string' && !isLocalPreset(preset.value);
|
|
1069
|
+
if (isPackage && !NEUTRAL_PRESETS.test(String(preset.value))) {
|
|
1070
|
+
const message = `The config's preset '${preset.value}' is a package the scan doesn't read, and it may set which files are tests; the scan takes Jest's defaults for what the config doesn't set.`;
|
|
1071
|
+
if (testMatchSetting === null && testRegex.length === 0)
|
|
1072
|
+
this.configs.reportConfig(config, message);
|
|
1073
|
+
}
|
|
1074
|
+
const displayName = settingAt(settings, ['displayName']);
|
|
1075
|
+
const named = displayName?.kind === 'object' ? settingAt(displayName, ['name']) : displayName;
|
|
1076
|
+
const name = named?.kind === 'value' && typeof named.value === 'string' ? named.value : '';
|
|
1077
|
+
const about = {
|
|
1078
|
+
name,
|
|
1079
|
+
testDir: rootDir,
|
|
1080
|
+
dependencies: [],
|
|
1081
|
+
settings,
|
|
1082
|
+
root: rootDir,
|
|
1083
|
+
container: null,
|
|
1084
|
+
config,
|
|
1085
|
+
grep: null,
|
|
1086
|
+
grepRoot: rootDir,
|
|
1087
|
+
runsByDefault: true,
|
|
1088
|
+
defaults: {
|
|
1089
|
+
// A project's own testTimeout counts from Jest 30; before, only the config Jest is started with sets it.
|
|
1090
|
+
timeout: (isJest30 ? settingAt(settings, ['testTimeout']) : null) ?? settingAt(global, ['testTimeout']),
|
|
1091
|
+
retry: null,
|
|
1092
|
+
repeatEach: null,
|
|
1093
|
+
repeats: null,
|
|
1094
|
+
parallel: null,
|
|
1095
|
+
tags: null,
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
config,
|
|
1100
|
+
settings,
|
|
1101
|
+
about,
|
|
1102
|
+
unknown: null,
|
|
1103
|
+
runs: (file) => {
|
|
1104
|
+
const unix = posix(file);
|
|
1105
|
+
if (refusal || !extensions.has(path.extname(file).slice(1)))
|
|
1106
|
+
return false;
|
|
1107
|
+
if (!roots.some((root) => isInside(root, file)) || moduleIgnored?.test(unix))
|
|
1108
|
+
return false;
|
|
1109
|
+
if (testMatch.length > 0 && !isTestMatch(unix))
|
|
1110
|
+
return false;
|
|
1111
|
+
if (ignored?.test(unix))
|
|
1112
|
+
return false;
|
|
1113
|
+
return testRegex.length === 0 || testRegex.some((expression) => matches(expression, unix))
|
|
1114
|
+
? 'test'
|
|
1115
|
+
: false;
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
432
1119
|
/**
|
|
433
1120
|
* The projects of a Vitest config, the one the runner starts with or, in Vitest 5, a config another lists, whose name
|
|
434
1121
|
* then goes around the names of its projects.
|
|
435
1122
|
*/
|
|
1123
|
+
/**
|
|
1124
|
+
* A project's settings over those of its `preset` when that is a local file, as `'../../jest.preset.js'`, which Jest
|
|
1125
|
+
* finds from `rootDir`: the project's own settings win, as Jest merges them. A preset that is a package isn't read.
|
|
1126
|
+
*/
|
|
1127
|
+
withPreset(config, settings, rootDir) {
|
|
1128
|
+
const preset = settingAt(settings, ['preset']);
|
|
1129
|
+
if (preset?.kind !== 'value' || typeof preset.value !== 'string' || !isLocalPreset(preset.value))
|
|
1130
|
+
return settings;
|
|
1131
|
+
const target = path.resolve(rootDir, replaceRootDir(rootDir, preset.value));
|
|
1132
|
+
const file = ['', '.js', '.json', '.cjs'].map((extension) => target + extension).find(isFileAt);
|
|
1133
|
+
if (!file) {
|
|
1134
|
+
this.configs.reportConfig(config, `The config's preset '${preset.value}' doesn't exist, so Jest fails to start.`);
|
|
1135
|
+
return settings;
|
|
1136
|
+
}
|
|
1137
|
+
const base = this.configs.at(file).settings;
|
|
1138
|
+
if (base.kind !== 'object' || settings.kind !== 'object')
|
|
1139
|
+
return settings;
|
|
1140
|
+
return { ...settings, keys: new Map([...base.keys, ...settings.keys]) };
|
|
1141
|
+
}
|
|
436
1142
|
vitestProjects(config, container = null) {
|
|
437
1143
|
const folder = path.dirname(config.file);
|
|
438
1144
|
const list = settingAt(config.settings, ['test', 'projects']);
|
|
@@ -502,7 +1208,26 @@ export class ConfigRuns {
|
|
|
502
1208
|
const ownTags = isVitest5 ? settingAt(entry, ['test', 'tags']) : null;
|
|
503
1209
|
const settings = mergeDeep(base, entry);
|
|
504
1210
|
const name = this.listedName(isVitest5 ? entry : settings, index, container);
|
|
505
|
-
|
|
1211
|
+
const root = this.inlineRoot(config, entry, folder);
|
|
1212
|
+
if (typeof root !== 'string')
|
|
1213
|
+
return [this.unknownProject(config, root.setting, root.key)];
|
|
1214
|
+
return this.vitestProject(config, settings, root, ownTags, name, true);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* The folder an inline project runs from. Without a root of its own, every version runs it from the config's root;
|
|
1218
|
+
* Vitest 5 also resolves its own root from there, where older versions resolve it from the config's folder.
|
|
1219
|
+
*/
|
|
1220
|
+
inlineRoot(config, entry, folder) {
|
|
1221
|
+
const parent = writtenRoot(config.settings);
|
|
1222
|
+
const parentRoot = typeof parent === 'string' ? path.resolve(folder, parent) : (parent ?? folder);
|
|
1223
|
+
const own = writtenRoot(entry);
|
|
1224
|
+
if (own === null)
|
|
1225
|
+
return parentRoot;
|
|
1226
|
+
if (typeof own !== 'string')
|
|
1227
|
+
return own;
|
|
1228
|
+
if (!atLeast(this.version, 5, 0))
|
|
1229
|
+
return path.resolve(folder, own);
|
|
1230
|
+
return typeof parentRoot === 'string' ? path.resolve(parentRoot, own) : parentRoot;
|
|
506
1231
|
}
|
|
507
1232
|
/**
|
|
508
1233
|
* The projects of a config file or folder `test.projects` names: the config's own, also the projects it lists in
|
|
@@ -527,10 +1252,11 @@ export class ConfigRuns {
|
|
|
527
1252
|
}
|
|
528
1253
|
/**
|
|
529
1254
|
* A Vitest project, with the name Vitest gives it: its files are found from `test.dir`, else `test.root` or `root`,
|
|
530
|
-
* else its folder. With browser mode on, each of `browser.instances` is
|
|
1255
|
+
* else its folder; with `isRoot`, `folder` is its root already. With browser mode on, each of `browser.instances` is
|
|
1256
|
+
* a project of its own, named as Vitest names it.
|
|
531
1257
|
*/
|
|
532
|
-
vitestProject(config, settings, folder, ownTags, name) {
|
|
533
|
-
const project = this.vitestFiles(config, settings, folder, ownTags, name);
|
|
1258
|
+
vitestProject(config, settings, folder, ownTags, name, isRoot = false) {
|
|
1259
|
+
const project = this.vitestFiles(config, settings, folder, ownTags, name, isRoot);
|
|
534
1260
|
if (project.unknown)
|
|
535
1261
|
return [project];
|
|
536
1262
|
const enabled = settingAt(settings, ['test', 'browser', 'enabled']);
|
|
@@ -553,11 +1279,11 @@ export class ConfigRuns {
|
|
|
553
1279
|
});
|
|
554
1280
|
}
|
|
555
1281
|
/** The files a Vitest project runs and its settings; `ownTags`, when set, in place of the tags in `settings`. */
|
|
556
|
-
vitestFiles(config, settings, folder, ownTags, name) {
|
|
1282
|
+
vitestFiles(config, settings, folder, ownTags, name, isRoot) {
|
|
557
1283
|
let root = folder;
|
|
558
1284
|
let dir = null;
|
|
559
1285
|
let hasTestRoot = false;
|
|
560
|
-
for (const key of ['test.root', 'root', 'test.dir']) {
|
|
1286
|
+
for (const key of isRoot ? ['test.dir'] : ['test.root', 'root', 'test.dir']) {
|
|
561
1287
|
const setting = settingAt(settings, key.split('.'));
|
|
562
1288
|
if (setting === null || isUnset(setting))
|
|
563
1289
|
continue;
|
|
@@ -618,7 +1344,7 @@ export class ConfigRuns {
|
|
|
618
1344
|
runsByDefault: true,
|
|
619
1345
|
defaults: {
|
|
620
1346
|
timeout: settingAt(settings, ['test', 'testTimeout']),
|
|
621
|
-
retry: this.vitestRetry(settingAt(settings, ['test', 'retry'])),
|
|
1347
|
+
retry: this.vitestRetry(config, settingAt(settings, ['test', 'retry'])),
|
|
622
1348
|
repeatEach: null,
|
|
623
1349
|
// Vitest 5 added `repeats` to the config.
|
|
624
1350
|
repeats: atLeast(this.version, 5, 0) ? settingAt(settings, ['test', 'repeats']) : null,
|
|
@@ -704,7 +1430,47 @@ function hasInSourceTests(file) {
|
|
|
704
1430
|
function unknownAt(setting) {
|
|
705
1431
|
return setting.kind === 'unknown' ? setting : { kind: 'unknown', where: null };
|
|
706
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* The root a config sets, `test.root` over Vite's `root`, as written; null when it sets none, and the setting and its
|
|
1435
|
+
* key when it can't be worked out.
|
|
1436
|
+
*/
|
|
1437
|
+
function writtenRoot(settings) {
|
|
1438
|
+
for (const key of ['test.root', 'root']) {
|
|
1439
|
+
const setting = settingAt(settings, key.split('.'));
|
|
1440
|
+
if (setting === null || isUnset(setting))
|
|
1441
|
+
continue;
|
|
1442
|
+
if (setting.kind !== 'value' || typeof setting.value !== 'string')
|
|
1443
|
+
return { setting: unknownAt(setting), key };
|
|
1444
|
+
return setting.value;
|
|
1445
|
+
}
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
707
1448
|
/** A config's settings without `test.projects`, as an inline project inherits them; both are known objects. */
|
|
1449
|
+
/**
|
|
1450
|
+
* Whether Mocha's lookup of one `spec` entry finds a file: an entry with glob characters is a glob, which takes any
|
|
1451
|
+
* file it matches; otherwise a file, the entry with one of the extensions added, or a folder, whose files with one of
|
|
1452
|
+
* the extensions it takes, without hidden ones, and those of folders below with `recursive`.
|
|
1453
|
+
*/
|
|
1454
|
+
function mochaSpec(folder, spec, extensions, recursive) {
|
|
1455
|
+
const target = path.resolve(folder, spec);
|
|
1456
|
+
if (picomatch.scan(posix(spec)).isGlob) {
|
|
1457
|
+
const isMatch = picomatch(posix(spec).replace(/^\.\//, ''));
|
|
1458
|
+
return (file) => isMatch(posix(path.relative(folder, file)));
|
|
1459
|
+
}
|
|
1460
|
+
const stat = statSync(target, { throwIfNoEntry: false });
|
|
1461
|
+
if (stat?.isFile())
|
|
1462
|
+
return (file) => file === target;
|
|
1463
|
+
if (!stat?.isDirectory())
|
|
1464
|
+
return (file) => extensions.some((extension) => file === `${target}.${extension}`);
|
|
1465
|
+
return (file) => {
|
|
1466
|
+
if (!isInside(target, file) || path.basename(file).startsWith('.'))
|
|
1467
|
+
return false;
|
|
1468
|
+
if (!recursive && path.dirname(file) !== target)
|
|
1469
|
+
return false;
|
|
1470
|
+
// An extension may have several parts, as `spec.js`.
|
|
1471
|
+
return extensions.some((extension) => file.endsWith(`.${extension}`));
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
708
1474
|
function withoutProjects(settings) {
|
|
709
1475
|
const root = settings;
|
|
710
1476
|
const test = root.keys.get('test');
|
|
@@ -913,21 +1679,19 @@ const UNKNOWN_DEFAULTS = {
|
|
|
913
1679
|
parallel: false,
|
|
914
1680
|
};
|
|
915
1681
|
/**
|
|
916
|
-
* What the config a file uses gives it: Playwright's `forbidOnly`, read from the top level
|
|
917
|
-
*
|
|
1682
|
+
* What the config a file uses gives it: Playwright's `forbidOnly` or Vitest's `allowOnly`, read from the top level
|
|
1683
|
+
* only, as the runners read them, and for each test, the projects that run it and what they set; null without a config.
|
|
918
1684
|
*/
|
|
919
1685
|
export function fileConfig(placement, framework, version, file) {
|
|
920
1686
|
const { config, projects } = placement;
|
|
921
1687
|
if (config === null) {
|
|
922
1688
|
// Without a config, Vitest defines no tags and keeps strictTags on.
|
|
923
1689
|
const tags = vitestTags(null, null, null, atLeast(version, 5, 0));
|
|
924
|
-
|
|
1690
|
+
const forbidOnly = onlyRule(framework, null);
|
|
1691
|
+
return framework === 'vitest' ? { forbidOnly, forTest: () => NO_CONFIG, tags, configTags: [] } : null;
|
|
925
1692
|
}
|
|
926
|
-
const forbidOnly = framework === 'playwright' ? settingAt(config.settings, ['forbidOnly']) : null;
|
|
927
1693
|
return {
|
|
928
|
-
forbidOnly:
|
|
929
|
-
? (unknownText(forbidOnly) ?? false)
|
|
930
|
-
: forbidOnly?.kind === 'value' && forbidOnly.value === true,
|
|
1694
|
+
forbidOnly: onlyRule(framework, config.settings),
|
|
931
1695
|
forTest(test, suites) {
|
|
932
1696
|
if (projects === null)
|
|
933
1697
|
return { projects: null, defaults: UNKNOWN_DEFAULTS };
|
|
@@ -939,9 +1703,241 @@ export function fileConfig(placement, framework, version, file) {
|
|
|
939
1703
|
configTags: placement.tags ?? [],
|
|
940
1704
|
};
|
|
941
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Why a `.only` fails: with Playwright's `forbidOnly`, the run, before any test; with Vitest's `allowOnly` off, the
|
|
1708
|
+
* test itself, which Vitest marks failed instead of running it (checked in 3.2.4, 4.1.12 and 5.1.0). `allowOnly` is
|
|
1709
|
+
* off by default when `process.env.CI` is set. Null when nothing fails.
|
|
1710
|
+
*/
|
|
1711
|
+
function onlyRule(framework, settings) {
|
|
1712
|
+
if (framework === 'playwright') {
|
|
1713
|
+
const forbid = settings && settingAt(settings, ['forbidOnly']);
|
|
1714
|
+
const condition = forbid?.kind === 'unknown' ? unknownText(forbid) : null;
|
|
1715
|
+
if (condition) {
|
|
1716
|
+
const message = `The config sets forbidOnly when ${condition}, so then this .only fails the run before any test runs.`;
|
|
1717
|
+
return { message, isSure: false };
|
|
1718
|
+
}
|
|
1719
|
+
if (forbid?.kind !== 'value' || forbid.value !== true)
|
|
1720
|
+
return null;
|
|
1721
|
+
return { message: 'The config sets forbidOnly, so this .only fails the run before any test runs.', isSure: true };
|
|
1722
|
+
}
|
|
1723
|
+
if (framework !== 'vitest')
|
|
1724
|
+
return null;
|
|
1725
|
+
const allow = settings && settingAt(settings, ['test', 'allowOnly']);
|
|
1726
|
+
const effect = 'Vitest fails this .only test instead of running it';
|
|
1727
|
+
if (allow === null || isUnset(allow)) {
|
|
1728
|
+
return {
|
|
1729
|
+
message: `Vitest's allowOnly is off when process.env.CI is set, as in CI, and then ${effect}.`,
|
|
1730
|
+
isSure: false,
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
const condition = allow.kind === 'unknown' ? unknownText(allow) : null;
|
|
1734
|
+
if (condition) {
|
|
1735
|
+
return { message: `The config sets allowOnly to ${condition}; when that is false, ${effect}.`, isSure: false };
|
|
1736
|
+
}
|
|
1737
|
+
// An object or a list is true, as is any value but `false` and the other falsy ones.
|
|
1738
|
+
if (allow.kind !== 'value' || allow.value)
|
|
1739
|
+
return null;
|
|
1740
|
+
return { message: `The config sets allowOnly to ${String(allow.value)}, so ${effect}.`, isSure: true };
|
|
1741
|
+
}
|
|
942
1742
|
/** The tags the projects that run a file define, when they all define the same; null otherwise. */
|
|
943
1743
|
function agreedTags(projects) {
|
|
944
1744
|
const [first, ...others] = (projects ?? []).map(({ defaults }) => defaults.tags);
|
|
945
1745
|
const text = JSON.stringify(first ?? null);
|
|
946
1746
|
return first && others.every((tags) => JSON.stringify(tags) === text) ? first : null;
|
|
947
1747
|
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Jest's `rootDir`: the config's folder, or the folder the config sets, from its folder, where an inline project's
|
|
1750
|
+
* `<rootDir>` is the config's folder too; unknown when it can't tell.
|
|
1751
|
+
*/
|
|
1752
|
+
function jestRootDir(settings, folder) {
|
|
1753
|
+
const setting = settingAt(settings, ['rootDir']);
|
|
1754
|
+
if (setting === null || isUnset(setting))
|
|
1755
|
+
return folder;
|
|
1756
|
+
return setting.kind === 'value' && typeof setting.value === 'string'
|
|
1757
|
+
? path.resolve(folder, replaceRootDir(folder, setting.value))
|
|
1758
|
+
: unknownAt(setting);
|
|
1759
|
+
}
|
|
1760
|
+
/** Jest's `replaceRootDirInPath`: `<rootDir>` at the start of a path stands for the project's `rootDir`. */
|
|
1761
|
+
function replaceRootDir(rootDir, item) {
|
|
1762
|
+
return item.startsWith('<rootDir>') ? path.join(rootDir, item.slice('<rootDir>'.length)) : item;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Jest's `globsToMatcher`: a path matches a glob that isn't negated and no negated glob leaves it out, or, when every
|
|
1766
|
+
* glob is negated, none leaves it out. Globs match the absolute path, dot files too.
|
|
1767
|
+
*/
|
|
1768
|
+
function jestGlobs(globs) {
|
|
1769
|
+
const matchers = globs.map((glob) => {
|
|
1770
|
+
const isMatch = picomatch(glob, { dot: true }, true);
|
|
1771
|
+
return { isMatch, negated: isMatch.state.negated || Boolean(isMatch.state.negatedExtglob) };
|
|
1772
|
+
});
|
|
1773
|
+
return (file) => {
|
|
1774
|
+
let kept;
|
|
1775
|
+
for (const { isMatch, negated } of matchers) {
|
|
1776
|
+
const matched = isMatch(file);
|
|
1777
|
+
if (!matched && negated)
|
|
1778
|
+
kept = false;
|
|
1779
|
+
else if (matched && !negated)
|
|
1780
|
+
kept = true;
|
|
1781
|
+
}
|
|
1782
|
+
return matchers.every(({ negated }) => negated) ? kept !== false : kept === true;
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
/** Whether a Jest setting is given: `null` and `undefined` leave Jest's default, as a `testMatch: null` beside `testRegex`. */
|
|
1786
|
+
function isJestSet(setting) {
|
|
1787
|
+
return setting !== null && !isUnset(setting) && !(setting.kind === 'value' && setting.value === null);
|
|
1788
|
+
}
|
|
1789
|
+
/** A Jest preset written as a path, which Jest reads from the project, rather than a package's name. */
|
|
1790
|
+
function isLocalPreset(preset) {
|
|
1791
|
+
return /^(\.{1,2}\/|\/|<rootDir>)/.test(preset);
|
|
1792
|
+
}
|
|
1793
|
+
function isFileAt(file) {
|
|
1794
|
+
return existsSync(file) && statSync(file).isFile();
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* The interface Mocha's `ui` gives a file's tests: BDD by default, or TDD. Another one, QUnit, exports or a package,
|
|
1798
|
+
* declares tests in a way the scan doesn't read, which is reported on the config, and the file is read as BDD.
|
|
1799
|
+
*/
|
|
1800
|
+
export function mochaInterface(placement, configs) {
|
|
1801
|
+
const { config } = placement;
|
|
1802
|
+
const ui = config ? settingAt(config.settings, ['ui']) : null;
|
|
1803
|
+
if (ui === null || isUnset(ui))
|
|
1804
|
+
return 'bdd';
|
|
1805
|
+
if (ui.kind !== 'value') {
|
|
1806
|
+
configs.reportUnknown(unknownAt(ui), 'ui', "the scan reads the tests as Mocha's default, BDD");
|
|
1807
|
+
return 'bdd';
|
|
1808
|
+
}
|
|
1809
|
+
if (ui.value === 'bdd' || ui.value === 'tdd')
|
|
1810
|
+
return ui.value;
|
|
1811
|
+
const message = `The config's ui '${String(ui.value)}' is an interface the scan doesn't read, so it reads the tests as Mocha's default, BDD.`;
|
|
1812
|
+
configs.reportConfig(config, message);
|
|
1813
|
+
return 'bdd';
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Whether an `exclude` entry names one of the config's `suites` rather than files: WebdriverIO leaves that suite out of
|
|
1817
|
+
* a `--suite` run, and takes it as no exclusion otherwise.
|
|
1818
|
+
*/
|
|
1819
|
+
function isSuiteName(settings, item) {
|
|
1820
|
+
if (/[/\\*?]/.test(item) || /\.(js|ts|mjs|cjs|es6|feature)$/i.test(item))
|
|
1821
|
+
return false;
|
|
1822
|
+
const suites = settingAt(settings, ['suites']);
|
|
1823
|
+
return suites?.kind === 'object' && suites.keys.has(item);
|
|
1824
|
+
}
|
|
1825
|
+
/** The interface WebdriverIO's `mochaOpts.ui` gives a file's tests, read as Mocha's `ui`. */
|
|
1826
|
+
export function webdriverioInterface(placement, configs) {
|
|
1827
|
+
if (placement.config === null || webdriverioFramework(placement, null) === 'jasmine')
|
|
1828
|
+
return 'bdd';
|
|
1829
|
+
const options = settingAt(placement.config.settings, ['mochaOpts']);
|
|
1830
|
+
return mochaInterface({ ...placement, config: { ...placement.config, settings: options ?? emptySettings() } }, configs);
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* The framework a WebdriverIO config runs the specs with: Mocha by default, or Jasmine. Cucumber runs feature files,
|
|
1834
|
+
* which the scan doesn't read; that and a value the scan can't work out are reported on the config when `configs` is
|
|
1835
|
+
* given, and the files are read as Mocha's.
|
|
1836
|
+
*/
|
|
1837
|
+
export function webdriverioFramework(placement, configs) {
|
|
1838
|
+
const { config } = placement;
|
|
1839
|
+
const framework = config ? settingAt(config.settings, ['framework']) : null;
|
|
1840
|
+
if (framework === null || isUnset(framework))
|
|
1841
|
+
return 'mocha';
|
|
1842
|
+
if (framework.kind === 'value' && (framework.value === 'mocha' || framework.value === 'jasmine'))
|
|
1843
|
+
return framework.value;
|
|
1844
|
+
if (framework.kind !== 'value') {
|
|
1845
|
+
configs?.reportUnknown(unknownAt(framework), 'framework', "the scan reads the tests as WebdriverIO's default, Mocha's");
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
const message = `The config's framework '${String(framework.value)}' is one the scan doesn't read, so it reads the tests as WebdriverIO's default, Mocha's.`;
|
|
1849
|
+
configs?.reportConfig(config, message);
|
|
1850
|
+
}
|
|
1851
|
+
return 'mocha';
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* How the config a file runs with maps its imports, for its dependencies: Vite's `resolve.alias` and Vitest's
|
|
1855
|
+
* `test.alias`, from the project and from the config file, and Jest's `moduleNameMapper`, `modulePaths` and
|
|
1856
|
+
* `moduleDirectories` other than `node_modules`, with `<rootDir>` in them. Null for the other runners, without a
|
|
1857
|
+
* config, or when it maps nothing.
|
|
1858
|
+
*/
|
|
1859
|
+
export function importMap(placement, framework) {
|
|
1860
|
+
const { config, settings } = placement;
|
|
1861
|
+
if (!config || !settings)
|
|
1862
|
+
return null;
|
|
1863
|
+
// The files of a project share its map, so the modules they import are read once.
|
|
1864
|
+
if (!importMaps.has(settings))
|
|
1865
|
+
importMaps.set(settings, readImportMap(config, settings, placement, framework));
|
|
1866
|
+
return importMaps.get(settings) ?? null;
|
|
1867
|
+
}
|
|
1868
|
+
const importMaps = new WeakMap();
|
|
1869
|
+
function readImportMap(config, settings, placement, framework) {
|
|
1870
|
+
const map = { aliases: [], paths: [], directories: [] };
|
|
1871
|
+
if (framework === 'vitest') {
|
|
1872
|
+
const sources = settings === config.settings ? [settings] : [settings, config.settings];
|
|
1873
|
+
for (const source of sources) {
|
|
1874
|
+
for (const key of [
|
|
1875
|
+
['test', 'alias'],
|
|
1876
|
+
['resolve', 'alias'],
|
|
1877
|
+
])
|
|
1878
|
+
map.aliases.push(...viteAliases(settingAt(source, key)));
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
else if (framework === 'jest') {
|
|
1882
|
+
const rootDir = placement.projects?.[0]?.root ?? path.dirname(config.file);
|
|
1883
|
+
const mapper = settingAt(settings, ['moduleNameMapper']);
|
|
1884
|
+
for (const [pattern, target] of mapper?.kind === 'object' ? mapper.keys : []) {
|
|
1885
|
+
const replacements = (target.kind === 'list' ? strings(target) : stringOf(target)).map((item) => replaceRootDir(rootDir, item));
|
|
1886
|
+
const find = regExp(pattern, '');
|
|
1887
|
+
if (find && replacements.length > 0)
|
|
1888
|
+
map.aliases.push({ find, replacements });
|
|
1889
|
+
}
|
|
1890
|
+
map.paths = strings(settingAt(settings, ['modulePaths'])).map((item) => path.resolve(rootDir, replaceRootDir(rootDir, item)));
|
|
1891
|
+
map.directories = strings(settingAt(settings, ['moduleDirectories'])).filter((name) => name !== 'node_modules');
|
|
1892
|
+
}
|
|
1893
|
+
return map.aliases.length + map.paths.length + map.directories.length > 0 ? map : null;
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Vite's aliases: an object of names and their replacements, or a list of `{ find, replacement }`, where `find` is a
|
|
1897
|
+
* name or a regex. Entries the scan can't work out are left out.
|
|
1898
|
+
*/
|
|
1899
|
+
function viteAliases(setting) {
|
|
1900
|
+
if (setting?.kind === 'object') {
|
|
1901
|
+
return [...setting.keys].flatMap(([find, target]) => stringOf(target).map((replacement) => ({ find, replacements: [replacement] })));
|
|
1902
|
+
}
|
|
1903
|
+
if (setting?.kind !== 'list')
|
|
1904
|
+
return [];
|
|
1905
|
+
return setting.items.flatMap((item) => {
|
|
1906
|
+
const find = settingAt(item, ['find']);
|
|
1907
|
+
const replacements = stringOf(settingAt(item, ['replacement']));
|
|
1908
|
+
const name = find?.kind === 'value' && typeof find.value === 'string' ? find.value : null;
|
|
1909
|
+
const pattern = find?.kind === 'regex' ? regExp(find.source, find.flags) : null;
|
|
1910
|
+
const written = name ?? pattern;
|
|
1911
|
+
return written !== null && replacements.length > 0 ? [{ find: written, replacements }] : [];
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
/** A setting's string as a list of one; empty for anything else. */
|
|
1915
|
+
function stringOf(setting) {
|
|
1916
|
+
return setting?.kind === 'value' && typeof setting.value === 'string' ? [setting.value] : [];
|
|
1917
|
+
}
|
|
1918
|
+
/** Whether a Node.js version runs TypeScript files without a flag: from 22.18 on the 22 line, and from 23.6. */
|
|
1919
|
+
function stripsTypes(version) {
|
|
1920
|
+
if (version === null)
|
|
1921
|
+
return true;
|
|
1922
|
+
const [major, minor] = version;
|
|
1923
|
+
return major > 23 || (major === 23 && minor >= 6) || (major === 22 && minor >= 18);
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* The `node --test` commands of the package.json script that runs node:test, the `test` script first, each as the
|
|
1927
|
+
* files it gives; none when no script runs it or there is no package.json.
|
|
1928
|
+
*/
|
|
1929
|
+
function nodeTestArguments(manifest) {
|
|
1930
|
+
let scripts = {};
|
|
1931
|
+
try {
|
|
1932
|
+
scripts = JSON.parse(readFileSync(manifest, 'utf8')).scripts ?? {};
|
|
1933
|
+
}
|
|
1934
|
+
catch {
|
|
1935
|
+
return [];
|
|
1936
|
+
}
|
|
1937
|
+
for (const script of [scripts.test, ...Object.values(scripts)]) {
|
|
1938
|
+
const commands = typeof script === 'string' ? nodeTestCommands(script) : [];
|
|
1939
|
+
if (commands.length > 0)
|
|
1940
|
+
return commands;
|
|
1941
|
+
}
|
|
1942
|
+
return [];
|
|
1943
|
+
}
|