Skip to content

fix(vitest-angular): reinitialize testbed per setup call#2244

Open
benpsnyder wants to merge 6 commits intoanalogjs:alphafrom
benpsnyder:fix/2222-vitest-isolation
Open

fix(vitest-angular): reinitialize testbed per setup call#2244
benpsnyder wants to merge 6 commits intoanalogjs:alphafrom
benpsnyder:fix/2222-vitest-isolation

Conversation

@benpsnyder
Copy link
Copy Markdown
Contributor

@benpsnyder benpsnyder commented Apr 5, 2026

PR Checklist

Closes #2222

Affected scope

  • Primary scope: vitest-angular
  • Secondary scopes: tests, docs

Recommended merge strategy for maintainer [optional]

  • Squash merge
  • Rebase merge
  • Other

Commit preservation note [optional]

What is the new behavior?

This PR hardens setupTestBed() against Vitest project-order leakage inside a shared worker/process and adds a supported debugging path for follow-up reports.

Before this change, setupTestBed() initialized Angular's test environment only once per process. If another setup file called setupTestBed() later with different options such as additional providers, those options were silently ignored because the global singleton guard skipped reinitialization.

This PR changes setupTestBed() to reset the current Angular test environment before reinitializing it on later calls. That keeps each setup file's configuration authoritative instead of depending on whichever project initialized first.

The PR also:

  • adds a focused regression project under tests/vitest-angular/src/reconfigure-test-bed that verifies repeated setupTestBed() calls in one Vitest process actually reconfigure the environment
  • documents that each setup file can call setupTestBed() independently without leaking providers/options across Vitest projects
  • adds scoped debug tokens analog:vitest:testbed and analog:vitest:testbed:v so future reports can capture setup/reconfigure behavior without patching local code

Test plan

  • nx format:check
  • pnpm nx build vitest-angular
  • pnpm test
  • Manual verification

Manual verification:

  • pnpm nx build vitest-angular
  • pnpm exec vitest run --config tests/vitest-angular/vitest.config.ts --project reconfigure-test-bed
  • pnpm exec vitest run --config tests/vitest-angular/vitest.config.ts

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

The existing reset-test-bed-between-tests fixture still passes. The new regression covers the different failure mode here: later setupTestBed() calls with different configuration being ignored in the same Vitest process.

For future issue reports, users can now run Vitest with:

  • DEBUG=analog:vitest:testbed pnpm exec vitest run ...
  • DEBUG=analog:vitest:testbed:v pnpm exec vitest run ...

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 5, 2026

Deploy Preview for analog-blog ready!

Name Link
🔨 Latest commit 32005fa
🔍 Latest deploy log https://app.netlify.com/projects/analog-blog/deploys/69de28f39aa7100008ded30b
😎 Deploy Preview https://deploy-preview-2244--analog-blog.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 5, 2026

Deploy Preview for analog-app ready!

Name Link
🔨 Latest commit 32005fa
🔍 Latest deploy log https://app.netlify.com/projects/analog-app/deploys/69de28f370f5c60008426a83
😎 Deploy Preview https://deploy-preview-2244--analog-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the scope:vitest-angular Changes in @analogjs/vitest-angular label Apr 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A global setup symbol was made specific (Symbol.for('@analogjs/vitest-angular/testbed-setup')). setupTestBed now always obtains a TestBed instance, computes destroyAfterEach from teardown?.destroyAfterEach ?? !browserMode, and conditionally enables debug logging from process.env.DEBUG and globalThis.localStorage?.getItem('debug'). On subsequent invocations it calls testBed.resetTestEnvironment() and then (re)defines TestModule and calls testBed.initTestEnvironment(...) on every call. The global setup flag is set unconditionally and teardown options merge the precomputed destroyAfterEach with any overrides. A new reconfigure-test-bed test project (token, setup, spec, vitest project) was added.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commit style with a supported scope (vitest-angular) and accurately describes the main change: reinitializing testbed per setup call.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, implementation changes, test plan, and debug tokens added.
Linked Issues check ✅ Passed The PR directly addresses issue #2222 by modifying setupTestBed() to reset and reinitialize TestBed on each call, adds regression tests in the reconfigure-test-bed project, and documents the behavior change.
Out of Scope Changes check ✅ Passed All changes are scoped to the objectives: vitest-angular core changes, regression test project, docs updates, and test configuration. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/docs-app/docs/features/testing/vitest.md (1)

158-159: Documentation accurately reflects the reconfiguration behavior.

The explanation correctly describes how subsequent setupTestBed() calls reset and replace the Angular test environment. This directly addresses the bug fix from issue #2222 where later calls were being ignored. The placement after the configuration options and the clear statement about preventing leakage across test projects is helpful.

Optional: Consider adding a concrete example reference

