From 9ea2885c7b93d5cbc9059091a6600c346946f5ba Mon Sep 17 00:00:00 2001 From: Antonio Ledebuhr Date: Tue, 25 Aug 2026 17:12:10 +0200 Subject: [PATCH] foundation: resolve platform in injection context for dot background teardown Co-authored-by: Cursor --- AGENTS.md | 1 + .../dot-background/dot-background.spec.ts | 25 +++++++++++++++++++ .../dot-background/dot-background.ts | 3 ++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9a57dd5..396bae6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,6 +100,7 @@ German and English are written idiomatically per language, never machine-transla - Use the injected `DOCUMENT` token when the document element is required (it works on server and browser) - No user-agent sniffing; use CSS media queries for device capability - Capability helpers live in `src/app/core/platform/browser.ts` and return conservative defaults on the server +- Helpers that use `inject()` must be called from a field initializer or constructor, never from a lifecycle hook or callback, and the resolved value must be stored on the instance - Every addressable route must remain prerenderable ## Accessibility checklist diff --git a/src/app/components/dot-background/dot-background.spec.ts b/src/app/components/dot-background/dot-background.spec.ts index 915284d..61bccfc 100644 --- a/src/app/components/dot-background/dot-background.spec.ts +++ b/src/app/components/dot-background/dot-background.spec.ts @@ -25,4 +25,29 @@ describe('DotBackground', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('does not throw when destroyed after browser initialization', async () => { + const gradient = { addColorStop: vi.fn() }; + const context = { + clearRect: vi.fn(), + beginPath: vi.fn(), + arc: vi.fn(), + fill: vi.fn(), + createRadialGradient: vi.fn(() => gradient), + }; + + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue( + context as unknown as CanvasRenderingContext2D, + ); + const animationFrameSpy = vi.spyOn(window, 'requestAnimationFrame').mockReturnValue(0); + + const initializedFixture = TestBed.createComponent(DotBackground); + initializedFixture.detectChanges(); + await initializedFixture.whenStable(); + + expect(() => initializedFixture.destroy()).not.toThrow(); + + animationFrameSpy.mockRestore(); + vi.restoreAllMocks(); + }); }); diff --git a/src/app/components/dot-background/dot-background.ts b/src/app/components/dot-background/dot-background.ts index 4030274..f15136b 100644 --- a/src/app/components/dot-background/dot-background.ts +++ b/src/app/components/dot-background/dot-background.ts @@ -21,6 +21,7 @@ export class DotBackground implements OnDestroy { private readonly ngZone = inject(NgZone); private readonly coarsePointer = prefersCoarsePointer(); + private readonly isBrowser = isBrowserPlatform(); private ctx: CanvasRenderingContext2D | undefined; private dots: Dot[] = []; @@ -50,7 +51,7 @@ export class DotBackground implements OnDestroy { cancelAnimationFrame(this.animationId); - if (isBrowserPlatform()) { + if (this.isBrowser) { window.removeEventListener('resize', this.resize); window.removeEventListener('mousemove', this.onMouseMove); window.removeEventListener('click', this.onMouseClick);