test-inventory 0.6.0 → 0.7.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.
@@ -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 Set();
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);
@@ -183,6 +210,7 @@ export class ConfigRuns {
183
210
  config,
184
211
  grep: null,
185
212
  grepRoot: '',
213
+ runsByDefault: true,
186
214
  defaults: NO_DEFAULTS,
187
215
  };
188
216
  return { config, settings: setting, about, runs: null, unknown: { setting, key } };
@@ -192,10 +220,68 @@ export class ConfigRuns {
192
220
  if (list?.kind === 'unknown')
193
221
  return [this.unknownProject(config, list, 'projects')];
194
222
  const items = list?.kind === 'list' ? list.items : [emptySettings()];
195
- return items.map((project) => this.playwrightProject(config, project));
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);
196
282
  }
197
283
  /** A Playwright project: its own settings, else the config's, else Playwright's defaults. */
198
- playwrightProject(config, project) {
284
+ playwrightProject(config, project, runsByDefault) {
199
285
  if (project.kind === 'unknown')
200
286
  return this.unknownProject(config, project, 'projects');
201
287
  const take = (key) => {
@@ -249,12 +335,11 @@ export class ConfigRuns {
249
335
  greps.push((title) => expressions.some((expression) => matches(expression, title)));
250
336
  }
251
337
  const [isGrepped, isInverted] = greps;
252
- const name = take('name');
253
338
  const dependencies = settingAt(project, ['dependencies']);
254
339
  // Playwright's root folder: the config's testDir, the start of each test's grep title.
255
340
  const configDir = settingAt(config.settings, ['testDir']);
256
341
  const about = {
257
- name: name?.setting.kind === 'value' && typeof name.setting.value === 'string' ? name.setting.value : '',
342
+ name: playwrightName(config, project),
258
343
  testDir: root,
259
344
  dependencies: strings(dependencies),
260
345
  // A project's own keys, as `snapshotPathTemplate`; the config's apply to every project, and are read there.
@@ -268,6 +353,7 @@ export class ConfigRuns {
268
353
  grepRoot: configDir?.kind === 'value' && typeof configDir.value === 'string'
269
354
  ? path.resolve(folder, configDir.value)
270
355
  : folder,
356
+ runsByDefault,
271
357
  defaults: {
272
358
  timeout: take('timeout')?.setting ?? null,
273
359
  retry: take('retries')?.setting ?? null,
@@ -283,7 +369,7 @@ export class ConfigRuns {
283
369
  about,
284
370
  unknown: null,
285
371
  runs: (file) => {
286
- if (!isInside(root, file))
372
+ if (runsByDefault === false || !isInside(root, file))
287
373
  return false;
288
374
  if (isIgnored(file) || !isMatch(file))
289
375
  return false;
@@ -343,7 +429,11 @@ export class ConfigRuns {
343
429
  }
344
430
  return rules;
345
431
  }
346
- vitestProjects(config) {
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) {
347
437
  const folder = path.dirname(config.file);
348
438
  const list = settingAt(config.settings, ['test', 'projects']);
349
439
  const isVitest3 = this.version !== null && this.version[0] < 4;
@@ -354,12 +444,15 @@ export class ConfigRuns {
354
444
  // Vitest 3 takes its projects from a workspace file, which the scan doesn't read.
355
445
  return [this.unknownProject(config, { kind: 'unknown', where: null }, 'test.projects')];
356
446
  }
447
+ const name = container ?? vitestName(config.settings, '', null);
357
448
  if (list === null)
358
- return this.vitestProject(config, config.settings, folder, null);
449
+ return this.vitestProject(config, config.settings, folder, null, name);
359
450
  if (list.kind !== 'list')
360
451
  return [this.unknownProject(config, unknownAt(list), 'test.projects')];
361
- this.expanding.add(config);
452
+ this.expanding.set(config, name);
362
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;
363
456
  const projects = list.items.flatMap((item) => {
364
457
  if (item.kind === 'value' && typeof item.value === 'string') {
365
458
  // `<rootDir>` is the config's folder, written with / as globs are.
@@ -368,10 +461,10 @@ export class ConfigRuns {
368
461
  if (pattern.startsWith('!') || picomatch.scan(pattern).isGlob)
369
462
  globs.push(pattern);
370
463
  else
371
- return this.vitestPath(config, path.resolve(folder, pattern));
464
+ return this.vitestPath(config, path.resolve(folder, pattern), container);
372
465
  return [];
373
466
  }
374
- return this.vitestEntry(config, item, folder);
467
+ return this.vitestEntry(config, item, folder, String(index++), container);
375
468
  });
376
469
  const matches = globs.length
377
470
  ? globSync(globs, {
@@ -383,12 +476,15 @@ export class ConfigRuns {
383
476
  ignore: ['**/node_modules/**'],
384
477
  })
385
478
  : [];
386
- projects.push(...matches.flatMap((match) => this.vitestPath(config, path.resolve(match))));
479
+ projects.push(...matches.flatMap((match) => this.vitestPath(config, path.resolve(match), container)));
387
480
  this.expanding.delete(config);
388
481
  return projects;
389
482
  }
390
- /** A project written in `test.projects`, which inherits the root config as each version of Vitest decides. */
391
- vitestEntry(config, entry, folder) {
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) {
392
488
  if (entry.kind !== 'object') {
393
489
  // A function or a promise, read by running the config, or a setting of the wrong type.
394
490
  return [this.unknownProject(config, unknownAt(entry), 'test.projects')];
@@ -401,31 +497,40 @@ export class ConfigRuns {
401
497
  else if (extended?.kind === 'value' ? extended.value === true : this.version === null || this.version[0] >= 5) {
402
498
  base = withoutProjects(config.settings);
403
499
  }
404
- // Vitest 5 takes a project's own tags in place of those it extends.
405
- const ownTags = atLeast(this.version, 5, 0) ? settingAt(entry, ['test', 'tags']) : null;
406
- return this.vitestProject(config, mergeDeep(base, entry), folder, ownTags);
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);
407
506
  }
408
507
  /**
409
508
  * The projects of a config file or folder `test.projects` names: the config's own, also the projects it lists in
410
- * 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.
411
511
  */
412
- vitestPath(config, target) {
512
+ vitestPath(config, target, container) {
413
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);
414
519
  if (!nested)
415
- return this.vitestProject(config, emptySettings(), target, null);
416
- const isContainer = (this.version === null || this.version[0] >= 5) && !this.expanding.has(nested);
520
+ return this.vitestProject(config, emptySettings(), target, null, name);
521
+ const isContainer = (this.version === null || this.version[0] >= 5) && listing === undefined;
417
522
  const projects = isContainer && settingAt(nested.settings, ['test', 'projects'])
418
- ? this.vitestProjects(nested)
419
- : this.vitestProject(nested, nested.settings, path.dirname(nested.file), null);
523
+ ? this.vitestProjects(nested, name)
524
+ : this.vitestProject(nested, nested.settings, folder, null, name);
420
525
  // The outermost config lists the others, so it is set last.
421
526
  return projects.map((project) => ({ ...project, about: { ...project.about, container: config } }));
422
527
  }
423
528
  /**
424
- * A Vitest project: its files are found from `test.dir`, else `test.root` or `root`, else its folder. With browser
425
- * 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.
426
531
  */
427
- vitestProject(config, settings, folder, ownTags) {
428
- 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);
429
534
  if (project.unknown)
430
535
  return [project];
431
536
  const enabled = settingAt(settings, ['test', 'browser', 'enabled']);
@@ -448,7 +553,7 @@ export class ConfigRuns {
448
553
  });
449
554
  }
450
555
  /** The files a Vitest project runs and its settings; `ownTags`, when set, in place of the tags in `settings`. */
451
- vitestFiles(config, settings, folder, ownTags) {
556
+ vitestFiles(config, settings, folder, ownTags, name) {
452
557
  let root = folder;
453
558
  let dir = null;
454
559
  let hasTestRoot = false;
@@ -500,10 +605,8 @@ export class ConfigRuns {
500
605
  flags.push(setting?.kind === 'value' && setting.value === true);
501
606
  }
502
607
  const [checksTypes, onlyTypes] = flags;
503
- const name = settingAt(settings, ['test', 'name']);
504
- const label = name?.kind === 'object' ? settingAt(name, ['label']) : name;
505
608
  const about = {
506
- name: label?.kind === 'value' && typeof label.value === 'string' ? label.value : '',
609
+ name,
507
610
  testDir: base,
508
611
  dependencies: [],
509
612
  settings,
@@ -512,6 +615,7 @@ export class ConfigRuns {
512
615
  config,
513
616
  grep: null,
514
617
  grepRoot: base,
618
+ runsByDefault: true,
515
619
  defaults: {
516
620
  timeout: settingAt(settings, ['test', 'testTimeout']),
517
621
  retry: this.vitestRetry(settingAt(settings, ['test', 'retry'])),
@@ -548,12 +652,41 @@ function matches(expression, text) {
548
652
  expression.lastIndex = 0;
549
653
  return expression.test(text);
550
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
+ }
551
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
+ }
552
675
  function strings(setting) {
553
676
  if (setting?.kind !== 'list')
554
677
  return [];
555
678
  return setting.items.flatMap((item) => (item.kind === 'value' && typeof item.value === 'string' ? [item.value] : []));
556
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
+ }
557
690
  /** A setting written as `undefined`, which the runners take as not set. */
558
691
  function isUnset(setting) {
559
692
  return setting.kind === 'value' && setting.value === undefined;
@@ -603,20 +736,26 @@ function folders(relative) {
603
736
  * The projects that run a test: those that run its file, less those whose Playwright `grep` or `grepInvert` leaves its
604
737
  * title out, matched as Playwright matches it, against the project's name, the file's path from the config's root
605
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
606
- * out.
739
+ * out, or the test is in a project the scan can't tell runs.
607
740
  */
608
- function projectsRunning(projects, test, suites, file) {
741
+ function projectsRunning(projects, test, suites, file, configTags) {
609
742
  const titles = [...test.suitePath, test.title];
610
- const isDynamic = test.hasDynamicTitle || suites.some((suite) => suite.hasDynamicTitle && test.suitePath.includes(suite.title));
611
- // Tags written into a title are in its text already; Playwright adds the others after it.
612
- const tags = test.tags.filter((tag) => !titles.some((title) => title.includes(tag)));
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)));
613
748
  const running = [];
614
749
  for (const project of projects) {
615
750
  const { name, grep, grepRoot } = project;
616
751
  if (grep && isDynamic)
617
752
  return null;
618
- if (!grep || grep([name, path.relative(grepRoot, file), ...titles, ...tags].join(' ')))
619
- running.push(project);
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);
620
759
  }
621
760
  return running;
622
761
  }
@@ -782,7 +921,7 @@ export function fileConfig(placement, framework, version, file) {
782
921
  if (config === null) {
783
922
  // Without a config, Vitest defines no tags and keeps strictTags on.
784
923
  const tags = vitestTags(null, null, null, atLeast(version, 5, 0));
785
- return framework === 'vitest' ? { forbidOnly: false, forTest: () => NO_CONFIG, tags } : null;
924
+ return framework === 'vitest' ? { forbidOnly: false, forTest: () => NO_CONFIG, tags, configTags: [] } : null;
786
925
  }
787
926
  const forbidOnly = framework === 'playwright' ? settingAt(config.settings, ['forbidOnly']) : null;
788
927
  return {
@@ -792,11 +931,12 @@ export function fileConfig(placement, framework, version, file) {
792
931
  forTest(test, suites) {
793
932
  if (projects === null)
794
933
  return { projects: null, defaults: UNKNOWN_DEFAULTS };
795
- const running = projectsRunning(projects, test, suites, file);
934
+ const running = projectsRunning(projects, test, suites, file, placement.tags);
796
935
  // A grep the scan can't match leaves the projects that run the file, which may still agree.
797
936
  return { projects: running?.map(({ name }) => name) ?? null, defaults: configDefaults(running ?? projects) };
798
937
  },
799
938
  tags: agreedTags(projects),
939
+ configTags: placement.tags ?? [],
800
940
  };
801
941
  }
802
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. It also merges their
483
- // `use`, `expect`, `build`, `webServer` and `projects`, which the scan doesn't read yet.
484
- return args.reduce((merged, arg) => spread(merged, this.evaluate(arg, frame, scopes), { frame, node: arg }), emptyObject());
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
@@ -2,17 +2,20 @@ import { type Node } from 'oxc-parser';
2
2
  import type { ConfigGap, RunsWith } from './config-dependencies.ts';
3
3
  import type { Extraction, Frame, PendingTest } from './extract.ts';
4
4
  import type { ModuleReader, ModuleSource, Problem, Span } from './model.ts';
5
- import type { ParsedFile } from './parse.ts';
5
+ import { type ParsedFile } from './parse.ts';
6
6
  import { type SourceText } from './source.ts';
7
7
  import type { Framework } from './types.ts';
8
8
  import { Values } from './values.ts';
9
9
  /** A file whose code a test can run: the scanned file, a module it imports, or a data file such as JSON. */
10
10
  interface Module {
11
11
  relativeFilePath: string;
12
- /** Null for a file that isn't JavaScript or TypeScript, which is taken by its text. */
12
+ /** Null for a file that isn't JavaScript or TypeScript, which is taken by its text; for a component, its scripts. */
13
13
  parsed: ParsedFile | null;
14
- /** The file's text with line endings read as `\n`, for a file that isn't code. */
15
- text: string | null;
14
+ /**
15
+ * The file's text with line endings read as `\n`, for a file that isn't code or a component; its bytes when it isn't
16
+ * UTF-8 text, such as an image.
17
+ */
18
+ text: string | Uint8Array | null;
16
19
  values: Values;
17
20
  readModule: ModuleReader | null;
18
21
  /** Its top-level code that runs when it is imported, and the modules it imports, which run then too. */
@@ -191,6 +194,11 @@ export declare class DependencyGraph {
191
194
  * takes the whole module, so that nothing it may run is left out.
192
195
  */
193
196
  private exported;
197
+ /**
198
+ * A name a module passes on from another, as `export { quote } from './nasdaq'` does, with the re-export itself: which
199
+ * module the name comes from is code too, so the barrel is among the files a change to which affects the test.
200
+ */
201
+ private passedOn;
194
202
  /** What `export * from source` gives for a name; null when the module behind it doesn't export it. */
195
203
  private reexported;
196
204
  /**