To help users visualize the multi-setup-file scenario, you could add a brief inline example or reference:

 Each Vitest setup file can call `setupTestBed()` with its own configuration. If multiple setup files run in the same Vitest process, the latest call replaces the active Angular test environment so project-specific providers and options do not leak across test projects.
+For example, if `setup-a.ts` and `setup-b.ts` both call `setupTestBed()` within the same Vitest worker, the configuration from `setup-b.ts` will be active.

However, the current concise explanation is perfectly adequate for most users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs-app/docs/features/testing/vitest.md` around lines 158 - 159, The
documentation correctly describes that subsequent calls to setupTestBed()
replace the Angular test environment, but to make this clearer add a short
concrete example or reference showing two Vitest setup files each calling
setupTestBed() with different providers/options and demonstrating that the later
call wins; update the paragraph following the configuration options to include
this example and reference to setupTestBed() and Vitest so readers can quickly
visualize the multi-setup-file scenario.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/docs-app/docs/features/testing/vitest.md`:
- Around line 158-159: The documentation correctly describes that subsequent
calls to setupTestBed() replace the Angular test environment, but to make this
clearer add a short concrete example or reference showing two Vitest setup files
each calling setupTestBed() with different providers/options and demonstrating
that the later call wins; update the paragraph following the configuration
options to include this example and reference to setupTestBed() and Vitest so
readers can quickly visualize the multi-setup-file scenario.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65dbe651-89d9-4e08-9c42-5edaf5cac59f

📥 Commits

Reviewing files that changed from the base of the PR and between 74e3e2e and d853d8d.

📒 Files selected for processing (2)
  • apps/docs-app/docs/features/testing/vitest.md
  • packages/vitest-angular/setup-testbed.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/vitest-angular/setup-testbed.ts

@brandonroberts
Copy link
Copy Markdown
Member

This needs to be validated against the Angular TestBed source code for compatibility and performance impact. I don't think this solution is a trivial one.

@brandonroberts brandonroberts added the blocked Blocked label Apr 6, 2026
@benpsnyder
Copy link
Copy Markdown
Contributor Author

benpsnyder commented Apr 12, 2026

This needs to be validated against the Angular TestBed source code for compatibility and performance impact. I don't think this solution is a trivial one.

I validated this against Angular's TestBed source and measured the specific reconfigure path before keeping the change.

  • resetTestEnvironment() in Angular already calls resetTestingModule() internally, so the previous implementation was doing that reset work twice before re-initializing the environment.
  • Angular also uses the resetTestEnvironment() -> initTestEnvironment(...) sequence in its own browser testing when it needs to switch environments, so the re-init approach itself is compatible with Angular's lifecycle.

Compatibility:

  • pnpm nx test tests-vitest-angular passed: 6 test files, 10 tests.
  • Observed end-to-end runtime for that focused validation run on my machine: 2.97s.

Performance:

  • I microbenchmarked the exact reconfigure cycle this change affects: setupTestBed(); setupTestBed({ providers: [...] }); TestBed.inject(...).
  • Environment: Node v24.14.1, Angular 21.2.8, JSDOM-backed browser globals.
  • Method: 50 warmups, 400 measured iterations per variant, plus 25 compatibility-only iterations per variant.

Results:

  • Previous double-reset path:
    mean 0.2557ms, median 0.2265ms, p95 0.3410ms, stddev 0.2194ms.
  • Current single-reset path:
    mean 0.2117ms, median 0.1921ms, p95 0.2610ms, stddev 0.1298ms.
  • Delta on the measured reconfigure path:
    0.0440ms lower mean latency per cycle, 17.2% faster on average.

