Migrate all core-web unit tests from Jest and Karma to Vitest
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
Migrate every unit test in the core-web Nx workspace to Vitest, retiring Jest and the two
remaining Karma test targets. This closes the Reliability objective of epic #32713 — "The testing
strategy is successfully migrated to Vitest for unit tests and Playwright for E2E tests" — and
feeds its Performance and Velocity objectives (CI wall time, local feedback loop).
This change does not touch a single line of product code. That is not an aspiration, it is the
scope boundary, and AC-1 makes CI enforce it. See Why this needs no manual QA.
Current state (measured on main @ 31c3672aa5)
| Runner | Projects | Spec files | How it is wired |
|---|---|---|---|
| Jest | 41 project globs | ~1,050 | @nx/jest/plugin with an explicit include list in nx.json; 51 jest.config.ts files |
| Karma | 2 | 3 | @angular/build:karma executor — libs/dotcms-js (3 specs), apps/dotcms-block-editor (0 specs) |
| Vitest (already migrated) | 4 | 35 | @nx/vitest — libs/sdk/analytics, libs/sdk/experiments, libs/sdk/vue, libs/edit-content-bridge |
| Stencil (out of scope) | 1 | — | @nxext/stencil:test on Stencil's own bundled Jest + TypeScript 5.8.3 |
| ~1,088 total |
Relevant versions already in the workspace: Angular 22.1.0, Nx 23.1.1, Vitest 4.0.18,
@nx/vitest 23.1.1, @vitest/coverage-v8, jsdom 28.1.0, happy-dom 15.7.4, Jest 30.2.0,
jest-preset-angular 17.0.0, ts-jest 29.4.6.
Vitest is not a new dependency and @nx/vitest is already registered in nx.json with
testTargetName: "test" — the same target name @nx/jest/plugin uses. Migrating a project is
therefore: remove it from the Jest include list, add a Vitest config, delete jest.config.ts.
The nx test target name never changes, which is what keeps CI untouched (see AC-6).
There is effectively no Jasmine left
Worth stating plainly, because it resizes the "Jasmine → Vitest" half of this work from "1,088
specs" to "3 specs":
- Zero real Jasmine API usage in the workspace. All 4
jasmine.*matches across every spec file
are inside comments describing a past mechanical migration (apps/dotcms-ui/.../menu-guard.service.spec.ts:62
and three siblings). Nojasmine.createSpyObj/createSpycall sites remain. - Neither
jasmine-corenorkarmais inpackage.json. The twokarma.conf.jsfiles are
orphaned config. apps/dotcms-block-editorhas 0 spec files yet still carries atesttarget, akarma.conf.js,
asrc/test.tsand a polyfills entry — all dead code.- The 3 surviving
libs/dotcms-jsspecs are already written in modern style (@openng/spectator,
TestBed), not Jasmine idiom. They only happen to run on the Karma executor.
So the Karma track is: delete one project's dead config, port three spec files.
Why this needs no manual QA
PR #37198 (the strict-mode rollout, epic #35932 — 1,455 files, +18,002/−11,088) is still open and
unmerged. The blocker was not a defect found in review: it was the cost of manually QA-ing a diff
that large to prove nothing broke. That concern was legitimate there — strict mode edited
product code: null guards, changed wire payloads, null→undefined substitutions in
dot-pages.store.ts and dot-container-list.store.ts. Product code changed, so product behavior
could have changed, so somebody had to look.
This migration is categorically different, and the difference is machine-checkable. Swapping a
test runner changes which process executes assertions. It cannot change what the application ships:
| #37198 (strict mode) | This issue (Vitest) | |
|---|---|---|
| Product source files changed | many | zero |
| Shipped bundle affected | yes (type-driven refactors) | no — test files are not bundled |
| Runtime behavior can differ | yes | no |
| Requires human judgement per file | yes | no — mechanical and uniform |
| Provable by CI | partially | yes, completely |
The argument "it only touches tests, so users cannot be affected" is only as good as its
enforcement — asserted in a PR description it is a promise, and a reviewer is right not to take a
promise on a 1,000-file diff. AC-1 turns it into a required check: CI fails the PR if the diff
contains any file outside an allowlist of test files and test configuration. A reviewer then does
not need to audit 1,000 files or run a regression pass; they need to confirm one green check.
Combined with AC-2 (test-count parity — the same number of tests execute, with no new skips), the
two checks answer both questions a reviewer actually has: did product code change? (no, provably)
and are all the tests still really running? (yes, provably). Neither answer requires a human
clicking through the admin UI.
Scope
In scope
- All 41 Jest project globs → Vitest (~1,050 spec files).
- Both
@angular/build:karmatargets → Vitest, via@nx/vitestfor workspace uniformity. - Removal of Jest and Karma dependencies, configs, presets and stale
nx.jsonwiring. - Generator defaults, so new projects arrive on Vitest.
Out of scope
libs/dotcms-webcomponents— runs on@nxext/stencil:test, which uses Stencil's own bundled
Jest and its own TypeScript 5.8.3. Migrating it means migrating off Stencil's test harness; it is
not in the@nx/jest/plugininclude list and is untouched here. Needs its own issue.- E2E / Playwright — a separate objective of epic #32713.
- Backend tests (JUnit, integration, Postman).
- Any product-code change. If a spec cannot be migrated without editing product source, that
file is dropped from this PR and raised as its own issue rather than smuggled in — AC-1 enforces
this by failing the build. - Rewriting test logic. Assertions, mocks and structure are ported as-is. Improving weak tests
is separate work; mixing it in would break test-count parity and re-introduce the need for review
judgement.
Delivery approach
One PR migrating all 41 projects at once (decided: no mixed Jest/Vitest end state, no long-lived
transition period).
Because AC-1 proves the diff contains no product code, PR size and PR risk are decoupled here —
a 1,000-file diff that provably touches only test files carries no more user-facing risk than a
10-file one. Two things keep the review itself tractable:
- One commit per project, so review and
git bisectcan proceed project by project inside the
single PR. - A codemod for the mechanical rewrites (
jest.fn→vi.fn,jest.mock→vi.mock,
jest.spyOn→vi.spyOn, timer APIs,jest.Mocktype references), so the diff is uniform and
reviewable by pattern rather than file by file. Hand-edits are limited to what the codemod cannot
do and called out in the PR description.
Flagging one factor, not disputing the decision: a single PR of this size still competes for
review throughput and will need rebasing against everymainmerge that touches a spec file
(the strict-mode branch hits this repeatedly). If it stalls for that reason rather than a QA
reason, splitting by project is the fallback — the per-project commits make that a cheap pivot.
Acceptance Criteria
Enforcement — the part that replaces manual QA
- AC-1 — CI fails the PR if any non-test file changes. A required check inspects the diff
against the merge base and fails on any path outside this allowlist:**/*.spec.ts,
**/*.test.ts,**/test-setup.ts,**/src/test.ts,**/jest.config.*,**/vite.config.*,
**/vitest.config.*,**/tsconfig.spec.json,**/karma.conf.js,jest.preset.js,
nx.json,package.json,pnpm-lock.yaml,**/project.json,core-web/pom.xml, and
*.md. Any product source path fails the build. The check is committed as a reusable script
so the next test-infra migration inherits it. - AC-2 — Test-count parity per project. For every migrated project the number of executed
tests matches the Jest baseline, with zero new skipped/todo tests. Baseline and post-
migration counts are recorded per project in the PR description; any intentional difference
is justified line by line. - AC-3 — Suite is green across all 41 migrated projects plus the 2 former Karma projects,
with no--passWithNoTestsmasking an empty project and no retry flags hiding flakiness.
Migration completeness
- AC-4 — Zero Jest remaining. No
jest.config.*in the repo (excluding
libs/dotcms-webcomponents);jest,jest-preset-angular,ts-jest,jest-environment-jsdom
and@happy-dom/jest-environmentare removed frompackage.json;jest.preset.jsis deleted;
the@nx/jest/pluginentry and its 41-globincludelist are gone fromnx.json. - AC-5 — Zero Karma remaining. Both
@angular/build:karmatargets are gone.libs/dotcms-js
runs its 3 specs on Vitest.apps/dotcms-block-editor's dead test target,karma.conf.js,
src/test.tsand polyfills entry are deleted rather than migrated. The stale
{workspaceRoot}/karma.conf.jsentry innx.json→targetDefaults.test.inputsis removed
(it already points at a file that does not exist). - AC-6 — CI needs no pipeline edit. The
unit-testexecution incore-web/pom.xml
(nx run-many -t test --exclude=tag:skip:test) is unchanged, because every migrated project
keeps the target nametest. Demonstrated by the PR containing no workflow/pipeline diff
beyond what AC-1 permits. - AC-7 — DOM environment preserved per project. Projects on
@happy-dom/jest-environment
(7),jsdom(1) andnode(9) map to the equivalent Vitestenvironment, so no test
silently gains or loses DOM APIs. The mapping is recorded in the PR description. - AC-8 — New projects arrive on Vitest.
nx.json→generatorssets
unitTestRunner: "vitest"for@nx/angular:applicationand@nx/angular:library(both
currently pin"jest"). Without this the migration erodes with the next generated project. - AC-9 — Stale Jasmine comments cleaned. The 4 comments referencing
jasmine.createSpyObj
describe a migration two runners ago and will be actively misleading once Jest is gone.
Type-checking must not regress (see Known interactions)
- AC-10 — Every project's specs are still type-checked.
tools/plugins/typecheck-spec.plugin.mjs
steps aside for any project with avite.config.*, handingtypecheckto@nx/vite/plugin.
Adding Vitest configs to 41 projects triggers that hand-off workspace-wide. Verify
nx run-many -t typecheck --exclude=tag:skip:typecheckstill resolves a realtypecheck
target for every project that ships specs — project count before == after — and that a
deliberately introduced spec type error still fails the build in a migrated project.
A silent hand-off failure would re-open exactly the hole that plugin was written to close, and
would quietly undo part of epic #35932.
Documentation & measurement
- AC-11 — CI wall time measured and reported. Unit-test wall time before vs. after is
recorded in the PR description (no hard threshold is required to pass — runner variance and
Nx cache state make a fixed target non-reproducible; the number is reported for the epic's
Performance objective). - AC-12 —
core-web/CLAUDE.mdtesting guidance updated. Its "Testing (Jest + Spectator)"
section prescribes Jest specifics (isolatedModules: truein everytsconfig.spec.json,
jest.mock()ordering,jest.useFakeTimers(),--testPathPatterns) that become wrong or
misleading under Vitest. Note in particular that the section's warning — "nx run <project>:test
does not type-check", which is true because ts-jest transpiles only — needs re-verifying
under Vitest rather than copied across.
Additional Context
References
- Vitest — official Jest migration guide: https://vitest.dev/guide/migration/jest.html
- Angular — migrating to Vitest: https://angular.dev/guide/testing/migrating-to-vitest
- Nx — modern Angular testing with Nx: https://nx.dev/blog/modern-angular-testing-with-nx
- Parent epic: #32713 · Strict-mode epic whose PR is the precedent: #35932 / PR #37198
@angular/build@22.1.0 already ships a unit-test builder whose runner option defaults to
vitest (enum: ["karma", "vitest"]) — the path angular.dev documents. This issue deliberately
uses @nx/vitest instead, including for the 2 Karma projects, to keep one runner configuration
style across the workspace rather than maintaining an Angular-builder style alongside an Nx-plugin
style. Worth knowing the native builder exists if the Nx plugin proves limiting.
Known interactions and risks
| Risk | Detail | Mitigation |
|---|---|---|
| Spec type-checking silently lost | The typecheck-spec plugin defers to @nx/vite/plugin whenever a vite.config.* appears. Adding 41 Vitest configs triggers this everywhere at once. |
AC-10 — assert target count parity and prove enforcement with a deliberate type error. |
Rebase churn against main |
~1,050 spec files conflict with any concurrent spec edit. | One commit per project; land promptly; rebase rather than merge-commit chains. |
@openng/spectator under Vitest |
All specs use @openng/spectator@1.0.1, imported as @openng/spectator/jest per current guidance. |
Confirm the Vitest entry point early — this is the single highest-risk unknown and should be spiked on one project first. |
| Angular 22 + Vitest + Nx interplay | Three independently versioned pieces. | 4 projects already run Vitest — use libs/sdk/vue / libs/edit-content-bridge as the working reference. |
| Coverage thresholds | @vitest/coverage-v8 reports differently from Jest's istanbul. |
Report coverage per project; do not treat small deltas as regressions. |
Suggested order of work
- Spike
@openng/spectator+ Angular + Vitest on one representative Angular project
(libs/portlets/dot-content-drive/portletis the config reference percore-web/CLAUDE.md). - Build the AC-1 diff-allowlist check first, so every later commit is verified by it.
- Write the codemod; run it project by project, one commit each.
- Karma projects: delete
dotcms-block-editor's dead config, portdotcms-js's 3 specs. - Strip Jest deps/configs/plugin wiring; flip generator defaults (AC-8).
- Verify AC-10 type-checking parity, then measure wall time (AC-11) and update docs (AC-12).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with nx.json, the existing Jest configuration files, and the @nx/vitest setup, then inspect tools/plugins/typecheck-spec.plugin.mjs and core-web/pom.xml. Use the listed project globs and the three libs/dotcms-js specs to map the migration. Done means all in-scope projects run on Vitest with test-count parity, no Jest or Karma wiring remains, typecheck still resolves, and CI needs no pipeline change.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- build-system, ci-cd, testing-qa
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100