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-runs.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
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. */
|
|
@@ -114,6 +117,9 @@ export class ConfigRuns {
|
|
|
114
117
|
folderStates = new Map();
|
|
115
118
|
/** The project Jest runs with when no config is found up to the root. */
|
|
116
119
|
jestDefaults = null;
|
|
120
|
+
mochaDefaults = null;
|
|
121
|
+
nodeRuns = null;
|
|
122
|
+
bunFiles = null;
|
|
117
123
|
/** The Vitest configs whose projects are being found, with their names, to stop at one whose projects name itself. */
|
|
118
124
|
expanding = new Map();
|
|
119
125
|
constructor(configs, framework, version, findsFiles) {
|
|
@@ -132,6 +138,18 @@ export class ConfigRuns {
|
|
|
132
138
|
return { ...placement, tags: this.configTags(placement.config) };
|
|
133
139
|
}
|
|
134
140
|
place(file) {
|
|
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);
|
|
135
153
|
// Jest started at the root reads the config there only, and the others through its `projects`.
|
|
136
154
|
const rootConfig = this.framework === 'jest' ? this.configs.inFolder(this.configs.root) : null;
|
|
137
155
|
const around = this.framework !== 'jest' ? this.configs.around(file) : rootConfig ? [rootConfig] : [];
|
|
@@ -207,6 +225,470 @@ export class ConfigRuns {
|
|
|
207
225
|
const project = this.projectsOf(config).find((item) => item.runs && item.about.name === name);
|
|
208
226
|
return project ? { about: project.about, runs: project.runs } : null;
|
|
209
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
|
+
}
|
|
210
692
|
projectsOf(config) {
|
|
211
693
|
let projects = this.projects.get(config);
|
|
212
694
|
if (!projects) {
|
|
@@ -214,6 +696,14 @@ export class ConfigRuns {
|
|
|
214
696
|
projects = this.playwrightProjects(config);
|
|
215
697
|
else if (this.framework === 'jest')
|
|
216
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)];
|
|
217
707
|
else
|
|
218
708
|
projects = this.vitestProjects(config);
|
|
219
709
|
this.projects.set(config, projects);
|
|
@@ -221,8 +711,12 @@ export class ConfigRuns {
|
|
|
221
711
|
return projects;
|
|
222
712
|
}
|
|
223
713
|
/** Vitest's `retry`: before 4.1, which reads only a count, an object is one the scan can't work out. */
|
|
224
|
-
vitestRetry(setting) {
|
|
225
|
-
|
|
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);
|
|
226
720
|
}
|
|
227
721
|
/** A project whose files can't be worked out, because of `setting` under `key`: it may run any file. */
|
|
228
722
|
unknownProject(config, setting, key) {
|
|
@@ -714,7 +1208,26 @@ export class ConfigRuns {
|
|
|
714
1208
|
const ownTags = isVitest5 ? settingAt(entry, ['test', 'tags']) : null;
|
|
715
1209
|
const settings = mergeDeep(base, entry);
|
|
716
1210
|
const name = this.listedName(isVitest5 ? entry : settings, index, container);
|
|
717
|
-
|
|
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;
|
|
718
1231
|
}
|
|
719
1232
|
/**
|
|
720
1233
|
* The projects of a config file or folder `test.projects` names: the config's own, also the projects it lists in
|
|
@@ -739,10 +1252,11 @@ export class ConfigRuns {
|
|
|
739
1252
|
}
|
|
740
1253
|
/**
|
|
741
1254
|
* A Vitest project, with the name Vitest gives it: its files are found from `test.dir`, else `test.root` or `root`,
|
|
742
|
-
* 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.
|
|
743
1257
|
*/
|
|
744
|
-
vitestProject(config, settings, folder, ownTags, name) {
|
|
745
|
-
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);
|
|
746
1260
|
if (project.unknown)
|
|
747
1261
|
return [project];
|
|
748
1262
|
const enabled = settingAt(settings, ['test', 'browser', 'enabled']);
|
|
@@ -765,11 +1279,11 @@ export class ConfigRuns {
|
|
|
765
1279
|
});
|
|
766
1280
|
}
|
|
767
1281
|
/** The files a Vitest project runs and its settings; `ownTags`, when set, in place of the tags in `settings`. */
|
|
768
|
-
vitestFiles(config, settings, folder, ownTags, name) {
|
|
1282
|
+
vitestFiles(config, settings, folder, ownTags, name, isRoot) {
|
|
769
1283
|
let root = folder;
|
|
770
1284
|
let dir = null;
|
|
771
1285
|
let hasTestRoot = false;
|
|
772
|
-
for (const key of ['test.root', 'root', 'test.dir']) {
|
|
1286
|
+
for (const key of isRoot ? ['test.dir'] : ['test.root', 'root', 'test.dir']) {
|
|
773
1287
|
const setting = settingAt(settings, key.split('.'));
|
|
774
1288
|
if (setting === null || isUnset(setting))
|
|
775
1289
|
continue;
|
|
@@ -830,7 +1344,7 @@ export class ConfigRuns {
|
|
|
830
1344
|
runsByDefault: true,
|
|
831
1345
|
defaults: {
|
|
832
1346
|
timeout: settingAt(settings, ['test', 'testTimeout']),
|
|
833
|
-
retry: this.vitestRetry(settingAt(settings, ['test', 'retry'])),
|
|
1347
|
+
retry: this.vitestRetry(config, settingAt(settings, ['test', 'retry'])),
|
|
834
1348
|
repeatEach: null,
|
|
835
1349
|
// Vitest 5 added `repeats` to the config.
|
|
836
1350
|
repeats: atLeast(this.version, 5, 0) ? settingAt(settings, ['test', 'repeats']) : null,
|
|
@@ -916,7 +1430,47 @@ function hasInSourceTests(file) {
|
|
|
916
1430
|
function unknownAt(setting) {
|
|
917
1431
|
return setting.kind === 'unknown' ? setting : { kind: 'unknown', where: null };
|
|
918
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
|
+
}
|
|
919
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
|
+
}
|
|
920
1474
|
function withoutProjects(settings) {
|
|
921
1475
|
const root = settings;
|
|
922
1476
|
const test = root.keys.get('test');
|
|
@@ -1125,21 +1679,19 @@ const UNKNOWN_DEFAULTS = {
|
|
|
1125
1679
|
parallel: false,
|
|
1126
1680
|
};
|
|
1127
1681
|
/**
|
|
1128
|
-
* What the config a file uses gives it: Playwright's `forbidOnly`, read from the top level
|
|
1129
|
-
*
|
|
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.
|
|
1130
1684
|
*/
|
|
1131
1685
|
export function fileConfig(placement, framework, version, file) {
|
|
1132
1686
|
const { config, projects } = placement;
|
|
1133
1687
|
if (config === null) {
|
|
1134
1688
|
// Without a config, Vitest defines no tags and keeps strictTags on.
|
|
1135
1689
|
const tags = vitestTags(null, null, null, atLeast(version, 5, 0));
|
|
1136
|
-
|
|
1690
|
+
const forbidOnly = onlyRule(framework, null);
|
|
1691
|
+
return framework === 'vitest' ? { forbidOnly, forTest: () => NO_CONFIG, tags, configTags: [] } : null;
|
|
1137
1692
|
}
|
|
1138
|
-
const forbidOnly = framework === 'playwright' ? settingAt(config.settings, ['forbidOnly']) : null;
|
|
1139
1693
|
return {
|
|
1140
|
-
forbidOnly:
|
|
1141
|
-
? (unknownText(forbidOnly) ?? false)
|
|
1142
|
-
: forbidOnly?.kind === 'value' && forbidOnly.value === true,
|
|
1694
|
+
forbidOnly: onlyRule(framework, config.settings),
|
|
1143
1695
|
forTest(test, suites) {
|
|
1144
1696
|
if (projects === null)
|
|
1145
1697
|
return { projects: null, defaults: UNKNOWN_DEFAULTS };
|
|
@@ -1151,6 +1703,42 @@ export function fileConfig(placement, framework, version, file) {
|
|
|
1151
1703
|
configTags: placement.tags ?? [],
|
|
1152
1704
|
};
|
|
1153
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
|
+
}
|
|
1154
1742
|
/** The tags the projects that run a file define, when they all define the same; null otherwise. */
|
|
1155
1743
|
function agreedTags(projects) {
|
|
1156
1744
|
const [first, ...others] = (projects ?? []).map(({ defaults }) => defaults.tags);
|
|
@@ -1205,3 +1793,151 @@ function isLocalPreset(preset) {
|
|
|
1205
1793
|
function isFileAt(file) {
|
|
1206
1794
|
return existsSync(file) && statSync(file).isFile();
|
|
1207
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
|
+
}
|