So the change is source-backed for compatibility, and the measured impact on the code path it changes is a small but real performance improvement rather than a regression.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vitest-angular/setup-testbed.ts`:
- Around line 105-124: The assignment to the ANGULAR_TESTBED_SETUP flag is done
before testBed.initTestEnvironment(), risking a stale true flag if
initTestEnvironment() throws; move the (globalThis as
any)[ANGULAR_TESTBED_SETUP] = true assignment so it runs only after
testBed.initTestEnvironment(...) completes successfully, and ensure any error
from testBed.initTestEnvironment lets the flag stay unset so subsequent calls
will attempt initialization (and avoid calling resetTestEnvironment() when no
successful init occurred).
- Around line 84-85: The beforeEach/afterEach cleanup hooks are always
registered, causing duplicate handlers on repeated setupTestBed() calls; guard
the hook registration using the same Symbol guard used for testbed
initialization: check the initialization Symbol (the one defined alongside
setupTestBed) on globalThis, and only call beforeEach(getCleanupHook(false)) and
afterEach(getCleanupHook(true)) when that Symbol is not set, then set the Symbol
to true after registering to prevent future duplicates; reference getCleanupHook
and setupTestBed so you add the guard in the same module and reuse the existing
Symbol used for testbed initialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1dbe8977-d68b-4a45-adb9-27cb182ad137

📥 Commits

Reviewing files that changed from the base of the PR and between d853d8d and 46ccb22.

📒 Files selected for processing (2)
  • packages/vitest-angular/setup-testbed.ts
  • tests/vitest-angular/vitest.config.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/vitest-angular/vitest.config.ts

Comment thread packages/vitest-angular/setup-testbed.ts
Comment thread packages/vitest-angular/setup-testbed.ts
@benpsnyder
Copy link
Copy Markdown
Contributor Author

benpsnyder commented Apr 12, 2026

@coderabbitai I verified this against the current code and did not apply the suggested fix.

  • The observation is real: packages/vitest-angular/setup-testbed.ts always registers beforeEach(getCleanupHook(false)) and afterEach(getCleanupHook(true)), and tests/vitest-angular/src/reconfigure-test-bed/test-setup.ts calls setupTestBed() twice, so that setup file does register duplicate cleanup handlers.
  • The proposed fix is not safe as written. Reusing ANGULAR_TESTBED_SETUP as a global guard for hook registration suppresses cleanup hook registration in later setup files, because that symbol also tracks successful TestBed initialization.
  • When I validated that guarded variant earlier, the later reset-test-bed-between-tests browser project failed with Cannot configure the test module when the test module has already been instantiated, which shows cleanup was skipped where it was still needed.
  • Because of that, I did not change the code for this finding.

Verification:

  • Current code still passes pnpm nx test tests-vitest-angular: 6 test files, 10 tests.

Conclusion:

  • This finding is partially correct about duplicate registration in a repeated setupTestBed() call.
  • The requested fix should not be applied in its proposed form.
  • Any deduping needs a separate guard that does not share state with the TestBed initialization flag.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Tip

For best results, initiate chat on the files or code changes.

@benpsnyder Thanks for the thorough investigation and clear write-up. Your analysis is correct — conflating the TestBed initialization flag with a hook registration guard into the same symbol (ANGULAR_TESTBED_SETUP) would break inter-project isolation, and the reset-test-bed-between-tests failure demonstrates exactly why. Acknowledged that the fix was not applied.

For reference, a safe deduplication path (if ever desired) would require a separate module-scoped or global guard symbol solely for tracking whether hooks have been registered for the current setup-file context — completely independent of ANGULAR_TESTBED_SETUP. That's a future concern though, not a blocker here.


✏️ Learnings added
Learnt from: benpsnyder
URL: https://github.com/analogjs/analog/pull/2244

Timestamp: 2026-04-12T05:27:14.596Z
Learning: In `packages/vitest-angular/setup-testbed.ts`, the `ANGULAR_TESTBED_SETUP` global symbol tracks TestBed initialization state and MUST NOT be reused as a guard for `beforeEach`/`afterEach` cleanup hook registration deduplication. Reusing it suppresses hook registration in later setup files (i.e., distinct Vitest projects whose setup files also call `setupTestBed()`), causing `Cannot configure the test module when the test module has already been instantiated` failures in the `reset-test-bed-between-tests` project. If duplicate hook registration from repeated `setupTestBed()` calls within the same setup file needs to be deduplicated, a separate guard symbol (independent of `ANGULAR_TESTBED_SETUP`) must be introduced. Validated in PR `#2244`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-04T19:18:17.269Z
Learning: In `packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts`, `angularVitestPlugins()` helpers are NOT the HMR toggle. HMR is controlled by dedicated HMR helper plugins gated by `shouldEnableHmr()`. Tests for the HMR option (`hmr: false`, `liveReload: true`, etc.) must assert against the HMR helper plugins under a development-like env, not against the always-present Vitest plugins. See PR `#2226`, `packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts` line 24 for the corrected test pattern.

