Skip to content
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ node_modules
/.sass-cache
/connect.lock
/coverage
**/.coverage/**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which tool uses .coverage folders? 🤨

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one could be used to exclude colocated coverage folders of each project.

/examples/react-todos-app/coverage
/libpeerconnection.log
npm-debug.log
Expand Down
31 changes: 13 additions & 18 deletions e2e/ci-e2e/vitest.e2e.config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
import { createE2eConfig } from '../../testing/test-setup-config/src/lib/vitest-setup-presets.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',
export default createE2eConfig(
'ci-e2e',
{
projectRoot: new URL('../../', import.meta.url),
cacheKey: 'ci-e2e',
},
{
test: {
testTimeout: 60_000,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be a default for the e2e target

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked other files and there are many different values, like 20k, 40k, 80k, 60k, 120k.
Just making sure that we want to have 60k as default?

Copy link
Collaborator

@BioPhoton BioPhoton Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm.... I would put 20 as default. Or if there are more projects with 40 lets go with 40. And for the projects with higher or lower times go with a override.

Did not realise this is already an overwrite..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked that and:

Timeout Occurences
20s 6
40s 1
60s 1
80s 2
120s 1

I suggest then use 20s as default (20_000ms)

Copy link
Collaborator

@matejchalk matejchalk Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeouts might actually be a justifiable exception to keeping everything unified. 🤔 Some E2E tests require a generous test timeout, but it wouldn't be optimal to have a high test timeout everywhere.

I would handle these kinds of situations with a limited options object. For example:

  • without custom options:
    export default createIntTestConfig('utils');
  • with custom options (custom { testTimeout?: number } type, not the full Vitest config)
    export default createIntTestConfig('utils', { testTimeout: 40_000 });

However, it would be worth considering if we really need to configure a custom timeout for a whole project. It's also possible to override timeouts per individual describe or it blocks. This may be a better targeted approach. We want to avoid higher timeouts if possible, but if they're not needed, it's best to know for which test precisely this is the case. Then we could have 3 fixed project-level timeouts for each kind of test (unit tests should be much faster than E2E tests, for example).

globalSetup: ['./global-setup.ts'],
coverage: { enabled: false },
},
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'],
},
});
);
34 changes: 12 additions & 22 deletions e2e/plugin-typescript-e2e/vitest.e2e.config.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
import { createE2eConfig } from '../../testing/test-setup-config/src/lib/vitest-setup-presets.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',
export default createE2eConfig(
'plugin-typescript-e2e',
{
projectRoot: new URL('../../', import.meta.url),
cacheKey: 'plugin-typescript-e2e',
},
{
test: {
testTimeout: 20_000,
coverage: { enabled: true },
},
environment: 'node',
include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'],
},
});
);
39 changes: 13 additions & 26 deletions packages/core/vitest.int.config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
import {
createIntConfig,
setupPresets,
} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.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'],
export default createIntConfig(
'core',
{
projectRoot: new URL('../../', import.meta.url),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "project root" terminology is inconsistent with how Nx uses it. For this example, the project root would be the packages/core directory. So based on the naming, I would expect projectRoot: import.meta.url. The new URL('../..', import.meta.url) value would match what Nx calls the workspace root.

In fact, since the workspace root is the same for each project, I don't see any reason this needs to be a parameter for the factory function. It would be more encapsulated and reliable if the factory function created the absolute path to the workspace root internally.

},
{
test: {
setupFiles: [...setupPresets.int.base, ...setupPresets.int.portalClient],
},
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',
],
},
});
);
50 changes: 18 additions & 32 deletions packages/core/vitest.unit.config.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
import {
createUnitConfig,
setupPresets,
} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.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'],
export default createUnitConfig(
'core',
{
projectRoot: new URL('../../', import.meta.url),
},
{
test: {
setupFiles: [
...setupPresets.unit.base,
...setupPresets.unit.git,
...setupPresets.unit.portalClient,
...setupPresets.unit.matchersCore,
],
},
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',
],
},
});
);
40 changes: 14 additions & 26 deletions packages/utils/vitest.int.config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
import {
createIntConfig,
setupPresets,
} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.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'],
export default createIntConfig(
'utils',
{
projectRoot: new URL('../../', import.meta.url),
},
{
test: {
coverage: { exclude: ['perf/**'] },
setupFiles: [...setupPresets.int.base, ...setupPresets.int.cliui],
},
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',
],
},
});
);
54 changes: 20 additions & 34 deletions packages/utils/vitest.unit.config.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,24 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js';
import {
createUnitConfig,
setupPresets,
} from '../../testing/test-setup-config/src/lib/vitest-setup-presets.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'],
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'] },
Comment on lines +14 to +15
Copy link
Collaborator

@matejchalk matejchalk Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest of avoiding overrides per project (see previous comment), I would apply this wider file pattern for each project. Most projects don't happen to use .type.test.ts files, but so what? There's no harm in having them configured. If we decide to add type tests to another project, then it would be most convenient to just start adding files with .type.test.ts extension with no extra configuration effort.

coverage: { exclude: ['perf/**'] },
Copy link
Collaborator

@matejchalk matejchalk Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to limit how much variation we have between each project's Vitest configs. Because they were all separate files until now, there are some minor inconsistencies between them. However, in most cases, I don't believe there's a compelling reason for those variations; it just happened to evolve that way.

One such example is the perf folder. Some projects use it, some projects don't. The main thing is we want to stick with this convention for the whole monorepo. Everything in {projectRoot}/perf is non-production code and therefore not relevant for test coverage. Therefore, for both simplicity and consistency, I would simply add coverage: { exclude: ['perf/**'] } to each project, regardless of whether it has a perf folder or not. There's no harm in excluding a folder that doesn't exist.

setupFiles: [
...setupPresets.unit.base,
...setupPresets.unit.matchersCore,
...setupPresets.unit.matcherPath,
],
},
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',
],
},
});
);
24 changes: 24 additions & 0 deletions testing/test-setup-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Vitest config factory and setup presets

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
Comment on lines +5 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm only interested in using this project, not maintaining it, then do I need to know about these files? I would think not; they look like implementation details. The project README should first document usage (i.e., your Examples section). Any internal information should be moved to the bottom of the README, as it's least likely to be relevant. That's if it's even needed - IMHO, the file names are pretty self-explanatory, and I can look them up in src if I need to.


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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename the exported functions to createUnitTestConfig, createIntTestConfig, and createE2ETestConfig. It would make them more self-descriptive, and it works better in English.


### Examples

**Using defaults:**

```ts
export default createUnitConfig('my-package', import.meta.url);
```

**Extending default setup files:**

```ts
export default createIntConfig('my-package', import.meta.url, {
setupFiles: [...setupPresets.int.base, ...setupPresets.int.git, './custom-setup.ts'],
});
```
Comment on lines +12 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would include import statements in these examples. Otherwise, it's not fully clear where createUnitConfig, createIntConfig, and setupPresets are coming from.

12 changes: 12 additions & 0 deletions testing/test-setup-config/eslint.config.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
});
12 changes: 12 additions & 0 deletions testing/test-setup-config/project.json
Original file line number Diff line number Diff line change
@@ -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"]
}
14 changes: 14 additions & 0 deletions testing/test-setup-config/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading