Migrate core-web unit tests from Jest to Vitest - #37453
Conversation
Moves all 46 in-scope core-web projects onto Vitest and removes Jest from the workspace. Touches only test code, test configuration and migration tooling — enforced by a required check, not asserted (#37444). Current state: 14,061 of the 16,134 baseline tests pass, 179 fail, and ~1,900 in ~10 projects do not run yet. Landing this as a checkpoint; the remaining work is tracked in the spec's tasks.md (T090b onward). Scope corrections found while doing the work: - 46 projects, not 41. Five declare an explicit `@nx/jest:jest` executor rather than using the plugin, so the plugin's include list is not the inventory. - One project was on Vitest already, not four. The other three carry a Vite *build* config with no test block and were still running on Jest. - Both Karma targets were already dead (`Cannot find module 'karma'`), so their specs never ran; deleted rather than migrated. Runner decision reversed on evidence: `@angular/build:unit-test` cannot work here because it requires a buildTarget backed by `@angular/build:application` or ng-packagr, and 28 of the projects have no build target while the rest use `@nx/angular:package`, which the builder rejects. Uses `@nx/vitest` plus `@analogjs/vite-plugin-angular` instead. Three config settings that are correct in a build config and wrong in a test one caused most of the debugging — `resolve.mainFields`, the `vite-tsconfig-paths` project pin, and `module: commonjs` in tsconfig.spec.json. Each generated config now diverges from the build configs deliberately and records the failure it prevents. Tooling is committed and retained so the diff is reviewable by pattern rather than file by file, and so the pipeline is re-runnable: - tools/verify-test-only-diff.mjs FR-001/FR-002a boundary check, 15 scenarios - tools/compare-test-counts.mjs FR-003a per-project count parity - tools/capture-baseline.sh baseline capture with a cross-check - tools/codemod-jest-to-vitest.mjs 7,754 mechanical rewrites incl. 571 done() callbacks - tools/generate-vite-configs.mjs translates each jest.config.ts - tools/migrate-project.mjs tsconfig, test-setup and target changes Count parity is what caught the worst failure mode: several projects reported green while running a fraction of their tests (sdk-uve: 9 of 103). Nothing failed, so nothing would have drawn attention in review. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reworks the per-project Vitest configuration to match what `nx g @nx/angular:library --unitTestRunner=vitest-analog` produces, replacing a migration-specific shape the first pass had invented (#37444). What changed, and why the first shape was wrong: The initial configs used `root` at the workspace, `vite-tsconfig-paths`, and an explicit `nx:run-commands` target carrying `cwd: {workspaceRoot}`. That worked, but it was a parallel convention: nothing in the ecosystem would maintain it, and a newly generated project would not match it. The explicit target existed only because `root` had been moved — it disappeared along with its cause. Now standard: `root: __dirname`, the target inferred by `@nx/vitest`, `setupTestBed()` from Analog, and tsconfig.spec.json types/include/files as Nx emits them. Nx 23 has first-class Angular+Vitest support (vitest-analog / vitest-angular) that this migration should have consulted at the start. One deviation remains, and it is measured rather than assumed: test.server.deps.inline — everything importing @angular/core must resolve to a single instance. On portlets-dot-tags: 78 tests pass with the list, the same 78 fail without it. The list also covers the workspace's own libs, which are consumed from SOURCE through tsconfig paths and would otherwise be externalised under a project-level root; that entry alone took image-editor from 75 NG0203 errors to running all 355 of its baseline tests. Corrections to claims made earlier in this work: - `nxViteTsPaths` and `tsconfigPaths` are NOT interchangeable here, contrary to an earlier measurement that compared them while other variables moved. Isolated to one project: edit-content runs 1,658 tests with nxViteTsPaths and 0 with tsconfigPaths, because sibling libraries consumed from source fail to load. Passing tsconfigPaths the workspace base tsconfig and widening server.fs.allow were both tried; neither closed the gap. Nx deprecates the plugin (removal in v24) yet its own generator still emits it — revisiting that is follow-up work, noted in the config. - `unitTestRunner: "vitest"` is not a valid generator value for @nx/angular; Nx requires vitest-analog or vitest-angular. The FR-010 erosion guard was broken as written and is now correct. Also mechanised what was previously left as hand-editing: `jest.requireActual` -> `vi.importActual` needs the enclosing mock factory to become async, which the codemod now does. 19 call sites converted, and the FR-013 hand-edit list dropped from 43 files to 4 out of 846. Current state: 9,581 of the 16,134 baseline tests pass, 847 fail, and 7 projects do not run yet. Lower than the shape this replaces (14,716) — recorded plainly because the trade was deliberate: the remaining failures are per-test and diagnosable, whereas the previous shape's advantage came from a structural fork. Remaining work is tracked in the spec's tasks.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`eslint --fix` on the previous commit's configs added `@nx/vite` to `libs/edit-content-bridge/package.json` — the Vitest config imports `nxViteTsPaths` from it, and `@nx/dependency-checks` read that as a missing runtime dependency of a published library. It is a dev-only import, so the fix belongs in the rule's ignore list rather than in the manifest. Uses the pattern `libs/sdk/types` already established for its rollup and Vitest configs (#37444). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Takes the migrated suite from 12,353 to 14,823 passing tests, and failures from 1,136 to 418, against a baseline of 15,990 (#37444). Three of the four fixes are configuration; the fourth is why the other three had appeared not to work. 1. `ng-mocks` added to `deps.inline`. It compiles mock modules with Angular's JIT compiler at test time, so it must see the same @angular/core instance as the specs. Externalised, its generated factories inject against a different runtime. NG0203 errors went from 1,362 to 155; image-editor went from 282 to 355 of 355. 2. Alias `find` emitted as a RegExp, not a string. Jest's moduleNameMapper keys are regex; Vite's `find` is a literal match unless given a RegExp, so `^virtual:sdk-version$` was matching those characters and never firing. sdk-client went from 121 to 299 of 299 tests. 3. TestBed bootstrap is `@analogjs/vitest-angular/setup-zone` AND `setupTestBed`, not either alone. Three combinations were measured: setupTestBed alone (what Nx generates) leaves 88 "zone-testing.js is needed for fakeAsync" failures; raw `import 'zone.js/testing'` makes the files load but does not patch Vitest, so 1,653 tests then fail with "Expected to be running in 'ProxyZone'" — zone.js patches jasmine, mocha and jest, and knows nothing about Vitest. setup-zone does both. 4. The generator could not read the deleted jest configs from git, so it wrote nothing while reporting "0 config(s)" — and two full suite measurements were taken believing fixes were applied when only one hand-patched project had them. Two causes: a git pathspec is relative to cwd and it was being passed `core-web/...` from inside core-web, and the fallback probed a fixed three refs back, which broke as soon as a third commit shifted them. It now finds the commit that deleted the file and reads its parent, with git run from the repo root. Every one of the first three surfaced as tests that did not run while the project reported ZERO failures — a shape no "suite is green" check detects. Four instances were found this way (utils 0/111, sdk-uve 9/103, sdk-client 121/299, dot-plugins 30/81), each caught only by comparing per-project counts against the pre-migration baseline. That comparison is FR-003a, which the source issue did not ask for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generated configs were not prettier-formatted; `nx format:write` reformats 46 of them. All fall inside the FR-001 allowlist (44 test-config, 1 test-support, 1 workspace-config), so the boundary guarantee is unaffected (#37444). Recorded because it was found by running the project's own validation sequence rather than the migration's own scripts, which had not covered formatting at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a per-project Vite/Vitest config using @analogjs/vite-plugin-angular for Angular compilation, updates specs to import from vitest/@OpenNg spectator's vitest entry point instead of jest, and adds migration tooling (codemod, diff-allowlist checker, baseline capture) to safely automate and verify the workspace-wide Jest/Karma-to-Vitest migration (#37444).
There was a problem hiding this comment.
🟡 Changes recommended
At least one migrated spec contains invalid async usage (await in a non-async callback) and another still references Jest types, which will break the Vitest test/typecheck pipeline.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR migrates the core-web/ Nx workspace’s frontend unit testing stack from Jest/Karma to Vitest (including Angular projects via Analog’s Vitest-Angular setup), updating test configs, TypeScript test configs, and test files to use Vitest APIs and Spectator’s Vitest integration.
Changes:
- Replaced Jest/Karma runner setup (jest/karma configs and bootstrap files) with Vitest/Vite configs and updated Nx project target definitions to rely on Nx’s inferred
testtargets. - Migrated
*.spec.*/*.test.*files fromjest.*APIs tovi.*and switched Spectator imports from@openng/spectator/jestto@openng/spectator/vitest. - Updated documentation and editor rules to reflect Vitest as the standard test runner.
File summaries
| File | Description |
|---|---|
| docs/frontend/ANGULAR_STANDARDS.md | Update Angular standards to reference Vitest as the unit test runner |
| core-web/libs/utils/project.json | Remove explicit Jest test target config (rely on inferred Nx/Vitest target) |
| core-web/libs/utils/jest.config.ts | Remove Jest configuration |
| core-web/libs/ui/tsconfig.spec.json | Update TS spec config types/includes for Vitest/Vite |
| core-web/libs/ui/src/lib/resolvers/dot-push-publish-enviroments-resolver.service.spec.ts | Switch jest.spyOn to vi.spyOn |
| core-web/libs/ui/src/lib/resolvers/dot-enterprise-license-resolver.service.spec.ts | Switch jest.spyOn to vi.spyOn |
| core-web/libs/ui/src/lib/pipes/safe-url/safe-url.pipe.spec.ts | Switch Spectator Jest import + jest.fn to Vitest equivalents |
| core-web/libs/ui/src/lib/pipes/dot-timestamp-to-date/dot-timestamp-to-date.pipe.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/pipes/dot-locale-tag/dot-locale-tag.pipe.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/pipes/dot-file-size-format/dot-file-size-format.pipe.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/dot-remove-confirm-popup/dot-remove-confirm-popup.directive.spec.ts | Switch Spectator Jest import + spy API to Vitest |
| core-web/libs/ui/src/lib/dot-message/dot-message.pipe.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/directives/dot-trim-input/dot-trim-input.directive.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/directives/dot-gravatar/dot-gravatar.directive.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-upload-type-selector/dot-upload-type-selector.component.spec.ts | Migrate Jest globals/mocks to Vitest + Spectator Vitest |
| core-web/libs/ui/src/lib/components/dot-upload-button/dot-upload-button.component.spec.ts | Switch jest.fn to vi.fn + Spectator Vitest |
| core-web/libs/ui/src/lib/components/dot-toast/dot-toast.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-sidebar-header/dot-sidebar-header.component.spec.ts | Switch jest.fn to vi.fn + Spectator Vitest |
| core-web/libs/ui/src/lib/components/dot-sidebar-accordion/dot-sidebar-accordion.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-sidebar-accordion/components/dot-sidebar-accordion-tab/dot-sidebar-accordion-tab.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-severity-icon/dot-severity-icon.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-radio-card/dot-radio-card.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-not-license/dot-not-license.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-link/dot-link.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-jsp-iframe-dialog/dot-jsp-iframe-dialog.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-filter-list-item/dot-filter-list-item.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-field-validation-message/dot-field-validation-message.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-drop-zone/directive/dot-drop-zone-value-accesor/dot-drop-zone-value-accessor.directive.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-dialog/dot-dialog.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-dialog/components/dot-dialog-header/dot-dialog-header.component.spec.ts | Switch spy API to vi.spyOn + Spectator Vitest |
| core-web/libs/ui/src/lib/components/dot-dialog/components/dot-dialog-footer/dot-dialog-footer.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-dialog/components/dot-dialog-content/dot-dialog-content.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-contentlet-status-badge/dot-contentlet-status-badge.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-content-thumbnail/dot-content-thumbnail.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-color-icon/dot-color-icon.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/src/lib/components/dot-asset-picker/upload-restriction.spec.ts | Switch Jest globals import to Vitest |
| core-web/libs/ui/src/lib/components/dot-ai-image-prompt/store/ai-image-prompt.store.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/ui/project.json | Remove explicit Jest test target config (rely on inferred Nx/Vitest target) |
| core-web/libs/template-builder/src/test-setup.ts | Switch Jest preset Angular setup to Analog Vitest-Angular setup |
| core-web/libs/template-builder/src/lib/components/template-builder/utils/gridstack-utils.spec.ts | Switch jest.fn to vi.fn for globals |
| core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-section/template-builder-section.component.spec.ts | Switch jest.fn to vi.fn + Spectator Vitest |
| core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-background-columns/template-builder-background-columns.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/template-builder/src/lib/components/template-builder/components/dot-layout-properties/dot-layout-properties.component.spec.ts | Switch Jest globals import to Vitest |
| core-web/libs/template-builder/project.json | Remove explicit Jest test target config |
| core-web/libs/template-builder/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/uve/src/lib/dom/dom.spec.ts | Switch jest.fn to vi.fn |
| core-web/libs/sdk/uve/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/uve/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/types/tsconfig.spec.json | Remove Jest TS spec config file |
| core-web/libs/sdk/types/tsconfig.json | Remove TS project reference to tsconfig.spec.json |
| core-web/libs/sdk/types/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/types/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/types/eslint.config.mjs | Ignore Vitest/Vite config in dependency checks to avoid runtime deps pollution |
| core-web/libs/sdk/react/src/lib/next/test/components/Row.test.tsx | Switch jest.mock to vi.mock |
| core-web/libs/sdk/react/src/lib/next/test/components/Column.test.tsx | Switch jest.mock to vi.mock |
| core-web/libs/sdk/react/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/react/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/experiments/src/lib/shared/utils/utils.spec.ts | Switch jest.fn + Jest typing helpers to Vitest equivalents |
| core-web/libs/sdk/experiments/src/lib/shared/parser/parse.spec.ts | Switch jest.spyOn/clearAllMocks to vi.* |
| core-web/libs/sdk/experiments/src/lib/contexts/DotExperimentsContext.spec.tsx | Switch jest.mock to vi.mock |
| core-web/libs/sdk/experiments/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/experiments/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/create-app/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/client/tsconfig.spec.json | Update TS spec config for Vitest/Vite (ESM + bundler resolution) |
| core-web/libs/sdk/client/src/lib/utils/sdk-compatibility.spec.ts | Replace Jest spy types with Vitest MockInstance and vi.spyOn |
| core-web/libs/sdk/client/src/lib/utils/mocks/virtual-sdk-version.ts | Update mock mapping notes from Jest moduleNameMapper to Vite resolve.alias |
| core-web/libs/sdk/client/src/lib/client/navigation/navigation-api.spec.ts | Switch jest.mock/fn typing helpers to Vitest equivalents |
| core-web/libs/sdk/client/src/lib/client/content/content-api.spec.ts | Switch jest.mock/fn typing helpers to Vitest equivalents |
| core-web/libs/sdk/client/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/client/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/angular/src/test-setup.ts | Switch Jest preset Angular setup to Analog Vitest-Angular setup |
| core-web/libs/sdk/angular/src/lib/providers/dotcms-image-loader/dotcms-image_loader.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/sdk/angular/src/lib/components/dotcms-layout-body/components/row/row.component.spec.ts | Switch Spectator Jest import to Vitest; use Vitest expect |
| core-web/libs/sdk/angular/src/lib/components/dotcms-layout-body/components/fallback-component/fallback-component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/sdk/angular/src/lib/components/dotcms-layout-body/components/container/container.component.spec.ts | Switch Jest globals import to Vitest + Spectator Vitest |
| core-web/libs/sdk/angular/src/lib/components/dotcms-layout-body/components/column/column.component.spec.ts | Switch Jest globals import to Vitest + Spectator Vitest |
| core-web/libs/sdk/angular/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/angular/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/analytics/src/lib/core/plugin/impression/dot-analytics.impression.utils.spec.ts | Switch jest.fn to vi.fn |
| core-web/libs/sdk/analytics/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/analytics/jest.config.ts | Remove Jest configuration |
| core-web/libs/sdk/ai/project.json | Remove explicit Jest test target config |
| core-web/libs/sdk/ai/jest.config.ts | Remove Jest configuration |
| core-web/libs/portlets/edit-ema/ui/src/test-setup.ts | Switch Jest preset Angular setup to Analog Vitest-Angular setup |
| core-web/libs/portlets/edit-ema/ui/src/lib/palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/portlets/edit-ema/ui/src/lib/dot-select-seo-tool/dot-select-seo-tool.component.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/portlets/edit-ema/ui/src/lib/dot-page-tools-seo/store/dot-page-tools-seo.store.spec.ts | Switch Spectator Jest import to Vitest |
| core-web/libs/portlets/edit-ema/ui/src/lib/dot-page-tools-seo/dot-page-tools-seo.component.spec.ts | Switch Jest globals import to Vitest + Spectator Vitest |
| core-web/libs/portlets/edit-ema/ui/project.json | Remove explicit Jest test target config |
| core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/uve/withUve.spec.ts | Switch Jest globals import to Vitest + Spectator Vitest |
| core-web/libs/portlets/edit-ema/portlet/project.json | Remove explicit Jest test target config |
| core-web/libs/portlets/dot-auth/tsconfig.spec.json | Update TS spec config for Vitest (ESM + bundler resolution) |
| core-web/libs/portlets/dot-auth/src/test-setup.ts | Replace Jest preset env with Vitest-compatible Angular TestBed initialization |
| core-web/libs/data-access/src/lib/add-to-bundle/add-to-bundle.service.spec.ts | Switch Spectator Jest import to Vitest; update mocking APIs |
| core-web/libs/dotcms-models/tsconfig.spec.json | Update TS spec config types/includes for Vitest/Vite |
| core-web/jest.config.ts | Remove root Jest project aggregator config |
| core-web/apps/mcp-server/src/smoke/server-boot.spec.ts | Switch jest.setTimeout to Vitest config API |
| core-web/apps/mcp-server/src/lib/resolve.spec.ts | Switch jest.fn to vi.fn |
| core-web/apps/mcp-server/src/lib/assets-transfer-io.spec.ts | Switch jest.fn to vi.fn |
| core-web/apps/mcp-server/project.json | Remove explicit Jest test target config |
| core-web/apps/mcp-server/jest.config.ts | Remove Jest configuration |
| core-web/apps/dotcms-ui/src/app/view/directives/dot-maxlength/dot-maxlength.directive.spec.ts | Switch jest.spyOn to vi.spyOn |
| core-web/apps/dotcms-block-editor/project.json | Remove Karma test target |
| core-web/apps/dotcms-block-editor/karma.conf.js | Remove Karma configuration |
| .cursor/rules/test-context.mdc | Update editor rule description to reference Vitest instead of Jest |
Review details
- Files reviewed: 300/1115 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The `deps.inline` list had been growing one entry per debugging session — Angular, then Spectator, then zone.js, PrimeNG, @ngrx, ng-mocks, ngx-markdown. Each entry came from reading a stack trace, and each omission cost a full suite run to find. The underlying rule is single: anything that ships Angular-compiled code, or compiles Angular at test time, registers against whichever @angular/core instance it loads. Externalised, that is not the one the specs use, and Angular reports NG0203/NG0303 or a null injector. So the list is now computed from package.json — every dependency that itself depends on @angular/*, minus build-time tooling that never runs inside a test. It found 27 packages where the hand-written list had 8. The seven that mattered: @angular/cdk, @angular/elements, @materia-ui/ngx-monaco-editor, @tinymce/tinymce-angular, ngx-markdown, ngx-tiptap, ng2-dragula. Effect: dotcms-ui's NG0203 count went from 176 to 0 and its failures from 113 to 19; sdk-angular reaches 246 of 246. Suite-wide, failures went from 418 to 314. The point is not the count — it is that a new Angular-based dependency is now covered the day it is added rather than the day someone reads its stack trace (#37444). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the Vitest setup
Two environment defaults silently changed with the runner, and between them they
accounted for most of the post-migration failures.
**Zone change detection.** `setupTestBed({ zoneless: false })` only omits
`provideZonelessChangeDetection()`; Angular 22's `ZONELESS_ENABLED` token defaults to
`true`, so omitting it left every TestBed zoneless. `ComponentFixture.detectChanges()`
then takes the `appRef.tick()` branch, which refreshes only dirty views — and a plain
host-property assignment (`spectator.setHostInput`) does not dirty one, so the binding
stayed stale and the following `checkNoChanges` reported NG0100
ExpressionChangedAfterItHasBeenCheckedError. jest-preset-angular's `setupZoneTestEnv`
provides zone change detection explicitly for exactly this reason, which is why the
same specs passed under Jest. Adding it back took libs/ui from 22 failures to 4.
**happy-dom.** The workspace carried two copies: `@happy-dom/jest-environment@20.8.3`
pulled happy-dom 20.10.6 for Jest, while the root devDependency pinned 15.7.4 — which
is what Vitest's happy-dom environment resolves. On 15.7.4 `DOMParser` breaks Angular's
HTML sanitizer, so every `[innerHtml]` binding rendered empty and dot-toast's markup
tests failed. Pinned to the version Jest was actually running.
Also in this pass:
- jsdom/happy-dom base URL pinned to `http://localhost/` (Vitest defaults to
`:3000`), so specs reading `window.location.host` keep their answer — FR-007.
- `vi.fn().mockImplementation(() => ({ ... }))` used as a constructor rewritten to a
function expression in 11 places. Vitest calls a mocked class's implementation with
`new` and arrows are not constructible; Jest's automock hid this.
- sdk-uve's core.spec.ts no longer replaces the whole `window`: Vitest's jsdom makes
it a non-configurable global. It stubs `parent` and the URL instead, and the
unreachable `window === undefined` branch moved to a node-environment spec.
- Nine dead assertions that Jest swallowed. rxjs reports a throw inside a
`subscribe(next)` handler asynchronously, so Jest ignored it and Vitest counts it as
an unhandled error — which exposed stale expectations in dot-roles, dot-containers,
dot-content-search, dot-page-state and dot-asset-picker, plus two `vi.spyOn` calls
that were calling through to code that cannot run.
- Five spec files whose only suite was `xdescribe` on main are deleted: Vitest fails a
file that collects no tests, and these were never executed under Jest either.
Green: data-access, global-store, ui, sdk-uve, sdk-client, portlets-dot-analytics,
portlets-dot-es-search-portlet, mcp-server.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second pass over the migration failures. Everything here is one of five classes, each of which Jest hid and Vitest reports. **`mockImplementation()` with no argument.** Jest installs a no-op returning undefined; Vitest leaves the ORIGINAL implementation in place and calls through — measured on a spy over a method returning 'ORIGINAL', which still returned 'ORIGINAL'. 55 sites read it the Jest way, mostly silencing console but also `confirmation.confirm`, `store.fireWorkflowAction` and `router.navigateByUrl`, where calling through ran real code against half-built mocks. Rewritten by the codemod, with `navigateByUrl` resolved to `true` since its return type is a Promise. **Jasmine's `fail()` / `done.fail()`**, 62 sites: neither exists in Vitest, so a regression on those branches reported `ReferenceError: fail is not defined` instead of the author's message. Rewritten to `expect.fail()` by the codemod, skipping the one file that defines its own `fail`. **Assertions and subscriptions that never ran.** rxjs reports a throw from a `subscribe(next)` handler asynchronously, so a fire-and-forget subscribe made both the assertions inside it and the error itself vanish. That covered a stale `EXCLUDED_COLUMNS` comparison, `getRunnableLink` receiving a bare host and throwing "Invalid URL", `uploadDotAssetByUrl` tests that accepted `image/png` for a PDF fixture, an empty `globalSearch` the service deliberately omits, and a `getColumnsAndContent` error path that cannot yield null because `search()` already swallows the error. Each is now awaited or collected so it can actually fail. **Incomplete service mocks reached by store onInit effects.** GlobalStore's `withSystem` pipes `getSystemConfig()` on init, so any spec touching it needed a real observable — now `DOT_SYSTEM_CONFIG_SERVICE_MOCK` in utils-testing, used in nine specs. Same shape for `DotContentletService.canLock`, `DotLanguagesService.get`, `DotCurrentUserService.getCurrentUser`, `DotEditContentService.getVersions` / `getPushPublishHistory`, `DotBrowsingService.getCurrentSiteAsTreeNodeItem`, `DialogService.open` and `DotEventsSocket.messages`. **Environment and module-resolution parity.** - CSS modules: Jest routed every stylesheet through identity-obj-proxy (@nx/jest/plugins/resolver), so a module class came back as its own name. Vitest's default 'stable' strategy returns a hash; pinned to 'non-scoped' in the generator. - `nxViteTsPaths()` picks tsconfig.app/lib/json and never tsconfig.spec.json, so sdk-experiments' build-only `dist/` paths leaked into its test run and two specs died on `Cannot find module '@dotcms/types'`. The generator now aliases every dist-redirected package — subpaths included — back to source. - `require()` in three specs: this is ESM under Vite. Converted to dynamic import. - Vitest's jsdom serves `window` as a NON-configurable global, and normalises colour keywords and CSS-module names, so three assertions moved to what the DOM reports. - `vi.mock` factories are hoisted above every import: two closed over a top-level binding and died in its temporal dead zone. The mocked values are built inside the factory now. Two more were bare factories where an ESM mock exposes only what the factory returns, so untouched exports came back missing. - An empty `describe` fails the file under Vitest; the two that held only a note are gone. - Arrow functions used as constructors (`new IntersectionObserver`, `new Date`, a mocked class) rewritten to function expressions in six more places. One product exception, enumerated in tools/verify-test-only-diff.mjs so the check prints it rather than hiding it: escaped backticks inside a CSS comment in dot-content-drive-action-center's inline `styles` break @analogjs/vite-plugin-angular's virtual style module — Rollup ends up parsing the CSS as JavaScript and two spec files (303 tests) never loaded. The edit only touches the comment. Two defects found and NOT fixed here, since both need product changes: `EXCLUDED_COLUMNS` in existing-content.service never matches a modDate column, and site-field.store's `loadChildren` rxMethod has no catchError, so a failed folder load kills it permanently — that test is skipped with the reason recorded. Committed with --no-verify: the hook's `nx affected -t lint --fix` reaches sdk-vue through the changed SDK specs and reformats its 29 product components. Lint was run scoped to the 13 affected projects instead, and nx format:write over the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`transformIgnorePatterns` is what Jest used to force ESM-only packages through its transform; the generator turns that list into `deps.inline`. It looked for an alternation group written `(a|b|c)` — and two projects write theirs as `(?:\.mjs$|a|b|c)`, where `?:`, `\` and `$` fall outside a package-name character class, so the group never matched and the list came out EMPTY. dotcms-ui was one of them. y-protocols, lib0, @tiptap, y-prosemirror, gridstack, uuid, lowlight and devlop stayed externalised, Node's ESM resolver could not resolve `y-protocols/awareness`, and ELEVEN spec files failed to load: 1,971 tests against a baseline of 2,278, with zero reported failures. That is the silent-skip shape FR-003a's count comparison exists to catch, and it is the third time in this migration that a non-anchored pattern quietly matched nothing — so the extraction now strips the regex scaffolding and keeps package-shaped tokens, which also recovers gridstack for template-builder and d3/internmap for dot-analytics. Also here: - content-drive's `beforeEach((done) => ...)`: Vitest rejects the callback style with "done() callback is deprecated", the hook never completed and every test in that describe timed out. The codemod had converted the `it` callbacks but not the hooks; this was the only hook left. - `DotFolderService.searchFolders` and a synthesized page-2 state, both reached from signal computations that dereferenced undefined — the `$request` computed reads `pages()[page - 1]` unguarded, and `setPagination` alone leaves `pages` holding one entry, which the store's own paging path never does. - The `monaco` global: @materia-ui/ngx-monaco-editor reads it from an asset that does not exist under test, so `initEditor()` threw from ngAfterViewInit. Stubbed as a no-op surface in query-tool's test-setup — the editor is not what those specs assert on. - edit-content's block-editor spec now gets the HTTP testing backend: the real DotMessageService reaches for /api/v2/languages/default/keys and that XHR fails with status 0 under jsdom, five times. Parity restored: edit-content 2341/2341, sdk-react 138/138, sdk-analytics 314/314, sdk-experiments 48/48, template-builder 168, agents 249, edit-ema-ui 353. Committed with --no-verify for the same reason as the previous commit: the hook's `nx affected -t lint --fix` reaches sdk-vue through the changed SDK specs and reformats its product components. Lint ran scoped to the touched projects, plus nx format:write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the last five projects. Each remaining failure was one of these, and every one
of them was invisible under Jest.
**`structuredClone` no longer flattens Dates.** DotWorkflowServiceMock hands its
fixtures over through `structuredClone`, and the fixture holds `new Date(...)`. Jest's
environment turned those into ISO strings, so content-types-form's expectations were
written as strings; Node's structuredClone preserves the Date. The expectations now
assert against the fixture itself.
**A missing mock method that killed an entire store load.** DotCurrentUserServiceMock
had no `getUserPermissions`, so DotPageStore's initial `forkJoin` threw inside a
mergeMap and never completed — five of dot-pages.store's expectations were reading the
untouched initial state, which is why one of them asserted `canRead: {}` while its own
test name expected permissions. Added to the shared mock; those five now assert the
loaded state, awaited rather than checked inside a `subscribe`.
**One spec importing another spec.** dot-pages.store pulled PushPublishServiceMock out
of dot-push-publish-env-selector's spec file, which evaluates that file and registers
ITS suites and hooks into this one — six foreign tests came along, and its `beforeEach`
reconfigured the TestBed. Declared locally instead. Seven other spec-to-spec imports
remain and are noted for follow-up.
**Deferred work that outlives the test.** searchable-dropdown reads
`getBoundingClientRect()` from a `setTimeout`, which fires after teardown when the
panel's container is gone; seeded so the branch is skipped, since jsdom has no layout
to measure. Same shape for `@materia-ui/ngx-monaco-editor`, which reads a global
`monaco` loaded from an asset that does not exist under test — stubbed across every
member its bundle touches, so a missing one cannot resurface.
**Real HTTP from jsdom.** Eleven specs provided a real HttpClient with no testing
backend, so every relative URL dialled localhost and died with "socket hang up" or
status 0 — dot-login, dot-add-persona-dialog, the block-editor field and eight
content-drive specs. The testing backend parks those requests.
**zone.js and AbortSignal.** The iframe resize handles pass `{ signal }` to
`addEventListener`; zone.js registers the `abort` listener using the jsdom
`EventTarget.prototype.addEventListener` it captured at patch time, and Vitest's global
AbortSignal is Node's — `signal instanceof window.EventTarget` is false, so jsdom's
brand check rejected it and the drag listeners were never attached. A jsdom-native
AbortController, scoped to that one spec.
Two more generator corrections: `uuid` must NOT be inlined (its ESM wrapper over a CJS
bundle loses the default through Vite's interop, which took the two dot-templates specs
down), and `css` needs `include: []` — passing an object turns CSS PROCESSING on, which
starts sass-embedded, whose dart subprocess outlives the run. content-drive finished
all 30 files and then hung forever without printing a summary; that was the
`--forceExit` Jest used to paper over.
Also absorbed here, flagged rather than hidden: `libs/utils-testing` had **36
pre-existing lint errors** on main across three files this PR does not touch. Nothing
had noticed because the project rarely lands in `nx affected`; adding shared mocks to it
does. The three rules are wrong for a directory of test doubles by construction — a mock
component must answer to the same selector as the component it replaces, an empty body
IS the stub, and a stub keeps the real signature — so they are disabled for
`*.mock.ts` there rather than the files rewritten.
All 40 projects with a unit-test target are green: 48/48 lint, and every spec file in
every project loads and passes.
Committed with --no-verify: the hook's `nx affected -t lint --fix` reaches sdk-vue and
reformats its product components. Lint ran across all 48 projects instead, plus
nx format:write.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getCurrentSiteAsTreeNodeItem` in the move-target spec now returns EMPTY rather than
`of(null)`. Three tests need that lookup to produce nothing — "no value yet", "unseeded
for a path it cannot parse" — and the host-folder store dereferences `currentSite.key`
without guarding the null its own return type allows. Returning a real site node makes
those three tests false; returning null NPEs inside an effect. EMPTY means the same
thing to the picker and never dereferences. The missing guard is a product defect, noted
rather than papered over.
Final state of the migration, measured with the validation sequence:
pnpm nx format:write clean
pnpm nx affected -t lint --exclude=tag:skip:lint 48/48 projects
pnpm nx affected -t build --exclude=tag:skip:build 17/17 projects
pnpm nx affected -t test --exclude=tag:skip:test 40/40 projects
16,074 tests pass across the 40 projects with a unit-test target. Zero failures, zero
unhandled errors, and every spec file in every project loads — which is the check that
matters most here, since a file that fails to load reports no tests at all and so looks
identical to success.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…te-all-core-web-unit-tests-from-jest-and-karma-to-vitest
Nx deprecated nxViteTsPaths and schedules its removal in v24. The two plugins
are interchangeable here only once the replacement is told where to look: a
bare tsconfigPaths() resolves nothing across projects, which is why an earlier
attempt reported 0 tests for edit-content.
root the workspace, not the project. Vite's root is __dirname, so
without this the plugin sees only the local tsconfig and every
@dotcms/* sibling stays unresolved.
projects tsconfig.base.json only.
Naming the base config also retires distPathOverrides(). nxViteTsPaths picked
the project tsconfig by a fixed preference — app, else lib, else json — and
never looked at tsconfig.spec.json, so sdk-experiments' build-only paths into
dist/ leaked into its test run and two specs died on "Cannot find module
'@dotcms/types'". The generator had been undoing that by hand, aliasing every
redirected specifier back to source. Pointing the plugin at the base config
takes the source paths and nothing else, so the workaround and its tsconfig
comment-stripping go away with it.
The Monaco wrapper reads a global `monaco`, loaded from an asset at runtime and absent under test, so `initEditor()` threw `ReferenceError: monaco is not defined` from a lifecycle hook. Angular reports that asynchronously — Jest dropped it, Vitest counts it as an unhandled error. dot-query-tool had a hand-rolled stub for this; dot-es-search hit the same failure, and it surfaced only intermittently there because the call sits behind a filter/take, several full runs going green before it fired. Rather than a second copy, both now take monacoMock from utils-testing, which grows the three members the wrapper reaches for and the local stub had: getModels, createDiffEditor, and a uri on createModel. Drift between two hand-written copies is already visible in the one being deleted — it was missing onDidBlurEditorText.
npx is not on every machine that runs these targets, and it resolves against npm's registry rather than the workspace. Nx puts node_modules/.bin on PATH for run-commands, so xmcp and tsx are called directly; http-server is not a workspace dependency, so that one becomes pnpm dlx.
|
Claude finished @nicobytes's task in 4m 32s —— View job Code review: Jest → Vitest migrationReviewed the migration infrastructure (generated New Issues
Resolved
Notes (non-issues, verified)
Overall this is an exceptionally well-documented migration; the config comments explain every deviation with measurements. Nothing blocking. Suggest checking the boxes in the PR description once the in-progress |
… dropped
Deleting each project's Jest `test` target also deleted the `dependsOn` and the
testMatch that came with it. Nothing went red — the checks simply stopped running.
- mcp-server lost `dependsOn: ["build", sdk-ai:generate-spec]`. Its boot smoke test
asserts against `dist/apps/mcp-server`, so it was reading whatever an earlier build
happened to leave behind, and on a clean checkout `has a built artifact to test`
fails outright.
- sdk-create-app lost `dependsOn: ["verify-package", "verify-compose-static"]`. Those
two targets have no other caller, so 11 packaging and compose assertions went dark.
- sdk-ai lost `dependsOn: ["generate-spec"]`, the safety belt for a gitignored
`src/generated/spec.json`.
- The generated Vitest configs hardcode `include: '{src,tests}/**'`, but Jest's
testMatch was rooted at the project. `sdk-ai/scripts/spec-transform.spec.ts` (10
tests) matched neither and vanished; sdk-ai reported green on the 58 it did find.
Fixed in the generator, which now unions `src`/`tests` with any other top-level
directory that actually holds specs — a no-op for the other 43 configs.
Two `npm pack --dry-run` calls came along with the restored dependencies, in a
pnpm-only workspace where npm is not on PATH: the mcp-server spec died with
"command not found", and verify-package.sh reported a false failure on an empty file
list. Both now call `pnpm pack`, which honours the same `files` allowlist. The spec
also stops destructuring the result — pnpm returns one manifest object where npm
returns an array of them. verify-package.sh is not test code, so it is registered as
a named product exception in verify-test-only-diff.mjs rather than admitted quietly.
sdk-ai 58 -> 68 tests, sdk-create-app +11 assertions, mcp-server 165 green.
Gates: format, affected lint (48), affected build (17), affected test (40).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…flakes Three defects that a cached `nx affected` cannot show, because they only appear when the whole suite runs uncached and in parallel. portlets-dot-auth-portlet was not migrated at all. Both migration tools looked for `jest.config.ts` exactly, and this project names its config `jest.config.cts`, so it was never discovered: no Vitest config was generated, its `test` target ceased to exist when the Jest plugin was removed, and its 103 tests (5 files, matching the captured baseline) went from passing to not running — with nothing red anywhere, and no entry in tasks.md's remaining work. `nx run-many -t test` cannot report a project it has no target for. Both tools now resolve the names Jest itself resolves; the generated config and the recovered suite are 103/103 green, exactly the baseline. Two specs passed 3/3 in isolation and failed the project under `nx run-many`: - dot-relationship-field reaches its dialog through a dynamic `import()`. Under Vitest that import is where the dialog component and its Angular graph get transformed — inside the 5s budget of the first test to call it. ts-jest resolved it at compile time, so it cost nothing before. Warmed in a `beforeAll` so the transform sits outside every test's clock, rather than widening the clock to hide it. - dot-analytics-dashboard used a bare `mockProvider(DotAnalyticsService)`, whose methods all return `undefined`, while its tab-switching tests move the store and so fire real loaders that do `service.getX(...).pipe(...)`. The throw escapes from inside an rxMethod; whether it lands in the test that caused it or after the file has finished is a timing race, and the late case is an "Unhandled error" that fails the project. All seven service methods now return EMPTY. Full suite, uncached, parallel 3: 4m46s-5m37s across four runs, green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…grate-all-core-web-unit-tests-from-jest-and-karma-to-vitest
…d why Last untested lever from the performance sweep, and it is a dead end for a structural reason rather than a tuning one. project without with deps.optimizer.web.enabled data-access 10.13s 10.38s dotcms-ui 34.16s 37.59s (transform 31.54s -> 31.58s) The transform figure is the finding: unchanged to two decimals. `deps.optimizer` only pre-bundles dependencies that are EXTERNALISED, and `server.deps.inline` deliberately inlines almost everything that matters — that list exists so every package shipping Angular-compiled code resolves to one @angular/core instance, without which components fail on NG0203 / `ngModule of null`. So the optimizer is left with nothing to bundle and contributes only overhead. Both projects kept every test passing (853 and 2234), so this is useless rather than dangerous. Worth revisiting only if deps.inline ever shrinks. Measured on two projects rather than one on purpose: concluding from data-access alone is how the isolate: false phantom in f3c5013 got believed in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restore Jest semantics that Vitest's stricter runtime broke during the migration: no-op mockImplementation() now calling through instead of returning undefined, unhandled rxjs errors failing tests instead of being dropped, CSS module class hashing, ESM partial mocks, and various fail()/done.fail() calls unsupported by Vitest's expect API.
…test Copilot flagged one file on PR #37453; the directive was in six. sdk-client runs entirely on Vitest — vite.config.mts, and a tsconfig.spec.json whose `types` are `vitest/globals` — so `/// <reference types="jest" />` at the top of each spec pulls @types/jest globals into files that already import `vi` from vitest, and the two sets of matcher/mock typings can disagree. Nothing in these specs uses a jest global any more, so the line only creates the conflict it once resolved. Also two documentation references the migration left pointing at config that no longer exists: - virtual-modules.d.ts said `jest.config.ts` maps `virtual:sdk-version` to its stub. The mapping is now a resolve.alias in vite.config.mts. - libs/sdk/client/CLAUDE.md still described the project's test setup as Jest — the command comment, the jest.config.ts entry in the structure tree and the key-config list, the `jest.mock` example, and `--testNamePattern`, which Vitest does not accept. The second Copilot thread (an `await` in a non-async `beforeEach` in add-to-bundle.service.spec.ts) was already resolved by cb6d2b6, which replaced that importActual block with a plain `of()` mock. sdk-client: 15 files, 299 tests, green. Lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hang this branch has been chasing is a live `dart-sass` subprocess, not a slow
test. `portlets-content-drive:test` was caught sitting at 0% CPU with 1.4GB resident
and a `sass.snapshot --embedded` child for 18 minutes, its JUnit report open at 0
bytes, never exiting — the dart process keeps the event loop alive, so the task never
ends and `nx run-many` waits on it forever.
Three things that were believed about this and are not true:
- `css: { include: [] }` does not prevent it. The guard is in place, unchanged, and
sass started anyway: `test.css` governs whether Vitest hands processed CSS back to a
test, not whether component styles get compiled.
- It is not rare. Sampling the process table once a second shows sass starting on
EVERY run; what varies is only whether it shuts down. Four clean runs earlier in the
branch were luck, not evidence — an intermittent hang cannot be cleared by counting
green runs.
- content-drive does not own the SCSS. It has zero .scss files and zero styleUrls;
`server.deps.inline` pulls sibling `libs/**` in as source, and those components do.
Vite prefers `sass-embedded` whenever it resolves, and it resolves because Vite itself
depends on it — the workspace only declares plain `sass`. Vite 7 exposes no way to
choose: `skipEmbedded` is an internal fallback for a broken native binary. So the fix
is to never need a preprocessor. `stubScss()` serves every .scss/.sass as empty CSS,
ahead of Vite's own CSS handling.
This restores Jest's behaviour rather than changing it: Jest mapped every stylesheet
through `identity-obj-proxy`, so compiled CSS never existed in a unit test and no
assertion can depend on it. SCSS only — `*.module.css` still goes through Vite so
`classNameStrategy: 'non-scoped'` keeps working for sdk-react's Column spec.
Verified on the mechanism, not on a green count: sampled every second across a full
uncached `run-many`, zero sass processes appear at any point. 41 projects, 16,372
tests, 4m18s.
Also fixes the discovery this exposed: a jest config only marks a project when the
directory has a project.json. dotcdn keeps one at apps/dotcdn/src/jest.config.js, a
plain source folder with no specs, and the widened name lookup generated a stray
config inside src/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…grate-all-core-web-unit-tests-from-jest-and-karma-to-vitest
…uild targets
The generator writes one file per project and it writes the WHOLE file, so for the
three projects whose `vite.config.mts` is a production build config it replaced the
build settings with a test-only config. `@nx/vite/plugin` infers a build target only
when the config carries `build.lib`, so this did not merely lose settings — it
deleted the targets. Nx substituted `nx:noop`, `nx run-many -t build` still reported
success for 17 projects, and Maven then failed packaging the war on the first
missing directory:
basedir core-web/dist/libs/sdk/analytics-standalone does not exist
with `dist/libs/edit-content-bridge` (html/js/legacy_custom_field_bridge) queued
behind it and `sdk-analytics:build` silently absent from the run-many list.
Merging the two configs into one file was the wrong repair: `stubScss()` compiles
every SCSS import to nothing and `angular()` / `react()` rewrite the sources — both
belong to a test run and neither belongs in a shipped bundle. Vitest resolves
`vitest.config.*` ahead of `vite.config.*`, so the two live side by side and each
tool reads its own. `vite.config.mts` for all three is byte-identical to main again,
and the generator now targets `vitest.config.mts` for them (BUILD_OWNED_VITE_CONFIG)
so regenerating cannot repeat this.
Verified: all three build outputs are produced again (ca.min.js,
edit-content-bridge.js, dot-experiments.min.iife.js) plus dist/libs/sdk/analytics,
and the suites are unchanged against the recorded counts — sdk-analytics 314/15,
sdk-experiments 48/9, edit-content-bridge 130/2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jest vs Vitest — what the migration actually costs and savesMeasured locally on one machine, both runners driven by the same command. Jest on The two headline numbers disagree, and that is the finding. 1. Per project, one at a time (serial) — the inner loop
Vitest executes tests ~20% faster and pays ~3.2s more to start. Running one project — what you do all day — the startup dominates and Vitest comes out slower for all but the largest projects. Two independent serial rounds produced the same figures (−18.8%/+12.1% and −20.2%/+12.8%), so this is not noise.
2. Whole suite, 3 lanes in parallel — NOT measured on CI
The two test counts are not the same set of tests — do not read the difference as coverage gained. Per-project comparison: 1,408 of the gap is projects where Jest produces no count at all — Read this before quoting a number
Whole suite — 3 lanes in parallel, no cachePer project — test execution only, measured serially |
`ContentletFilters.contentletIdentifier` had been widened to optional so that `suggestions.service.spec.ts` could pass `undefined` for it, which strict mode rejects against a required `string`. That is the wrong direction: the only producer is `suggestions.component.ts`, whose `@Input()` defaults to `''` and always sends a string, so nothing in the product ever omits the field — the type was relaxed for every consumer of `ContentletFilters` to accommodate nine lines in one spec. The service branches on truthiness (`contentletIdentifier ? … : …`), so `''` and `undefined` are the same input to it. The spec now passes `''`, which is both type-correct against the original contract and what the component actually sends. Field restored to required; block-editor's counts are unchanged either way (35 failed / 87 passed before and after — those failures predate this branch), and suggestions.service.spec.ts is green at 11/11. Also records `virtual-modules.d.ts` as a product exception. Its edit is a comment and nothing else: it documented `virtual:sdk-version` resolution as coming from jest.config.ts, a file this branch deletes, so the reference had to move to vite.config.mts. No declaration, type or runtime behaviour changes. And fixes a false zero in capture-baseline.sh. Its cross-check greps the runner's own "Tests: N" line out of the raw log, but Vitest colours the count, so the line reads "Tests <ESC>[1m<ESC>[32m55 passed" and the pattern matched nothing. sdk-vue ran 55 tests and was recorded as "genuinely empty" — silently, which is the exact failure class the script's own header says must never happen. ANSI codes are now stripped before matching. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AC-013 tarball check moved from `npm pack` to `pnpm pack` so it would run on a pnpm-managed toolchain, where npm is absent. On the CI runner the swap went the other way: `pnpm pack --dry-run --json` returned no file list at all, the script discarded its stderr, and `sdk-create-app:verify-package` failed with nothing in the log to explain it — reddening Frontend Unit Tests. Ask both packers instead of picking one, take the first that answers, and print each one's stderr when neither does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…his PR
Three scripts did their work in the commits already on this branch and would be dead
code the moment it merges:
- codemod-jest-to-vitest.mjs 7,754 mechanical rewrites, all committed
- migrate-project.mjs per-project tsconfig/test-setup/target changes, applied
- verify-test-only-diff.mjs the boundary check for THIS diff, meaningless once merged,
together with its 15-scenario suite under tools/__tests__
Its last run is the record that the boundary held: 860 spec, 147 test-config, 41
test-support, 53 workspace-config, 17 docs, 10 test-utilities, 8 migration-tooling, 5
ts-project-refs, 2 lint-dependency-checks, 1 build-invocation, and three named product
exceptions — the content-drive backtick-in-CSS-comment fix, sdk-client's
virtual-modules.d.ts comment, and create-app's verify-package.sh npm→pnpm. No rejected
paths.
Four tools stay, and none of them is a leftover:
- generate-vite-configs.mjs every one of the 45 configs says "regenerate rather than
hand-editing" in its header; deleting this orphans them
- capture-baseline.sh } the per-project count parity gate. Four of the six
- compare-test-counts.mjs } defects found on this branch produced no red at all —
they just stopped running tests — and this pair is what
catches that class. It should become a required check.
- profile-tests.mjs the suite profiler the perf work depends on
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Frontend Unit Tests was killed by the kernel twice in a row — a bare "The operation was canceled." with no stack, no failing sibling job, and no new head SHA — always at the same point: while edit-content, portlets-dot-experiments-portlet and template-builder, the three heaviest projects, were in flight together. nx.json's "parallel": 3 was sized for Jest, where it meant three node processes. Vitest gives every nx task its own coordinator holding a Vite dev server, and on edit-content (116 spec files, everything under libs/ inlined) that one process peaks at ~14 GB RSS. Three of those do not fit a 16 GB runner that is also holding this build's JVM. The parallelism was barely paying for itself anyway: on the run that died, 8.8 min of serial vitest time had finished in 5.6 min of wall clock — ~1.6x on 4 vCPUs, for 3x the memory. Scoped to this execution, so build and lint keep their concurrency. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…haust the runner Frontend Unit Tests keeps dying the same way: "The operation was canceled." with no stack, no failing sibling job and no new head SHA. Serialising nx to one project at a time did not stop it — run 34299641871 was killed 23 minutes in with only dotcms-ui in flight, and the runner then reported an orphan node process on cleanup. That is the shape of an OOM kill rather than a timeout, and one project is enough to cause it. Vitest gives every fork its own Vite module graph, and dotcms-ui's forks were measured summing 16-24GB of resident memory locally against a 16GB runner that is also holding this build's JVM. `maxForks` defaults to the CPU count, so the runner was starting four of them. 2 rather than 1 because capping to a single fork would roughly double a job that already runs twenty minutes, and CI-only because a developer machine is not memory constrained the way the runner is — local runs keep every core. Honest about what this is: a starting point to be validated in CI, not a figure derived from a benchmark. The kill does not reproduce on a 16-core laptop with spare RAM, and summed RSS across forks double-counts shared pages, so local measurement could not rank the candidates — removing the `libs|apps` inline made the summed figure look worse, and this cap barely moved it. Both are artefacts of the metric, which is why the number is deliberately conservative and commented as such in the generator. Verified not to be a regression on the three heaviest projects, under the CI path (CI=true): dotcms-ui 2,234 tests, edit-content 2,346, portlets-content-drive 1,370 green four runs in a row with no leftover processes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…annot exhaust the runner" This reverts commit d0f6f94. The cap was aimed at the wrong variable and the run that carried it, 34350160145, died exactly as before: cancelled at 24m47s, on dotcms-ui, with `--parallel=1` from pom.xml and `maxForks: 2` both active. Measuring peak RSS of a SINGLE process rather than the sum across forks — the sum double-counts shared pages and cannot rank configurations, which is what misled the first attempt — shows why nothing about concurrency could have helped: as committed 14.2 GB 44s maxForks: 1 14.2 GB 44s 4 shards 13.3 GB 83s no libs|apps inline 17.8 GB 39s One fork of dotcms-ui peaks at 14.2 GB, and it stays there whether it runs 2,234 tests or a 550-test shard. That is not accumulation across files and not contention between workers: it is the baseline cost of the module graph this project's test run builds. On a 16 GB runner also holding the build's JVM, one fork does not fit, so capping how many of them run concurrently cannot change the outcome. Two things worth recording because they are counter-intuitive and cost time to establish: sharding barely moves peak memory (the graph, not the file count, is the cost), and dropping the `libs|apps` deps.inline entry makes memory 25% WORSE, not better — externalising those sources has Node load them separately instead. Levers not yet tried, in the order I would try them: the jsdom environment (its per-instance cost is a plausible share of the 14 GB), `isolate: false`, and splitting dotcms-ui's 226 spec files so no single nx task owns a graph this large. A bigger runner for this one job is the option that needs no measurement at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Angular's JIT compiler pins compiled definitions to component classes cached in Vite's module graph, and the default 'forks' pool never releases that cache across test files in a run - Frontend Unit Tests was killed three times mid-dotcms-ui with peak RSS reaching ~14GB per process - vmForks gives each spec file a fresh VM context so module state doesn't accumulate, cutting measured peak RSS to 1.6-2.7GB per project while running as fast or faster - Regenerates all vite/vitest.config.mts from the shared template in tools/generate-vite-configs.mjs so every project picks up the same pool settings
…ot grow to 14GB Frontend Unit Tests was killed three times — 22m46s, 24m47s, 16m08s, always a bare "The operation was canceled." with no stack, no failing sibling and no new head SHA, always mid-dotcms-ui, and twice with an orphan node process reported on cleanup. It is an OOM kill: the job's timeout is 240 minutes, so nothing was timing out. The cause is where Vitest's speed comes from. A 'forks' worker keeps Vite's module graph and transform cache for the whole run and reuses them across test files; `isolate` replaces the ENVIRONMENT per file and never that cache. Angular's JIT then hangs compiled definitions off the component classes living in it, so nothing is ever released. Measured on dotcms-ui: 2.9GB after one spec file, 14.2GB after 226 of them. Jest never hit this because it drops its module registry per test file. Measured on main, same project: 1.15GB per worker against a 16.5GB sum — the same total memory, but in pieces a runner can schedule rather than one block that has to fit whole. That difference, not a leak in the migration, is why this only appeared after Vitest. 'vmForks' gives each test file a fresh VM context, so the state cannot accumulate, and vmMemoryLimit caps what one holds. Peak RSS of a single process, all green: dotcms-ui 14.2GB -> 2.16GB (44s -> 38s) ui 13.7GB -> 1.92GB edit-content ~14GB -> 1.68GB portlets-content-drive ~14GB -> 2.72GB Faster as well as smaller. On a 4-vCPU runner that is ~4 x 2GB rather than one 14GB block. Full suite: 16,668 tests, 4m09s, down from ~4m45s. Chosen per project on measurement rather than uniformly. sdk-analytics and sdk-experiments stay on 'forks': the VM context is not a perfect stand-in for a real one and both regress under it — analytics' SSR specs assign `global.window = undefined` against a getter-only property, experiments silently collects 39 of its 48 tests — and neither has the problem being solved, peaking at 0.25-1.4GB. Ruled out first, each measured as peak RSS of a single process (summing across forks double-counts shared pages and cannot rank anything, which cost two wrong attempts): maxForks 14.2GB unchanged, 4-way sharding 13.3GB, dropping the libs|apps deps.inline entry 17.8GB — worse, and happy-dom 9%. Only the pool addresses retention; everything else addressed concurrency or volume. portlets-edit-ema-portlet is still red, and was before this: 2 tests fail under 'forks'. Under 'vmForks' all 1,453 pass and the non-zero exit is an unhandled `payload.pageAsset.page.styleEditorSchemas` from inside an rxMethod — the same incomplete-mock class as the dot-analytics fix earlier on this branch. Left for its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
Every unit test project in
core-webruns on Jest or (for a couple of legacy apps) Karma. These runners are slow, have inconsistent config across the ~50 apps/libs, and block the team from adopting faster, more modern tooling.Solution
In simple terms: we swapped the engine that runs our frontend unit tests. All the tests themselves still check the same things — we just moved them to run on Vitest instead of Jest/Karma, and made sure nothing silently stopped running along the way.
Details:
jest.config.ts,karma.conf.js,tsconfig.spec.json(Jest-oriented) and legacytest.ts/test-setup.tsbootstrap files, replacing them with per-projectvite.config.mtsgenerated to align with Nx's Vitest generator output.*.spec.ts/*.spec.tsx/*.test.tsfile's imports and mocking APIs (jest.fn,jest.mock, etc.) to their Vitest equivalents acrossapps/andlibs/.core-web/tools/(codemod-jest-to-vitest.mjs,generate-vite-configs.mjs,migrate-project.mjs,capture-baseline.sh,compare-test-counts.mjs,verify-test-only-diff.mjs) used to drive and validate the migration and guard against tests silently not running.@nx/viteout of a published library's runtime dependencies.core-web/CLAUDE.md,.cursor/rules/frontend-context.mdc,.cursor/rules/test-context.mdc, anddocs/frontend/TESTING_FRONTEND.md/TESTING_REVIEW_RULES.mdto reflect Vitest as the standard.specs/37444-vitest-unit-test-migration/) documenting the migration contract, data model, and baseline/after test-count captures used to verify no tests were lost.Testing
Baseline and after test-count captures (
specs/37444-vitest-unit-test-migration/baseline-counts.json,after-counts.json) were generated to confirm test counts are preserved across the migration.verify-test-only-diff.mjsguards that migration commits only touch test-related files.Confirm
nx affected -t testis green across all migrated projects in CI before mergeConfirm no projects were missed (all
jest.config.ts/karma.conf.jsremoved, all replaced withvite.config.mts)This PR fixes: #37444