Learnt from: eblocha
Repo: analogjs/analog PR: 2212
File: packages/astro-angular/src/client-ngh.ts:40-45
Timestamp: 2026-04-06T13:25:40.588Z
Learning: In `packages/astro-angular/src/client-ngh.ts`, the `ɵSSR_CONTENT_INTEGRITY_MARKER` comment node is intentionally prepended to `document.body` on every Angular island hydration without a deduplication guard. This is safe: Angular's integrity check only requires that at least one marker is present at the start of `<body>` — multiple markers are harmless. The risk Angular guards against is markers being stripped/rewritten by CDNs or minifiers, not duplicates. Always prepending ensures the marker is at the front regardless of ordering with other islands. Confirmed valid; do not flag this as a bug in future reviews.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-05T04:29:55.140Z
Learning: In `apps/tailwind-debug-app-e2e/tests/component-css-hmr.spec.ts` (PR `#2226`, commit 9e0be352), the E2E test was reframed from asserting "CSS updates without full reload" to serving as a repro harness for tailwindlabs/tailwindcss#19903. It now asserts: (1) stylesheet edit detected, (2) `environment.hot.send` emits `full-reload`, (3) app state resets after reload, (4) stack trace includes `updateModules`. Do not suggest reverting this to the previous assertion that CSS updates without reload — the test intentionally documents the current (broken) upstream behavior.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-05T04:18:48.628Z
Learning: In `packages/vite-plugin-angular/src/lib/component-resolvers.ts`, `getAngularComponentMetadata()` (and the internal `collectComponentUrls` helper) is restricted to actual `Component(...)` decorator objects only. Do not flag or suggest reverting this restriction — it was added deliberately (PR `#2226`, commit c37ff35f2) to prevent false-positive metadata extraction from non-component decorators.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-03-25T07:19:07.510Z
Learning: In `analogjs/analog`, the `e2e:analog` suite (`apps/analog-app-e2e` / `json-ld.spec.ts`) has a pre-existing failure introduced in commit `9ed63a6b` (typed routes + JSON-LD): the prerender step produces empty `<analogjs-root></analogjs-root>` because Angular SSR does not execute during Nitro's prerender phase, so JSON-LD scripts are never injected. This failure is present on the `alpha` branch and is not caused by the `feat/investigate-nitro-vite-plugin` migration (PR `#2188`).

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vitest-angular-tools/src/schematics/setup/index.spec.ts:10-13
Timestamp: 2026-03-22T22:19:24.618Z
Learning: In `packages/vitest-angular-tools/src/schematics/setup/index.spec.ts`, the `collectionPath` constant intentionally points to `node_modules/analogjs/vitest-angular/src/lib/tools/collection.json` — the Vite/CJS build output destination for the `vitest-angular-tools` package (configured in its `project.json` `outputs`). The `SchematicTestRunner` requires the compiled CommonJS schematics at that location and cannot use the uncompiled TypeScript sources. Do not flag this path as incorrect or suggest replacing it with a relative source path like `../../../collection.json`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/src/lib/route-manifest.spec.ts:723-734
Timestamp: 2026-03-24T13:22:40.861Z
Learning: In `packages/router/src/lib/route-manifest.spec.ts` (and the corresponding `detectSchemaExports` implementation in `packages/router/manifest/src/index.ts`), the regex-based schema export detector intentionally false-positives on commented-out `export const routeParamsSchema`/`export const routeQuerySchema` lines. This is an accepted v1 limitation explicitly documented in the test. The test expectation `expect(result.hasParamsSchema).toBe(true)` for commented-out content is correct and intentional. Do not flag this as a bug or suggest changing the expectation to `false` — it will be addressed if/when schema detection is upgraded to AST-based parsing.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vite-plugin-angular-tools/vite.config.lib.ts:10-11
Timestamp: 2026-03-22T22:11:42.083Z
Learning: In `packages/vite-plugin-angular-tools/vite.config.lib.ts` (and other `vite.config.lib.ts` build configs in analogjs/analog), the import of `oxcDtsPlugin` from `../../tools/build/shared-plugins.ts` uses a relative cross-package path with an `// eslint-disable-next-line nx/enforce-module-boundaries` suppression. This is intentional and correct: `tools/build/shared-plugins.ts` is a build-time utility with no registered TypeScript path alias in `tsconfig.base.json`. Do not flag this relative import or its eslint suppression as a violation — suggesting a non-existent workspace alias would break the import.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/src/lib/route-path.typetest.spec.ts:62-155
Timestamp: 2026-03-24T13:22:48.562Z
Learning: In `packages/router/src/lib/route-path.typetest.spec.ts`, the `MockRouteTable` interface intentionally does NOT augment `AnalogRouteTable`. The file header states that since `AnalogRouteTable` is empty by default (populated only by generated code), type utility correctness is validated by testing conditional types directly against `MockRouteTable`. Do not flag this as a missing augmentation or suggest adding `declare module '...' { interface AnalogRouteTable extends MockRouteTable {} }`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/manifest/src/index.ts:161-165
Timestamp: 2026-03-24T13:22:29.071Z
Learning: In `packages/router/manifest/src/index.ts`, `detectSchemaExports` intentionally uses simple line-start regexes (`/^\s*export\s+const\s+routeParamsSchema\b/m`) without stripping comments first. This is a known v1 limitation: a commented-out `// export const routeParamsSchema` will produce a false positive. The trade-off is explicitly accepted and documented in the test at `packages/router/src/lib/route-manifest.spec.ts` around line 723 ("Simple regex doesn't filter comments — acceptable for v1."). Do not flag this as a bug or propose AST-based detection; the fix is deferred to a future version.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vitest-angular-tools/vite.config.lib.ts:30-34
Timestamp: 2026-03-22T23:32:34.648Z
Learning: In `packages/vitest-angular-tools/vite.config.lib.ts`, `build.emptyOutDir` is intentionally set to `false`. The output directory is `node_modules/analogjs/vitest-angular/src/lib/tools`, which is a subdirectory of the `vitest-angular` package output. Setting `emptyOutDir: true` would delete sibling files already written by the parent `vitest-angular` build (which runs immediately before `nx build vitest-angular-tools`). Do not flag this as a bug. The same pattern applies to `packages/vite-plugin-angular-tools/vite.config.lib.ts`.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Tip

