import { readdirSync, readFileSync } from 'fs'; import { join } from 'path'; import { defineConfig } from 'vitest/config'; import { svgPath } from './scripts/svg-path-plugin.js'; const EXCLUSIONS = [ '.eslintrc.cjs', 'docs/**', 'tests/**', // Web-components. 'src/card.ts', 'src/components/**/*.ts', 'src/editor.ts', // Timeline controller (can be added later). 'src/components-lib/timeline/controller.ts', // HA patches. 'src/patches/**/*.ts', ]; const TEST_DIRECTORY = 'tests'; const findTestFiles = (directory: string): string[] => { const files: string[] = []; for (const entry of readdirSync(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isDirectory()) { files.push(...findTestFiles(path)); } else if (entry.name.endsWith('.test.ts')) { files.push(path); } } return files; }; // 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 => { 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 = (group: string): string[] => findTestFiles(TEST_DIRECTORY).filter((file) => getGroup(file) === group); export default defineConfig({ plugins: [svgPath()], test: { server: { deps: { // These dependencies import without extensions. // Related: https://github.com/vitest-dev/vitest/issues/2313 inline: ['ha-nunjucks', 'ts-py-datetime'], }, }, // Forked child processes start and tear down faster here than worker // threads, which matters when every test file needs a fresh one. pool: 'forks', // 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. // - Various console outputs (that are expected/tested). onConsoleLog: () => false, coverage: { exclude: EXCLUSIONS, // 'v8' reads coverage from the JavaScript engine, so it runs about twice // as fast as 'istanbul', which has to rewrite every source file first. // Known gap: 'istanbul' will flag a default parameter value that no test // ever falls back on -- 'v8' will not, but it is not worth the // significant performance slowdown to cover this rare case. provider: 'v8', thresholds: { perFile: true, 'src/**/*.ts': { statements: 100, branches: 100, functions: 100, lines: 100, }, }, }, }, });