chore: Upgrade to Vitest 4 and regroup tests for performance (#2618)

This commit is contained in:
Dermot Duffy
2026-07-26 11:28:58 -07:00
committed by GitHub
parent a0fd464d23
commit 0c3d46ad3d
36 changed files with 1041 additions and 1207 deletions
+64 -27
View File
@@ -23,23 +23,6 @@ const EXCLUSIONS = [
];
const TEST_DIRECTORY = 'tests';
const INCLUSIONS = [`${TEST_DIRECTORY}/**/*.test.ts`];
// For test performance reasons, tests are split into two groups:
//
// - `shared`: run non-isolated, so they share one loaded copy of the source
// tree instead of each re-importing it.
// - `isolated`: given their own module registry per file, because sharing one
// would change their behaviour. A file calling `vi.mock()` cannot replace a
// module an earlier file already loaded unmocked; a file loading the template
// renderer reads browser globals as the renderer loads; and a file needing a
// DOM leaves modules holding a `window` that is torn down when it finishes,
// which breaks any later file that reaches one of those modules.
//
// Absent this variable every test file runs isolated, which is both the safe
// default and what coverage requires (istanbul only counts a module's top-level
// code the first time it runs).
const GROUP = process.env.VITEST_GROUP;
const findTestFiles = (directory: string): string[] => {
const files: string[] = [];
@@ -54,16 +37,21 @@ const findTestFiles = (directory: string): string[] => {
return files;
};
const REQUIRE_ISOLATION_REGEXP =
/\bvi\.(do)?mock\(|loadRenderer|stubConnectedHomeAssistant|@vitest-environment\s+jsdom/;
// Each test file is read and sorted into one of the projects below, which
// explain what these two properties cost.
const REQUIRE_ISOLATION_REGEXP = /\bvi\.(do)?mock\(/;
const REQUIRE_DOM_REGEXP = /@vitest-environment\s+jsdom/;
const getGroup = (file: string): string =>
REQUIRE_ISOLATION_REGEXP.test(readFileSync(file, 'utf-8')) ? 'isolated' : 'shared';
const getGroup = (file: string): string => {
const contents = readFileSync(file, 'utf-8');
if (REQUIRE_ISOLATION_REGEXP.test(contents)) {
return 'isolated';
}
return REQUIRE_DOM_REGEXP.test(contents) ? 'shared-jsdom' : 'shared-node';
};
const getInclusions = (): string[] =>
GROUP
? findTestFiles(TEST_DIRECTORY).filter((file) => getGroup(file) === GROUP)
: INCLUSIONS;
const getInclusions = (group: string): string[] =>
findTestFiles(TEST_DIRECTORY).filter((file) => getGroup(file) === group);
export default defineConfig({
plugins: [svgPath()],
@@ -75,13 +63,62 @@ export default defineConfig({
inline: ['ha-nunjucks', 'ts-py-datetime'],
},
},
include: getInclusions(),
// Forked child processes start and tear down faster here than worker
// threads, which matters when every test file needs a fresh one.
pool: 'forks',
isolate: !GROUP || GROUP === 'isolated',
// Importing the source tree costs far more than running the tests in it, so
// files are grouped by whether they can share one loaded copy of it. Each
// project below is a group that can, or the one that cannot.
projects: [
{
extends: true,
test: {
// Nothing stops these sharing, so they run against a single loaded
// copy of the source tree. Most of the suite is here, and anything
// moved out of here pays to import that tree again.
name: 'shared-node',
include: getInclusions('shared-node'),
isolate: false,
},
},
{
extends: true,
test: {
// These share too, but only with each other. A module loaded under
// jsdom holds a reference to that DOM, so a worker that moves from a
// DOM file to a non-DOM one drops its loaded copy of the source tree
// and imports it again. A project's files are spread across workers
// without regard to environment, so putting both kinds in one project
// leaves every worker alternating between them and reloading as it
// goes, and the more workers there are the more often that happens. A
// project per environment hands each worker a run of files that all
// want the same one. The file-level `@vitest-environment jsdom`
// comments are what sort files into here, and are redundant once they
// arrive.
//
// They share one `document` as well, so a file that redefines part of
// it must leave the property configurable for the files that follow.
name: 'shared-jsdom',
include: getInclusions('shared-jsdom'),
environment: 'jsdom',
isolate: false,
},
},
{
extends: true,
test: {
// `vi.mock()` cannot replace a module that an earlier file already
// loaded unmocked, so these cannot share a loaded source tree with
// anything, including each other. They get a registry per file and
// pay the full import cost.
name: 'isolated',
include: getInclusions('isolated'),
isolate: true,
},
},
],
// Hide console writing to keep output clean, usual sources of noise:
// - Unnecessary Lit dev-mode warnings.