For best results, initiate chat on the files or code changes.

@benpsnyder — Thanks for the thorough investigation and the clear write-up. Your analysis is correct:

  • The duplicate hook registration per repeated setupTestBed() call is a real observation, but the safe fix requires a separate guard symbol that is independent of ANGULAR_TESTBED_SETUP, precisely because ANGULAR_TESTBED_SETUP serves double duty as the TestBed initialization flag — reusing it for hook deduplication collapses two distinct lifecycle concerns into one symbol and breaks later setup files.
  • The reset-test-bed-between-tests failure you reproduced (Cannot configure the test module when the test module has already been instantiated) is the correct signal that cleanup hooks were suppressed where they were still needed.

I've already captured this constraint in the knowledge base (learning 278597ef) so future reviews won't re-propose the unsafe form of this fix.

If the duplicate registration in a single setup file ever becomes a concern worth addressing, the right path is a new global symbol scoped exclusively to hook deduplication — something like Symbol.for('@analogjs/vitest-angular/hooks-registered') — with no shared state with the initialization flag.


✏️ Learnings added
Learnt from: benpsnyder
URL: https://github.com/analogjs/analog/pull/2244

Timestamp: 2026-04-12T05:27:41.848Z
Learning: In `packages/vitest-angular/setup-testbed.ts`, the duplicate `beforeEach`/`afterEach` cleanup hook registration that occurs when a single setup file calls `setupTestBed()` more than once is a known, accepted limitation. The safe fix requires a separate global guard symbol (e.g., `Symbol.for('analogjs/vitest-angular/hooks-registered')`) scoped exclusively to hook-registration deduplication, independent of `ANGULAR_TESTBED_SETUP`. Using `ANGULAR_TESTBED_SETUP` as that guard suppresses hook registration in later Vitest project setup files and causes `Cannot configure the test module when the test module has already been instantiated` failures in the `reset-test-bed-between-tests` project. Validated in PR `#2244`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-12T05:27:14.596Z
Learning: In `packages/vitest-angular/setup-testbed.ts`, the `ANGULAR_TESTBED_SETUP` global symbol tracks TestBed initialization state and MUST NOT be reused as a guard for `beforeEach`/`afterEach` cleanup hook registration deduplication. Reusing it suppresses hook registration in later setup files (i.e., distinct Vitest projects whose setup files also call `setupTestBed()`), causing `Cannot configure the test module when the test module has already been instantiated` failures in the `reset-test-bed-between-tests` project. If duplicate hook registration from repeated `setupTestBed()` calls within the same setup file needs to be deduplicated, a separate guard symbol (independent of `ANGULAR_TESTBED_SETUP`) must be introduced. Validated in PR `#2244`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-04T19:18:17.269Z
Learning: In `packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts`, `angularVitestPlugins()` helpers are NOT the HMR toggle. HMR is controlled by dedicated HMR helper plugins gated by `shouldEnableHmr()`. Tests for the HMR option (`hmr: false`, `liveReload: true`, etc.) must assert against the HMR helper plugins under a development-like env, not against the always-present Vitest plugins. See PR `#2226`, `packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts` line 24 for the corrected test pattern.

