Skip to content

Conversation

poltorak
Copy link
Collaborator

@poltorak poltorak commented Sep 5, 2025

Centralize and Standardize Vitest Configurations

What Changed

  • New centralized configuration system: Created testing/test-setup-config package with a factory-based approach for generating Vitest configurations
  • Setup presets: Introduced modular setup file presets that can be composed for different test types (unit, integration, e2e)
  • Simplified configuration files: Refactored 6 existing Vitest config files to use the new centralized system, reducing them from verbose configurations to simple function calls

Benefits

  • Eliminates duplication: Removes repetitive boilerplate across multiple vitest config files
  • Ensures consistency: Standardizes test configurations, coverage settings, and setup files across all packages
  • Improves maintainability: Changes to test configuration defaults now only need to be made in one place
  • Better developer experience: New configs are more readable and easier to customize with sensible defaults and preset combinations

The new system provides createUnitConfig, createIntConfig, and createE2eConfig factory functions that automatically handle common configuration patterns while still allowing customization through override parameters.

Relates to first part of #1065

@github-actions github-actions bot added 📖 Project documentation improvements or additions to the project documentation 🔬 testing writing tests 🛠️ tooling labels Sep 5, 2025
Copy link

nx-cloud bot commented Sep 5, 2025

View your CI Pipeline Execution ↗ for commit 7008a48

Command Status Duration Result
nx code-pushup --nx-bail -- compare ✅ Succeeded 53s View ↗
nx code-pushup --nx-bail -- ✅ Succeeded 1m 2s View ↗
nx code-pushup --nx-bail -- print-config --outp... ✅ Succeeded 3m 48s View ↗

☁️ Nx Cloud last updated this comment at 2025-10-06 15:34:39 UTC

Copy link

pkg-pr-new bot commented Sep 5, 2025

Open in StackBlitz

@code-pushup/ci

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/ci@1107

@code-pushup/cli

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/cli@1107

@code-pushup/core

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/core@1107

@code-pushup/create-cli

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/create-cli@1107

@code-pushup/models

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/models@1107

@code-pushup/nx-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/nx-plugin@1107

@code-pushup/coverage-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/coverage-plugin@1107

@code-pushup/eslint-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/eslint-plugin@1107

@code-pushup/js-packages-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/js-packages-plugin@1107

@code-pushup/jsdocs-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/jsdocs-plugin@1107

@code-pushup/lighthouse-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/lighthouse-plugin@1107

@code-pushup/typescript-plugin

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/typescript-plugin@1107

@code-pushup/utils

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/utils@1107

@code-pushup/models-transformers

npm i https://pkg.pr.new/code-pushup/cli/@code-pushup/models-transformers@1107

commit: 7008a48

Copy link
Contributor

github-actions bot commented Sep 5, 2025

Code PushUp

🤨 Code PushUp report has both improvements and regressions – compared current commit 5a5fcfa with previous commit 33714e2.

🕵️ See full comparison in Code PushUp portal 🔍

🏷️ Categories

🏷️ Category ⭐ Previous score ⭐ Current score 🔄 Score change
Performance 🔴 36 🔴 33 ↓ −2.6
Updates 🟡 84 🟡 84 ↓ −0.2
Code coverage 🟡 90 🟡 90 ↓ −0.1
Security 🟡 56 🟡 56
Accessibility 🟢 92 🟢 92
Best Practices 🟢 100 🟢 100
SEO 🟡 61 🟡 61
Type Safety 🟢 100 🟢 100
Bug prevention 🟢 100 🟢 100
Miscellaneous 🟢 100 🟢 100
Code style 🟢 100 🟢 100
Documentation 🔴 24 🔴 24
👎 3 groups regressed, 👍 4 audits improved, 👎 4 audits regressed, 14 audits changed without impacting score

🗃️ Groups

🔌 Plugin 🗃️ Group ⭐ Previous score ⭐ Current score 🔄 Score change
Lighthouse Performance 🔴 36 🔴 33 ↓ −2.6
JS Packages NPM outdated dependencies 🟡 84 🟡 84 ↓ −0.2
Code coverage Code coverage metrics 🟡 90 🟡 90 ↓ −0.1

18 other groups are unchanged.

🛡️ Audits

