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/ci-e2e/global-setup.ts b/e2e/ci-e2e/global-setup.ts
deleted file mode 100644
index 92893c11c..000000000
--- a/e2e/ci-e2e/global-setup.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable functional/immutable-data */
-
-const originalCI = process.env['CI'];
-
-export function setup() {
- // package is expected to run in CI environment
- process.env['CI'] = 'true';
-}
-
-export function teardown() {
- if (originalCI === undefined) {
- delete process.env['CI'];
- } else {
- process.env['CI'] = originalCI;
- }
-}
diff --git a/e2e/ci-e2e/tsconfig.test.json b/e2e/ci-e2e/tsconfig.test.json
index 5248dd739..307a55e79 100644
--- a/e2e/ci-e2e/tsconfig.test.json
+++ b/e2e/ci-e2e/tsconfig.test.json
@@ -8,7 +8,6 @@
"vitest.e2e.config.ts",
"tests/**/*.e2e.test.ts",
"tests/**/*.d.ts",
- "mocks/**/*.ts",
- "global-setup.ts"
+ "mocks/**/*.ts"
]
}
diff --git a/e2e/ci-e2e/vitest.e2e.config.ts b/e2e/ci-e2e/vitest.e2e.config.ts
index 90df62fed..ff9d2856e 100644
--- a/e2e/ci-e2e/vitest.e2e.config.ts
+++ b/e2e/ci-e2e/vitest.e2e.config.ts
@@ -1,22 +1,6 @@
///
-import { defineConfig } from 'vitest/config';
-import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
+import { createE2ETestConfig } from '../../testing/test-setup-config/src/index.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 createE2ETestConfig('ci-e2e', {
+ testTimeout: 60_000,
});
diff --git a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts
index c34d55a36..41a602360 100644
--- a/e2e/plugin-typescript-e2e/vitest.e2e.config.ts
+++ b/e2e/plugin-typescript-e2e/vitest.e2e.config.ts
@@ -1,26 +1,6 @@
///
-import { defineConfig } from 'vitest/config';
-import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
+import { createE2ETestConfig } from '../../testing/test-setup-config/src/index.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 createE2ETestConfig('plugin-typescript-e2e', {
+ testTimeout: 20_000,
});
diff --git a/global-setup.ts b/global-setup.ts
index 65522d48f..76e5c8dea 100644
--- a/global-setup.ts
+++ b/global-setup.ts
@@ -1,3 +1,18 @@
-export async function setup() {
+/* eslint-disable functional/immutable-data */
+
+const originalCI = process.env['CI'];
+
+export function setup() {
process.env.TZ = 'UTC';
+
+ // package is expected to run in CI environment
+ process.env['CI'] = 'true';
+}
+
+export function teardown() {
+ if (originalCI === undefined) {
+ delete process.env['CI'];
+ } else {
+ process.env['CI'] = originalCI;
+ }
}
diff --git a/packages/core/vitest.int.config.ts b/packages/core/vitest.int.config.ts
index 7ff35029f..f16b4cd5b 100644
--- a/packages/core/vitest.int.config.ts
+++ b/packages/core/vitest.int.config.ts
@@ -1,30 +1,4 @@
///
-import { defineConfig } from 'vitest/config';
-import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
+import { createIntTestConfig } from '../../testing/test-setup-config/src/index.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 createIntTestConfig('core');
diff --git a/packages/core/vitest.unit.config.ts b/packages/core/vitest.unit.config.ts
index c46850c41..698818f7a 100644
--- a/packages/core/vitest.unit.config.ts
+++ b/packages/core/vitest.unit.config.ts
@@ -1,36 +1,4 @@
///
-import { defineConfig } from 'vitest/config';
-import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
+import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.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 createUnitTestConfig('core');
diff --git a/packages/utils/vitest.int.config.ts b/packages/utils/vitest.int.config.ts
index b72908490..2ec648817 100644
--- a/packages/utils/vitest.int.config.ts
+++ b/packages/utils/vitest.int.config.ts
@@ -1,30 +1,4 @@
///
-import { defineConfig } from 'vitest/config';
-import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
+import { createIntTestConfig } from '../../testing/test-setup-config/src/index.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 createIntTestConfig('utils');
diff --git a/packages/utils/vitest.unit.config.ts b/packages/utils/vitest.unit.config.ts
index f55eb2326..f85142065 100644
--- a/packages/utils/vitest.unit.config.ts
+++ b/packages/utils/vitest.unit.config.ts
@@ -1,38 +1,4 @@
///
-import { defineConfig } from 'vitest/config';
-import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
+import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.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 createUnitTestConfig('utils');
diff --git a/testing/test-setup-config/README.md b/testing/test-setup-config/README.md
new file mode 100644
index 000000000..a7750a4cf
--- /dev/null
+++ b/testing/test-setup-config/README.md
@@ -0,0 +1,49 @@
+## Vitest Config Factory
+
+Standardized Vitest configuration for the Code PushUp monorepo.
+
+### Usage
+
+**Unit tests:**
+
+```typescript
+import { createUnitTestConfig } from '@code-pushup/test-setup-config';
+
+export default createUnitTestConfig('my-package');
+```
+
+**Integration tests:**
+
+```typescript
+import { createIntTestConfig } from '@code-pushup/test-setup-config';
+
+export default createIntTestConfig('my-package');
+```
+
+**E2E tests:**
+
+```typescript
+import { createE2ETestConfig } from '@code-pushup/test-setup-config';
+
+export default createE2ETestConfig('my-e2e');
+
+// With options:
+export default createE2ETestConfig('my-e2e', {
+ testTimeout: 60_000,
+});
+```
+
+### Advanced: Overriding Config
+
+For edge cases, use the spread operator to override any property:
+
+```typescript
+const baseConfig = createE2ETestConfig('my-e2e');
+export default {
+ ...baseConfig,
+ test: {
+ ...(baseConfig as any).test,
+ globalSetup: ['./custom-setup.ts'],
+ },
+};
+```
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..68d9a93c6
--- /dev/null
+++ b/testing/test-setup-config/src/index.ts
@@ -0,0 +1,9 @@
+export {
+ createUnitTestConfig,
+ createIntTestConfig,
+ createE2ETestConfig,
+} from './lib/vitest-setup-presets.js';
+
+export type { E2ETestOptions } from './lib/vitest-config-factory.js';
+
+export { getSetupFiles } from './lib/vitest-setup-files.js';
diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts
new file mode 100644
index 000000000..21235ebef
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts
@@ -0,0 +1,81 @@
+import type { CoverageOptions, InlineConfig } from 'vitest';
+import { type UserConfig as ViteUserConfig, defineConfig } from 'vitest/config';
+import { getSetupFiles } from './vitest-setup-files.js';
+import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js';
+
+export type TestKind = 'unit' | 'int' | 'e2e';
+
+export type E2ETestOptions = {
+ testTimeout?: number;
+};
+
+export type VitestConfig = ViteUserConfig & { test?: InlineConfig };
+
+function getIncludePatterns(kind: TestKind): string[] {
+ switch (kind) {
+ case 'unit':
+ return [
+ 'src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
+ 'src/**/*.type.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
+ ];
+ case 'int':
+ return ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'];
+ case 'e2e':
+ return ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'];
+ }
+}
+
+function getGlobalSetup(kind: TestKind): string[] | undefined {
+ return kind === 'e2e' ? undefined : ['../../global-setup.ts'];
+}
+
+function buildCoverageConfig(
+ projectKey: string,
+ kind: TestKind,
+): CoverageOptions | undefined {
+ if (kind === 'e2e') {
+ return undefined;
+ }
+
+ const defaultExclude = ['mocks/**', '**/types.ts', 'perf/**'];
+ const reportsDirectory = `../../coverage/${projectKey}/${kind}-tests`;
+
+ return {
+ reporter: ['text', 'lcov'],
+ reportsDirectory,
+ exclude: defaultExclude,
+ };
+}
+
+export function createVitestConfig(
+ projectKey: string,
+ kind: TestKind,
+ options?: E2ETestOptions,
+): ViteUserConfig {
+ const coverage = buildCoverageConfig(projectKey, kind);
+
+ const config: VitestConfig = {
+ cacheDir: `../../node_modules/.vite/${projectKey}`,
+ test: {
+ reporters: ['basic'],
+ globals: true,
+ cache: {
+ dir: '../../node_modules/.vitest',
+ },
+ alias: tsconfigPathAliases(),
+ pool: 'threads',
+ poolOptions: { threads: { singleThread: true } },
+ environment: 'node',
+ include: getIncludePatterns(kind),
+ globalSetup: getGlobalSetup(kind),
+ setupFiles: [...getSetupFiles(kind)],
+ ...(options?.testTimeout ? { testTimeout: options.testTimeout } : {}),
+ ...(coverage ? { coverage } : {}),
+ ...(kind === 'unit'
+ ? { typecheck: { include: ['**/*.type.test.ts'] } }
+ : {}),
+ },
+ };
+
+ return defineConfig(config);
+}
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
new file mode 100644
index 000000000..a6b80933d
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts
@@ -0,0 +1,356 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { defineConfig } from 'vitest/config';
+import type { E2ETestOptions, TestKind } from './vitest-config-factory.js';
+import { createVitestConfig } from './vitest-config-factory.js';
+
+vi.mock('vitest/config', async importOriginal => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ defineConfig: vi.fn(config => config),
+ };
+});
+
+vi.mock('./vitest-tsconfig-path-aliases.js', () => ({
+ tsconfigPathAliases: vi
+ .fn()
+ .mockReturnValue([{ find: '@test/alias', replacement: '/mock/path' }]),
+}));
+
+describe('createVitestConfig', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('unit test configuration', () => {
+ it('should create a complete unit test config with all defaults', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ expect(config).toEqual(
+ expect.objectContaining({
+ cacheDir: '../../node_modules/.vite/test-package',
+ test: expect.objectContaining({
+ reporters: ['basic'],
+ globals: true,
+ cache: { dir: '../../node_modules/.vitest' },
+ alias: expect.any(Array),
+ pool: 'threads',
+ poolOptions: { threads: { singleThread: true } },
+ environment: 'node',
+ include: [
+ 'src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
+ 'src/**/*.type.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
+ ],
+ globalSetup: ['../../global-setup.ts'],
+ setupFiles: expect.arrayContaining([
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ '../../testing/test-setup/src/lib/reset.mocks.ts',
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ ]),
+ coverage: expect.objectContaining({
+ reporter: ['text', 'lcov'],
+ reportsDirectory: '../../coverage/test-package/unit-tests',
+ exclude: ['mocks/**', '**/types.ts', 'perf/**'],
+ }),
+ typecheck: { include: ['**/*.type.test.ts'] },
+ }),
+ }),
+ );
+ expect(defineConfig).toHaveBeenCalledWith(config);
+ });
+
+ it('should include all required setup files for unit tests', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ const setupFiles = (config as any).test.setupFiles;
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/reset.mocks.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/cliui.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/git.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/portal-client.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/jest-extended.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+ );
+ });
+
+ it('should include type test pattern in unit tests', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ expect((config as any).test.include).toContain(
+ 'src/**/*.type.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
+ );
+ });
+
+ it('should enable typecheck for unit tests', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ expect((config as any).test.typecheck).toEqual({
+ include: ['**/*.type.test.ts'],
+ });
+ });
+
+ it('should always include perf/** in coverage exclusions', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ expect((config as any).test.coverage.exclude).toContain('perf/**');
+ });
+ });
+
+ describe('integration test configuration', () => {
+ it('should create a complete integration test config', () => {
+ const config = createVitestConfig('test-package', 'int');
+
+ expect(config).toEqual(
+ expect.objectContaining({
+ cacheDir: '../../node_modules/.vite/test-package',
+ test: expect.objectContaining({
+ reporters: ['basic'],
+ globals: true,
+ include: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ globalSetup: ['../../global-setup.ts'],
+ coverage: expect.objectContaining({
+ reportsDirectory: '../../coverage/test-package/int-tests',
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should include correct setup files for integration tests', () => {
+ const config = createVitestConfig('test-package', 'int');
+
+ const setupFiles = (config as any).test.setupFiles;
+ // Should include console mock
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ );
+ // Should NOT include fs, cliui, or git mocks (integration tests need real implementations)
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/cliui.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/git.mock.ts',
+ );
+ // Should include all matchers
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+ );
+ });
+
+ it('should not enable typecheck for integration tests', () => {
+ const config = createVitestConfig('test-package', 'int');
+
+ expect((config as any).test.typecheck).toBeUndefined();
+ });
+ });
+
+ describe('e2e test configuration', () => {
+ it('should create e2e config without coverage by default', () => {
+ const config = createVitestConfig('test-package', 'e2e');
+
+ expect(config).toEqual(
+ expect.objectContaining({
+ cacheDir: '../../node_modules/.vite/test-package',
+ test: expect.objectContaining({
+ reporters: ['basic'],
+ globals: true,
+ include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ globalSetup: undefined,
+ }),
+ }),
+ );
+ expect((config as any).test.coverage).toBeUndefined();
+ });
+
+ it('should include minimal setup files for e2e tests', () => {
+ const config = createVitestConfig('test-package', 'e2e');
+
+ const setupFiles = (config as any).test.setupFiles;
+ // Should only include reset mocks
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/reset.mocks.ts',
+ );
+ // Should NOT include console, fs, git, etc.
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ );
+ // Should include all matchers
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+ );
+ });
+
+ it('should support custom testTimeout option', () => {
+ const options: E2ETestOptions = { testTimeout: 60_000 };
+ const config = createVitestConfig('test-package', 'e2e', options);
+
+ expect((config as any).test.testTimeout).toBe(60_000);
+ });
+
+ it('should support multiple options together', () => {
+ const options: E2ETestOptions = {
+ testTimeout: 30_000,
+ };
+ const config = createVitestConfig('test-package', 'e2e', options);
+
+ expect((config as any).test.testTimeout).toBe(30_000);
+ expect((config as any).test.coverage).toBeUndefined();
+ });
+ });
+
+ describe('cacheDir naming', () => {
+ it('should use projectKey for cacheDir', () => {
+ const config = createVitestConfig('my-custom-name', 'unit');
+
+ expect(config.cacheDir).toBe('../../node_modules/.vite/my-custom-name');
+ });
+
+ it('should use projectKey for coverage directory', () => {
+ const config = createVitestConfig('my-package', 'unit');
+
+ expect((config as any).test.coverage.reportsDirectory).toBe(
+ '../../coverage/my-package/unit-tests',
+ );
+ });
+ });
+
+ describe('test kind variations', () => {
+ it('should handle all test kinds correctly', () => {
+ const testKinds: TestKind[] = ['unit', 'int', 'e2e'];
+
+ testKinds.forEach(kind => {
+ const config = createVitestConfig('test-package', kind);
+
+ const expectedIncludes = {
+ unit: [
+ 'src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
+ 'src/**/*.type.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],
+ );
+
+ const expectedGlobalSetup = {
+ unit: ['../../global-setup.ts'],
+ int: ['../../global-setup.ts'],
+ e2e: undefined,
+ };
+
+ expect((config as any).test.globalSetup).toStrictEqual(
+ expectedGlobalSetup[kind],
+ );
+ });
+ });
+ });
+
+ describe('coverage configuration', () => {
+ it('should enable coverage for unit tests by default', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ expect((config as any).test.coverage).toBeDefined();
+ expect((config as any).test.coverage.reporter).toEqual(['text', 'lcov']);
+ });
+
+ it('should enable coverage for integration tests by default', () => {
+ const config = createVitestConfig('test-package', 'int');
+
+ expect((config as any).test.coverage).toBeDefined();
+ });
+
+ it('should disable coverage for e2e tests by default', () => {
+ const config = createVitestConfig('test-package', 'e2e');
+
+ expect((config as any).test.coverage).toBeUndefined();
+ });
+
+ it('should always exclude mocks, types.ts, and perf folders', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ expect((config as any).test.coverage.exclude).toEqual([
+ 'mocks/**',
+ '**/types.ts',
+ 'perf/**',
+ ]);
+ });
+ });
+
+ describe('relative paths', () => {
+ it('should use relative paths for all file references', () => {
+ const config = createVitestConfig('test-package', 'unit');
+
+ // Setup files should be relative
+ const setupFiles = (config as any).test.setupFiles;
+ expect(setupFiles[0]).toMatch(/^\.\.\/\.\.\//);
+
+ // GlobalSetup should be relative
+ expect((config as any).test.globalSetup[0]).toBe('../../global-setup.ts');
+
+ // Cache dirs should be relative
+ expect(config.cacheDir).toMatch(/^\.\.\/\.\.\//);
+ expect((config as any).test.cache.dir).toMatch(/^\.\.\/\.\.\//);
+
+ // Coverage directory should be relative
+ expect((config as any).test.coverage.reportsDirectory).toMatch(
+ /^\.\.\/\.\.\//,
+ );
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle empty projectKey gracefully', () => {
+ const config = createVitestConfig('', 'unit');
+
+ expect(config.cacheDir).toBe('../../node_modules/.vite/');
+ expect((config as any).test.coverage.reportsDirectory).toBe(
+ '../../coverage//unit-tests',
+ );
+ });
+
+ it('should handle projectKey with special characters', () => {
+ const config = createVitestConfig('my-special_package.v2', 'unit');
+
+ expect(config.cacheDir).toBe(
+ '../../node_modules/.vite/my-special_package.v2',
+ );
+ });
+
+ it('should not modify config when no options provided to e2e', () => {
+ const config = createVitestConfig('test-package', 'e2e');
+
+ expect((config as any).test.testTimeout).toBeUndefined();
+ expect((config as any).test.globalSetup).toBeUndefined();
+ });
+ });
+});
diff --git a/testing/test-setup-config/src/lib/vitest-setup-files.ts b/testing/test-setup-config/src/lib/vitest-setup-files.ts
new file mode 100644
index 000000000..332c9f759
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-setup-files.ts
@@ -0,0 +1,72 @@
+import type { TestKind } from './vitest-config-factory.js';
+
+/**
+ * Setup files for unit tests.
+ *
+ * These paths are relative to the config file location (typically `packages//vitest.unit.config.ts`),
+ * which is why they use `../../` to navigate to the workspace root first.
+ */
+const UNIT_TEST_SETUP_FILES = [
+ '../../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/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',
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+] as const;
+
+/**
+ * Setup files for integration tests.
+ *
+ * These paths are relative to the config file location (typically `packages//vitest.int.config.ts`),
+ * which is why they use `../../` to navigate to the workspace root first.
+ */
+const INT_TEST_SETUP_FILES = [
+ '../../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/jest-extended.matcher.ts',
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+] as const;
+
+/**
+ * Setup files for E2E tests.
+ *
+ * These paths are relative to the config file location (typically `e2e//vitest.e2e.config.ts`),
+ * which is why they use `../../` to navigate to the workspace root first.
+ */
+const E2E_TEST_SETUP_FILES = [
+ '../../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/jest-extended.matcher.ts',
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+] as const;
+
+/**
+ * Returns the appropriate setup files for the given test kind.
+ *
+ * @param kind - The type of test (unit, int, or e2e)
+ * @returns Array of setup file paths relative to the config file location
+ *
+ * @example
+ * ```typescript
+ * const setupFiles = getSetupFiles('unit');
+ * // Returns all unit test setup files including mocks and matchers
+ * ```
+ */
+export function getSetupFiles(kind: TestKind): readonly string[] {
+ switch (kind) {
+ case 'unit':
+ return UNIT_TEST_SETUP_FILES;
+ case 'int':
+ return INT_TEST_SETUP_FILES;
+ case 'e2e':
+ return E2E_TEST_SETUP_FILES;
+ }
+}
diff --git a/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts b/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts
new file mode 100644
index 000000000..b3ad4c445
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts
@@ -0,0 +1,175 @@
+import { describe, expect, it } from 'vitest';
+import { getSetupFiles } from './vitest-setup-files.js';
+
+describe('vitest-setup-files', () => {
+ describe('getSetupFiles', () => {
+ describe('unit test setup files', () => {
+ it('should return all required setup files for unit tests', () => {
+ const setupFiles = getSetupFiles('unit');
+
+ expect(setupFiles).toHaveLength(10);
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/reset.mocks.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/cliui.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/git.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/portal-client.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/jest-extended.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+ );
+ });
+ });
+
+ describe('integration test setup files', () => {
+ it('should return exactly 6 setup files with correct includes', () => {
+ const setupFiles = getSetupFiles('int');
+
+ expect(setupFiles).toHaveLength(6);
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/reset.mocks.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/jest-extended.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+ );
+ });
+
+ it('should NOT include fs, cliui, git, and portal-client mocks for integration tests', () => {
+ const setupFiles = getSetupFiles('int');
+
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/cliui.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/git.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/portal-client.mock.ts',
+ );
+ });
+ });
+
+ describe('e2e test setup files', () => {
+ it('should return exactly 5 setup files with minimal mocks', () => {
+ const setupFiles = getSetupFiles('e2e');
+
+ expect(setupFiles).toHaveLength(5);
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/reset.mocks.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/jest-extended.matcher.ts',
+ );
+ expect(setupFiles).toContain(
+ '../../testing/test-setup/src/lib/extend/path.matcher.ts',
+ );
+ });
+
+ it('should NOT include any other mocks for e2e tests', () => {
+ const setupFiles = getSetupFiles('e2e');
+
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/console.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/fs.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/git.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/cliui.mock.ts',
+ );
+ expect(setupFiles).not.toContain(
+ '../../testing/test-setup/src/lib/portal-client.mock.ts',
+ );
+ });
+ });
+
+ describe('relative paths', () => {
+ it('should return paths relative to config file location', () => {
+ const unitFiles = getSetupFiles('unit');
+ const intFiles = getSetupFiles('int');
+ const e2eFiles = getSetupFiles('e2e');
+
+ // All paths should start with ../../
+ [...unitFiles, ...intFiles, ...e2eFiles].forEach(path => {
+ expect(path).toMatch(/^\.\.\/\.\.\//);
+ });
+ });
+ });
+
+ describe('return type', () => {
+ it('should return a readonly array', () => {
+ const setupFiles = getSetupFiles('unit');
+
+ // TypeScript will enforce readonly at compile time,
+ // but we can verify it's an array at runtime
+ expect(Array.isArray(setupFiles)).toBe(true);
+ });
+ });
+
+ describe('test kind differences', () => {
+ it('should return different setup files for different test kinds', () => {
+ const unitFiles = getSetupFiles('unit');
+ const intFiles = getSetupFiles('int');
+ const e2eFiles = getSetupFiles('e2e');
+
+ // Different lengths means different setup files
+ expect(unitFiles.length).not.toBe(intFiles.length);
+ expect(intFiles.length).not.toBe(e2eFiles.length);
+ expect(unitFiles.length).not.toBe(e2eFiles.length);
+ });
+
+ it('should show hierarchy: unit has most, e2e has least', () => {
+ const unitFiles = getSetupFiles('unit');
+ const intFiles = getSetupFiles('int');
+ const e2eFiles = getSetupFiles('e2e');
+
+ expect(unitFiles.length).toBeGreaterThan(intFiles.length);
+ expect(intFiles.length).toBeGreaterThan(e2eFiles.length);
+ });
+ });
+ });
+});
diff --git a/testing/test-setup-config/src/lib/vitest-setup-presets.ts b/testing/test-setup-config/src/lib/vitest-setup-presets.ts
new file mode 100644
index 000000000..b3502ef86
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-setup-presets.ts
@@ -0,0 +1,70 @@
+import type { UserConfig as ViteUserConfig } from 'vitest/config';
+import {
+ type E2ETestOptions,
+ createVitestConfig,
+} from './vitest-config-factory.js';
+
+/**
+ * Creates a standardized Vitest configuration for unit tests.
+ *
+ * @param projectKey - The project name (used for cache and coverage directory naming)
+ * @returns Vitest configuration object
+ *
+ * @example
+ * ```typescript
+ * export default createUnitTestConfig('my-package');
+ * ```
+ */
+export function createUnitTestConfig(projectKey: string): ViteUserConfig {
+ return createVitestConfig(projectKey, 'unit');
+}
+
+/**
+ * Creates a standardized Vitest configuration for integration tests.
+ *
+ * @param projectKey - The project name (used for cache and coverage directory naming)
+ * @returns Vitest configuration object
+ *
+ * @example
+ * ```typescript
+ * export default createIntTestConfig('my-package');
+ * ```
+ */
+export function createIntTestConfig(projectKey: string): ViteUserConfig {
+ return createVitestConfig(projectKey, 'int');
+}
+
+/**
+ * Creates a standardized Vitest configuration for E2E tests.
+ *
+ * @param projectKey - The project name (used for cache and coverage directory naming)
+ * @param options - Optional configuration for E2E tests
+ * @returns Vitest configuration object
+ *
+ * @example
+ * ```typescript
+ * // Basic usage
+ * export default createE2ETestConfig('my-e2e');
+ *
+ * // With options
+ * export default createE2ETestConfig('my-e2e', {
+ * testTimeout: 60_000,
+ * });
+ *
+ * // Override any config using spread operator
+ * const baseConfig = createE2ETestConfig('my-e2e', { testTimeout: 60_000 });
+ * export default {
+ * ...baseConfig,
+ * test: {
+ * ...(baseConfig as any).test,
+ * globalSetup: ['./custom-setup.ts'],
+ * },
+ * };
+ * ```
+ */
+export function createE2ETestConfig(
+ projectKey: string,
+ options?: E2ETestOptions,
+): ViteUserConfig {
+ return createVitestConfig(projectKey, 'e2e', options);
+}
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
new file mode 100644
index 000000000..e94b7e3e9
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts
@@ -0,0 +1,154 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import * as configFactory from './vitest-config-factory.js';
+import {
+ createE2ETestConfig,
+ createIntTestConfig,
+ createUnitTestConfig,
+} from './vitest-setup-presets.js';
+
+vi.mock('./vitest-config-factory.js', () => ({
+ createVitestConfig: vi.fn().mockReturnValue('mocked-config'),
+}));
+
+const MOCK_PROJECT_KEY = 'test-package';
+
+describe('vitest-setup-presets', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('createUnitTestConfig', () => {
+ it('should call createVitestConfig with unit kind', () => {
+ const result = createUnitTestConfig(MOCK_PROJECT_KEY);
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ MOCK_PROJECT_KEY,
+ 'unit',
+ );
+ expect(result).toBe('mocked-config');
+ });
+
+ it('should handle different project names', () => {
+ createUnitTestConfig('my-custom-package');
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ 'my-custom-package',
+ 'unit',
+ );
+ });
+
+ it('should handle empty projectKey', () => {
+ createUnitTestConfig('');
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith('', 'unit');
+ });
+ });
+
+ describe('createIntTestConfig', () => {
+ it('should call createVitestConfig with int kind', () => {
+ const result = createIntTestConfig(MOCK_PROJECT_KEY);
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ MOCK_PROJECT_KEY,
+ 'int',
+ );
+ expect(result).toBe('mocked-config');
+ });
+
+ it('should handle different project names', () => {
+ createIntTestConfig('integration-package');
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ 'integration-package',
+ 'int',
+ );
+ });
+ });
+
+ describe('createE2ETestConfig', () => {
+ it('should call createVitestConfig with e2e kind and no options', () => {
+ const result = createE2ETestConfig(MOCK_PROJECT_KEY);
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ MOCK_PROJECT_KEY,
+ 'e2e',
+ undefined,
+ );
+ expect(result).toBe('mocked-config');
+ });
+
+ it('should pass options to createVitestConfig', () => {
+ const options = {
+ testTimeout: 60_000,
+ };
+
+ createE2ETestConfig(MOCK_PROJECT_KEY, options);
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ MOCK_PROJECT_KEY,
+ 'e2e',
+ options,
+ );
+ });
+
+ it('should handle testTimeout option', () => {
+ createE2ETestConfig(MOCK_PROJECT_KEY, { testTimeout: 30_000 });
+
+ expect(configFactory.createVitestConfig).toHaveBeenCalledWith(
+ MOCK_PROJECT_KEY,
+ 'e2e',
+ { testTimeout: 30_000 },
+ );
+ });
+ });
+
+ describe('function naming', () => {
+ it('should use clear descriptive names', () => {
+ expect(createUnitTestConfig).toBeDefined();
+ expect(createIntTestConfig).toBeDefined();
+ expect(createE2ETestConfig).toBeDefined();
+ });
+ });
+
+ describe('integration with factory', () => {
+ it('should call factory with correct test kinds', () => {
+ createUnitTestConfig('pkg1');
+ createIntTestConfig('pkg2');
+ createE2ETestConfig('pkg3');
+
+ expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith(
+ 1,
+ 'pkg1',
+ 'unit',
+ );
+ expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith(
+ 2,
+ 'pkg2',
+ 'int',
+ );
+ expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith(
+ 3,
+ 'pkg3',
+ 'e2e',
+ undefined,
+ );
+ });
+
+ it('should return whatever the factory returns', () => {
+ const mockConfigs = {
+ unit: { test: 'unit-config' },
+ int: { test: 'int-config' },
+ e2e: { test: 'e2e-config' },
+ };
+
+ vi.mocked(configFactory.createVitestConfig)
+ .mockReturnValueOnce(mockConfigs.unit as any)
+ .mockReturnValueOnce(mockConfigs.int as any)
+ .mockReturnValueOnce(mockConfigs.e2e as any);
+
+ expect(createUnitTestConfig('test')).toBe(mockConfigs.unit);
+ expect(createIntTestConfig('test')).toBe(mockConfigs.int);
+ expect(createE2ETestConfig('test')).toBe(mockConfigs.e2e);
+ });
+ });
+});
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..f1a9cc0c3
--- /dev/null
+++ b/testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts
@@ -0,0 +1,29 @@
+import path from 'node:path';
+import { loadConfig } from 'tsconfig-paths';
+import type { Alias, AliasOptions } from 'vite';
+
+/**
+ * Loads TypeScript path aliases from tsconfig.base.json for use in Vitest.
+ * Uses process.cwd() as the workspace root to load the tsconfig.
+ */
+export function tsconfigPathAliases(): AliasOptions {
+ const tsconfigPath = path.resolve(process.cwd(), '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,
+ // Make paths relative to workspace root (../../ from config file)
+ replacement: path.resolve(process.cwd(), relativePath),
+ }),
+ );
+}
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..1c135686a
--- /dev/null
+++ b/testing/test-setup-config/vitest.unit.config.ts
@@ -0,0 +1,4 @@
+///
+import { createUnitTestConfig } from './src/index.js';
+
+export default createUnitTestConfig('test-setup-config');
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/tools/vitest-tsconfig-path-aliases.ts b/tools/vitest-tsconfig-path-aliases.ts
index ac8be04df..aec88e199 100644
--- a/tools/vitest-tsconfig-path-aliases.ts
+++ b/tools/vitest-tsconfig-path-aliases.ts
@@ -1,8 +1,13 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
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
+ ? path.resolve(fileURLToPath(projectRootUrl), 'tsconfig.base.json')
+ : '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}`,
@@ -14,7 +19,9 @@ export function tsconfigPathAliases(): 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,
}),
);
}
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"