Learnt from: eblocha
Repo: analogjs/analog PR: 2212
File: packages/astro-angular/src/client-ngh.ts:40-45
Timestamp: 2026-04-06T13:25:40.588Z
Learning: In `packages/astro-angular/src/client-ngh.ts`, the `ɵSSR_CONTENT_INTEGRITY_MARKER` comment node is intentionally prepended to `document.body` on every Angular island hydration without a deduplication guard. This is safe: Angular's integrity check only requires that at least one marker is present at the start of `<body>` — multiple markers are harmless. The risk Angular guards against is markers being stripped/rewritten by CDNs or minifiers, not duplicates. Always prepending ensures the marker is at the front regardless of ordering with other islands. Confirmed valid; do not flag this as a bug in future reviews.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-05T04:29:55.140Z
Learning: In `apps/tailwind-debug-app-e2e/tests/component-css-hmr.spec.ts` (PR `#2226`, commit 9e0be352), the E2E test was reframed from asserting "CSS updates without full reload" to serving as a repro harness for tailwindlabs/tailwindcss#19903. It now asserts: (1) stylesheet edit detected, (2) `environment.hot.send` emits `full-reload`, (3) app state resets after reload, (4) stack trace includes `updateModules`. Do not suggest reverting this to the previous assertion that CSS updates without reload — the test intentionally documents the current (broken) upstream behavior.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-05T04:18:48.628Z
Learning: In `packages/vite-plugin-angular/src/lib/component-resolvers.ts`, `getAngularComponentMetadata()` (and the internal `collectComponentUrls` helper) is restricted to actual `Component(...)` decorator objects only. Do not flag or suggest reverting this restriction — it was added deliberately (PR `#2226`, commit c37ff35f2) to prevent false-positive metadata extraction from non-component decorators.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-03-25T07:19:07.510Z
Learning: In `analogjs/analog`, the `e2e:analog` suite (`apps/analog-app-e2e` / `json-ld.spec.ts`) has a pre-existing failure introduced in commit `9ed63a6b` (typed routes + JSON-LD): the prerender step produces empty `<analogjs-root></analogjs-root>` because Angular SSR does not execute during Nitro's prerender phase, so JSON-LD scripts are never injected. This failure is present on the `alpha` branch and is not caused by the `feat/investigate-nitro-vite-plugin` migration (PR `#2188`).

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vitest-angular-tools/src/schematics/setup/index.spec.ts:10-13
Timestamp: 2026-03-22T22:19:24.618Z
Learning: In `packages/vitest-angular-tools/src/schematics/setup/index.spec.ts`, the `collectionPath` constant intentionally points to `node_modules/analogjs/vitest-angular/src/lib/tools/collection.json` — the Vite/CJS build output destination for the `vitest-angular-tools` package (configured in its `project.json` `outputs`). The `SchematicTestRunner` requires the compiled CommonJS schematics at that location and cannot use the uncompiled TypeScript sources. Do not flag this path as incorrect or suggest replacing it with a relative source path like `../../../collection.json`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/src/lib/route-manifest.spec.ts:723-734
Timestamp: 2026-03-24T13:22:40.861Z
Learning: In `packages/router/src/lib/route-manifest.spec.ts` (and the corresponding `detectSchemaExports` implementation in `packages/router/manifest/src/index.ts`), the regex-based schema export detector intentionally false-positives on commented-out `export const routeParamsSchema`/`export const routeQuerySchema` lines. This is an accepted v1 limitation explicitly documented in the test. The test expectation `expect(result.hasParamsSchema).toBe(true)` for commented-out content is correct and intentional. Do not flag this as a bug or suggest changing the expectation to `false` — it will be addressed if/when schema detection is upgraded to AST-based parsing.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/manifest/src/index.ts:161-165
Timestamp: 2026-03-24T13:22:29.071Z
Learning: In `packages/router/manifest/src/index.ts`, `detectSchemaExports` intentionally uses simple line-start regexes (`/^\s*export\s+const\s+routeParamsSchema\b/m`) without stripping comments first. This is a known v1 limitation: a commented-out `// export const routeParamsSchema` will produce a false positive. The trade-off is explicitly accepted and documented in the test at `packages/router/src/lib/route-manifest.spec.ts` around line 723 ("Simple regex doesn't filter comments — acceptable for v1."). Do not flag this as a bug or propose AST-based detection; the fix is deferred to a future version.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/src/lib/route-path.typetest.spec.ts:62-155
Timestamp: 2026-03-24T13:22:48.562Z
Learning: In `packages/router/src/lib/route-path.typetest.spec.ts`, the `MockRouteTable` interface intentionally does NOT augment `AnalogRouteTable`. The file header states that since `AnalogRouteTable` is empty by default (populated only by generated code), type utility correctness is validated by testing conditional types directly against `MockRouteTable`. Do not flag this as a missing augmentation or suggest adding `declare module '...' { interface AnalogRouteTable extends MockRouteTable {} }`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vite-plugin-angular-tools/vite.config.lib.ts:10-11
Timestamp: 2026-03-22T22:11:42.083Z
Learning: In `packages/vite-plugin-angular-tools/vite.config.lib.ts` (and other `vite.config.lib.ts` build configs in analogjs/analog), the import of `oxcDtsPlugin` from `../../tools/build/shared-plugins.ts` uses a relative cross-package path with an `// eslint-disable-next-line nx/enforce-module-boundaries` suppression. This is intentional and correct: `tools/build/shared-plugins.ts` is a build-time utility with no registered TypeScript path alias in `tsconfig.base.json`. Do not flag this relative import or its eslint suppression as a violation — suggesting a non-existent workspace alias would break the import.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vitest-angular-tools/vite.config.lib.ts:30-34
Timestamp: 2026-03-22T23:32:34.648Z
Learning: In `packages/vitest-angular-tools/vite.config.lib.ts`, `build.emptyOutDir` is intentionally set to `false`. The output directory is `node_modules/analogjs/vitest-angular/src/lib/tools`, which is a subdirectory of the `vitest-angular` package output. Setting `emptyOutDir: true` would delete sibling files already written by the parent `vitest-angular` build (which runs immediately before `nx build vitest-angular-tools`). Do not flag this as a bug. The same pattern applies to `packages/vite-plugin-angular-tools/vite.config.lib.ts`.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Tip

For best results, initiate chat on the files or code changes.