🔌 Plugin 🛡️ Audit 📏 Previous value 📏 Current value 🔄 Value change
Lighthouse Total Blocking Time 🟥 1,640 ms 🟥 2,770 ms ↑ +69.2 %
Lighthouse First Contentful Paint 🟥 3.3 s 🟥 3.1 s ↓ −4.5 %
Lighthouse Speed Index 🟥 7.2 s 🟥 7.6 s ↑ +6.1 %
Code coverage Line coverage 🟨 86.3 % 🟨 84.4 % ↓ −2.1 %
JS Packages Outdated NPM dev dependencies. 🟨 59 outdated package versions (27 major, 24 minor, 8 patch) 🟨 59 outdated package versions (28 major, 23 minor, 8 patch)  +0 %
Lighthouse Time to Interactive 🟥 13.6 s 🟥 13.3 s ↓ −2.2 %
Code coverage Function coverage 🟩 92.5 % 🟩 92.6 % ↑ +0.1 %
Code coverage Branch coverage 🟨 85.5 % 🟨 85.5 % ↑ +0.1 %
Lighthouse Avoids enormous network payloads 🟩 Total size was 2,039 KiB 🟩 Total size was 2,034 KiB ↓ −0.3 %
Lighthouse Minimizes main-thread work 🟥 11.9 s 🟥 14.4 s ↑ +21.6 %
Lighthouse JavaScript execution time 🟥 5.0 s 🟥 6.5 s ↑ +28.3 %
Lighthouse Uses efficient cache policy on static assets 🟨 30 resources found 🟨 30 resources found ↓ −0.1 %
Lighthouse Largest Contentful Paint 🟥 11.3 s 🟥 11.9 s ↑ +5.7 %
Lighthouse Max Potential First Input Delay 🟥 1,390 ms 🟥 1,890 ms ↑ +36.3 %
Lighthouse Metrics 🟩 100% 🟩 100% ↓ −2.2 %
Lighthouse Reduce unused CSS 🟥 Potential savings of 102 KiB 🟥 Potential savings of 102 KiB ↓ −32.6 %
Lighthouse Reduce unused JavaScript 🟥 Potential savings of 179 KiB 🟥 Potential savings of 180 KiB ↓ −15.4 %
Lighthouse Server Backend Latencies 🟩 1,540 ms 🟩 1,400 ms ↓ −8.9 %
Lighthouse Initial server response time was short 🟩 Root document took 600 ms 🟩 Root document took 470 ms ↓ −20.6 %
Lighthouse Network Round Trip Times 🟩 50 ms 🟩 10 ms ↓ −77.8 %
Lighthouse Remove duplicate modules in JavaScript bundles 🟥 Potential savings of 78 KiB 🟥 Potential savings of 99 KiB ↑ +3.3 %
JS Packages Vulnerabilities for NPM dev dependencies. 🟥 11 vulnerabilities (1 critical, 5 moderate, 5 low) 🟥 15 vulnerabilities (1 critical, 5 moderate, 9 low) ↑ +36.4 %

588 other audits are unchanged.

Copy link
Collaborator

@BioPhoton BioPhoton left a comment

Choose a reason for hiding this comment

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

Thanks for cleaning up our test config mess :)

I left some comments.

  • please add a proper PR description that contains the scope and planned changes
  • move the config code under a Nx project in testing/vitest-setup. I updated the issue accordingly.

@poltorak poltorak marked this pull request as ready for review September 10, 2025 06:45
Comment on lines 127 to 132
const testTimeout =
kind === 'unit'
? TEST_TIMEOUTS.MEDIUM
: kind === 'int'
? TEST_TIMEOUTS.LONG
: TEST_TIMEOUTS.E2E;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's avoid conditionals in tests. It makes the test code harder to read and adds noise.

