From 5fb5436e693eb2d1bb4002575d9689717e11bd01 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Fri, 5 Sep 2025 13:13:39 +0200 Subject: [PATCH 01/13] feat(): create common vitest config setup --- tools/README.md | 154 +++++++++++++++++++++++++++++++++ tools/vitest-config-factory.ts | 151 ++++++++++++++++++++++++++++++++ tools/vitest-setup-presets.ts | 33 +++++++ 3 files changed, 338 insertions(+) create mode 100644 tools/README.md create mode 100644 tools/vitest-config-factory.ts create mode 100644 tools/vitest-setup-presets.ts diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000..cd88d502f --- /dev/null +++ b/tools/README.md @@ -0,0 +1,154 @@ +## Vitest config factory and setup presets + +This folder contains utilities to centralize and standardize Vitest configuration across the monorepo. + +### Files + +- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults. +- `vitest-setup-presets.ts`: reusable groups of `setupFiles` paths by test kind. + +### Goals + +- Reduce duplication across `vitest.*.config.ts` files. +- Keep per-package intent clear with minimal overrides. +- Provide safe defaults and easy extension points. + +### Quick start + +Use the factory in your suite configs (project root is required): + +```ts +/// +import { createE2eConfig, createIntConfig, createUnitConfig } from '../../tools/vitest-config-factory.js'; + +export default createUnitConfig('core', { + projectRoot: new URL('../../', import.meta.url), +}); +``` + +Creators: + +- `createUnitConfig(projectKey, { projectRoot, ...options })` +- `createIntConfig(projectKey, { projectRoot, ...options })` +- `createE2eConfig(projectKey, { projectRoot, ...options })` + +`projectKey` is used for cache and coverage directories. + +### Defaults + +Common to all kinds: + +- `reporters: ['basic']`, `globals: true`, `environment: 'node'` +- `alias: tsconfigPathAliases()` +- `pool: 'threads'` with `singleThread: true` +- Cache directories resolved from `projectRoot` (absolute paths) + +Coverage: + +- Unit/Int: enabled by default, reports to `/coverage//-tests` +- E2E: disabled by default +- Default exclude: `['mocks/**', '**/types.ts']` + +Global setup: + +- Unit/Int: `['/global-setup.ts']` by default +- E2E: none by default (set per-suite if needed) + +Include patterns: + +- Unit: `src/**/*.unit.test.*` +- Int: `src/**/*.int.test.*` +- E2E: `tests/**/*.e2e.test.*` + +### setupFiles strategy + +Baseline `setupFiles` are injected automatically by kind: + +- Unit baseline: `console.mock.ts`, `reset.mocks.ts` +- Int baseline: `console.mock.ts`, `reset.mocks.ts` +- E2E baseline: `reset.mocks.ts` + +Extend with additional files using `options.setupFiles` — they append after the baseline (paths are project-root-relative): + +```ts +export default createUnitConfig('core', { + projectRoot: new URL('../../', import.meta.url), + setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts'], +}); +``` + +Replace entirely using `overrideSetupFiles: true` (paths are project-root-relative): + +```ts +export default createUnitConfig('core', { + projectRoot: new URL('../../', import.meta.url), + overrideSetupFiles: true, + setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts', 'testing/test-setup/src/lib/fs.mock.ts'], +}); +``` + +### Using presets directly + +`vitest-setup-presets.ts` exposes grouped arrays you can compose if needed: + +```ts +import { setupPresets } from '../../tools/vitest-setup-presets.js'; + +export default createIntConfig('core', { + projectRoot: new URL('../../', import.meta.url), + setupFiles: [...setupPresets.int.portalClient], +}); +``` + +Preset keys: + +- `setupPresets.unit.{base,cliui,fs,git,portalClient,matchersCore,matcherPath}` +- `setupPresets.int.{base,cliui,fs,git,portalClient,matcherPath,chromePath}` +- `setupPresets.e2e.{base}` + +### Options reference + +`CreateVitestConfigOptions` (required + optional): + +- `projectKey` (string): coverage/cache naming. +- `kind` ('unit' | 'int' | 'e2e'): test kind. +- `projectRoot` (string | URL): absolute root for all paths. +- `include?: string[]`: override default include globs. +- `setupFiles?: string[]`: extra setup files (appended to baseline; project-root-relative). +- `overrideSetupFiles?: boolean`: skip baseline and use only provided list. +- `globalSetup?: string[]`: override default global setup (project-root-relative). +- `coverage?: { enabled?, exclude?, reportsSubdir? }` +- `testTimeout?: number`: e.g., for E2E. +- `typecheckInclude?: string[]`: include patterns for Vitest typecheck. +- `cacheKey?: string`: custom cache dir suffix. + +### Path and URL resolution + +- The factory requires `projectRoot` (string path or `URL`). +- Internally, it converts `projectRoot` into a `URL` and resolves all paths with `new URL(relativePath, projectRoot).pathname` to produce absolute filesystem paths. +- Affected fields: + - `cacheDir`, `test.cache.dir` + - `coverage.reportsDirectory` + - default `globalSetup` + - baseline `setupFiles` from presets and any extras you pass +- Expected inputs: + - `setupFiles` and `globalSetup` you pass should be project-root-relative strings. + - No `../../` paths are needed in configs; moving the factory won’t break resolution. + +### Merging behavior (arrays and overrides) + +- `setupFiles`: + - Baseline files (by kind) are injected automatically. + - Extras in `options.setupFiles` are appended after the baseline. + - Set `overrideSetupFiles: true` to replace the list entirely. +- `coverage.exclude`: + - Defaults to `['mocks/**', '**/types.ts']`. + - If you provide excludes, they are appended to the defaults. +- `include`, `globalSetup`, `testTimeout`, `typecheck.include`: + - If provided, they override the defaults for that suite. + +### Notes + +- Imports use `.js` extensions to work under ESM. +- No de-duplication of `setupFiles`. Avoid adding duplicates. +- You can opt-in to coverage for E2E by passing `coverage.enabled: true`. diff --git a/tools/vitest-config-factory.ts b/tools/vitest-config-factory.ts new file mode 100644 index 000000000..aa8b0c170 --- /dev/null +++ b/tools/vitest-config-factory.ts @@ -0,0 +1,151 @@ +/// +import { pathToFileURL } from 'node:url'; +import { defineConfig, mergeConfig } from 'vite'; +import type { UserConfig as ViteUserConfig } from 'vite'; +import type { CoverageOptions } from 'vitest'; +import { setupPresets } from './vitest-setup-presets.js'; +import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js'; + +export type TestKind = 'unit' | 'int' | 'e2e'; + +export interface CreateVitestConfigOptions { + projectKey: string; + kind: TestKind; + projectRoot: string | URL; + include?: string[]; + setupFiles?: string[]; + /** If true, the factory will not inject the baseline setupFiles for the given kind. */ + overrideSetupFiles?: boolean; + globalSetup?: string[]; + coverage?: { + enabled?: boolean; + exclude?: string[]; + reportsSubdir?: string; + }; + testTimeout?: number; + typecheckInclude?: string[]; + cacheKey?: string; +} + +export function createVitestConfig( + options: CreateVitestConfigOptions, +): ViteUserConfig { + const projectRootUrl: URL = + typeof options.projectRoot === 'string' + ? pathToFileURL( + options.projectRoot.endsWith('/') + ? options.projectRoot + : options.projectRoot + '/', + ) + : options.projectRoot; + const cacheDirName = options.cacheKey ?? options.projectKey; + const reportsSubdir = + options.coverage?.reportsSubdir ?? `${options.kind}-tests`; + const coverageEnabled = options.coverage?.enabled ?? options.kind !== 'e2e'; + const defaultGlobalSetup = + options.kind === 'e2e' + ? undefined + : [new URL('global-setup.ts', projectRootUrl).pathname]; + + type VitestAwareUserConfig = ViteUserConfig & { test?: unknown }; + const baselineSetupByKind: Record = { + unit: setupPresets.unit.base, + int: setupPresets.int.base, + e2e: setupPresets.e2e.base, + } as const; + + const resolveFromRoot = (relativePath: string): string => + new URL(relativePath, projectRootUrl).pathname; + const mapToAbsolute = ( + paths: readonly string[] | undefined, + ): string[] | undefined => + paths == null ? paths : paths.map(resolveFromRoot); + + const defaultExclude = ['mocks/**', '**/types.ts']; + + const baselineSetupAbs = mapToAbsolute(baselineSetupByKind[options.kind])!; + const extraSetupAbs = mapToAbsolute(options.setupFiles) ?? []; + const finalSetupFiles = options.overrideSetupFiles + ? extraSetupAbs + : extraSetupAbs.length > 0 + ? [...baselineSetupAbs, ...extraSetupAbs] + : undefined; // let base keep baseline when no extras + + const baseConfig: VitestAwareUserConfig = { + cacheDir: new URL(`node_modules/.vite/${cacheDirName}`, projectRootUrl) + .pathname, + test: { + reporters: ['basic'], + globals: true, + cache: { dir: new URL('node_modules/.vitest', projectRootUrl).pathname }, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + environment: 'node', + include: + options.kind === 'unit' + ? ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] + : options.kind === 'int' + ? ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] + : ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + globalSetup: defaultGlobalSetup, + setupFiles: baselineSetupAbs, + ...(coverageEnabled + ? { + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: new URL( + `coverage/${options.projectKey}/${reportsSubdir}`, + projectRootUrl, + ).pathname, + exclude: defaultExclude, + } as CoverageOptions, + } + : {}), + }, + }; + + const overrideConfig: VitestAwareUserConfig = { + test: { + ...(options.include ? { include: options.include } : {}), + ...(options.globalSetup + ? { globalSetup: mapToAbsolute(options.globalSetup) } + : {}), + ...(finalSetupFiles ? { setupFiles: finalSetupFiles } : {}), + ...(options.typecheckInclude + ? { typecheck: { include: options.typecheckInclude } } + : {}), + ...(options.testTimeout != null + ? { testTimeout: options.testTimeout } + : {}), + ...(coverageEnabled && options.coverage?.exclude + ? { + coverage: { + exclude: [...defaultExclude, ...options.coverage.exclude], + } as CoverageOptions, + } + : {}), + }, + }; + + const merged = mergeConfig( + baseConfig as ViteUserConfig, + overrideConfig as ViteUserConfig, + ); + return defineConfig(merged); +} + +export const createUnitConfig = ( + projectKey: string, + rest: Omit, +): ViteUserConfig => createVitestConfig({ projectKey, kind: 'unit', ...rest }); + +export const createIntConfig = ( + projectKey: string, + rest: Omit, +): ViteUserConfig => createVitestConfig({ projectKey, kind: 'int', ...rest }); + +export const createE2eConfig = ( + projectKey: string, + rest: Omit, +): ViteUserConfig => createVitestConfig({ projectKey, kind: 'e2e', ...rest }); diff --git a/tools/vitest-setup-presets.ts b/tools/vitest-setup-presets.ts new file mode 100644 index 000000000..4af8b329c --- /dev/null +++ b/tools/vitest-setup-presets.ts @@ -0,0 +1,33 @@ +export const setupPresets = { + unit: { + base: [ + 'testing/test-setup/src/lib/console.mock.ts', + 'testing/test-setup/src/lib/reset.mocks.ts', + ], + cliui: ['testing/test-setup/src/lib/cliui.mock.ts'], + fs: ['testing/test-setup/src/lib/fs.mock.ts'], + git: ['testing/test-setup/src/lib/git.mock.ts'], + portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], + matchersCore: [ + 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', + 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', + ], + matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], + }, + int: { + base: [ + 'testing/test-setup/src/lib/console.mock.ts', + 'testing/test-setup/src/lib/reset.mocks.ts', + ], + cliui: ['testing/test-setup/src/lib/cliui.mock.ts'], + fs: ['testing/test-setup/src/lib/fs.mock.ts'], + git: ['testing/test-setup/src/lib/git.mock.ts'], + portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], + matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], + chromePath: ['testing/test-setup/src/lib/chrome-path.mock.ts'], + }, + e2e: { + base: ['testing/test-setup/src/lib/reset.mocks.ts'], + }, +} as const; From db63fa17b636e5464de859d571057667077190c0 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Fri, 5 Sep 2025 13:14:37 +0200 Subject: [PATCH 02/13] refactor(): wip use common vitest on few files --- e2e/ci-e2e/vitest.e2e.config.ts | 26 +++------- .../vitest.e2e.config.ts | 29 +++-------- packages/core/vitest.int.config.ts | 31 ++---------- packages/core/vitest.unit.config.ts | 45 +++++------------ packages/utils/vitest.int.config.ts | 32 ++---------- packages/utils/vitest.unit.config.ts | 49 ++++++------------- 6 files changed, 48 insertions(+), 164 deletions(-) diff --git a/e2e/ci-e2e/vitest.e2e.config.ts b/e2e/ci-e2e/vitest.e2e.config.ts index 188911141..21f998dcd 100644 --- a/e2e/ci-e2e/vitest.e2e.config.ts +++ b/e2e/ci-e2e/vitest.e2e.config.ts @@ -1,22 +1,10 @@ /// -import { defineConfig } from 'vite'; -import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; +import { createE2eConfig } from '../../tools/vitest-config-factory.js'; -export default defineConfig({ - cacheDir: '../../node_modules/.vite/ci-e2e', - test: { - reporters: ['basic'], - testTimeout: 60_000, - globals: true, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - cache: { - dir: '../../node_modules/.vitest', - }, - environment: 'node', - include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - globalSetup: './global-setup.ts', - setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'], - }, +export default createE2eConfig('ci-e2e', { + projectRoot: new URL('../../', import.meta.url), + testTimeout: 60_000, + globalSetup: ['./global-setup.ts'], + coverage: { enabled: false }, + cacheKey: 'ci-e2e', }); diff --git a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts index 8f30d049b..62ddab384 100644 --- a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts +++ b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts @@ -1,26 +1,9 @@ /// -import { defineConfig } from 'vite'; -import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; +import { createE2eConfig } from '../../tools/vitest-config-factory.js'; -export default defineConfig({ - cacheDir: '../../node_modules/.vite/plugin-typescript-e2e', - test: { - reporters: ['basic'], - testTimeout: 20_000, - globals: true, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: '../../coverage/plugin-typescript-e2e/e2e-tests', - exclude: ['mocks/**', '**/types.ts'], - }, - cache: { - dir: '../../node_modules/.vitest', - }, - environment: 'node', - include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'], - }, +export default createE2eConfig('plugin-typescript-e2e', { + projectRoot: new URL('../../', import.meta.url), + testTimeout: 20_000, + coverage: { enabled: true, reportsSubdir: 'e2e-tests' }, + cacheKey: 'plugin-typescript-e2e', }); diff --git a/packages/core/vitest.int.config.ts b/packages/core/vitest.int.config.ts index 819c3a5bb..4d7b344ae 100644 --- a/packages/core/vitest.int.config.ts +++ b/packages/core/vitest.int.config.ts @@ -1,30 +1,7 @@ /// -import { defineConfig } from 'vite'; -import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; +import { createIntConfig } from '../../tools/vitest-config-factory.js'; -export default defineConfig({ - cacheDir: '../../node_modules/.vite/core', - test: { - reporters: ['basic'], - globals: true, - cache: { - dir: '../../node_modules/.vitest', - }, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: '../../coverage/core/int-tests', - exclude: ['mocks/**', '**/types.ts'], - }, - environment: 'node', - include: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - globalSetup: ['../../global-setup.ts'], - setupFiles: [ - '../../testing/test-setup/src/lib/console.mock.ts', - '../../testing/test-setup/src/lib/reset.mocks.ts', - '../../testing/test-setup/src/lib/portal-client.mock.ts', - ], - }, +export default createIntConfig('core', { + projectRoot: new URL('../../', import.meta.url), + setupFiles: ['testing/test-setup/src/lib/portal-client.mock.ts'], }); diff --git a/packages/core/vitest.unit.config.ts b/packages/core/vitest.unit.config.ts index 820eb538c..cd5cf0996 100644 --- a/packages/core/vitest.unit.config.ts +++ b/packages/core/vitest.unit.config.ts @@ -1,36 +1,15 @@ /// -import { defineConfig } from 'vite'; -import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; +import { createUnitConfig } from '../../tools/vitest-config-factory.js'; -export default defineConfig({ - cacheDir: '../../node_modules/.vite/core', - test: { - reporters: ['basic'], - globals: true, - cache: { - dir: '../../node_modules/.vitest', - }, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: '../../coverage/core/unit-tests', - exclude: ['mocks/**', '**/types.ts'], - }, - environment: 'node', - include: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - globalSetup: ['../../global-setup.ts'], - setupFiles: [ - '../../testing/test-setup/src/lib/cliui.mock.ts', - '../../testing/test-setup/src/lib/fs.mock.ts', - '../../testing/test-setup/src/lib/git.mock.ts', - '../../testing/test-setup/src/lib/console.mock.ts', - '../../testing/test-setup/src/lib/reset.mocks.ts', - '../../testing/test-setup/src/lib/portal-client.mock.ts', - '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', - '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts', - '../../testing/test-setup/src/lib/extend/jest-extended.matcher.ts', - ], - }, +export default createUnitConfig('core', { + projectRoot: new URL('../../', import.meta.url), + setupFiles: [ + 'testing/test-setup/src/lib/cliui.mock.ts', + 'testing/test-setup/src/lib/fs.mock.ts', + 'testing/test-setup/src/lib/git.mock.ts', + 'testing/test-setup/src/lib/portal-client.mock.ts', + 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', + 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', + ], }); diff --git a/packages/utils/vitest.int.config.ts b/packages/utils/vitest.int.config.ts index 61c01ddad..8b6b2ae1f 100644 --- a/packages/utils/vitest.int.config.ts +++ b/packages/utils/vitest.int.config.ts @@ -1,30 +1,8 @@ /// -import { defineConfig } from 'vite'; -import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; +import { createIntConfig } from '../../tools/vitest-config-factory.js'; -export default defineConfig({ - cacheDir: '../../node_modules/.vite/utils', - test: { - reporters: ['basic'], - globals: true, - cache: { - dir: '../../node_modules/.vitest', - }, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: '../../coverage/utils/int-tests', - exclude: ['mocks/**', 'perf/**', '**/types.ts'], - }, - environment: 'node', - include: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - globalSetup: ['../../global-setup.ts'], - setupFiles: [ - '../../testing/test-setup/src/lib/cliui.mock.ts', - '../../testing/test-setup/src/lib/console.mock.ts', - '../../testing/test-setup/src/lib/reset.mocks.ts', - ], - }, +export default createIntConfig('utils', { + projectRoot: new URL('../../', import.meta.url), + setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts'], + coverage: { exclude: ['perf/**'] }, }); diff --git a/packages/utils/vitest.unit.config.ts b/packages/utils/vitest.unit.config.ts index ac74f7af6..295c96dc9 100644 --- a/packages/utils/vitest.unit.config.ts +++ b/packages/utils/vitest.unit.config.ts @@ -1,38 +1,17 @@ /// -import { defineConfig } from 'vite'; -import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; +import { createUnitConfig } from '../../tools/vitest-config-factory.js'; -export default defineConfig({ - cacheDir: '../../node_modules/.vite/utils', - test: { - reporters: ['basic'], - globals: true, - cache: { - dir: '../../node_modules/.vitest', - }, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: '../../coverage/utils/unit-tests', - exclude: ['mocks/**', 'perf/**', '**/types.ts'], - }, - environment: 'node', - include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - typecheck: { - include: ['**/*.type.test.ts'], - }, - globalSetup: ['../../global-setup.ts'], - setupFiles: [ - '../../testing/test-setup/src/lib/cliui.mock.ts', - '../../testing/test-setup/src/lib/fs.mock.ts', - '../../testing/test-setup/src/lib/console.mock.ts', - '../../testing/test-setup/src/lib/reset.mocks.ts', - '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', - '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts', - '../../testing/test-setup/src/lib/extend/path.matcher.ts', - '../../testing/test-setup/src/lib/extend/jest-extended.matcher.ts', - ], - }, +export default createUnitConfig('utils', { + projectRoot: new URL('../../', import.meta.url), + include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + typecheckInclude: ['**/*.type.test.ts'], + setupFiles: [ + 'testing/test-setup/src/lib/cliui.mock.ts', + 'testing/test-setup/src/lib/fs.mock.ts', + 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', + 'testing/test-setup/src/lib/extend/path.matcher.ts', + 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', + ], + coverage: { exclude: ['perf/**'] }, }); From 45055f543c26fd20f15f1cc3efc06c72bb2fe6bf Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Fri, 5 Sep 2025 14:08:16 +0200 Subject: [PATCH 03/13] refactor(): change coverage reports directory --- .gitignore | 1 + e2e/plugin-typescript-e2e/vitest.e2e.config.ts | 2 +- tools/README.md | 6 +++--- tools/vitest-config-factory.ts | 7 +++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 5e706fa10..e6c77e8f8 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ node_modules /.sass-cache /connect.lock /coverage +**/.coverage/** /examples/react-todos-app/coverage /libpeerconnection.log npm-debug.log diff --git a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts index 62ddab384..75eabdab7 100644 --- a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts +++ b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts @@ -4,6 +4,6 @@ import { createE2eConfig } from '../../tools/vitest-config-factory.js'; export default createE2eConfig('plugin-typescript-e2e', { projectRoot: new URL('../../', import.meta.url), testTimeout: 20_000, - coverage: { enabled: true, reportsSubdir: 'e2e-tests' }, + coverage: { enabled: true }, cacheKey: 'plugin-typescript-e2e', }); diff --git a/tools/README.md b/tools/README.md index cd88d502f..e8fcd8875 100644 --- a/tools/README.md +++ b/tools/README.md @@ -45,8 +45,8 @@ Common to all kinds: Coverage: -- Unit/Int: enabled by default, reports to `/coverage//-tests` -- E2E: disabled by default +- Unit/Int: enabled by default, reports to `/packages//.coverage` +- E2E: disabled by default, reports to `/e2e//.coverage` if enabled - Default exclude: `['mocks/**', '**/types.ts']` Global setup: @@ -117,7 +117,7 @@ Preset keys: - `setupFiles?: string[]`: extra setup files (appended to baseline; project-root-relative). - `overrideSetupFiles?: boolean`: skip baseline and use only provided list. - `globalSetup?: string[]`: override default global setup (project-root-relative). -- `coverage?: { enabled?, exclude?, reportsSubdir? }` +- `coverage?: { enabled?, exclude? }` - `testTimeout?: number`: e.g., for E2E. - `typecheckInclude?: string[]`: include patterns for Vitest typecheck. - `cacheKey?: string`: custom cache dir suffix. diff --git a/tools/vitest-config-factory.ts b/tools/vitest-config-factory.ts index aa8b0c170..a4ec3b252 100644 --- a/tools/vitest-config-factory.ts +++ b/tools/vitest-config-factory.ts @@ -20,7 +20,6 @@ export interface CreateVitestConfigOptions { coverage?: { enabled?: boolean; exclude?: string[]; - reportsSubdir?: string; }; testTimeout?: number; typecheckInclude?: string[]; @@ -39,8 +38,6 @@ export function createVitestConfig( ) : options.projectRoot; const cacheDirName = options.cacheKey ?? options.projectKey; - const reportsSubdir = - options.coverage?.reportsSubdir ?? `${options.kind}-tests`; const coverageEnabled = options.coverage?.enabled ?? options.kind !== 'e2e'; const defaultGlobalSetup = options.kind === 'e2e' @@ -95,7 +92,9 @@ export function createVitestConfig( coverage: { reporter: ['text', 'lcov'], reportsDirectory: new URL( - `coverage/${options.projectKey}/${reportsSubdir}`, + options.kind === 'e2e' + ? `e2e/${options.projectKey}/.coverage` + : `packages/${options.projectKey}/.coverage`, projectRootUrl, ).pathname, exclude: defaultExclude, From a4275cf5f80d69b631cef767bb775e994fac5ebe Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 10:25:32 +0200 Subject: [PATCH 04/13] refactor(): simplify vitest factory and split abstract logic --- e2e/ci-e2e/vitest.e2e.config.ts | 23 ++- .../vitest.e2e.config.ts | 21 +- packages/core/vitest.int.config.ts | 20 +- packages/core/vitest.unit.config.ts | 33 +-- packages/utils/vitest.int.config.ts | 22 +- packages/utils/vitest.unit.config.ts | 37 ++-- testing/test-setup/src/lib/config/README.md | 71 +++++++ .../src/lib/config/vitest-config-factory.ts | 195 ++++++++++++++++++ .../src/lib/config/vitest-setup-presets.ts | 84 ++++++++ tools/README.md | 154 -------------- tools/vitest-config-factory.ts | 150 -------------- tools/vitest-setup-presets.ts | 33 --- tools/vitest-tsconfig-path-aliases.ts | 7 +- 13 files changed, 457 insertions(+), 393 deletions(-) create mode 100644 testing/test-setup/src/lib/config/README.md create mode 100644 testing/test-setup/src/lib/config/vitest-config-factory.ts create mode 100644 testing/test-setup/src/lib/config/vitest-setup-presets.ts delete mode 100644 tools/README.md delete mode 100644 tools/vitest-config-factory.ts delete mode 100644 tools/vitest-setup-presets.ts diff --git a/e2e/ci-e2e/vitest.e2e.config.ts b/e2e/ci-e2e/vitest.e2e.config.ts index 21f998dcd..730db91cf 100644 --- a/e2e/ci-e2e/vitest.e2e.config.ts +++ b/e2e/ci-e2e/vitest.e2e.config.ts @@ -1,10 +1,17 @@ /// -import { createE2eConfig } from '../../tools/vitest-config-factory.js'; +import { createE2eConfig } from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; -export default createE2eConfig('ci-e2e', { - projectRoot: new URL('../../', import.meta.url), - testTimeout: 60_000, - globalSetup: ['./global-setup.ts'], - coverage: { enabled: false }, - cacheKey: 'ci-e2e', -}); +export default createE2eConfig( + 'ci-e2e', + { + projectRoot: new URL('../../', import.meta.url), + cacheKey: 'ci-e2e', + }, + { + test: { + testTimeout: 60_000, + globalSetup: ['./global-setup.ts'], + coverage: { enabled: false }, + }, + }, +); diff --git a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts index 75eabdab7..a9a75bb81 100644 --- a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts +++ b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts @@ -1,9 +1,16 @@ /// -import { createE2eConfig } from '../../tools/vitest-config-factory.js'; +import { createE2eConfig } from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; -export default createE2eConfig('plugin-typescript-e2e', { - projectRoot: new URL('../../', import.meta.url), - testTimeout: 20_000, - coverage: { enabled: true }, - cacheKey: 'plugin-typescript-e2e', -}); +export default createE2eConfig( + 'plugin-typescript-e2e', + { + projectRoot: new URL('../../', import.meta.url), + cacheKey: 'plugin-typescript-e2e', + }, + { + test: { + testTimeout: 20_000, + coverage: { enabled: true }, + }, + }, +); diff --git a/packages/core/vitest.int.config.ts b/packages/core/vitest.int.config.ts index 4d7b344ae..b32e9afd9 100644 --- a/packages/core/vitest.int.config.ts +++ b/packages/core/vitest.int.config.ts @@ -1,7 +1,17 @@ /// -import { createIntConfig } from '../../tools/vitest-config-factory.js'; +import { + createIntConfig, + setupPresets, +} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; -export default createIntConfig('core', { - projectRoot: new URL('../../', import.meta.url), - setupFiles: ['testing/test-setup/src/lib/portal-client.mock.ts'], -}); +export default createIntConfig( + 'core', + { + projectRoot: new URL('../../', import.meta.url), + }, + { + test: { + setupFiles: [...setupPresets.int.base, ...setupPresets.int.portalClient], + }, + }, +); diff --git a/packages/core/vitest.unit.config.ts b/packages/core/vitest.unit.config.ts index cd5cf0996..09b28c623 100644 --- a/packages/core/vitest.unit.config.ts +++ b/packages/core/vitest.unit.config.ts @@ -1,15 +1,22 @@ /// -import { createUnitConfig } from '../../tools/vitest-config-factory.js'; +import { + createUnitConfig, + setupPresets, +} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; -export default createUnitConfig('core', { - projectRoot: new URL('../../', import.meta.url), - setupFiles: [ - 'testing/test-setup/src/lib/cliui.mock.ts', - 'testing/test-setup/src/lib/fs.mock.ts', - 'testing/test-setup/src/lib/git.mock.ts', - 'testing/test-setup/src/lib/portal-client.mock.ts', - 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', - 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', - 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', - ], -}); +export default createUnitConfig( + 'core', + { + projectRoot: new URL('../../', import.meta.url), + }, + { + test: { + setupFiles: [ + ...setupPresets.unit.base, + ...setupPresets.unit.git, + ...setupPresets.unit.portalClient, + ...setupPresets.unit.matchersCore, + ], + }, + }, +); diff --git a/packages/utils/vitest.int.config.ts b/packages/utils/vitest.int.config.ts index 8b6b2ae1f..0f75d8b29 100644 --- a/packages/utils/vitest.int.config.ts +++ b/packages/utils/vitest.int.config.ts @@ -1,8 +1,18 @@ /// -import { createIntConfig } from '../../tools/vitest-config-factory.js'; +import { + createIntConfig, + setupPresets, +} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; -export default createIntConfig('utils', { - projectRoot: new URL('../../', import.meta.url), - setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts'], - coverage: { exclude: ['perf/**'] }, -}); +export default createIntConfig( + 'utils', + { + projectRoot: new URL('../../', import.meta.url), + }, + { + test: { + coverage: { exclude: ['perf/**'] }, + setupFiles: [...setupPresets.int.base, ...setupPresets.int.cliui], + }, + }, +); diff --git a/packages/utils/vitest.unit.config.ts b/packages/utils/vitest.unit.config.ts index 295c96dc9..2030ba17d 100644 --- a/packages/utils/vitest.unit.config.ts +++ b/packages/utils/vitest.unit.config.ts @@ -1,17 +1,24 @@ /// -import { createUnitConfig } from '../../tools/vitest-config-factory.js'; +import { + createUnitConfig, + setupPresets, +} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; -export default createUnitConfig('utils', { - projectRoot: new URL('../../', import.meta.url), - include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - typecheckInclude: ['**/*.type.test.ts'], - setupFiles: [ - 'testing/test-setup/src/lib/cliui.mock.ts', - 'testing/test-setup/src/lib/fs.mock.ts', - 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', - 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', - 'testing/test-setup/src/lib/extend/path.matcher.ts', - 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', - ], - coverage: { exclude: ['perf/**'] }, -}); +export default createUnitConfig( + 'utils', + { + projectRoot: new URL('../../', import.meta.url), + }, + { + test: { + include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + typecheck: { include: ['**/*.type.test.ts'] }, + coverage: { exclude: ['perf/**'] }, + setupFiles: [ + ...setupPresets.unit.base, + ...setupPresets.unit.matchersCore, + ...setupPresets.unit.matcherPath, + ], + }, + }, +); diff --git a/testing/test-setup/src/lib/config/README.md b/testing/test-setup/src/lib/config/README.md new file mode 100644 index 000000000..ce364a69c --- /dev/null +++ b/testing/test-setup/src/lib/config/README.md @@ -0,0 +1,71 @@ +## Vitest config factory and setup presets + +This folder contains utilities to centralize and standardize Vitest configuration across the monorepo. + +### Files + +- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults. +- `vitest-setup-presets.ts`: provides create functions and exportable setup file groups. + +### Goals + +- Reduce duplication across `vitest.*.config.ts` files. +- Automatically include common setup files for each test type. +- Allow easy extension when additional setup files are needed. + +### How it works + +The create functions (`createUnitConfig`, `createIntConfig`, `createE2eConfig`) automatically include base setup files appropriate for each test type. If you need additional or different setup files, provide them in the test overrides and they will be used instead. + +### Defaults + +Common to all kinds: + +- `reporters: ['basic']`, `globals: true`, `environment: 'node'` +- `alias: tsconfigPathAliases()` +- `pool: 'threads'` with `singleThread: true` +- Cache directories resolved from `projectRoot` (absolute paths) + +Coverage: + +- Unit/Int: enabled by default, reports to `/packages//.coverage` +- E2E: disabled by default, reports to `/e2e//.coverage` if enabled +- Default exclude: `['mocks/**', '**/types.ts']` + +Global setup: + +- Unit/Int: `['/global-setup.ts']` by default +- E2E: none by default (set per-suite if needed) + +Include patterns: + +- Unit: `src/**/*.unit.test.*` +- Int: `src/**/*.int.test.*` +- E2E: `tests/**/*.e2e.test.*` + +### Setup files behavior + +**Automatic inclusion**: Each test type automatically includes its base setup files: + +- Unit tests: console mocking, cleanup, common UI/filesystem mocks, and basic matchers +- Integration tests: console mocking and cleanup only +- E2E tests: cleanup only + +**Custom setup files**: To use additional or different setup files, provide them in the test configuration overrides. The exported `setupPresets` object contains grouped setup files that can be combined as needed. + +**Available setup file groups**: + +- `setupPresets.unit.{base, git, portalClient, matchersCore, matcherPath}` +- `setupPresets.int.{base, cliui, fs, git, portalClient, matcherPath, chromePath}` +- `setupPresets.e2e.{base}` + +### Key parameters + +- `projectKey`: Used for cache and coverage directory naming +- `projectRoot`: Required path/URL to the project root for resolving all paths +- Standard Vitest configuration options can be provided in the overrides parameter + +### Notes + +- Coverage is enabled by default for unit/int tests, disabled by default for E2E tests +- All path resolution is handled automatically relative to the provided `projectRoot` diff --git a/testing/test-setup/src/lib/config/vitest-config-factory.ts b/testing/test-setup/src/lib/config/vitest-config-factory.ts new file mode 100644 index 000000000..b7c397da3 --- /dev/null +++ b/testing/test-setup/src/lib/config/vitest-config-factory.ts @@ -0,0 +1,195 @@ +import { pathToFileURL } from 'node:url'; +import { + type UserConfig as ViteUserConfig, + defineConfig, + mergeConfig, +} from 'vite'; +import type { CoverageOptions, InlineConfig } from 'vitest'; +import { tsconfigPathAliases } from '../../../../../tools/vitest-tsconfig-path-aliases.js'; + +export type TestKind = 'unit' | 'int' | 'e2e'; + +export type VitestConfigFactoryOptions = { + projectKey: string; + kind: TestKind; + projectRoot: string | URL; + cacheKey?: string; +}; + +export type VitestOverrides = ViteUserConfig & { test?: InlineConfig }; +export type ConfigRestParams = Pick< + VitestConfigFactoryOptions, + 'projectRoot' | 'cacheKey' +>; + +export function createVitestConfig( + options: VitestConfigFactoryOptions, + overrides: VitestOverrides = {}, +): ViteUserConfig { + const projectRootUrl: URL = + typeof options.projectRoot === 'string' + ? pathToFileURL( + options.projectRoot.endsWith('/') + ? options.projectRoot + : `${options.projectRoot}/`, + ) + : options.projectRoot; + const cacheDirName = options.cacheKey ?? options.projectKey; + + const coverageEnabled = + overrides.test?.coverage?.enabled ?? options.kind !== 'e2e'; + + const overrideSetupFiles = overrides.test?.setupFiles; + const setupFiles = overrideSetupFiles + ? toAbsolutePaths(projectRootUrl, normalizeSetupFiles(overrideSetupFiles)) + : []; + + const baseConfig = buildBaseConfig({ + projectKey: options.projectKey, + kind: options.kind, + projectRootUrl, + cacheDirName, + coverageEnabled, + setupFiles, + overrideExclude: + (overrides.test?.coverage?.exclude as string[] | undefined) ?? [], + }); + + const normalizedOverrides = sanitizeOverrides(overrides); + const merged = mergeConfig( + baseConfig as ViteUserConfig, + normalizedOverrides as ViteUserConfig, + ); + return defineConfig(merged); +} + +function toAbsolutePaths( + projectRootUrl: URL, + paths?: readonly string[], +): string[] { + return paths && paths.length > 0 + ? paths.filter(Boolean).map(p => new URL(p, projectRootUrl).pathname) + : []; +} + +function normalizeSetupFiles(setupFiles: string | readonly string[]): string[] { + return Array.isArray(setupFiles) + ? (setupFiles as string[]) + : [setupFiles as string]; +} + +function defaultInclude(kind: TestKind): string[] { + return kind === 'unit' + ? ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] + : kind === 'int' + ? ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] + : ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']; +} + +function defaultGlobalSetup( + kind: TestKind, + projectRootUrl: URL, +): string[] | undefined { + return kind === 'e2e' + ? undefined + : [new URL('global-setup.ts', projectRootUrl).pathname]; +} + +function buildCoverageConfig(params: { + projectKey: string; + kind: TestKind; + projectRootUrl: URL; + overrideExclude?: string[]; +}): CoverageOptions { + const defaultExclude = ['mocks/**', '**/types.ts']; + const reportsDirectory = new URL( + params.kind === 'e2e' + ? `e2e/${params.projectKey}/.coverage` + : `packages/${params.projectKey}/.coverage/${params.kind}-tests`, + params.projectRootUrl, + ).pathname; + return { + reporter: ['text', 'lcov'], + reportsDirectory, + exclude: + params.overrideExclude && params.overrideExclude.length > 0 + ? [...defaultExclude, ...params.overrideExclude] + : defaultExclude, + }; +} + +function buildBaseConfig(params: { + projectKey: string; + kind: TestKind; + projectRootUrl: URL; + cacheDirName: string; + coverageEnabled: boolean; + setupFiles: string[]; + overrideExclude: string[]; +}): VitestOverrides { + const cfg: VitestOverrides = { + cacheDir: new URL( + `node_modules/.vite/${params.cacheDirName}`, + params.projectRootUrl, + ).pathname, + test: { + reporters: ['basic'], + globals: true, + cache: { + dir: new URL('node_modules/.vitest', params.projectRootUrl).pathname, + }, + alias: tsconfigPathAliases(params.projectRootUrl), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + environment: 'node', + include: defaultInclude(params.kind), + globalSetup: defaultGlobalSetup(params.kind, params.projectRootUrl), + setupFiles: params.setupFiles, + ...(params.coverageEnabled + ? { + coverage: buildCoverageConfig({ + projectKey: params.projectKey, + kind: params.kind, + projectRootUrl: params.projectRootUrl, + overrideExclude: params.overrideExclude, + }), + } + : {}), + }, + }; + return cfg; +} + +function sanitizeCoverageOptions( + coverage: unknown, +): CoverageOptions | undefined { + if (!coverage) { + return undefined; + } + + const { + enabled: _en, + exclude: _ex, + ...rest + } = coverage as CoverageOptions & { + enabled?: boolean; + exclude?: string[]; + }; + return rest as CoverageOptions; +} + +function sanitizeOverrides(overrides: VitestOverrides): VitestOverrides { + if (!overrides?.test) { + return overrides; + } + + // Remove setupFiles from sanitization since we handle it directly in main logic + const { setupFiles: _sf, coverage, ...restTest } = overrides.test; + const sanitizedCoverage = sanitizeCoverageOptions(coverage); + + const sanitizedTest: InlineConfig = sanitizedCoverage + ? { ...restTest, coverage: sanitizedCoverage } + : restTest; + + return { ...overrides, test: sanitizedTest }; +} diff --git a/testing/test-setup/src/lib/config/vitest-setup-presets.ts b/testing/test-setup/src/lib/config/vitest-setup-presets.ts new file mode 100644 index 000000000..d887747fa --- /dev/null +++ b/testing/test-setup/src/lib/config/vitest-setup-presets.ts @@ -0,0 +1,84 @@ +import { + type ConfigRestParams, + type VitestOverrides, + createVitestConfig, +} from './vitest-config-factory.js'; + +const CONSOLE_MOCK_PATH = 'testing/test-setup/src/lib/console.mock.ts'; +const RESET_MOCKS_PATH = 'testing/test-setup/src/lib/reset.mocks.ts'; + +export const setupPresets = { + unit: { + base: [ + CONSOLE_MOCK_PATH, + RESET_MOCKS_PATH, + 'testing/test-setup/src/lib/cliui.mock.ts', + 'testing/test-setup/src/lib/fs.mock.ts', + 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + ], + git: ['testing/test-setup/src/lib/git.mock.ts'], + portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], + matchersCore: [ + 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', + 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', + ], + matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], + }, + int: { + base: [CONSOLE_MOCK_PATH, RESET_MOCKS_PATH], + cliui: ['testing/test-setup/src/lib/cliui.mock.ts'], + fs: ['testing/test-setup/src/lib/fs.mock.ts'], + git: ['testing/test-setup/src/lib/git.mock.ts'], + portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], + matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], + chromePath: ['testing/test-setup/src/lib/chrome-path.mock.ts'], + }, + e2e: { + base: [RESET_MOCKS_PATH], + }, +} as const; + +export const createUnitConfig = ( + projectKey: string, + rest: ConfigRestParams, + overrides?: VitestOverrides, +) => { + const finalSetupFiles = overrides?.test?.setupFiles ?? [ + ...setupPresets.unit.base, + ]; + + return createVitestConfig( + { projectKey, kind: 'unit', ...rest }, + { ...overrides, test: { ...overrides?.test, setupFiles: finalSetupFiles } }, + ); +}; + +export const createIntConfig = ( + projectKey: string, + rest: ConfigRestParams, + overrides?: VitestOverrides, +) => { + const finalSetupFiles = overrides?.test?.setupFiles ?? [ + ...setupPresets.int.base, + ]; + + return createVitestConfig( + { projectKey, kind: 'int', ...rest }, + { ...overrides, test: { ...overrides?.test, setupFiles: finalSetupFiles } }, + ); +}; + +export const createE2eConfig = ( + projectKey: string, + rest: ConfigRestParams, + overrides?: VitestOverrides, +) => { + const finalSetupFiles = overrides?.test?.setupFiles ?? [ + ...setupPresets.e2e.base, + ]; + + return createVitestConfig( + { projectKey, kind: 'e2e', ...rest }, + { ...overrides, test: { ...overrides?.test, setupFiles: finalSetupFiles } }, + ); +}; diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index e8fcd8875..000000000 --- a/tools/README.md +++ /dev/null @@ -1,154 +0,0 @@ -## Vitest config factory and setup presets - -This folder contains utilities to centralize and standardize Vitest configuration across the monorepo. - -### Files - -- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults. -- `vitest-setup-presets.ts`: reusable groups of `setupFiles` paths by test kind. - -### Goals - -- Reduce duplication across `vitest.*.config.ts` files. -- Keep per-package intent clear with minimal overrides. -- Provide safe defaults and easy extension points. - -### Quick start - -Use the factory in your suite configs (project root is required): - -```ts -/// -import { createE2eConfig, createIntConfig, createUnitConfig } from '../../tools/vitest-config-factory.js'; - -export default createUnitConfig('core', { - projectRoot: new URL('../../', import.meta.url), -}); -``` - -Creators: - -- `createUnitConfig(projectKey, { projectRoot, ...options })` -- `createIntConfig(projectKey, { projectRoot, ...options })` -- `createE2eConfig(projectKey, { projectRoot, ...options })` - -`projectKey` is used for cache and coverage directories. - -### Defaults - -Common to all kinds: - -- `reporters: ['basic']`, `globals: true`, `environment: 'node'` -- `alias: tsconfigPathAliases()` -- `pool: 'threads'` with `singleThread: true` -- Cache directories resolved from `projectRoot` (absolute paths) - -Coverage: - -- Unit/Int: enabled by default, reports to `/packages//.coverage` -- E2E: disabled by default, reports to `/e2e//.coverage` if enabled -- Default exclude: `['mocks/**', '**/types.ts']` - -Global setup: - -- Unit/Int: `['/global-setup.ts']` by default -- E2E: none by default (set per-suite if needed) - -Include patterns: - -- Unit: `src/**/*.unit.test.*` -- Int: `src/**/*.int.test.*` -- E2E: `tests/**/*.e2e.test.*` - -### setupFiles strategy - -Baseline `setupFiles` are injected automatically by kind: - -- Unit baseline: `console.mock.ts`, `reset.mocks.ts` -- Int baseline: `console.mock.ts`, `reset.mocks.ts` -- E2E baseline: `reset.mocks.ts` - -Extend with additional files using `options.setupFiles` — they append after the baseline (paths are project-root-relative): - -```ts -export default createUnitConfig('core', { - projectRoot: new URL('../../', import.meta.url), - setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts'], -}); -``` - -Replace entirely using `overrideSetupFiles: true` (paths are project-root-relative): - -```ts -export default createUnitConfig('core', { - projectRoot: new URL('../../', import.meta.url), - overrideSetupFiles: true, - setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts', 'testing/test-setup/src/lib/fs.mock.ts'], -}); -``` - -### Using presets directly - -`vitest-setup-presets.ts` exposes grouped arrays you can compose if needed: - -```ts -import { setupPresets } from '../../tools/vitest-setup-presets.js'; - -export default createIntConfig('core', { - projectRoot: new URL('../../', import.meta.url), - setupFiles: [...setupPresets.int.portalClient], -}); -``` - -Preset keys: - -- `setupPresets.unit.{base,cliui,fs,git,portalClient,matchersCore,matcherPath}` -- `setupPresets.int.{base,cliui,fs,git,portalClient,matcherPath,chromePath}` -- `setupPresets.e2e.{base}` - -### Options reference - -`CreateVitestConfigOptions` (required + optional): - -- `projectKey` (string): coverage/cache naming. -- `kind` ('unit' | 'int' | 'e2e'): test kind. -- `projectRoot` (string | URL): absolute root for all paths. -- `include?: string[]`: override default include globs. -- `setupFiles?: string[]`: extra setup files (appended to baseline; project-root-relative). -- `overrideSetupFiles?: boolean`: skip baseline and use only provided list. -- `globalSetup?: string[]`: override default global setup (project-root-relative). -- `coverage?: { enabled?, exclude? }` -- `testTimeout?: number`: e.g., for E2E. -- `typecheckInclude?: string[]`: include patterns for Vitest typecheck. -- `cacheKey?: string`: custom cache dir suffix. - -### Path and URL resolution - -- The factory requires `projectRoot` (string path or `URL`). -- Internally, it converts `projectRoot` into a `URL` and resolves all paths with `new URL(relativePath, projectRoot).pathname` to produce absolute filesystem paths. -- Affected fields: - - `cacheDir`, `test.cache.dir` - - `coverage.reportsDirectory` - - default `globalSetup` - - baseline `setupFiles` from presets and any extras you pass -- Expected inputs: - - `setupFiles` and `globalSetup` you pass should be project-root-relative strings. - - No `../../` paths are needed in configs; moving the factory won’t break resolution. - -### Merging behavior (arrays and overrides) - -- `setupFiles`: - - Baseline files (by kind) are injected automatically. - - Extras in `options.setupFiles` are appended after the baseline. - - Set `overrideSetupFiles: true` to replace the list entirely. -- `coverage.exclude`: - - Defaults to `['mocks/**', '**/types.ts']`. - - If you provide excludes, they are appended to the defaults. -- `include`, `globalSetup`, `testTimeout`, `typecheck.include`: - - If provided, they override the defaults for that suite. - -### Notes - -- Imports use `.js` extensions to work under ESM. -- No de-duplication of `setupFiles`. Avoid adding duplicates. -- You can opt-in to coverage for E2E by passing `coverage.enabled: true`. diff --git a/tools/vitest-config-factory.ts b/tools/vitest-config-factory.ts deleted file mode 100644 index a4ec3b252..000000000 --- a/tools/vitest-config-factory.ts +++ /dev/null @@ -1,150 +0,0 @@ -/// -import { pathToFileURL } from 'node:url'; -import { defineConfig, mergeConfig } from 'vite'; -import type { UserConfig as ViteUserConfig } from 'vite'; -import type { CoverageOptions } from 'vitest'; -import { setupPresets } from './vitest-setup-presets.js'; -import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js'; - -export type TestKind = 'unit' | 'int' | 'e2e'; - -export interface CreateVitestConfigOptions { - projectKey: string; - kind: TestKind; - projectRoot: string | URL; - include?: string[]; - setupFiles?: string[]; - /** If true, the factory will not inject the baseline setupFiles for the given kind. */ - overrideSetupFiles?: boolean; - globalSetup?: string[]; - coverage?: { - enabled?: boolean; - exclude?: string[]; - }; - testTimeout?: number; - typecheckInclude?: string[]; - cacheKey?: string; -} - -export function createVitestConfig( - options: CreateVitestConfigOptions, -): ViteUserConfig { - const projectRootUrl: URL = - typeof options.projectRoot === 'string' - ? pathToFileURL( - options.projectRoot.endsWith('/') - ? options.projectRoot - : options.projectRoot + '/', - ) - : options.projectRoot; - const cacheDirName = options.cacheKey ?? options.projectKey; - const coverageEnabled = options.coverage?.enabled ?? options.kind !== 'e2e'; - const defaultGlobalSetup = - options.kind === 'e2e' - ? undefined - : [new URL('global-setup.ts', projectRootUrl).pathname]; - - type VitestAwareUserConfig = ViteUserConfig & { test?: unknown }; - const baselineSetupByKind: Record = { - unit: setupPresets.unit.base, - int: setupPresets.int.base, - e2e: setupPresets.e2e.base, - } as const; - - const resolveFromRoot = (relativePath: string): string => - new URL(relativePath, projectRootUrl).pathname; - const mapToAbsolute = ( - paths: readonly string[] | undefined, - ): string[] | undefined => - paths == null ? paths : paths.map(resolveFromRoot); - - const defaultExclude = ['mocks/**', '**/types.ts']; - - const baselineSetupAbs = mapToAbsolute(baselineSetupByKind[options.kind])!; - const extraSetupAbs = mapToAbsolute(options.setupFiles) ?? []; - const finalSetupFiles = options.overrideSetupFiles - ? extraSetupAbs - : extraSetupAbs.length > 0 - ? [...baselineSetupAbs, ...extraSetupAbs] - : undefined; // let base keep baseline when no extras - - const baseConfig: VitestAwareUserConfig = { - cacheDir: new URL(`node_modules/.vite/${cacheDirName}`, projectRootUrl) - .pathname, - test: { - reporters: ['basic'], - globals: true, - cache: { dir: new URL('node_modules/.vitest', projectRootUrl).pathname }, - alias: tsconfigPathAliases(), - pool: 'threads', - poolOptions: { threads: { singleThread: true } }, - environment: 'node', - include: - options.kind === 'unit' - ? ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] - : options.kind === 'int' - ? ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] - : ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - globalSetup: defaultGlobalSetup, - setupFiles: baselineSetupAbs, - ...(coverageEnabled - ? { - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: new URL( - options.kind === 'e2e' - ? `e2e/${options.projectKey}/.coverage` - : `packages/${options.projectKey}/.coverage`, - projectRootUrl, - ).pathname, - exclude: defaultExclude, - } as CoverageOptions, - } - : {}), - }, - }; - - const overrideConfig: VitestAwareUserConfig = { - test: { - ...(options.include ? { include: options.include } : {}), - ...(options.globalSetup - ? { globalSetup: mapToAbsolute(options.globalSetup) } - : {}), - ...(finalSetupFiles ? { setupFiles: finalSetupFiles } : {}), - ...(options.typecheckInclude - ? { typecheck: { include: options.typecheckInclude } } - : {}), - ...(options.testTimeout != null - ? { testTimeout: options.testTimeout } - : {}), - ...(coverageEnabled && options.coverage?.exclude - ? { - coverage: { - exclude: [...defaultExclude, ...options.coverage.exclude], - } as CoverageOptions, - } - : {}), - }, - }; - - const merged = mergeConfig( - baseConfig as ViteUserConfig, - overrideConfig as ViteUserConfig, - ); - return defineConfig(merged); -} - -export const createUnitConfig = ( - projectKey: string, - rest: Omit, -): ViteUserConfig => createVitestConfig({ projectKey, kind: 'unit', ...rest }); - -export const createIntConfig = ( - projectKey: string, - rest: Omit, -): ViteUserConfig => createVitestConfig({ projectKey, kind: 'int', ...rest }); - -export const createE2eConfig = ( - projectKey: string, - rest: Omit, -): ViteUserConfig => createVitestConfig({ projectKey, kind: 'e2e', ...rest }); diff --git a/tools/vitest-setup-presets.ts b/tools/vitest-setup-presets.ts deleted file mode 100644 index 4af8b329c..000000000 --- a/tools/vitest-setup-presets.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const setupPresets = { - unit: { - base: [ - 'testing/test-setup/src/lib/console.mock.ts', - 'testing/test-setup/src/lib/reset.mocks.ts', - ], - cliui: ['testing/test-setup/src/lib/cliui.mock.ts'], - fs: ['testing/test-setup/src/lib/fs.mock.ts'], - git: ['testing/test-setup/src/lib/git.mock.ts'], - portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], - matchersCore: [ - 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', - 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', - 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', - ], - matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], - }, - int: { - base: [ - 'testing/test-setup/src/lib/console.mock.ts', - 'testing/test-setup/src/lib/reset.mocks.ts', - ], - cliui: ['testing/test-setup/src/lib/cliui.mock.ts'], - fs: ['testing/test-setup/src/lib/fs.mock.ts'], - git: ['testing/test-setup/src/lib/git.mock.ts'], - portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], - matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], - chromePath: ['testing/test-setup/src/lib/chrome-path.mock.ts'], - }, - e2e: { - base: ['testing/test-setup/src/lib/reset.mocks.ts'], - }, -} as const; diff --git a/tools/vitest-tsconfig-path-aliases.ts b/tools/vitest-tsconfig-path-aliases.ts index ac8be04df..44d3353d0 100644 --- a/tools/vitest-tsconfig-path-aliases.ts +++ b/tools/vitest-tsconfig-path-aliases.ts @@ -1,8 +1,11 @@ import { loadConfig } from 'tsconfig-paths'; import type { Alias, AliasOptions } from 'vite'; -export function tsconfigPathAliases(): AliasOptions { - const result = loadConfig('tsconfig.base.json'); +export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions { + const tsconfigPath = projectRootUrl + ? new URL('tsconfig.base.json', projectRootUrl).pathname + : 'tsconfig.base.json'; + const result = loadConfig(tsconfigPath); if (result.resultType === 'failed') { throw new Error( `Failed to load path aliases from tsconfig for Vitest: ${result.message}`, From 998e8c05197f7acf36f1c0d3bc7d3283a7baafd9 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 12:00:47 +0200 Subject: [PATCH 05/13] docs(): update new tools readme --- testing/test-setup/README.md | 4 ++ testing/test-setup/src/lib/config/README.md | 73 ++++++++------------- 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/testing/test-setup/README.md b/testing/test-setup/README.md index db0ce1eba..3e9c2c236 100644 --- a/testing/test-setup/README.md +++ b/testing/test-setup/README.md @@ -4,6 +4,10 @@ This library contains test setup. More on this subject as well as all the testing strategy principles can be found on the GitHub [wiki](https://github.com/code-pushup/cli/wiki/Testing-Strategy#mocking). +## Shared config + +[README](./src/lib/config/README.md) how to use vitest config factory. + ## Mock setup In this library you can find all files that can be used in `setupFiles` property of `vitest.config.(unit|int|e2e).ts` files. Currently include: diff --git a/testing/test-setup/src/lib/config/README.md b/testing/test-setup/src/lib/config/README.md index ce364a69c..730c625a0 100644 --- a/testing/test-setup/src/lib/config/README.md +++ b/testing/test-setup/src/lib/config/README.md @@ -1,71 +1,50 @@ ## Vitest config factory and setup presets -This folder contains utilities to centralize and standardize Vitest configuration across the monorepo. +Utilities to centralize and standardize Vitest configuration across the monorepo. -### Files +- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults +- `vitest-setup-presets.ts`: provides create functions and exportable setup file groups -- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults. -- `vitest-setup-presets.ts`: provides create functions and exportable setup file groups. - -### Goals - -- Reduce duplication across `vitest.*.config.ts` files. -- Automatically include common setup files for each test type. -- Allow easy extension when additional setup files are needed. - -### How it works - -The create functions (`createUnitConfig`, `createIntConfig`, `createE2eConfig`) automatically include base setup files appropriate for each test type. If you need additional or different setup files, provide them in the test overrides and they will be used instead. +The create functions (`createUnitConfig`, `createIntConfig`, `createE2eConfig`) automatically include appropriate setup files for each test type. ### Defaults -Common to all kinds: - -- `reporters: ['basic']`, `globals: true`, `environment: 'node'` -- `alias: tsconfigPathAliases()` -- `pool: 'threads'` with `singleThread: true` -- Cache directories resolved from `projectRoot` (absolute paths) - -Coverage: - -- Unit/Int: enabled by default, reports to `/packages//.coverage` -- E2E: disabled by default, reports to `/e2e//.coverage` if enabled -- Default exclude: `['mocks/**', '**/types.ts']` +**Common**: `reporters: ['basic']`, `globals: true`, `environment: 'node'`, `pool: 'threads'` with `singleThread: true`, alias from tsconfig paths -Global setup: +**Coverage**: Unit/Int enabled (reports to `/packages//.coverage`), E2E disabled. Excludes `['mocks/**', '**/types.ts']` -- Unit/Int: `['/global-setup.ts']` by default -- E2E: none by default (set per-suite if needed) +**Global setup**: Unit/Int use `['/global-setup.ts']`, E2E none by default -Include patterns: +**Include patterns**: Unit `src/**/*.unit.test.*`, Int `src/**/*.int.test.*`, E2E `tests/**/*.e2e.test.*` -- Unit: `src/**/*.unit.test.*` -- Int: `src/**/*.int.test.*` -- E2E: `tests/**/*.e2e.test.*` +### Setup files -### Setup files behavior +**Automatic inclusion**: Unit (console mocking, cleanup, UI/filesystem mocks, basic matchers), Int (console mocking, cleanup), E2E (cleanup only) -**Automatic inclusion**: Each test type automatically includes its base setup files: - -- Unit tests: console mocking, cleanup, common UI/filesystem mocks, and basic matchers -- Integration tests: console mocking and cleanup only -- E2E tests: cleanup only - -**Custom setup files**: To use additional or different setup files, provide them in the test configuration overrides. The exported `setupPresets` object contains grouped setup files that can be combined as needed. - -**Available setup file groups**: +**Custom setup files**: ⚠️ Specifying `setupFiles` in overrides will completely replace the defaults. To extend the default list, manually combine them with `setupPresets`: - `setupPresets.unit.{base, git, portalClient, matchersCore, matcherPath}` - `setupPresets.int.{base, cliui, fs, git, portalClient, matcherPath, chromePath}` - `setupPresets.e2e.{base}` -### Key parameters +### Parameters - `projectKey`: Used for cache and coverage directory naming - `projectRoot`: Required path/URL to the project root for resolving all paths - Standard Vitest configuration options can be provided in the overrides parameter -### Notes +### Examples + +**Using defaults:** + +```ts +export default createUnitConfig('my-package', import.meta.url); +``` + +**Extending default setup files:** -- Coverage is enabled by default for unit/int tests, disabled by default for E2E tests -- All path resolution is handled automatically relative to the provided `projectRoot` +```ts +export default createIntConfig('my-package', import.meta.url, { + setupFiles: [...setupPresets.int.base, ...setupPresets.int.git, './custom-setup.ts'], +}); +``` From d8537531b7aab012bc3925e486d6f8195c501699 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 12:01:03 +0200 Subject: [PATCH 06/13] test(): add unit tests for new tools --- .../config/vitest-config-factory.unit.test.ts | 699 ++++++++++++++++++ .../config/vitest-setup-presets.unit.test.ts | 331 +++++++++ 2 files changed, 1030 insertions(+) create mode 100644 testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts create mode 100644 testing/test-setup/src/lib/config/vitest-setup-presets.unit.test.ts diff --git a/testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts b/testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts new file mode 100644 index 000000000..767f82314 --- /dev/null +++ b/testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts @@ -0,0 +1,699 @@ +import { pathToFileURL } from 'node:url'; +import { defineConfig } from 'vite'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + type TestKind, + type VitestConfigFactoryOptions, + type VitestOverrides, + createVitestConfig, +} from './vitest-config-factory.js'; + +// Only mock defineConfig - assume it works correctly, we're not testing Vite +// Use importOriginal to keep mergeConfig real while mocking defineConfig +vi.mock('vite', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + defineConfig: vi.fn(config => config), + }; +}); + +// Mock tsconfigPathAliases since it reads from filesystem and our fake paths don't exist +vi.mock('../../../../../tools/vitest-tsconfig-path-aliases.js', () => ({ + tsconfigPathAliases: vi.fn().mockReturnValue({ '@mock/alias': '/mock/path' }), +})); + +const MOCK_PROJECT_ROOT_STRING = '/Users/test/project'; +const MOCK_PROJECT_ROOT_URL = pathToFileURL(`${MOCK_PROJECT_ROOT_STRING}/`); + +const TEST_TIMEOUTS = { + SHORT: 5000, + MEDIUM: 10_000, + LONG: 30_000, +} as const; + +const EXPECTED_INCLUDES = { + unit: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + int: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + e2e: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], +} as const; + +const DEFAULT_EXCLUDES = ['mocks/**', '**/types.ts'] as const; + +const expectCoverageConfig = (config: any, expectedProps: Partial) => { + expect(config.test.coverage).toEqual(expect.objectContaining(expectedProps)); +}; + +describe('createVitestConfig', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('basic functionality', () => { + it('should create a basic unit test config with string projectRoot', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const config = createVitestConfig(options); + + expect(config).toEqual( + expect.objectContaining({ + cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/test-package`, + test: expect.objectContaining({ + reporters: ['basic'], + globals: true, + cache: { + dir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vitest`, + }, + alias: expect.any(Object), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + environment: 'node', + include: EXPECTED_INCLUDES.unit, + globalSetup: [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + setupFiles: [], + coverage: expect.objectContaining({ + reporter: ['text', 'lcov'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + exclude: DEFAULT_EXCLUDES, + }), + }), + }), + ); + expect(defineConfig).toHaveBeenCalledWith(config); + expect(defineConfig).toHaveBeenCalledTimes(1); + }); + + it('should create a basic unit test config with URL projectRoot', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_URL, + }; + + const config = createVitestConfig(options); + + expect(config).toEqual( + expect.objectContaining({ + cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/test-package`, + test: expect.objectContaining({ + include: EXPECTED_INCLUDES.unit, + globalSetup: [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + }), + }), + ); + }); + + it('should handle projectRoot string without trailing slash', () => { + const projectRoot = '/Users/test/project'; + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot, + }; + + const config = createVitestConfig(options); + + expect((config as any).test.alias).toBeDefined(); + }); + + it('should handle projectRoot string with trailing slash', () => { + const projectRoot = '/Users/test/project/'; + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot, + }; + + const config = createVitestConfig(options); + + expect((config as any).test.alias).toBeDefined(); + }); + }); + + describe('test kind variations', () => { + it('should create integration test config', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'int', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const config = createVitestConfig(options); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + include: EXPECTED_INCLUDES.int, + globalSetup: [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + coverage: expect.objectContaining({ + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/int-tests`, + }), + }), + }), + ); + }); + + it('should create e2e test config without coverage by default', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'e2e', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const config = createVitestConfig(options); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + include: EXPECTED_INCLUDES.e2e, + globalSetup: undefined, + }), + }), + ); + + expect((config as any).test.coverage).toBeUndefined(); + }); + + it('should create e2e test config with coverage when explicitly enabled', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'e2e', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: { + enabled: true, + }, + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + include: EXPECTED_INCLUDES.e2e, + globalSetup: undefined, + coverage: expect.objectContaining({ + reporter: ['text', 'lcov'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/e2e/test-package/.coverage`, + exclude: DEFAULT_EXCLUDES, + }), + }), + }), + ); + }); + }); + + describe('cacheKey option', () => { + it('should use cacheKey when provided', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + cacheKey: 'custom-cache-key', + }; + + const config = createVitestConfig(options); + + expect(config).toEqual( + expect.objectContaining({ + cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/custom-cache-key`, + }), + ); + }); + + it('should fallback to projectKey when cacheKey is not provided', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const config = createVitestConfig(options); + + expect(config).toEqual( + expect.objectContaining({ + cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/test-package`, + }), + ); + }); + }); + + describe('setupFiles handling', () => { + it('should handle setupFiles as string in overrides', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + setupFiles: 'setup.ts', + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + setupFiles: [`${MOCK_PROJECT_ROOT_STRING}/setup.ts`], + }), + }), + ); + }); + + it('should handle setupFiles as array in overrides', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + setupFiles: ['setup1.ts', 'setup2.ts'], + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + setupFiles: [ + `${MOCK_PROJECT_ROOT_STRING}/setup1.ts`, + `${MOCK_PROJECT_ROOT_STRING}/setup2.ts`, + ], + }), + }), + ); + }); + + it('should filter out falsy values from setupFiles', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + setupFiles: ['setup1.ts', '', 'setup2.ts', null as any, 'setup3.ts'], + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + setupFiles: [ + `${MOCK_PROJECT_ROOT_STRING}/setup1.ts`, + `${MOCK_PROJECT_ROOT_STRING}/setup2.ts`, + `${MOCK_PROJECT_ROOT_STRING}/setup3.ts`, + ], + }), + }), + ); + }); + + it('should handle empty setupFiles array', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + setupFiles: [], + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + setupFiles: [], + }), + }), + ); + }); + + it('should use empty setupFiles when not provided in overrides', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const config = createVitestConfig(options, {}); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + setupFiles: [], + }), + }), + ); + }); + }); + + describe('coverage configuration', () => { + it('should apply custom coverage exclude paths', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: { + exclude: ['custom/**', 'ignore/**'], + }, + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + coverage: expect.objectContaining({ + reporter: ['text', 'lcov'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + exclude: [...DEFAULT_EXCLUDES, 'custom/**', 'ignore/**'], + }), + }), + }), + ); + }); + + it('should use default exclude when no custom excludes provided', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: { + exclude: [], + }, + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + test: expect.objectContaining({ + coverage: expect.objectContaining({ + reporter: ['text', 'lcov'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + exclude: DEFAULT_EXCLUDES, + }), + }), + }), + ); + }); + + it('should disable coverage when explicitly disabled', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: { + enabled: false, + }, + }, + }; + + const config = createVitestConfig(options, overrides); + + expect((config as any).test.coverage).toStrictEqual({}); + expect(config).toBeDefined(); + }); + + it('should sanitize coverage options by removing enabled and exclude from overrides', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: { + enabled: true, + exclude: ['custom/**'], + reporter: ['html', 'json'], + thresholds: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + }, + }, + }; + + const config = createVitestConfig(options, overrides); + expectCoverageConfig(config, { + reporter: ['text', 'lcov', 'html', 'json'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + exclude: [...DEFAULT_EXCLUDES, 'custom/**'], + thresholds: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + }); + + expect(config).toBeDefined(); + }); + }); + + describe('config merging and sanitization', () => { + it('should merge base config with overrides', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + testTimeout: TEST_TIMEOUTS.MEDIUM, + pool: 'forks' as any, + }, + }; + + const config = createVitestConfig(options, overrides); + + const testConfig = (config as any).test; + expect(testConfig.testTimeout).toBe(TEST_TIMEOUTS.MEDIUM); + expect(testConfig.pool).toBe('forks'); + }); + + it('should sanitize overrides by removing setupFiles from test config', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + setupFiles: ['should-be-removed.ts'], + testTimeout: TEST_TIMEOUTS.SHORT, + pool: 'forks' as any, + }, + }; + + const config = createVitestConfig(options, overrides); + + const testConfig = (config as any).test; + expect(testConfig.setupFiles).toEqual([ + `${MOCK_PROJECT_ROOT_STRING}/should-be-removed.ts`, + ]); + expect(testConfig.testTimeout).toBe(TEST_TIMEOUTS.SHORT); + expect(testConfig.pool).toBe('forks'); + }); + + it('should handle overrides without test config', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + build: { + target: 'node14', + }, + }; + + const config = createVitestConfig(options, overrides); + + expect((config as any).build.target).toBe('node14'); + }); + + it('should handle coverage options as undefined', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: undefined, + testTimeout: TEST_TIMEOUTS.SHORT, + }, + }; + + const config = createVitestConfig(options, overrides); + + expect((config as any).test.testTimeout).toBe(TEST_TIMEOUTS.SHORT); + expectCoverageConfig(config, { + reporter: ['text', 'lcov'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + exclude: DEFAULT_EXCLUDES, + }); + }); + + it('should handle coverage options as null', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'unit', + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const overrides: VitestOverrides = { + test: { + coverage: null as any, + testTimeout: TEST_TIMEOUTS.SHORT, + }, + }; + + const config = createVitestConfig(options, overrides); + + expect((config as any).test.testTimeout).toBe(TEST_TIMEOUTS.SHORT); + expectCoverageConfig(config, { + reporter: ['text', 'lcov'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + exclude: DEFAULT_EXCLUDES, + }); + }); + }); + + describe('edge cases and error handling', () => { + it('should handle all test kinds correctly', () => { + const testKinds: TestKind[] = ['unit', 'int', 'e2e']; + + testKinds.forEach(kind => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind, + projectRoot: MOCK_PROJECT_ROOT_STRING, + }; + + const config = createVitestConfig(options); + + const expectedIncludes = { + unit: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + int: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + e2e: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }; + + expect((config as any).test.include).toStrictEqual( + expectedIncludes[kind], + ); + expect((config as any).test.globalSetup).toStrictEqual( + kind === 'e2e' + ? undefined + : [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + ); + }); + }); + + it('should handle complex override scenarios', () => { + const options: VitestConfigFactoryOptions = { + projectKey: 'test-package', + kind: 'int', + projectRoot: MOCK_PROJECT_ROOT_STRING, + cacheKey: 'complex-scenario', + }; + + const overrides: VitestOverrides = { + test: { + setupFiles: ['setup1.ts', 'setup2.ts'], + coverage: { + enabled: true, + exclude: ['e2e/**', 'dist/**'], + reporter: ['lcov', 'text-summary'], + thresholds: { + global: { + statements: 90, + branches: 85, + functions: 90, + lines: 90, + }, + }, + }, + testTimeout: TEST_TIMEOUTS.LONG, + environment: 'jsdom' as any, + }, + build: { + target: 'es2020', + }, + }; + + const config = createVitestConfig(options, overrides); + + expect(config).toEqual( + expect.objectContaining({ + cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/complex-scenario`, + build: { + target: 'es2020', + }, + test: expect.objectContaining({ + setupFiles: [ + `${MOCK_PROJECT_ROOT_STRING}/setup1.ts`, + `${MOCK_PROJECT_ROOT_STRING}/setup2.ts`, + ], + testTimeout: TEST_TIMEOUTS.LONG, + environment: 'jsdom', + include: EXPECTED_INCLUDES.int, + coverage: expect.objectContaining({ + exclude: ['mocks/**', '**/types.ts', 'e2e/**', 'dist/**'], + reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/int-tests`, + }), + }), + }), + ); + }); + }); +}); diff --git a/testing/test-setup/src/lib/config/vitest-setup-presets.unit.test.ts b/testing/test-setup/src/lib/config/vitest-setup-presets.unit.test.ts new file mode 100644 index 000000000..8051a7588 --- /dev/null +++ b/testing/test-setup/src/lib/config/vitest-setup-presets.unit.test.ts @@ -0,0 +1,331 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { VitestOverrides } from './vitest-config-factory.js'; +import * as configFactory from './vitest-config-factory.js'; +import { + createE2eConfig, + createIntConfig, + createUnitConfig, + setupPresets, +} from './vitest-setup-presets.js'; + +vi.mock('./vitest-config-factory.js', () => ({ + createVitestConfig: vi.fn().mockReturnValue('mocked-config'), +})); + +const MOCK_PROJECT_KEY = 'test-package'; +const MOCK_CONFIG_REST_PARAMS = { + projectRoot: '/test/project', + cacheKey: 'test-cache', +}; + +const TEST_TIMEOUTS = { + SHORT: 5000, + MEDIUM: 10_000, + LONG: 15_000, + E2E: 60_000, +} as const; + +describe('setupPresets', () => { + it('should export correct unit setup presets', () => { + expect(setupPresets.unit).toEqual({ + base: [ + 'testing/test-setup/src/lib/console.mock.ts', + 'testing/test-setup/src/lib/reset.mocks.ts', + 'testing/test-setup/src/lib/cliui.mock.ts', + 'testing/test-setup/src/lib/fs.mock.ts', + 'testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + ], + git: ['testing/test-setup/src/lib/git.mock.ts'], + portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], + matchersCore: [ + 'testing/test-setup/src/lib/extend/markdown-table.matcher.ts', + 'testing/test-setup/src/lib/extend/jest-extended.matcher.ts', + ], + matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], + }); + }); + + it('should export correct integration setup presets', () => { + expect(setupPresets.int).toEqual({ + base: [ + 'testing/test-setup/src/lib/console.mock.ts', + 'testing/test-setup/src/lib/reset.mocks.ts', + ], + cliui: ['testing/test-setup/src/lib/cliui.mock.ts'], + fs: ['testing/test-setup/src/lib/fs.mock.ts'], + git: ['testing/test-setup/src/lib/git.mock.ts'], + portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'], + matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'], + chromePath: ['testing/test-setup/src/lib/chrome-path.mock.ts'], + }); + }); + + it('should export correct e2e setup presets', () => { + expect(setupPresets.e2e).toEqual({ + base: ['testing/test-setup/src/lib/reset.mocks.ts'], + }); + }); + + it('should be defined as a const object', () => { + expect(setupPresets).toBeDefined(); + expect(typeof setupPresets).toBe('object'); + }); +}); + +// Parameterized tests to eliminate duplication +describe.each([ + ['unit', setupPresets.unit.base, createUnitConfig], + ['int', setupPresets.int.base, createIntConfig], + ['e2e', setupPresets.e2e.base, createE2eConfig], +] as const)('%s config creation', (kind, baseSetupFiles, createFn) => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should call createVitestConfig with correct parameters and default setupFiles', () => { + createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: baseSetupFiles, + }, + }, + ); + }); + + it('should use custom setupFiles from overrides when provided', () => { + const customSetupFiles = [`${kind}-setup1.ts`, `${kind}-setup2.ts`]; + const overrides: VitestOverrides = { + test: { + setupFiles: customSetupFiles, + }, + }; + + createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: customSetupFiles, + }, + }, + ); + }); + + it('should merge other overrides correctly while using default setupFiles', () => { + const testTimeout = + kind === 'unit' + ? TEST_TIMEOUTS.MEDIUM + : kind === 'int' + ? TEST_TIMEOUTS.LONG + : TEST_TIMEOUTS.E2E; + + const overrides: VitestOverrides = { + test: { + testTimeout, + globals: false, + }, + build: { + target: 'es2020', + }, + }; + + createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + testTimeout, + globals: false, + setupFiles: baseSetupFiles, + }, + build: { + target: 'es2020', + }, + }, + ); + }); + + it('should handle overrides with custom setupFiles and other test options', () => { + const customSetupFiles = [`${kind}-custom.ts`]; + const overrides: VitestOverrides = { + test: { + setupFiles: customSetupFiles, + testTimeout: TEST_TIMEOUTS.SHORT, + environment: 'jsdom' as any, + }, + }; + + createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: customSetupFiles, + testTimeout: TEST_TIMEOUTS.SHORT, + environment: 'jsdom', + }, + }, + ); + }); + + it('should handle undefined overrides', () => { + createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, undefined); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: baseSetupFiles, + }, + }, + ); + }); + + it('should handle overrides without test config', () => { + const overrides: VitestOverrides = { + build: { + target: 'es2020', + }, + }; + + createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + build: { + target: 'es2020', + }, + test: { + setupFiles: baseSetupFiles, + }, + }, + ); + }); + + it('should return the result from createVitestConfig', () => { + const result = createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + + expect(result).toBe('mocked-config'); + }); + + it('should handle empty projectKey gracefully', () => { + const result = createFn('', MOCK_CONFIG_REST_PARAMS); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: '', + kind, + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: baseSetupFiles, + }, + }, + ); + expect(result).toBe('mocked-config'); + }); +}); + +describe('integration between preset functions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should use different setup presets for different test kinds', () => { + createUnitConfig('test-pkg', { projectRoot: '/test' }); + createIntConfig('test-pkg', { projectRoot: '/test' }); + createE2eConfig('test-pkg', { projectRoot: '/test' }); + + expect(configFactory.createVitestConfig).toHaveBeenCalledTimes(3); + + expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith( + 1, + { projectKey: 'test-pkg', kind: 'unit', projectRoot: '/test' }, + { test: { setupFiles: setupPresets.unit.base } }, + ); + + expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith( + 2, + { projectKey: 'test-pkg', kind: 'int', projectRoot: '/test' }, + { test: { setupFiles: setupPresets.int.base } }, + ); + + expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith( + 3, + { projectKey: 'test-pkg', kind: 'e2e', projectRoot: '/test' }, + { test: { setupFiles: setupPresets.e2e.base } }, + ); + }); + + it('should handle complex scenarios with all preset functions', () => { + const complexOverrides: VitestOverrides = { + test: { + setupFiles: ['global-setup.ts'], + testTimeout: TEST_TIMEOUTS.LONG, + coverage: { + enabled: true, + thresholds: { + global: { + statements: 90, + }, + }, + }, + }, + build: { + target: 'es2022', + }, + }; + + const restParams = { + projectRoot: '/complex/project', + cacheKey: 'complex-cache', + }; + + createUnitConfig('complex-unit', restParams, complexOverrides); + createIntConfig('complex-int', restParams, complexOverrides); + createE2eConfig('complex-e2e', restParams, complexOverrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledTimes(3); + + const calls = (configFactory.createVitestConfig as any).mock.calls; + calls.forEach((call: any) => { + expect(call[1].test.setupFiles).toStrictEqual(['global-setup.ts']); + }); + + expect(calls[0][0].kind).toBe('unit'); + expect(calls[1][0].kind).toBe('int'); + expect(calls[2][0].kind).toBe('e2e'); + }); +}); From fec5f10d1cdd43175cfd811338651c8cdc1b86a4 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 15:02:41 +0200 Subject: [PATCH 07/13] refactor(): fix circular dependency by creating new internal library --- e2e/ci-e2e/vitest.e2e.config.ts | 2 +- .../vitest.e2e.config.ts | 2 +- packages/core/vitest.int.config.ts | 2 +- packages/core/vitest.unit.config.ts | 2 +- packages/utils/vitest.int.config.ts | 2 +- packages/utils/vitest.unit.config.ts | 2 +- .../config => test-setup-config}/README.md | 0 testing/test-setup-config/eslint.config.js | 12 +++++++++ testing/test-setup-config/project.json | 12 +++++++++ testing/test-setup-config/src/index.ts | 14 +++++++++++ .../src/lib}/vitest-config-factory.ts | 2 +- .../lib}/vitest-config-factory.unit.test.ts | 2 +- .../src/lib}/vitest-setup-presets.ts | 0 .../lib}/vitest-setup-presets.unit.test.ts | 0 .../src/lib/vitest-tsconfig-path-aliases.ts | 25 +++++++++++++++++++ testing/test-setup-config/tsconfig.json | 22 ++++++++++++++++ testing/test-setup-config/tsconfig.lib.json | 15 +++++++++++ testing/test-setup-config/tsconfig.test.json | 14 +++++++++++ .../test-setup-config/vitest.unit.config.ts | 23 +++++++++++++++++ tsconfig.base.json | 3 +++ 20 files changed, 148 insertions(+), 8 deletions(-) rename testing/{test-setup/src/lib/config => test-setup-config}/README.md (100%) create mode 100644 testing/test-setup-config/eslint.config.js create mode 100644 testing/test-setup-config/project.json create mode 100644 testing/test-setup-config/src/index.ts rename testing/{test-setup/src/lib/config => test-setup-config/src/lib}/vitest-config-factory.ts (98%) rename testing/{test-setup/src/lib/config => test-setup-config/src/lib}/vitest-config-factory.unit.test.ts (99%) rename testing/{test-setup/src/lib/config => test-setup-config/src/lib}/vitest-setup-presets.ts (100%) rename testing/{test-setup/src/lib/config => test-setup-config/src/lib}/vitest-setup-presets.unit.test.ts (100%) create mode 100644 testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts create mode 100644 testing/test-setup-config/tsconfig.json create mode 100644 testing/test-setup-config/tsconfig.lib.json create mode 100644 testing/test-setup-config/tsconfig.test.json create mode 100644 testing/test-setup-config/vitest.unit.config.ts diff --git a/e2e/ci-e2e/vitest.e2e.config.ts b/e2e/ci-e2e/vitest.e2e.config.ts index 730db91cf..37ea143d8 100644 --- a/e2e/ci-e2e/vitest.e2e.config.ts +++ b/e2e/ci-e2e/vitest.e2e.config.ts @@ -1,5 +1,5 @@ /// -import { createE2eConfig } from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; +import { createE2eConfig } from '../../testing/test-setup-config/src/lib/vitest-setup-presets.js'; export default createE2eConfig( 'ci-e2e', diff --git a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts index a9a75bb81..1e9fd4768 100644 --- a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts +++ b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts @@ -1,5 +1,5 @@ /// -import { createE2eConfig } from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; +import { createE2eConfig } from '../../testing/test-setup-config/src/lib/vitest-setup-presets.js'; export default createE2eConfig( 'plugin-typescript-e2e', diff --git a/packages/core/vitest.int.config.ts b/packages/core/vitest.int.config.ts index b32e9afd9..981ee4e19 100644 --- a/packages/core/vitest.int.config.ts +++ b/packages/core/vitest.int.config.ts @@ -2,7 +2,7 @@ import { createIntConfig, setupPresets, -} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; +} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.js'; export default createIntConfig( 'core', diff --git a/packages/core/vitest.unit.config.ts b/packages/core/vitest.unit.config.ts index 09b28c623..d11f288f9 100644 --- a/packages/core/vitest.unit.config.ts +++ b/packages/core/vitest.unit.config.ts @@ -2,7 +2,7 @@ import { createUnitConfig, setupPresets, -} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; +} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.js'; export default createUnitConfig( 'core', diff --git a/packages/utils/vitest.int.config.ts b/packages/utils/vitest.int.config.ts index 0f75d8b29..1aeadf3d4 100644 --- a/packages/utils/vitest.int.config.ts +++ b/packages/utils/vitest.int.config.ts @@ -2,7 +2,7 @@ import { createIntConfig, setupPresets, -} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; +} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.js'; export default createIntConfig( 'utils', diff --git a/packages/utils/vitest.unit.config.ts b/packages/utils/vitest.unit.config.ts index 2030ba17d..66bb6dd51 100644 --- a/packages/utils/vitest.unit.config.ts +++ b/packages/utils/vitest.unit.config.ts @@ -2,7 +2,7 @@ import { createUnitConfig, setupPresets, -} from '../../testing/test-setup/src/lib/config/vitest-setup-presets.js'; +} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.js'; export default createUnitConfig( 'utils', diff --git a/testing/test-setup/src/lib/config/README.md b/testing/test-setup-config/README.md similarity index 100% rename from testing/test-setup/src/lib/config/README.md rename to testing/test-setup-config/README.md diff --git a/testing/test-setup-config/eslint.config.js b/testing/test-setup-config/eslint.config.js new file mode 100644 index 000000000..2656b27cb --- /dev/null +++ b/testing/test-setup-config/eslint.config.js @@ -0,0 +1,12 @@ +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; + +export default tseslint.config(...baseConfig, { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/testing/test-setup-config/project.json b/testing/test-setup-config/project.json new file mode 100644 index 000000000..c4b32ba8a --- /dev/null +++ b/testing/test-setup-config/project.json @@ -0,0 +1,12 @@ +{ + "name": "test-setup-config", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "testing/test-setup/src", + "projectType": "library", + "targets": { + "build": {}, + "lint": {}, + "unit-test": {} + }, + "tags": ["scope:shared", "type:testing"] +} diff --git a/testing/test-setup-config/src/index.ts b/testing/test-setup-config/src/index.ts new file mode 100644 index 000000000..ef10c13bc --- /dev/null +++ b/testing/test-setup-config/src/index.ts @@ -0,0 +1,14 @@ +export { + createVitestConfig, + type TestKind, + type VitestConfigFactoryOptions, + type VitestOverrides, + type ConfigRestParams, +} from './lib/vitest-config-factory.js'; + +export { + setupPresets, + createUnitConfig, + createIntConfig, + createE2eConfig, +} from './lib/vitest-setup-presets.js'; diff --git a/testing/test-setup/src/lib/config/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts similarity index 98% rename from testing/test-setup/src/lib/config/vitest-config-factory.ts rename to testing/test-setup-config/src/lib/vitest-config-factory.ts index b7c397da3..4ad7cae09 100644 --- a/testing/test-setup/src/lib/config/vitest-config-factory.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts @@ -5,7 +5,7 @@ import { mergeConfig, } from 'vite'; import type { CoverageOptions, InlineConfig } from 'vitest'; -import { tsconfigPathAliases } from '../../../../../tools/vitest-tsconfig-path-aliases.js'; +import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js'; export type TestKind = 'unit' | 'int' | 'e2e'; diff --git a/testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts similarity index 99% rename from testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts rename to testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts index 767f82314..91359434d 100644 --- a/testing/test-setup/src/lib/config/vitest-config-factory.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts @@ -19,7 +19,7 @@ vi.mock('vite', async importOriginal => { }); // Mock tsconfigPathAliases since it reads from filesystem and our fake paths don't exist -vi.mock('../../../../../tools/vitest-tsconfig-path-aliases.js', () => ({ +vi.mock('./vitest-tsconfig-path-aliases.js', () => ({ tsconfigPathAliases: vi.fn().mockReturnValue({ '@mock/alias': '/mock/path' }), })); diff --git a/testing/test-setup/src/lib/config/vitest-setup-presets.ts b/testing/test-setup-config/src/lib/vitest-setup-presets.ts similarity index 100% rename from testing/test-setup/src/lib/config/vitest-setup-presets.ts rename to testing/test-setup-config/src/lib/vitest-setup-presets.ts diff --git a/testing/test-setup/src/lib/config/vitest-setup-presets.unit.test.ts b/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts similarity index 100% rename from testing/test-setup/src/lib/config/vitest-setup-presets.unit.test.ts rename to testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts diff --git a/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts b/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts new file mode 100644 index 000000000..2566c1ae7 --- /dev/null +++ b/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts @@ -0,0 +1,25 @@ +import { loadConfig } from 'tsconfig-paths'; +import type { Alias, AliasOptions } from 'vite'; + +export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions { + const tsconfigPath = projectRootUrl + ? new URL('tsconfig.base.json', projectRootUrl).pathname + : 'tsconfig.base.json'; + const result = loadConfig(tsconfigPath); + if (result.resultType === 'failed') { + throw new Error( + `Failed to load path aliases from tsconfig for Vitest: ${result.message}`, + ); + } + return Object.entries(result.paths) + .map(([key, value]) => [key, value[0]]) + .filter((pair): pair is [string, string] => pair[1] != null) + .map( + ([importPath, relativePath]): Alias => ({ + find: importPath, + replacement: projectRootUrl + ? new URL(relativePath, projectRootUrl).pathname + : new URL(`../${relativePath}`, import.meta.url).pathname, + }), + ); +} diff --git a/testing/test-setup-config/tsconfig.json b/testing/test-setup-config/tsconfig.json new file mode 100644 index 000000000..465306e46 --- /dev/null +++ b/testing/test-setup-config/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.test.json" + } + ] +} diff --git a/testing/test-setup-config/tsconfig.lib.json b/testing/test-setup-config/tsconfig.lib.json new file mode 100644 index 000000000..3cc313086 --- /dev/null +++ b/testing/test-setup-config/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": [ + "vitest.unit.config.ts", + "src/vitest.d.ts", + "src/**/*.unit.test.ts", + "src/**/*.int.test.ts" + ] +} diff --git a/testing/test-setup-config/tsconfig.test.json b/testing/test-setup-config/tsconfig.test.json new file mode 100644 index 000000000..5fddc20ae --- /dev/null +++ b/testing/test-setup-config/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vitest.unit.config.ts", + "src/vitest.d.ts", + "src/**/*.unit.test.ts", + "src/**/*.d.ts", + "src/**/*.int.test.ts" + ] +} diff --git a/testing/test-setup-config/vitest.unit.config.ts b/testing/test-setup-config/vitest.unit.config.ts new file mode 100644 index 000000000..95bef2c28 --- /dev/null +++ b/testing/test-setup-config/vitest.unit.config.ts @@ -0,0 +1,23 @@ +/// +import { + createUnitConfig, + setupPresets, +} from './src/lib/vitest-setup-presets.js'; + +export default createUnitConfig( + 'test-setup-config', + { + projectRoot: new URL('../../', import.meta.url), + }, + { + test: { + include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: [...setupPresets.unit.base, ...setupPresets.unit.matcherPath], + coverage: { + enabled: true, + reporter: ['text', 'lcov'], + exclude: ['**/*.mock.{mjs,ts}', '**/*.config.{js,mjs,ts}'], + }, + }, + }, +); diff --git a/tsconfig.base.json b/tsconfig.base.json index 7f3b4d21f..4bca1c69f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -39,6 +39,9 @@ "@code-pushup/nx-plugin": ["packages/nx-plugin/src/index.ts"], "@code-pushup/test-nx-utils": ["testing/test-nx-utils/src/index.ts"], "@code-pushup/test-setup": ["testing/test-setup/src/index.ts"], + "@code-pushup/test-setup-config": [ + "testing/test-setup-config/src/index.ts" + ], "@code-pushup/test-utils": ["testing/test-utils/src/index.ts"], "@code-pushup/typescript-plugin": [ "packages/plugin-typescript/src/index.ts" From 7b831c3c582c4f32da3488f8899d1c9ad79e0466 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 15:09:22 +0200 Subject: [PATCH 08/13] fix: path resolution --- .../src/lib/vitest-tsconfig-path-aliases.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts b/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts index 2566c1ae7..aec88e199 100644 --- a/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts +++ b/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts @@ -1,9 +1,11 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { loadConfig } from 'tsconfig-paths'; import type { Alias, AliasOptions } from 'vite'; export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions { const tsconfigPath = projectRootUrl - ? new URL('tsconfig.base.json', projectRootUrl).pathname + ? path.resolve(fileURLToPath(projectRootUrl), 'tsconfig.base.json') : 'tsconfig.base.json'; const result = loadConfig(tsconfigPath); if (result.resultType === 'failed') { @@ -18,7 +20,7 @@ export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions { ([importPath, relativePath]): Alias => ({ find: importPath, replacement: projectRootUrl - ? new URL(relativePath, projectRootUrl).pathname + ? path.resolve(fileURLToPath(projectRootUrl), relativePath) : new URL(`../${relativePath}`, import.meta.url).pathname, }), ); From dd45d45e3fbcbf0a252c30826b1235b3030b2d4d Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 15:19:57 +0200 Subject: [PATCH 09/13] fix: path resolution #2 --- .../src/lib/vitest-config-factory.ts | 26 ++++++++++++------- tools/vitest-tsconfig-path-aliases.ts | 8 ++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts index 4ad7cae09..ec7cef34a 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts @@ -1,4 +1,5 @@ -import { pathToFileURL } from 'node:url'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { type UserConfig as ViteUserConfig, defineConfig, @@ -68,7 +69,9 @@ function toAbsolutePaths( paths?: readonly string[], ): string[] { return paths && paths.length > 0 - ? paths.filter(Boolean).map(p => new URL(p, projectRootUrl).pathname) + ? paths + .filter(Boolean) + .map(p => path.resolve(fileURLToPath(projectRootUrl), p)) : []; } @@ -92,7 +95,7 @@ function defaultGlobalSetup( ): string[] | undefined { return kind === 'e2e' ? undefined - : [new URL('global-setup.ts', projectRootUrl).pathname]; + : [path.resolve(fileURLToPath(projectRootUrl), 'global-setup.ts')]; } function buildCoverageConfig(params: { @@ -102,12 +105,12 @@ function buildCoverageConfig(params: { overrideExclude?: string[]; }): CoverageOptions { const defaultExclude = ['mocks/**', '**/types.ts']; - const reportsDirectory = new URL( + const reportsDirectory = path.resolve( + fileURLToPath(params.projectRootUrl), params.kind === 'e2e' ? `e2e/${params.projectKey}/.coverage` : `packages/${params.projectKey}/.coverage/${params.kind}-tests`, - params.projectRootUrl, - ).pathname; + ); return { reporter: ['text', 'lcov'], reportsDirectory, @@ -128,15 +131,18 @@ function buildBaseConfig(params: { overrideExclude: string[]; }): VitestOverrides { const cfg: VitestOverrides = { - cacheDir: new URL( + cacheDir: path.resolve( + fileURLToPath(params.projectRootUrl), `node_modules/.vite/${params.cacheDirName}`, - params.projectRootUrl, - ).pathname, + ), test: { reporters: ['basic'], globals: true, cache: { - dir: new URL('node_modules/.vitest', params.projectRootUrl).pathname, + dir: path.resolve( + fileURLToPath(params.projectRootUrl), + 'node_modules/.vitest', + ), }, alias: tsconfigPathAliases(params.projectRootUrl), pool: 'threads', diff --git a/tools/vitest-tsconfig-path-aliases.ts b/tools/vitest-tsconfig-path-aliases.ts index 44d3353d0..aec88e199 100644 --- a/tools/vitest-tsconfig-path-aliases.ts +++ b/tools/vitest-tsconfig-path-aliases.ts @@ -1,9 +1,11 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { loadConfig } from 'tsconfig-paths'; import type { Alias, AliasOptions } from 'vite'; export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions { const tsconfigPath = projectRootUrl - ? new URL('tsconfig.base.json', projectRootUrl).pathname + ? path.resolve(fileURLToPath(projectRootUrl), 'tsconfig.base.json') : 'tsconfig.base.json'; const result = loadConfig(tsconfigPath); if (result.resultType === 'failed') { @@ -17,7 +19,9 @@ export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions { .map( ([importPath, relativePath]): Alias => ({ find: importPath, - replacement: new URL(`../${relativePath}`, import.meta.url).pathname, + replacement: projectRootUrl + ? path.resolve(fileURLToPath(projectRootUrl), relativePath) + : new URL(`../${relativePath}`, import.meta.url).pathname, }), ); } From 413f9d7809117acf8fa116b16e7f21f49fdb8c29 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 15:27:13 +0200 Subject: [PATCH 10/13] fix: path resolution #3 --- .../src/lib/vitest-config-factory.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts index ec7cef34a..a045e477c 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts @@ -71,7 +71,7 @@ function toAbsolutePaths( return paths && paths.length > 0 ? paths .filter(Boolean) - .map(p => path.resolve(fileURLToPath(projectRootUrl), p)) + .map(p => path.resolve(getProjectRootPath(projectRootUrl), p)) : []; } @@ -95,7 +95,7 @@ function defaultGlobalSetup( ): string[] | undefined { return kind === 'e2e' ? undefined - : [path.resolve(fileURLToPath(projectRootUrl), 'global-setup.ts')]; + : [path.resolve(getProjectRootPath(projectRootUrl), 'global-setup.ts')]; } function buildCoverageConfig(params: { @@ -106,7 +106,7 @@ function buildCoverageConfig(params: { }): CoverageOptions { const defaultExclude = ['mocks/**', '**/types.ts']; const reportsDirectory = path.resolve( - fileURLToPath(params.projectRootUrl), + getProjectRootPath(params.projectRootUrl), params.kind === 'e2e' ? `e2e/${params.projectKey}/.coverage` : `packages/${params.projectKey}/.coverage/${params.kind}-tests`, @@ -132,7 +132,7 @@ function buildBaseConfig(params: { }): VitestOverrides { const cfg: VitestOverrides = { cacheDir: path.resolve( - fileURLToPath(params.projectRootUrl), + getProjectRootPath(params.projectRootUrl), `node_modules/.vite/${params.cacheDirName}`, ), test: { @@ -140,7 +140,7 @@ function buildBaseConfig(params: { globals: true, cache: { dir: path.resolve( - fileURLToPath(params.projectRootUrl), + getProjectRootPath(params.projectRootUrl), 'node_modules/.vitest', ), }, @@ -199,3 +199,13 @@ function sanitizeOverrides(overrides: VitestOverrides): VitestOverrides { return { ...overrides, test: sanitizedTest }; } + +function getProjectRootPath(projectRootUrl: URL): string { + try { + return fileURLToPath(projectRootUrl); + } catch { + // Fallback for non-file:// URLs or invalid URLs + const pathname = projectRootUrl.pathname; + return pathname.startsWith('/') ? pathname : `/${pathname}`; + } +} From 1f9d9a5ba2245110f6a0957c9ff3e1bd079f5b1e Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 15:37:13 +0200 Subject: [PATCH 11/13] fix: path resolution #4 --- .../lib/vitest-config-factory.unit.test.ts | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts index 91359434d..1ebe0cde5 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts @@ -1,3 +1,4 @@ +import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { defineConfig } from 'vite'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -26,6 +27,18 @@ vi.mock('./vitest-tsconfig-path-aliases.js', () => ({ const MOCK_PROJECT_ROOT_STRING = '/Users/test/project'; const MOCK_PROJECT_ROOT_URL = pathToFileURL(`${MOCK_PROJECT_ROOT_STRING}/`); +// Cross-platform path helpers to match what the actual code generates +const mockPath = (...segments: string[]) => + path.resolve(MOCK_PROJECT_ROOT_STRING, ...segments); +const mockCacheDir = (name: string) => mockPath('node_modules', '.vite', name); +const mockVitestCacheDir = () => mockPath('node_modules', '.vitest'); +const mockGlobalSetup = () => mockPath('global-setup.ts'); +const mockReportsDir = (projectKey: string, kind: string) => + kind === 'e2e' + ? mockPath('e2e', projectKey, '.coverage') + : mockPath('packages', projectKey, '.coverage', `${kind}-tests`); +const mockSetupFile = mockPath; + const TEST_TIMEOUTS = { SHORT: 5000, MEDIUM: 10_000, @@ -61,23 +74,23 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/test-package`, + cacheDir: mockCacheDir('test-package'), test: expect.objectContaining({ reporters: ['basic'], globals: true, cache: { - dir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vitest`, + dir: mockVitestCacheDir(), }, alias: expect.any(Object), pool: 'threads', poolOptions: { threads: { singleThread: true } }, environment: 'node', include: EXPECTED_INCLUDES.unit, - globalSetup: [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + globalSetup: [mockGlobalSetup()], setupFiles: [], coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + reportsDirectory: mockReportsDir('test-package', 'unit'), exclude: DEFAULT_EXCLUDES, }), }), @@ -98,10 +111,10 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/test-package`, + cacheDir: mockCacheDir('test-package'), test: expect.objectContaining({ include: EXPECTED_INCLUDES.unit, - globalSetup: [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + globalSetup: [mockGlobalSetup()], }), }), ); @@ -148,9 +161,9 @@ describe('createVitestConfig', () => { expect.objectContaining({ test: expect.objectContaining({ include: EXPECTED_INCLUDES.int, - globalSetup: [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + globalSetup: [mockGlobalSetup()], coverage: expect.objectContaining({ - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/int-tests`, + reportsDirectory: mockReportsDir('test-package', 'int'), }), }), }), @@ -202,7 +215,7 @@ describe('createVitestConfig', () => { globalSetup: undefined, coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/e2e/test-package/.coverage`, + reportsDirectory: mockReportsDir('test-package', 'e2e'), exclude: DEFAULT_EXCLUDES, }), }), @@ -224,7 +237,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/custom-cache-key`, + cacheDir: mockCacheDir('custom-cache-key'), }), ); }); @@ -240,7 +253,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/test-package`, + cacheDir: mockCacheDir('test-package'), }), ); }); @@ -265,7 +278,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ test: expect.objectContaining({ - setupFiles: [`${MOCK_PROJECT_ROOT_STRING}/setup.ts`], + setupFiles: [mockSetupFile('setup.ts')], }), }), ); @@ -290,8 +303,8 @@ describe('createVitestConfig', () => { expect.objectContaining({ test: expect.objectContaining({ setupFiles: [ - `${MOCK_PROJECT_ROOT_STRING}/setup1.ts`, - `${MOCK_PROJECT_ROOT_STRING}/setup2.ts`, + mockSetupFile('setup1.ts'), + mockSetupFile('setup2.ts'), ], }), }), @@ -317,9 +330,9 @@ describe('createVitestConfig', () => { expect.objectContaining({ test: expect.objectContaining({ setupFiles: [ - `${MOCK_PROJECT_ROOT_STRING}/setup1.ts`, - `${MOCK_PROJECT_ROOT_STRING}/setup2.ts`, - `${MOCK_PROJECT_ROOT_STRING}/setup3.ts`, + mockSetupFile('setup1.ts'), + mockSetupFile('setup2.ts'), + mockSetupFile('setup3.ts'), ], }), }), @@ -392,7 +405,7 @@ describe('createVitestConfig', () => { test: expect.objectContaining({ coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + reportsDirectory: mockReportsDir('test-package', 'unit'), exclude: [...DEFAULT_EXCLUDES, 'custom/**', 'ignore/**'], }), }), @@ -422,7 +435,7 @@ describe('createVitestConfig', () => { test: expect.objectContaining({ coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + reportsDirectory: mockReportsDir('test-package', 'unit'), exclude: DEFAULT_EXCLUDES, }), }), @@ -479,7 +492,7 @@ describe('createVitestConfig', () => { const config = createVitestConfig(options, overrides); expectCoverageConfig(config, { reporter: ['text', 'lcov', 'html', 'json'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + reportsDirectory: mockReportsDir('test-package', 'unit'), exclude: [...DEFAULT_EXCLUDES, 'custom/**'], thresholds: { global: { @@ -536,7 +549,7 @@ describe('createVitestConfig', () => { const testConfig = (config as any).test; expect(testConfig.setupFiles).toEqual([ - `${MOCK_PROJECT_ROOT_STRING}/should-be-removed.ts`, + mockSetupFile('should-be-removed.ts'), ]); expect(testConfig.testTimeout).toBe(TEST_TIMEOUTS.SHORT); expect(testConfig.pool).toBe('forks'); @@ -579,7 +592,7 @@ describe('createVitestConfig', () => { expect((config as any).test.testTimeout).toBe(TEST_TIMEOUTS.SHORT); expectCoverageConfig(config, { reporter: ['text', 'lcov'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + reportsDirectory: mockReportsDir('test-package', 'unit'), exclude: DEFAULT_EXCLUDES, }); }); @@ -603,7 +616,7 @@ describe('createVitestConfig', () => { expect((config as any).test.testTimeout).toBe(TEST_TIMEOUTS.SHORT); expectCoverageConfig(config, { reporter: ['text', 'lcov'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/unit-tests`, + reportsDirectory: mockReportsDir('test-package', 'unit'), exclude: DEFAULT_EXCLUDES, }); }); @@ -632,9 +645,7 @@ describe('createVitestConfig', () => { expectedIncludes[kind], ); expect((config as any).test.globalSetup).toStrictEqual( - kind === 'e2e' - ? undefined - : [`${MOCK_PROJECT_ROOT_STRING}/global-setup.ts`], + kind === 'e2e' ? undefined : [mockGlobalSetup()], ); }); }); @@ -681,15 +692,15 @@ describe('createVitestConfig', () => { }, test: expect.objectContaining({ setupFiles: [ - `${MOCK_PROJECT_ROOT_STRING}/setup1.ts`, - `${MOCK_PROJECT_ROOT_STRING}/setup2.ts`, + mockSetupFile('setup1.ts'), + mockSetupFile('setup2.ts'), ], testTimeout: TEST_TIMEOUTS.LONG, environment: 'jsdom', include: EXPECTED_INCLUDES.int, coverage: expect.objectContaining({ exclude: ['mocks/**', '**/types.ts', 'e2e/**', 'dist/**'], - reportsDirectory: `${MOCK_PROJECT_ROOT_STRING}/packages/test-package/.coverage/int-tests`, + reportsDirectory: mockReportsDir('test-package', 'int'), }), }), }), From 96cb4b071a84eafecdfa8f702b662917d2d4ed3a Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Tue, 9 Sep 2025 15:58:37 +0200 Subject: [PATCH 12/13] fix: path resolution #5 --- .../src/lib/vitest-config-factory.ts | 2 +- .../lib/vitest-config-factory.unit.test.ts | 120 +++++++++++------- 2 files changed, 78 insertions(+), 44 deletions(-) diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts index a045e477c..3421740ff 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts @@ -200,7 +200,7 @@ function sanitizeOverrides(overrides: VitestOverrides): VitestOverrides { return { ...overrides, test: sanitizedTest }; } -function getProjectRootPath(projectRootUrl: URL): string { +export function getProjectRootPath(projectRootUrl: URL): string { try { return fileURLToPath(projectRootUrl); } catch { diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts index 1ebe0cde5..c122a3bc5 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts @@ -7,6 +7,7 @@ import { type VitestConfigFactoryOptions, type VitestOverrides, createVitestConfig, + getProjectRootPath, } from './vitest-config-factory.js'; // Only mock defineConfig - assume it works correctly, we're not testing Vite @@ -27,17 +28,11 @@ vi.mock('./vitest-tsconfig-path-aliases.js', () => ({ const MOCK_PROJECT_ROOT_STRING = '/Users/test/project'; const MOCK_PROJECT_ROOT_URL = pathToFileURL(`${MOCK_PROJECT_ROOT_STRING}/`); -// Cross-platform path helpers to match what the actual code generates +// Simple path helpers - just use them directly in tests! const mockPath = (...segments: string[]) => path.resolve(MOCK_PROJECT_ROOT_STRING, ...segments); -const mockCacheDir = (name: string) => mockPath('node_modules', '.vite', name); -const mockVitestCacheDir = () => mockPath('node_modules', '.vitest'); -const mockGlobalSetup = () => mockPath('global-setup.ts'); -const mockReportsDir = (projectKey: string, kind: string) => - kind === 'e2e' - ? mockPath('e2e', projectKey, '.coverage') - : mockPath('packages', projectKey, '.coverage', `${kind}-tests`); -const mockSetupFile = mockPath; +const mockUrlPath = (url: URL, ...segments: string[]) => + path.resolve(getProjectRootPath(url), ...segments); const TEST_TIMEOUTS = { SHORT: 5000, @@ -74,23 +69,28 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: mockCacheDir('test-package'), + cacheDir: mockPath('node_modules', '.vite', 'test-package'), test: expect.objectContaining({ reporters: ['basic'], globals: true, cache: { - dir: mockVitestCacheDir(), + dir: mockPath('node_modules', '.vitest'), }, alias: expect.any(Object), pool: 'threads', poolOptions: { threads: { singleThread: true } }, environment: 'node', include: EXPECTED_INCLUDES.unit, - globalSetup: [mockGlobalSetup()], + globalSetup: [mockPath('global-setup.ts')], setupFiles: [], coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: mockReportsDir('test-package', 'unit'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'unit-tests', + ), exclude: DEFAULT_EXCLUDES, }), }), @@ -111,10 +111,17 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: mockCacheDir('test-package'), + cacheDir: mockUrlPath( + MOCK_PROJECT_ROOT_URL, + 'node_modules', + '.vite', + 'test-package', + ), test: expect.objectContaining({ include: EXPECTED_INCLUDES.unit, - globalSetup: [mockGlobalSetup()], + globalSetup: [ + mockUrlPath(MOCK_PROJECT_ROOT_URL, 'global-setup.ts'), + ], }), }), ); @@ -161,9 +168,14 @@ describe('createVitestConfig', () => { expect.objectContaining({ test: expect.objectContaining({ include: EXPECTED_INCLUDES.int, - globalSetup: [mockGlobalSetup()], + globalSetup: [mockPath('global-setup.ts')], coverage: expect.objectContaining({ - reportsDirectory: mockReportsDir('test-package', 'int'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'int-tests', + ), }), }), }), @@ -215,7 +227,7 @@ describe('createVitestConfig', () => { globalSetup: undefined, coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: mockReportsDir('test-package', 'e2e'), + reportsDirectory: mockPath('e2e', 'test-package', '.coverage'), exclude: DEFAULT_EXCLUDES, }), }), @@ -237,7 +249,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: mockCacheDir('custom-cache-key'), + cacheDir: mockPath('node_modules', '.vite', 'custom-cache-key'), }), ); }); @@ -253,7 +265,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: mockCacheDir('test-package'), + cacheDir: mockPath('node_modules', '.vite', 'test-package'), }), ); }); @@ -278,7 +290,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ test: expect.objectContaining({ - setupFiles: [mockSetupFile('setup.ts')], + setupFiles: [mockPath('setup.ts')], }), }), ); @@ -302,10 +314,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ test: expect.objectContaining({ - setupFiles: [ - mockSetupFile('setup1.ts'), - mockSetupFile('setup2.ts'), - ], + setupFiles: [mockPath('setup1.ts'), mockPath('setup2.ts')], }), }), ); @@ -330,9 +339,9 @@ describe('createVitestConfig', () => { expect.objectContaining({ test: expect.objectContaining({ setupFiles: [ - mockSetupFile('setup1.ts'), - mockSetupFile('setup2.ts'), - mockSetupFile('setup3.ts'), + mockPath('setup1.ts'), + mockPath('setup2.ts'), + mockPath('setup3.ts'), ], }), }), @@ -405,7 +414,12 @@ describe('createVitestConfig', () => { test: expect.objectContaining({ coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: mockReportsDir('test-package', 'unit'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'unit-tests', + ), exclude: [...DEFAULT_EXCLUDES, 'custom/**', 'ignore/**'], }), }), @@ -435,7 +449,12 @@ describe('createVitestConfig', () => { test: expect.objectContaining({ coverage: expect.objectContaining({ reporter: ['text', 'lcov'], - reportsDirectory: mockReportsDir('test-package', 'unit'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'unit-tests', + ), exclude: DEFAULT_EXCLUDES, }), }), @@ -492,7 +511,12 @@ describe('createVitestConfig', () => { const config = createVitestConfig(options, overrides); expectCoverageConfig(config, { reporter: ['text', 'lcov', 'html', 'json'], - reportsDirectory: mockReportsDir('test-package', 'unit'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'unit-tests', + ), exclude: [...DEFAULT_EXCLUDES, 'custom/**'], thresholds: { global: { @@ -548,9 +572,7 @@ describe('createVitestConfig', () => { const config = createVitestConfig(options, overrides); const testConfig = (config as any).test; - expect(testConfig.setupFiles).toEqual([ - mockSetupFile('should-be-removed.ts'), - ]); + expect(testConfig.setupFiles).toEqual([mockPath('should-be-removed.ts')]); expect(testConfig.testTimeout).toBe(TEST_TIMEOUTS.SHORT); expect(testConfig.pool).toBe('forks'); }); @@ -592,7 +614,12 @@ describe('createVitestConfig', () => { expect((config as any).test.testTimeout).toBe(TEST_TIMEOUTS.SHORT); expectCoverageConfig(config, { reporter: ['text', 'lcov'], - reportsDirectory: mockReportsDir('test-package', 'unit'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'unit-tests', + ), exclude: DEFAULT_EXCLUDES, }); }); @@ -616,7 +643,12 @@ describe('createVitestConfig', () => { expect((config as any).test.testTimeout).toBe(TEST_TIMEOUTS.SHORT); expectCoverageConfig(config, { reporter: ['text', 'lcov'], - reportsDirectory: mockReportsDir('test-package', 'unit'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'unit-tests', + ), exclude: DEFAULT_EXCLUDES, }); }); @@ -645,7 +677,7 @@ describe('createVitestConfig', () => { expectedIncludes[kind], ); expect((config as any).test.globalSetup).toStrictEqual( - kind === 'e2e' ? undefined : [mockGlobalSetup()], + kind === 'e2e' ? undefined : [mockPath('global-setup.ts')], ); }); }); @@ -686,21 +718,23 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: `${MOCK_PROJECT_ROOT_STRING}/node_modules/.vite/complex-scenario`, + cacheDir: mockPath('node_modules', '.vite', 'complex-scenario'), build: { target: 'es2020', }, test: expect.objectContaining({ - setupFiles: [ - mockSetupFile('setup1.ts'), - mockSetupFile('setup2.ts'), - ], + setupFiles: [mockPath('setup1.ts'), mockPath('setup2.ts')], testTimeout: TEST_TIMEOUTS.LONG, environment: 'jsdom', include: EXPECTED_INCLUDES.int, coverage: expect.objectContaining({ exclude: ['mocks/**', '**/types.ts', 'e2e/**', 'dist/**'], - reportsDirectory: mockReportsDir('test-package', 'int'), + reportsDirectory: mockPath( + 'packages', + 'test-package', + '.coverage', + 'int-tests', + ), }), }), }), From 7008a4890d6ad5b94c2bcc4796a4b176d990ff32 Mon Sep 17 00:00:00 2001 From: "Szymon.Poltorak" Date: Mon, 6 Oct 2025 17:16:29 +0200 Subject: [PATCH 13/13] refactor(testing): improve vitest config factory and test coverage --- testing/test-setup-config/README.md | 28 +- .../src/lib/vitest-config-factory.ts | 14 +- .../lib/vitest-config-factory.unit.test.ts | 8 +- .../src/lib/vitest-setup-presets.unit.test.ts | 408 ++++++++++++++++-- 4 files changed, 390 insertions(+), 68 deletions(-) diff --git a/testing/test-setup-config/README.md b/testing/test-setup-config/README.md index 730c625a0..42aa20ae8 100644 --- a/testing/test-setup-config/README.md +++ b/testing/test-setup-config/README.md @@ -5,33 +5,7 @@ Utilities to centralize and standardize Vitest configuration across the monorepo - `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults - `vitest-setup-presets.ts`: provides create functions and exportable setup file groups -The create functions (`createUnitConfig`, `createIntConfig`, `createE2eConfig`) automatically include appropriate setup files for each test type. - -### Defaults - -**Common**: `reporters: ['basic']`, `globals: true`, `environment: 'node'`, `pool: 'threads'` with `singleThread: true`, alias from tsconfig paths - -**Coverage**: Unit/Int enabled (reports to `/packages//.coverage`), E2E disabled. Excludes `['mocks/**', '**/types.ts']` - -**Global setup**: Unit/Int use `['/global-setup.ts']`, E2E none by default - -**Include patterns**: Unit `src/**/*.unit.test.*`, Int `src/**/*.int.test.*`, E2E `tests/**/*.e2e.test.*` - -### Setup files - -**Automatic inclusion**: Unit (console mocking, cleanup, UI/filesystem mocks, basic matchers), Int (console mocking, cleanup), E2E (cleanup only) - -**Custom setup files**: ⚠️ Specifying `setupFiles` in overrides will completely replace the defaults. To extend the default list, manually combine them with `setupPresets`: - -- `setupPresets.unit.{base, git, portalClient, matchersCore, matcherPath}` -- `setupPresets.int.{base, cliui, fs, git, portalClient, matcherPath, chromePath}` -- `setupPresets.e2e.{base}` - -### Parameters - -- `projectKey`: Used for cache and coverage directory naming -- `projectRoot`: Required path/URL to the project root for resolving all paths -- Standard Vitest configuration options can be provided in the overrides parameter +The create functions (`createUnitConfig`, `createIntConfig`, `createE2eConfig`) automatically include appropriate setup files for each test type. See the unit tests for detailed documentation of defaults, coverage settings, and setup file presets. ### Examples diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts index 3421740ff..31882ce9a 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts @@ -11,9 +11,21 @@ import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js'; export type TestKind = 'unit' | 'int' | 'e2e'; export type VitestConfigFactoryOptions = { + /** + * Used for cache and coverage directory naming + */ projectKey: string; + /** + * Required path/URL to the project root for resolving all paths + */ kind: TestKind; + /** + * The root directory of the project + */ projectRoot: string | URL; + /** + * Optional test cache directory name + */ cacheKey?: string; }; @@ -35,7 +47,7 @@ export function createVitestConfig( : `${options.projectRoot}/`, ) : options.projectRoot; - const cacheDirName = options.cacheKey ?? options.projectKey; + const cacheDirName = options.cacheKey ?? `cache-${options.projectKey}`; const coverageEnabled = overrides.test?.coverage?.enabled ?? options.kind !== 'e2e'; diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts index c122a3bc5..9eae528fb 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts @@ -69,7 +69,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: mockPath('node_modules', '.vite', 'test-package'), + cacheDir: mockPath('node_modules', '.vite', 'cache-test-package'), test: expect.objectContaining({ reporters: ['basic'], globals: true, @@ -115,7 +115,7 @@ describe('createVitestConfig', () => { MOCK_PROJECT_ROOT_URL, 'node_modules', '.vite', - 'test-package', + 'cache-test-package', ), test: expect.objectContaining({ include: EXPECTED_INCLUDES.unit, @@ -254,7 +254,7 @@ describe('createVitestConfig', () => { ); }); - it('should fallback to projectKey when cacheKey is not provided', () => { + it('should fallback to cache-{projectKey} when cacheKey is not provided', () => { const options: VitestConfigFactoryOptions = { projectKey: 'test-package', kind: 'unit', @@ -265,7 +265,7 @@ describe('createVitestConfig', () => { expect(config).toEqual( expect.objectContaining({ - cacheDir: mockPath('node_modules', '.vite', 'test-package'), + cacheDir: mockPath('node_modules', '.vite', 'cache-test-package'), }), ); }); diff --git a/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts b/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts index 8051a7588..cc303dd87 100644 --- a/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts @@ -72,47 +72,42 @@ describe('setupPresets', () => { }); }); -// Parameterized tests to eliminate duplication -describe.each([ - ['unit', setupPresets.unit.base, createUnitConfig], - ['int', setupPresets.int.base, createIntConfig], - ['e2e', setupPresets.e2e.base, createE2eConfig], -] as const)('%s config creation', (kind, baseSetupFiles, createFn) => { +describe('createUnitConfig', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should call createVitestConfig with correct parameters and default setupFiles', () => { - createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: MOCK_PROJECT_KEY, - kind, + kind: 'unit', ...MOCK_CONFIG_REST_PARAMS, }, { test: { - setupFiles: baseSetupFiles, + setupFiles: setupPresets.unit.base, }, }, ); }); it('should use custom setupFiles from overrides when provided', () => { - const customSetupFiles = [`${kind}-setup1.ts`, `${kind}-setup2.ts`]; + const customSetupFiles = ['unit-setup1.ts', 'unit-setup2.ts']; const overrides: VitestOverrides = { test: { setupFiles: customSetupFiles, }, }; - createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: MOCK_PROJECT_KEY, - kind, + kind: 'unit', ...MOCK_CONFIG_REST_PARAMS, }, { @@ -124,16 +119,357 @@ describe.each([ }); it('should merge other overrides correctly while using default setupFiles', () => { - const testTimeout = - kind === 'unit' - ? TEST_TIMEOUTS.MEDIUM - : kind === 'int' - ? TEST_TIMEOUTS.LONG - : TEST_TIMEOUTS.E2E; + const overrides: VitestOverrides = { + test: { + testTimeout: TEST_TIMEOUTS.MEDIUM, + globals: false, + }, + build: { + target: 'es2020', + }, + }; + + createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'unit', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + testTimeout: TEST_TIMEOUTS.MEDIUM, + globals: false, + setupFiles: setupPresets.unit.base, + }, + build: { + target: 'es2020', + }, + }, + ); + }); + it('should handle overrides with custom setupFiles and other test options', () => { + const customSetupFiles = ['unit-custom.ts']; + const overrides: VitestOverrides = { + test: { + setupFiles: customSetupFiles, + testTimeout: TEST_TIMEOUTS.SHORT, + environment: 'jsdom' as any, + }, + }; + + createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'unit', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: customSetupFiles, + testTimeout: TEST_TIMEOUTS.SHORT, + environment: 'jsdom', + }, + }, + ); + }); + + it('should handle undefined overrides', () => { + createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, undefined); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'unit', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: setupPresets.unit.base, + }, + }, + ); + }); + + it('should handle overrides without test config', () => { + const overrides: VitestOverrides = { + build: { + target: 'es2020', + }, + }; + + createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'unit', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + build: { + target: 'es2020', + }, + test: { + setupFiles: setupPresets.unit.base, + }, + }, + ); + }); + + it('should return the result from createVitestConfig', () => { + const result = createUnitConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + + expect(result).toBe('mocked-config'); + }); + + it('should handle empty projectKey gracefully', () => { + const result = createUnitConfig('', MOCK_CONFIG_REST_PARAMS); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: '', + kind: 'unit', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: setupPresets.unit.base, + }, + }, + ); + expect(result).toBe('mocked-config'); + }); +}); + +describe('createIntConfig', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should call createVitestConfig with correct parameters and default setupFiles', () => { + createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: setupPresets.int.base, + }, + }, + ); + }); + + it('should use custom setupFiles from overrides when provided', () => { + const customSetupFiles = ['int-setup1.ts', 'int-setup2.ts']; + const overrides: VitestOverrides = { + test: { + setupFiles: customSetupFiles, + }, + }; + + createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: customSetupFiles, + }, + }, + ); + }); + + it('should merge other overrides correctly while using default setupFiles', () => { + const overrides: VitestOverrides = { + test: { + testTimeout: TEST_TIMEOUTS.MEDIUM, + globals: false, + }, + build: { + target: 'es2020', + }, + }; + + createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + testTimeout: TEST_TIMEOUTS.MEDIUM, + globals: false, + setupFiles: setupPresets.int.base, + }, + build: { + target: 'es2020', + }, + }, + ); + }); + + it('should handle overrides with custom setupFiles and other test options', () => { + const customSetupFiles = ['int-custom.ts']; + const overrides: VitestOverrides = { + test: { + setupFiles: customSetupFiles, + testTimeout: TEST_TIMEOUTS.SHORT, + environment: 'jsdom' as any, + }, + }; + + createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: customSetupFiles, + testTimeout: TEST_TIMEOUTS.SHORT, + environment: 'jsdom', + }, + }, + ); + }); + + it('should handle undefined overrides', () => { + createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, undefined); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: setupPresets.int.base, + }, + }, + ); + }); + + it('should handle overrides without test config', () => { + const overrides: VitestOverrides = { + build: { + target: 'es2020', + }, + }; + + createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + build: { + target: 'es2020', + }, + test: { + setupFiles: setupPresets.int.base, + }, + }, + ); + }); + + it('should return the result from createVitestConfig', () => { + const result = createIntConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + + expect(result).toBe('mocked-config'); + }); + + it('should handle empty projectKey gracefully', () => { + const result = createIntConfig('', MOCK_CONFIG_REST_PARAMS); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: '', + kind: 'int', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: setupPresets.int.base, + }, + }, + ); + expect(result).toBe('mocked-config'); + }); +}); + +describe('createE2eConfig', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should call createVitestConfig with correct parameters and default setupFiles', () => { + createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'e2e', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: setupPresets.e2e.base, + }, + }, + ); + }); + + it('should use custom setupFiles from overrides when provided', () => { + const customSetupFiles = ['e2e-setup1.ts', 'e2e-setup2.ts']; + const overrides: VitestOverrides = { + test: { + setupFiles: customSetupFiles, + }, + }; + + createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + + expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + { + projectKey: MOCK_PROJECT_KEY, + kind: 'e2e', + ...MOCK_CONFIG_REST_PARAMS, + }, + { + test: { + setupFiles: customSetupFiles, + }, + }, + ); + }); + + it('should merge other overrides correctly while using default setupFiles', () => { const overrides: VitestOverrides = { test: { - testTimeout, + testTimeout: TEST_TIMEOUTS.MEDIUM, globals: false, }, build: { @@ -141,19 +477,19 @@ describe.each([ }, }; - createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: MOCK_PROJECT_KEY, - kind, + kind: 'e2e', ...MOCK_CONFIG_REST_PARAMS, }, { test: { - testTimeout, + testTimeout: TEST_TIMEOUTS.MEDIUM, globals: false, - setupFiles: baseSetupFiles, + setupFiles: setupPresets.e2e.base, }, build: { target: 'es2020', @@ -163,7 +499,7 @@ describe.each([ }); it('should handle overrides with custom setupFiles and other test options', () => { - const customSetupFiles = [`${kind}-custom.ts`]; + const customSetupFiles = ['e2e-custom.ts']; const overrides: VitestOverrides = { test: { setupFiles: customSetupFiles, @@ -172,12 +508,12 @@ describe.each([ }, }; - createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: MOCK_PROJECT_KEY, - kind, + kind: 'e2e', ...MOCK_CONFIG_REST_PARAMS, }, { @@ -191,17 +527,17 @@ describe.each([ }); it('should handle undefined overrides', () => { - createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, undefined); + createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, undefined); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: MOCK_PROJECT_KEY, - kind, + kind: 'e2e', ...MOCK_CONFIG_REST_PARAMS, }, { test: { - setupFiles: baseSetupFiles, + setupFiles: setupPresets.e2e.base, }, }, ); @@ -214,12 +550,12 @@ describe.each([ }, }; - createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); + createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS, overrides); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: MOCK_PROJECT_KEY, - kind, + kind: 'e2e', ...MOCK_CONFIG_REST_PARAMS, }, { @@ -227,30 +563,30 @@ describe.each([ target: 'es2020', }, test: { - setupFiles: baseSetupFiles, + setupFiles: setupPresets.e2e.base, }, }, ); }); it('should return the result from createVitestConfig', () => { - const result = createFn(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); + const result = createE2eConfig(MOCK_PROJECT_KEY, MOCK_CONFIG_REST_PARAMS); expect(result).toBe('mocked-config'); }); it('should handle empty projectKey gracefully', () => { - const result = createFn('', MOCK_CONFIG_REST_PARAMS); + const result = createE2eConfig('', MOCK_CONFIG_REST_PARAMS); expect(configFactory.createVitestConfig).toHaveBeenCalledWith( { projectKey: '', - kind, + kind: 'e2e', ...MOCK_CONFIG_REST_PARAMS, }, { test: { - setupFiles: baseSetupFiles, + setupFiles: setupPresets.e2e.base, }, }, );