@benpsnyder — This is fully consistent with what you shared earlier in the thread, and my previous reply already acknowledged it. The constraints are already captured in the knowledge base:

  • ANGULAR_TESTBED_SETUP must not be reused as a hook-registration guard — doing so collapses two distinct lifecycle concerns and suppresses cleanup in later Vitest project setup files (learning 278597ef).
  • The duplicate registration in a single setup file is a known, accepted limitation — any future deduplication must use a separate symbol like Symbol.for('@analogjs/vitest-angular/hooks-registered'), independent of the init flag (learning 3faa698c).

No additional action needed here; the knowledge base already reflects the correct constraint, and the current implementation passing all 10 tests confirms the PR is in good shape.


🧠 Learnings used
Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-12T05:27:41.848Z
Learning: In `packages/vitest-angular/setup-testbed.ts`, the duplicate `beforeEach`/`afterEach` cleanup hook registration that occurs when a single setup file calls `setupTestBed()` more than once is a known, accepted limitation. The safe fix requires a separate global guard symbol (e.g., `Symbol.for('analogjs/vitest-angular/hooks-registered')`) scoped exclusively to hook-registration deduplication, independent of `ANGULAR_TESTBED_SETUP`. Using `ANGULAR_TESTBED_SETUP` as that guard suppresses hook registration in later Vitest project setup files and causes `Cannot configure the test module when the test module has already been instantiated` failures in the `reset-test-bed-between-tests` project. Validated in PR `#2244`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-12T05:27:14.596Z
Learning: In `packages/vitest-angular/setup-testbed.ts`, the `ANGULAR_TESTBED_SETUP` global symbol tracks TestBed initialization state and MUST NOT be reused as a guard for `beforeEach`/`afterEach` cleanup hook registration deduplication. Reusing it suppresses hook registration in later setup files (i.e., distinct Vitest projects whose setup files also call `setupTestBed()`), causing `Cannot configure the test module when the test module has already been instantiated` failures in the `reset-test-bed-between-tests` project. If duplicate hook registration from repeated `setupTestBed()` calls within the same setup file needs to be deduplicated, a separate guard symbol (independent of `ANGULAR_TESTBED_SETUP`) must be introduced. Validated in PR `#2244`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vitest-angular-tools/vite.config.lib.ts:30-34
Timestamp: 2026-03-22T23:32:34.648Z
Learning: In `packages/vitest-angular-tools/vite.config.lib.ts`, `build.emptyOutDir` is intentionally set to `false`. The output directory is `node_modules/analogjs/vitest-angular/src/lib/tools`, which is a subdirectory of the `vitest-angular` package output. Setting `emptyOutDir: true` would delete sibling files already written by the parent `vitest-angular` build (which runs immediately before `nx build vitest-angular-tools`). Do not flag this as a bug. The same pattern applies to `packages/vite-plugin-angular-tools/vite.config.lib.ts`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-04T19:18:17.269Z
Learning: In `packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts`, `angularVitestPlugins()` helpers are NOT the HMR toggle. HMR is controlled by dedicated HMR helper plugins gated by `shouldEnableHmr()`. Tests for the HMR option (`hmr: false`, `liveReload: true`, etc.) must assert against the HMR helper plugins under a development-like env, not against the always-present Vitest plugins. See PR `#2226`, `packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts` line 24 for the corrected test pattern.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vite-plugin-angular-tools/vite.config.lib.ts:30-34
Timestamp: 2026-03-22T22:06:57.824Z
Learning: In `packages/vite-plugin-angular-tools/vite.config.lib.ts`, `build.emptyOutDir` is intentionally set to `false`. The output directory is `node_modules/analogjs/vite-plugin-angular/src/lib/tools`, which is a subdirectory of the `vite-plugin-angular` package output. Setting `emptyOutDir: true` would delete sibling files already written by the parent `vite-plugin-angular` build, breaking the combined build pipeline. Do not flag this as a bug.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-03-25T07:19:07.510Z
Learning: In `analogjs/analog`, the `e2e:analog` suite (`apps/analog-app-e2e` / `json-ld.spec.ts`) has a pre-existing failure introduced in commit `9ed63a6b` (typed routes + JSON-LD): the prerender step produces empty `<analogjs-root></analogjs-root>` because Angular SSR does not execute during Nitro's prerender phase, so JSON-LD scripts are never injected. This failure is present on the `alpha` branch and is not caused by the `feat/investigate-nitro-vite-plugin` migration (PR `#2188`).

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vite-plugin-angular-tools/vite.config.lib.ts:10-11
Timestamp: 2026-03-22T22:11:42.083Z
Learning: In `packages/vite-plugin-angular-tools/vite.config.lib.ts` (and other `vite.config.lib.ts` build configs in analogjs/analog), the import of `oxcDtsPlugin` from `../../tools/build/shared-plugins.ts` uses a relative cross-package path with an `// eslint-disable-next-line nx/enforce-module-boundaries` suppression. This is intentional and correct: `tools/build/shared-plugins.ts` is a build-time utility with no registered TypeScript path alias in `tsconfig.base.json`. Do not flag this relative import or its eslint suppression as a violation — suggesting a non-existent workspace alias would break the import.