['unit', setupPresets.unit.base, createUnitConfig],
['int', setupPresets.int.base, createIntConfig],
['e2e', setupPresets.e2e.base, createE2eConfig],
] as const)('%s config creation', (kind, baseSetupFiles, createFn) => {
Copy link
Collaborator

@BioPhoton BioPhoton Oct 4, 2025

Choose a reason for hiding this comment

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

The each block for describe is introducing conditional checks in the it blocks as well as const. It further makes the tests harder to read. Let's avoid conditionals in tests as good as possible.

If you want to test basic values you could have a separate it block using the each for static things like defaults that are always the same. In the specific cases you can use expect.objectContaining to keep is focused.

see example for conditionaly

export function createVitestConfig(
options: VitestConfigFactoryOptions,
overrides: VitestOverrides = {},
): ViteUserConfig {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest excluding overrides from the logic and push it to the user. This avoids complexity and reduces maintenance efforts to sync with vitest changes. WDYT @matejchalk

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please let me know which direction we should follow:

  • Easier override handling ( current solution ) - in this case we take care of most of the overrides but on the other hand we are exposed to vitest changes (if the happen) and we need to keep up it them
  • We go easy way and move transfer the "burden" of overrides to end consumer / user.

Both approaches have pros and cons. I don't feel like I'm the right person to make a decision here 😅

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I've answered this comprehensively in my review. 😁

Unless there's a really good reason for projects to have something configured differently from others, I would hard-code it in the factory functions.

projectKey: string;
kind: TestKind;
projectRootUrl: URL;
cacheDirName: string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

you could derive the cacheDirName from the projectKey. Internally if needed you can modify if needed e.g. cache-${projectKey}.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is it suggestion or request change 😄 ?
Deriving cacheDirName from projectKey in form: cache-${projectKey} sounds ok but makes me wonder, aren't we making some hidden bond between project key and cache directory?

Maybe we could consider cacheDirName as optional which fallbacks to cache-${projectKey} if not specified?
Let me know @BioPhoton

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

EDIT:
We already using such bond for coverage, let's do the same for cache. I'll update it

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.

Maybe we could consider cacheDirName as optional which fallbacks to ...

Yes better!

cache-${projectKey} if not specified?

the "cache-" was just something to give example. In general the cache key (and directory) should be unique across the repository. That is the reason we are about it in the first place.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Method you highlighted in comment is kind of "internal", the fallback is done in line 50

Is that ok acceptable or do you suggest other approach to that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think your current implementation is unnecessarily flexible.

const cacheDirName = options.cacheKey ?? `cache-${options.projectKey}`;

The only thing we care about is that the cache key is unique. Since Nx project names are unique, these can be used. But we don't need to override this per project.

},
{
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).

Copy link
Collaborator

@BioPhoton BioPhoton left a comment

Choose a reason for hiding this comment

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

Thanks for the changes! Nice testing tool! There is Readme, and tests, lovely.
I left some first comments and will wait for one feedback on the override logic for further comments.

@poltorak poltorak force-pushed the refactor/vitest-configurations branch from 9433ca5 to 7008a48 Compare October 6, 2025 15:17
Copy link
Collaborator

@matejchalk matejchalk left a comment

Choose a reason for hiding this comment

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

Hi, thanks for your contribution 🙂 I think it looks promising, but ...

I don't think this PR goes far enough towards unification. Ideally, only 2 parameters1 would determine the entire Vitest config:

  • Nx project name (e.g., cli, plugin-eslint, ci-e2e) 2
  • test kind ('unit' | 'int' | 'e2e')

My main criticism of this PR is that it doesn't commit to unification over flexibility. I believe we need less flexibility than it may appear, so I would go all in on unification. If we find some exception, we should question whether that exception is justified and get rid of it if possible.

In the current implementation, some things are automatically inferred, but there are many (IMHO unnecessary) optional arguments, and anything can be overridden by providing a Vitest config object. To me, this is a leaky abstraction.

I've suggested specifics on what can be unified in my comments. Let me know if I can clarify anything 🙂 I realize you couldn't reasonably have known many of these things, so no worries.

Footnotes

  1. By "parameter", I don't strictly mean a function parameter. Your approach of exporting different factory functions for each test kind also works, for example.

  2. It may turn out we also need something like projectRoot (e.g., 'packages/core'). In that case, we could either derive it via @nx/devkit's createProjectGraphAsync, provide the project folder instead and derive its name using path.basename, or add another parameter.

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: {
include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
typecheck: { include: ['**/*.type.test.ts'] },
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.

Comment on lines +14 to +15
include: ['src/**/*.{unit,type}.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
typecheck: { include: ['**/*.type.test.ts'] },
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.

Comment on lines +7 to +39
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;
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 think this is unnecessarily granular. I understand that it may appear like each project needs to pick and choose precisely which mocks and matchers to include. But I believe there should actually be a consistent set for each test type. For example:

  • all tests should include reset.mock.ts
  • all unit and integration tests should include console.mock.ts and git.mock.ts, but no E2E tests should include these mocks
  • all unit tests should include fs.mock.ts, but no integration or E2E tests should
  • all tests can use any of our custom matchers

In the end, I think the test kind ('unit' | 'int' | 'e2e') should fully determine the list of setupFiles. So the factory functions need not support this override per project.

- `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. 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.

Comment on lines +12 to +24
**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'],
});
```
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.

Comment on lines +5 to +6
- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults
- `vitest-setup-presets.ts`: provides create functions and exportable setup file groups
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.


## Shared config

[README](./src/lib/config/README.md) how to use vitest config factory.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The link is broken, and this doesn't really work as an English sentence.

Suggested change
[README](./src/lib/config/README.md) how to use vitest config factory.
See [`@code-pushup/test-setup-config` docs](../test-setup-config/README.md) on how to use our Vitest config factory.


export function tsconfigPathAliases(): AliasOptions {
const result = loadConfig('tsconfig.base.json');
export function tsconfigPathAliases(projectRootUrl?: URL): AliasOptions {
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.

This function is now duplicated in testing/test-setup-config/src/lib/vitest-tsconfig-path-aliases.ts. I'm guessing we don't need it here in tools anymore?

/.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? 🤨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📖 Project documentation improvements or additions to the project documentation 🔬 testing writing tests 🛠️ tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants