test-inventory 0.5.0 → 0.6.1
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 +103 -0
- package/dist/bin.js +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +23 -2
- package/dist/config-dependencies.d.ts +62 -0
- package/dist/config-dependencies.js +231 -0
- package/dist/config-runs.d.ts +47 -5
- package/dist/config-runs.js +205 -42
- package/dist/config.js +65 -3
- package/dist/dependency-hash.d.ts +250 -0
- package/dist/dependency-hash.js +1533 -0
- package/dist/diagnostics.js +1 -0
- package/dist/extract.d.ts +12 -0
- package/dist/extract.js +19 -6
- package/dist/filter.d.ts +6 -0
- package/dist/filter.js +11 -1
- package/dist/frameworks/common.d.ts +5 -0
- package/dist/frameworks/common.js +11 -0
- package/dist/frameworks/playwright.js +10 -2
- package/dist/frameworks/vitest.d.ts +1 -1
- package/dist/frameworks/vitest.js +36 -6
- package/dist/hash.d.ts +1 -1
- package/dist/model.d.ts +14 -2
- package/dist/parse.d.ts +6 -0
- package/dist/parse.js +204 -0
- package/dist/records.d.ts +6 -1
- package/dist/records.js +10 -2
- package/dist/resolve.js +28 -8
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +11 -2
- package/dist/scan.js +73 -2
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
- package/schema/v1.json +21 -1
package/dist/config-runs.js
CHANGED
|
@@ -101,8 +101,8 @@ export class ConfigRuns {
|
|
|
101
101
|
projects = new Map();
|
|
102
102
|
ignoreRules = new Map();
|
|
103
103
|
folderStates = new Map();
|
|
104
|
-
/** The Vitest configs whose projects are being found, to stop at one whose projects name itself. */
|
|
105
|
-
expanding = new
|
|
104
|
+
/** The Vitest configs whose projects are being found, with their names, to stop at one whose projects name itself. */
|
|
105
|
+
expanding = new Map();
|
|
106
106
|
constructor(configs, framework, version, findsFiles) {
|
|
107
107
|
this.configs = configs;
|
|
108
108
|
this.framework = framework;
|
|
@@ -115,6 +115,10 @@ export class ConfigRuns {
|
|
|
115
115
|
* nearest config.
|
|
116
116
|
*/
|
|
117
117
|
placementOf(file) {
|
|
118
|
+
const placement = this.place(file);
|
|
119
|
+
return { ...placement, tags: this.configTags(placement.config) };
|
|
120
|
+
}
|
|
121
|
+
place(file) {
|
|
118
122
|
const around = this.configs.around(file);
|
|
119
123
|
for (const config of around) {
|
|
120
124
|
const running = this.projectsOf(config).flatMap((project) => {
|
|
@@ -140,6 +144,29 @@ export class ConfigRuns {
|
|
|
140
144
|
projects: nearest ? [] : null,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Playwright's config `tag`, from 1.57, which it gives every test before its own tags; null when the scan can't work
|
|
149
|
+
* it out. That is reported where the config writes `tag`; a config whose settings come from code the scan can't
|
|
150
|
+
* follow is reported there already, as its projects can't be worked out either.
|
|
151
|
+
*/
|
|
152
|
+
configTags(config) {
|
|
153
|
+
if (config === null || this.framework !== 'playwright' || !atLeast(this.version, 1, 57))
|
|
154
|
+
return [];
|
|
155
|
+
const setting = settingAt(config.settings, ['tag']);
|
|
156
|
+
if (setting === null || isUnset(setting))
|
|
157
|
+
return [];
|
|
158
|
+
const tags = [];
|
|
159
|
+
for (const item of setting.kind === 'list' ? setting.items : [setting]) {
|
|
160
|
+
if (item.kind !== 'value' || typeof item.value !== 'string') {
|
|
161
|
+
if (config.settings.kind === 'object' && config.settings.keys.has('tag')) {
|
|
162
|
+
this.configs.reportUnknown(unknownAt(item), 'tag', "the tests' tags leave it out");
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
tags.push(item.value);
|
|
167
|
+
}
|
|
168
|
+
return tags;
|
|
169
|
+
}
|
|
143
170
|
/** The projects of a config the output lists; null when the scan can't work them out. */
|
|
144
171
|
projectsIn(config) {
|
|
145
172
|
const projects = this.projectsOf(config);
|
|
@@ -151,6 +178,14 @@ export class ConfigRuns {
|
|
|
151
178
|
dependencies: about.dependencies,
|
|
152
179
|
}));
|
|
153
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* A config's project, named as the runner names it, with the files it runs; null when there is none, or the scan
|
|
183
|
+
* can't tell which files it runs.
|
|
184
|
+
*/
|
|
185
|
+
projectNamed(config, name) {
|
|
186
|
+
const project = this.projectsOf(config).find((item) => item.runs && item.about.name === name);
|
|
187
|
+
return project ? { about: project.about, runs: project.runs } : null;
|
|
188
|
+
}
|
|
154
189
|
projectsOf(config) {
|
|
155
190
|
let projects = this.projects.get(config);
|
|
156
191
|
if (!projects) {
|
|
@@ -169,8 +204,13 @@ export class ConfigRuns {
|
|
|
169
204
|
name: '',
|
|
170
205
|
testDir: path.dirname(config.file),
|
|
171
206
|
dependencies: [],
|
|
207
|
+
settings: setting,
|
|
208
|
+
root: path.dirname(config.file),
|
|
209
|
+
container: null,
|
|
210
|
+
config,
|
|
172
211
|
grep: null,
|
|
173
212
|
grepRoot: '',
|
|
213
|
+
runsByDefault: true,
|
|
174
214
|
defaults: NO_DEFAULTS,
|
|
175
215
|
};
|
|
176
216
|
return { config, settings: setting, about, runs: null, unknown: { setting, key } };
|
|
@@ -180,10 +220,68 @@ export class ConfigRuns {
|
|
|
180
220
|
if (list?.kind === 'unknown')
|
|
181
221
|
return [this.unknownProject(config, list, 'projects')];
|
|
182
222
|
const items = list?.kind === 'list' ? list.items : [emptySettings()];
|
|
183
|
-
|
|
223
|
+
const running = this.runsByDefault(config, items);
|
|
224
|
+
return items.map((project) => this.playwrightProject(config, project, running.get(project)));
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Whether Playwright runs each project without `--project`: from 1.64, not with `default: false`, unless a project it
|
|
228
|
+
* runs lists it in `dependencies` or `teardown`, directly or through others. Null when the scan can't tell.
|
|
229
|
+
*/
|
|
230
|
+
runsByDefault(config, projects) {
|
|
231
|
+
const isRead = atLeast(this.version, 1, 64);
|
|
232
|
+
const runs = new Map(projects.map((project) => [project, !isRead]));
|
|
233
|
+
const names = projects.map((project) => playwrightName(config, project));
|
|
234
|
+
const visit = (project, value) => {
|
|
235
|
+
const current = runs.get(project);
|
|
236
|
+
if (current === true || current === value)
|
|
237
|
+
return;
|
|
238
|
+
runs.set(project, value);
|
|
239
|
+
const teardown = settingAt(project, ['teardown']);
|
|
240
|
+
const dependencies = settingAt(project, ['dependencies']);
|
|
241
|
+
const needed = strings(dependencies);
|
|
242
|
+
if (teardown?.kind === 'value' && typeof teardown.value === 'string')
|
|
243
|
+
needed.push(teardown.value);
|
|
244
|
+
// A list the scan can't work out may name any project, which then may run.
|
|
245
|
+
const unknowns = [teardown, dependencies, ...(dependencies?.kind === 'list' ? dependencies.items : [])].filter((setting) => setting?.kind === 'unknown');
|
|
246
|
+
for (const unknown of unknowns) {
|
|
247
|
+
this.configs.reportUnknown(unknown, 'projects dependencies', "the scan can't tell which projects run the tests");
|
|
248
|
+
}
|
|
249
|
+
for (const [index, other] of projects.entries()) {
|
|
250
|
+
if (needed.includes(names[index]))
|
|
251
|
+
visit(other, value);
|
|
252
|
+
else if (unknowns.length > 0)
|
|
253
|
+
visit(other, null);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
// The projects that surely run go first, so that what they pull in is sure to run too.
|
|
257
|
+
for (const value of isRead ? [true, null] : []) {
|
|
258
|
+
for (const project of projects) {
|
|
259
|
+
const setting = settingAt(project, ['default']);
|
|
260
|
+
const seed = setting?.kind === 'unknown' ? null : !(setting?.kind === 'value' && setting.value === false);
|
|
261
|
+
if (seed !== value)
|
|
262
|
+
continue;
|
|
263
|
+
if (seed === null && project.kind !== 'unknown') {
|
|
264
|
+
this.configs.reportUnknown(setting, 'projects default', "the scan can't tell which projects run the tests");
|
|
265
|
+
}
|
|
266
|
+
visit(project, value);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return runs;
|
|
270
|
+
}
|
|
271
|
+
/** The name of a project a Vitest config lists, with a warning when its `test.name` can't be worked out. */
|
|
272
|
+
listedName(settings, fallback, container) {
|
|
273
|
+
const name = settingAt(settings, ['test', 'name']);
|
|
274
|
+
const label = name?.kind === 'object' ? settingAt(name, ['label']) : name;
|
|
275
|
+
// Only a name written in a config the scan reads; a config it can't read is reported as a whole.
|
|
276
|
+
const holder = name?.kind === 'object' ? name : settingAt(settings, ['test']);
|
|
277
|
+
const isWritten = holder?.kind === 'object' && [...holder.keys.values()].includes(label);
|
|
278
|
+
if (label?.kind === 'unknown' && isWritten) {
|
|
279
|
+
this.configs.reportUnknown(label, 'test.name', 'the project is named after its folder or its place in the list');
|
|
280
|
+
}
|
|
281
|
+
return vitestName(settings, fallback, container);
|
|
184
282
|
}
|
|
185
283
|
/** A Playwright project: its own settings, else the config's, else Playwright's defaults. */
|
|
186
|
-
playwrightProject(config, project) {
|
|
284
|
+
playwrightProject(config, project, runsByDefault) {
|
|
187
285
|
if (project.kind === 'unknown')
|
|
188
286
|
return this.unknownProject(config, project, 'projects');
|
|
189
287
|
const take = (key) => {
|
|
@@ -237,20 +335,25 @@ export class ConfigRuns {
|
|
|
237
335
|
greps.push((title) => expressions.some((expression) => matches(expression, title)));
|
|
238
336
|
}
|
|
239
337
|
const [isGrepped, isInverted] = greps;
|
|
240
|
-
const name = take('name');
|
|
241
338
|
const dependencies = settingAt(project, ['dependencies']);
|
|
242
339
|
// Playwright's root folder: the config's testDir, the start of each test's grep title.
|
|
243
340
|
const configDir = settingAt(config.settings, ['testDir']);
|
|
244
341
|
const about = {
|
|
245
|
-
name:
|
|
342
|
+
name: playwrightName(config, project),
|
|
246
343
|
testDir: root,
|
|
247
344
|
dependencies: strings(dependencies),
|
|
345
|
+
// A project's own keys, as `snapshotPathTemplate`; the config's apply to every project, and are read there.
|
|
346
|
+
settings: project,
|
|
347
|
+
root: folder,
|
|
348
|
+
container: null,
|
|
349
|
+
config,
|
|
248
350
|
grep: grep || grepInvert
|
|
249
351
|
? (title) => (grep === null || isGrepped(title)) && !(grepInvert && isInverted(title))
|
|
250
352
|
: null,
|
|
251
353
|
grepRoot: configDir?.kind === 'value' && typeof configDir.value === 'string'
|
|
252
354
|
? path.resolve(folder, configDir.value)
|
|
253
355
|
: folder,
|
|
356
|
+
runsByDefault,
|
|
254
357
|
defaults: {
|
|
255
358
|
timeout: take('timeout')?.setting ?? null,
|
|
256
359
|
retry: take('retries')?.setting ?? null,
|
|
@@ -266,7 +369,7 @@ export class ConfigRuns {
|
|
|
266
369
|
about,
|
|
267
370
|
unknown: null,
|
|
268
371
|
runs: (file) => {
|
|
269
|
-
if (!isInside(root, file))
|
|
372
|
+
if (runsByDefault === false || !isInside(root, file))
|
|
270
373
|
return false;
|
|
271
374
|
if (isIgnored(file) || !isMatch(file))
|
|
272
375
|
return false;
|
|
@@ -326,7 +429,11 @@ export class ConfigRuns {
|
|
|
326
429
|
}
|
|
327
430
|
return rules;
|
|
328
431
|
}
|
|
329
|
-
|
|
432
|
+
/**
|
|
433
|
+
* The projects of a Vitest config, the one the runner starts with or, in Vitest 5, a config another lists, whose name
|
|
434
|
+
* then goes around the names of its projects.
|
|
435
|
+
*/
|
|
436
|
+
vitestProjects(config, container = null) {
|
|
330
437
|
const folder = path.dirname(config.file);
|
|
331
438
|
const list = settingAt(config.settings, ['test', 'projects']);
|
|
332
439
|
const isVitest3 = this.version !== null && this.version[0] < 4;
|
|
@@ -337,12 +444,15 @@ export class ConfigRuns {
|
|
|
337
444
|
// Vitest 3 takes its projects from a workspace file, which the scan doesn't read.
|
|
338
445
|
return [this.unknownProject(config, { kind: 'unknown', where: null }, 'test.projects')];
|
|
339
446
|
}
|
|
447
|
+
const name = container ?? vitestName(config.settings, '', null);
|
|
340
448
|
if (list === null)
|
|
341
|
-
return this.vitestProject(config, config.settings, folder, null);
|
|
449
|
+
return this.vitestProject(config, config.settings, folder, null, name);
|
|
342
450
|
if (list.kind !== 'list')
|
|
343
451
|
return [this.unknownProject(config, unknownAt(list), 'test.projects')];
|
|
344
|
-
this.expanding.
|
|
452
|
+
this.expanding.set(config, name);
|
|
345
453
|
const globs = [];
|
|
454
|
+
// Vitest names an inline project without a name after its place among the entries that aren't paths.
|
|
455
|
+
let index = 0;
|
|
346
456
|
const projects = list.items.flatMap((item) => {
|
|
347
457
|
if (item.kind === 'value' && typeof item.value === 'string') {
|
|
348
458
|
// `<rootDir>` is the config's folder, written with / as globs are.
|
|
@@ -351,10 +461,10 @@ export class ConfigRuns {
|
|
|
351
461
|
if (pattern.startsWith('!') || picomatch.scan(pattern).isGlob)
|
|
352
462
|
globs.push(pattern);
|
|
353
463
|
else
|
|
354
|
-
return this.vitestPath(config, path.resolve(folder, pattern));
|
|
464
|
+
return this.vitestPath(config, path.resolve(folder, pattern), container);
|
|
355
465
|
return [];
|
|
356
466
|
}
|
|
357
|
-
return this.vitestEntry(config, item, folder);
|
|
467
|
+
return this.vitestEntry(config, item, folder, String(index++), container);
|
|
358
468
|
});
|
|
359
469
|
const matches = globs.length
|
|
360
470
|
? globSync(globs, {
|
|
@@ -366,12 +476,15 @@ export class ConfigRuns {
|
|
|
366
476
|
ignore: ['**/node_modules/**'],
|
|
367
477
|
})
|
|
368
478
|
: [];
|
|
369
|
-
projects.push(...matches.flatMap((match) => this.vitestPath(config, path.resolve(match))));
|
|
479
|
+
projects.push(...matches.flatMap((match) => this.vitestPath(config, path.resolve(match), container)));
|
|
370
480
|
this.expanding.delete(config);
|
|
371
481
|
return projects;
|
|
372
482
|
}
|
|
373
|
-
/**
|
|
374
|
-
|
|
483
|
+
/**
|
|
484
|
+
* A project written in `test.projects`, which inherits the root config as each version of Vitest decides, named
|
|
485
|
+
* after `index` when it has no name of its own.
|
|
486
|
+
*/
|
|
487
|
+
vitestEntry(config, entry, folder, index, container) {
|
|
375
488
|
if (entry.kind !== 'object') {
|
|
376
489
|
// A function or a promise, read by running the config, or a setting of the wrong type.
|
|
377
490
|
return [this.unknownProject(config, unknownAt(entry), 'test.projects')];
|
|
@@ -384,29 +497,40 @@ export class ConfigRuns {
|
|
|
384
497
|
else if (extended?.kind === 'value' ? extended.value === true : this.version === null || this.version[0] >= 5) {
|
|
385
498
|
base = withoutProjects(config.settings);
|
|
386
499
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
500
|
+
const isVitest5 = atLeast(this.version, 5, 0);
|
|
501
|
+
// Vitest 5 takes a project's own tags in place of those it extends, and doesn't pass on the name.
|
|
502
|
+
const ownTags = isVitest5 ? settingAt(entry, ['test', 'tags']) : null;
|
|
503
|
+
const settings = mergeDeep(base, entry);
|
|
504
|
+
const name = this.listedName(isVitest5 ? entry : settings, index, container);
|
|
505
|
+
return this.vitestProject(config, settings, folder, ownTags, name);
|
|
390
506
|
}
|
|
391
507
|
/**
|
|
392
508
|
* The projects of a config file or folder `test.projects` names: the config's own, also the projects it lists in
|
|
393
|
-
* Vitest 5, or a project with Vitest's defaults for a folder without a config.
|
|
509
|
+
* Vitest 5, or a project with Vitest's defaults for a folder without a config. Without a name, a project is named
|
|
510
|
+
* after the package.json in its folder, else the folder.
|
|
394
511
|
*/
|
|
395
|
-
vitestPath(config, target) {
|
|
512
|
+
vitestPath(config, target, container) {
|
|
396
513
|
const nested = VITEST_CONFIG.test(path.basename(target)) ? this.configs.at(target) : this.configs.inFolder(target);
|
|
514
|
+
const folder = nested ? path.dirname(nested.file) : target;
|
|
515
|
+
// A config that lists itself also runs as a project, under its own name.
|
|
516
|
+
const listing = nested && this.expanding.get(nested);
|
|
517
|
+
const fallback = packageName(folder) ?? path.basename(folder);
|
|
518
|
+
const name = listing ?? this.listedName(nested?.settings ?? emptySettings(), fallback, container);
|
|
397
519
|
if (!nested)
|
|
398
|
-
return this.vitestProject(config, emptySettings(), target, null);
|
|
399
|
-
const isContainer = (this.version === null || this.version[0] >= 5) &&
|
|
400
|
-
|
|
401
|
-
? this.vitestProjects(nested)
|
|
402
|
-
: this.vitestProject(nested, nested.settings,
|
|
520
|
+
return this.vitestProject(config, emptySettings(), target, null, name);
|
|
521
|
+
const isContainer = (this.version === null || this.version[0] >= 5) && listing === undefined;
|
|
522
|
+
const projects = isContainer && settingAt(nested.settings, ['test', 'projects'])
|
|
523
|
+
? this.vitestProjects(nested, name)
|
|
524
|
+
: this.vitestProject(nested, nested.settings, folder, null, name);
|
|
525
|
+
// The outermost config lists the others, so it is set last.
|
|
526
|
+
return projects.map((project) => ({ ...project, about: { ...project.about, container: config } }));
|
|
403
527
|
}
|
|
404
528
|
/**
|
|
405
|
-
* A Vitest project: its files are found from `test.dir`, else `test.root` or `root`,
|
|
406
|
-
* mode on, each of `browser.instances` is a project of its own, named as Vitest names it.
|
|
529
|
+
* 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 a project of its own, named as Vitest names it.
|
|
407
531
|
*/
|
|
408
|
-
vitestProject(config, settings, folder, ownTags) {
|
|
409
|
-
const project = this.vitestFiles(config, settings, folder, ownTags);
|
|
532
|
+
vitestProject(config, settings, folder, ownTags, name) {
|
|
533
|
+
const project = this.vitestFiles(config, settings, folder, ownTags, name);
|
|
410
534
|
if (project.unknown)
|
|
411
535
|
return [project];
|
|
412
536
|
const enabled = settingAt(settings, ['test', 'browser', 'enabled']);
|
|
@@ -429,7 +553,7 @@ export class ConfigRuns {
|
|
|
429
553
|
});
|
|
430
554
|
}
|
|
431
555
|
/** The files a Vitest project runs and its settings; `ownTags`, when set, in place of the tags in `settings`. */
|
|
432
|
-
vitestFiles(config, settings, folder, ownTags) {
|
|
556
|
+
vitestFiles(config, settings, folder, ownTags, name) {
|
|
433
557
|
let root = folder;
|
|
434
558
|
let dir = null;
|
|
435
559
|
let hasTestRoot = false;
|
|
@@ -481,14 +605,17 @@ export class ConfigRuns {
|
|
|
481
605
|
flags.push(setting?.kind === 'value' && setting.value === true);
|
|
482
606
|
}
|
|
483
607
|
const [checksTypes, onlyTypes] = flags;
|
|
484
|
-
const name = settingAt(settings, ['test', 'name']);
|
|
485
|
-
const label = name?.kind === 'object' ? settingAt(name, ['label']) : name;
|
|
486
608
|
const about = {
|
|
487
|
-
name
|
|
609
|
+
name,
|
|
488
610
|
testDir: base,
|
|
489
611
|
dependencies: [],
|
|
612
|
+
settings,
|
|
613
|
+
root,
|
|
614
|
+
container: null,
|
|
615
|
+
config,
|
|
490
616
|
grep: null,
|
|
491
617
|
grepRoot: base,
|
|
618
|
+
runsByDefault: true,
|
|
492
619
|
defaults: {
|
|
493
620
|
timeout: settingAt(settings, ['test', 'testTimeout']),
|
|
494
621
|
retry: this.vitestRetry(settingAt(settings, ['test', 'retry'])),
|
|
@@ -525,12 +652,41 @@ function matches(expression, text) {
|
|
|
525
652
|
expression.lastIndex = 0;
|
|
526
653
|
return expression.test(text);
|
|
527
654
|
}
|
|
655
|
+
/** A Playwright project's name: its own, else the config's, else empty. */
|
|
656
|
+
function playwrightName(config, project) {
|
|
657
|
+
for (const setting of [settingAt(project, ['name']), settingAt(config.settings, ['name'])]) {
|
|
658
|
+
if (setting && !isUnset(setting)) {
|
|
659
|
+
return setting.kind === 'value' && typeof setting.value === 'string' ? setting.value : '';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return '';
|
|
663
|
+
}
|
|
528
664
|
/** The strings of a list setting, such as Playwright's `dependencies`; none for anything else. */
|
|
665
|
+
/**
|
|
666
|
+
* A Vitest project's name: its `test.name`, or the name's `label`, else `fallback`; in Vitest 5, inside the name of the
|
|
667
|
+
* config that lists it, as `web (unit)`.
|
|
668
|
+
*/
|
|
669
|
+
function vitestName(settings, fallback, container) {
|
|
670
|
+
const name = settingAt(settings, ['test', 'name']);
|
|
671
|
+
const label = name?.kind === 'object' ? settingAt(name, ['label']) : name;
|
|
672
|
+
const own = (label?.kind === 'value' && typeof label.value === 'string' && label.value) || fallback;
|
|
673
|
+
return container === null ? own : `${container} (${own})`;
|
|
674
|
+
}
|
|
529
675
|
function strings(setting) {
|
|
530
676
|
if (setting?.kind !== 'list')
|
|
531
677
|
return [];
|
|
532
678
|
return setting.items.flatMap((item) => (item.kind === 'value' && typeof item.value === 'string' ? [item.value] : []));
|
|
533
679
|
}
|
|
680
|
+
/** The `name` of the package.json in a folder; null without one, or when it has none. */
|
|
681
|
+
function packageName(folder) {
|
|
682
|
+
try {
|
|
683
|
+
const { name } = JSON.parse(readFileSync(path.join(folder, 'package.json'), 'utf8'));
|
|
684
|
+
return typeof name === 'string' && name !== '' ? name : null;
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
534
690
|
/** A setting written as `undefined`, which the runners take as not set. */
|
|
535
691
|
function isUnset(setting) {
|
|
536
692
|
return setting.kind === 'value' && setting.value === undefined;
|
|
@@ -580,20 +736,26 @@ function folders(relative) {
|
|
|
580
736
|
* The projects that run a test: those that run its file, less those whose Playwright `grep` or `grepInvert` leaves its
|
|
581
737
|
* title out, matched as Playwright matches it, against the project's name, the file's path from the config's root
|
|
582
738
|
* folder, the titles of the describes and the test and their tags. Null when a `grep` meets a title the scan can't work
|
|
583
|
-
* out.
|
|
739
|
+
* out, or the test is in a project the scan can't tell runs.
|
|
584
740
|
*/
|
|
585
|
-
function projectsRunning(projects, test, suites, file) {
|
|
741
|
+
function projectsRunning(projects, test, suites, file, configTags) {
|
|
586
742
|
const titles = [...test.suitePath, test.title];
|
|
587
|
-
const isDynamic = test.hasDynamicTitle ||
|
|
588
|
-
|
|
589
|
-
|
|
743
|
+
const isDynamic = test.hasDynamicTitle ||
|
|
744
|
+
configTags === null ||
|
|
745
|
+
suites.some((suite) => suite.hasDynamicTitle && test.suitePath.includes(suite.title));
|
|
746
|
+
// Tags written into a title are in its text already; Playwright adds the config's after the file, the others after it.
|
|
747
|
+
const tags = test.tags.filter((tag) => !configTags?.includes(tag) && !titles.some((title) => title.includes(tag)));
|
|
590
748
|
const running = [];
|
|
591
749
|
for (const project of projects) {
|
|
592
750
|
const { name, grep, grepRoot } = project;
|
|
593
751
|
if (grep && isDynamic)
|
|
594
752
|
return null;
|
|
595
|
-
|
|
596
|
-
|
|
753
|
+
const text = [name, path.relative(grepRoot, file), ...(configTags ?? []), ...titles, ...tags].join(' ');
|
|
754
|
+
if (grep && !grep(text))
|
|
755
|
+
continue;
|
|
756
|
+
if (project.runsByDefault === null)
|
|
757
|
+
return null;
|
|
758
|
+
running.push(project);
|
|
597
759
|
}
|
|
598
760
|
return running;
|
|
599
761
|
}
|
|
@@ -759,7 +921,7 @@ export function fileConfig(placement, framework, version, file) {
|
|
|
759
921
|
if (config === null) {
|
|
760
922
|
// Without a config, Vitest defines no tags and keeps strictTags on.
|
|
761
923
|
const tags = vitestTags(null, null, null, atLeast(version, 5, 0));
|
|
762
|
-
return framework === 'vitest' ? { forbidOnly: false, forTest: () => NO_CONFIG, tags } : null;
|
|
924
|
+
return framework === 'vitest' ? { forbidOnly: false, forTest: () => NO_CONFIG, tags, configTags: [] } : null;
|
|
763
925
|
}
|
|
764
926
|
const forbidOnly = framework === 'playwright' ? settingAt(config.settings, ['forbidOnly']) : null;
|
|
765
927
|
return {
|
|
@@ -769,11 +931,12 @@ export function fileConfig(placement, framework, version, file) {
|
|
|
769
931
|
forTest(test, suites) {
|
|
770
932
|
if (projects === null)
|
|
771
933
|
return { projects: null, defaults: UNKNOWN_DEFAULTS };
|
|
772
|
-
const running = projectsRunning(projects, test, suites, file);
|
|
934
|
+
const running = projectsRunning(projects, test, suites, file, placement.tags);
|
|
773
935
|
// A grep the scan can't match leaves the projects that run the file, which may still agree.
|
|
774
936
|
return { projects: running?.map(({ name }) => name) ?? null, defaults: configDefaults(running ?? projects) };
|
|
775
937
|
},
|
|
776
938
|
tags: agreedTags(projects),
|
|
939
|
+
configTags: placement.tags ?? [],
|
|
777
940
|
};
|
|
778
941
|
}
|
|
779
942
|
/** The tags the projects that run a file define, when they all define the same; null otherwise. */
|
package/dist/config.js
CHANGED
|
@@ -140,6 +140,68 @@ function spread(base, over, at) {
|
|
|
140
140
|
return { kind: 'object', keys: new Map(over.keys), rest: over.rest };
|
|
141
141
|
return { kind: 'object', keys: new Map([...base.keys, ...over.keys]), rest: base.rest };
|
|
142
142
|
}
|
|
143
|
+
/** `{ ...a, ...b }` of two objects either of which may be unset, as Playwright merges `use` and `expect`. */
|
|
144
|
+
function assign(a, b, at) {
|
|
145
|
+
return [a, b].reduce((merged, over) => (over ? spread(merged, over, at) : merged), emptyObject());
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Playwright's `defineConfig(base, over)`: `over`'s keys replace `base`'s, but `expect`, `use` and `build` merge key by
|
|
149
|
+
* key, and `projects` by name. `webServer`, whose lists Playwright joins, isn't read.
|
|
150
|
+
*/
|
|
151
|
+
function playwrightMerge(base, over, at) {
|
|
152
|
+
const merged = spread(base, over, at);
|
|
153
|
+
if (over.kind !== 'object' || over.rest)
|
|
154
|
+
return merged;
|
|
155
|
+
for (const key of ['expect', 'use', 'build']) {
|
|
156
|
+
if (merged.keys.has(key))
|
|
157
|
+
merged.keys.set(key, assign(child(base, key), child(over, key), at));
|
|
158
|
+
}
|
|
159
|
+
const overrides = child(over, 'projects');
|
|
160
|
+
// Without projects of its own, `over` keeps those of `base`; with `projects: undefined`, it drops them.
|
|
161
|
+
if (overrides === null || isNullish(overrides))
|
|
162
|
+
return merged;
|
|
163
|
+
const projects = child(base, 'projects');
|
|
164
|
+
const items = projects === null || isNullish(projects) ? [] : projects.kind === 'list' ? projects.items : null;
|
|
165
|
+
const isKnown = items !== null && overrides.kind === 'list';
|
|
166
|
+
merged.keys.set('projects', isKnown ? mergeProjects(items, overrides.items, at) : unknown(at));
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Playwright's projects of a config with those of a config over it merged in by name, with their `use` key by key; the
|
|
171
|
+
* others are added after, the last of each name. Unknown when the scan can't tell a project's name.
|
|
172
|
+
*/
|
|
173
|
+
function mergeProjects(base, overrides, at) {
|
|
174
|
+
const byName = new Map();
|
|
175
|
+
for (const project of overrides) {
|
|
176
|
+
const name = projectName(project);
|
|
177
|
+
if (name === null)
|
|
178
|
+
return unknown(at);
|
|
179
|
+
byName.set(name, project);
|
|
180
|
+
}
|
|
181
|
+
const projects = [];
|
|
182
|
+
for (const project of base) {
|
|
183
|
+
const name = projectName(project);
|
|
184
|
+
if (name === null)
|
|
185
|
+
return unknown(at);
|
|
186
|
+
const override = byName.get(name);
|
|
187
|
+
byName.delete(name);
|
|
188
|
+
if (!override) {
|
|
189
|
+
projects.push(project);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const merged = spread(spread(emptyObject(), project, at), override, at);
|
|
193
|
+
merged.keys.set('use', assign(child(project, 'use'), child(override, 'use'), at));
|
|
194
|
+
projects.push(merged);
|
|
195
|
+
}
|
|
196
|
+
return { kind: 'list', items: [...projects, ...byName.values()] };
|
|
197
|
+
}
|
|
198
|
+
/** The name Playwright merges a project by: undefined when unset, null when the scan can't tell. */
|
|
199
|
+
function projectName(project) {
|
|
200
|
+
const name = child(project, 'name');
|
|
201
|
+
if (name === null || (name.kind === 'value' && name.value === undefined))
|
|
202
|
+
return undefined;
|
|
203
|
+
return name.kind === 'value' && typeof name.value === 'string' ? name.value : null;
|
|
204
|
+
}
|
|
143
205
|
/**
|
|
144
206
|
* One of two settings, as the two sides of a condition: what both agree on is known, and the rest is unknown at `at`,
|
|
145
207
|
* where the condition is written. Null is a setting neither side sets.
|
|
@@ -479,9 +541,9 @@ class ConfigCode {
|
|
|
479
541
|
// Vitest's defineConfig() and defineProject() return the config they are given, or the one a function returns.
|
|
480
542
|
return isFunctionNode(args[0]) ? this.returned(args[0], frame, scopes) : this.evaluate(args[0], frame, scopes);
|
|
481
543
|
}
|
|
482
|
-
// Playwright's defineConfig() merges the configs it is given, each over the ones before it.
|
|
483
|
-
|
|
484
|
-
return
|
|
544
|
+
// Playwright's defineConfig() merges the configs it is given, each over the ones before it.
|
|
545
|
+
const [first, ...others] = args;
|
|
546
|
+
return others.reduce((merged, arg) => playwrightMerge(merged, this.evaluate(arg, frame, scopes), { frame, node: arg }), spread(emptyObject(), this.evaluate(first, frame, scopes), { frame, node: first }));
|
|
485
547
|
}
|
|
486
548
|
/**
|
|
487
549
|
* Where a function a config calls comes from: `name` of `import { name } from 'module'` or
|