Learnt from: eblocha
Repo: analogjs/analog PR: 2212
File: packages/astro-angular/src/client-ngh.ts:40-45
Timestamp: 2026-04-06T13:25:40.588Z
Learning: In `packages/astro-angular/src/client-ngh.ts`, the `ɵSSR_CONTENT_INTEGRITY_MARKER` comment node is intentionally prepended to `document.body` on every Angular island hydration without a deduplication guard. This is safe: Angular's integrity check only requires that at least one marker is present at the start of `<body>` — multiple markers are harmless. The risk Angular guards against is markers being stripped/rewritten by CDNs or minifiers, not duplicates. Always prepending ensures the marker is at the front regardless of ordering with other islands. Confirmed valid; do not flag this as a bug in future reviews.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/content-plugin/tsdown.config.ts:24-24
Timestamp: 2026-03-22T22:10:37.534Z
Learning: In the analogjs/analog monorepo, `tsdown` (and Vite lib) build configs intentionally write their `outDir` directly into `node_modules/analogjs/<package>` (not `dist/`). This is the monorepo-wide convention that enables Nx caching and local dev package resolution without a separate `npm link` step. Do not flag this pattern as an antipattern or suggest routing output to `dist/` instead.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2144
File: packages/vitest-angular-tools/src/schematics/setup/index.spec.ts:10-13
Timestamp: 2026-03-22T22:19:24.618Z
Learning: In `packages/vitest-angular-tools/src/schematics/setup/index.spec.ts`, the `collectionPath` constant intentionally points to `node_modules/analogjs/vitest-angular/src/lib/tools/collection.json` — the Vite/CJS build output destination for the `vitest-angular-tools` package (configured in its `project.json` `outputs`). The `SchematicTestRunner` requires the compiled CommonJS schematics at that location and cannot use the uncompiled TypeScript sources. Do not flag this path as incorrect or suggest replacing it with a relative source path like `../../../collection.json`.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-05T04:18:48.628Z
Learning: In `packages/vite-plugin-angular/src/lib/component-resolvers.ts`, `getAngularComponentMetadata()` (and the internal `collectComponentUrls` helper) is restricted to actual `Component(...)` decorator objects only. Do not flag or suggest reverting this restriction — it was added deliberately (PR `#2226`, commit c37ff35f2) to prevent false-positive metadata extraction from non-component decorators.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 2125
File: packages/router/src/lib/route-manifest.spec.ts:723-734
Timestamp: 2026-03-24T13:22:40.861Z
Learning: In `packages/router/src/lib/route-manifest.spec.ts` (and the corresponding `detectSchemaExports` implementation in `packages/router/manifest/src/index.ts`), the regex-based schema export detector intentionally false-positives on commented-out `export const routeParamsSchema`/`export const routeQuerySchema` lines. This is an accepted v1 limitation explicitly documented in the test. The test expectation `expect(result.hasParamsSchema).toBe(true)` for commented-out content is correct and intentional. Do not flag this as a bug or suggest changing the expectation to `false` — it will be addressed if/when schema detection is upgraded to AST-based parsing.

Learnt from: benpsnyder
Repo: analogjs/analog PR: 0
File: :0-0
Timestamp: 2026-04-05T04:29:55.140Z
Learning: In `apps/tailwind-debug-app-e2e/tests/component-css-hmr.spec.ts` (PR `#2226`, commit 9e0be352), the E2E test was reframed from asserting "CSS updates without full reload" to serving as a repro harness for tailwindlabs/tailwindcss#19903. It now asserts: (1) stylesheet edit detected, (2) `environment.hot.send` emits `full-reload`, (3) app state resets after reload, (4) stack trace includes `updateModules`. Do not suggest reverting this to the previous assertion that CSS updates without reload — the test intentionally documents the current (broken) upstream behavior.

Copy link
Copy Markdown
Member

@brandonroberts brandonroberts left a comment

Choose a reason for hiding this comment

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

Having one test does not validate that the TestBed doesn't leak across tests. This needs to be validated as a solution for Angular. Reconfiguring the TestBed environment everytime is not a proposed solution in Angular

@benpsnyder
Copy link
Copy Markdown
Contributor Author

Having one test does not validate that the TestBed doesn't leak across tests. This needs to be validated as a solution for Angular. Reconfiguring the TestBed environment everytime is not a proposed solution in Angular

I am a bit confused as to how to proceed. May I recommend ... let's take this to discord chat and maybe put this up against our internal repo and run a batter of tests.

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

Labels

blocked Blocked scope:docs Documentation changes scope:vitest-angular Changes in @analogjs/vitest-angular

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants