dotCMS / dotCMS/core

Migrate all core-web unit tests from Jest and Karma to Vitest

Open
#37,444 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS : Build Team : Falcon Type : Test Automation
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/vitestlibs/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). No jasmine.createSpyObj / createSpy call sites remain.
  • Neither jasmine-core nor karma is in package.json. The two karma.conf.js files are
    orphaned config.
  • apps/dotcms-block-editor has 0 spec files yet still carries a test target, a karma.conf.js,
    a src/test.ts and a polyfills entry — all dead code.
  • The 3 surviving libs/dotcms-js specs 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, nullundefined 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:karma targets → Vitest, via @nx/vitest for workspace uniformity.
  • Removal of Jest and Karma dependencies, configs, presets and stale nx.json wiring.
  • 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/plugin include 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 bisect can proceed project by project inside the
    single PR.
  • A codemod for the mechanical rewrites (jest.fnvi.fn, jest.mockvi.mock,
    jest.spyOnvi.spyOn, timer APIs, jest.Mock type 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 every main merge 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 --passWithNoTests masking 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-environment are removed from package.json; jest.preset.js is deleted;
    the @nx/jest/plugin entry and its 41-glob include list are gone from nx.json.
  • AC-5 — Zero Karma remaining. Both @angular/build:karma targets are gone. libs/dotcms-js
    runs its 3 specs on Vitest. apps/dotcms-block-editor's dead test target, karma.conf.js,
    src/test.ts and polyfills entry are deleted rather than migrated. The stale
    {workspaceRoot}/karma.conf.js entry in nx.jsontargetDefaults.test.inputs is removed
    (it already points at a file that does not exist).
  • AC-6 — CI needs no pipeline edit. The unit-test execution in core-web/pom.xml
    (nx run-many -t test --exclude=tag:skip:test) is unchanged, because every migrated project
    keeps the target name test. 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) and node (9) map to the equivalent Vitest environment, 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.jsongenerators sets
    unitTestRunner: "vitest" for @nx/angular:application and @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 a vite.config.*, handing typecheck to @nx/vite/plugin.
    Adding Vitest configs to 41 projects triggers that hand-off workspace-wide. Verify
    nx run-many -t typecheck --exclude=tag:skip:typecheck still resolves a real typecheck
    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.md testing guidance updated. Its "Testing (Jest + Spectator)"
    section prescribes Jest specifics (isolatedModules: true in every tsconfig.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

@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
  1. Spike @openng/spectator + Angular + Vitest on one representative Angular project
    (libs/portlets/dot-content-drive/portlet is the config reference per core-web/CLAUDE.md).
  2. Build the AC-1 diff-allowlist check first, so every later commit is verified by it.
  3. Write the codemod; run it project by project, one commit each.
  4. Karma projects: delete dotcms-block-editor's dead config, port dotcms-js's 3 specs.
  5. Strip Jest deps/configs/plugin wiring; flip generator defaults (AC-8).
  6. Verify AC-10 type-checking parity, then measure wall time (AC-11) and update docs (AC-12).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.