From 20db3d0d8f3739756e1ca0f792e58d0feaf783e3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 29 May 2026 19:04:22 +0200 Subject: [PATCH] test(stammbaum): cover animateView rAF tween + server 401/500 paths (#692) Add a deterministic stubbed-rAF test for animateView's animated path (was only covering the reduced-motion branch) and assert the server load redirects on 401 and throws on a network 500 (QA review). Co-Authored-By: Claude Opus 4.8 --- .../lib/person/genealogy/animateView.test.ts | 25 +++++++++++++++++++ .../src/routes/stammbaum/page.server.test.ts | 21 ++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/frontend/src/lib/person/genealogy/animateView.test.ts b/frontend/src/lib/person/genealogy/animateView.test.ts index f25fda18..8c5e44ad 100644 --- a/frontend/src/lib/person/genealogy/animateView.test.ts +++ b/frontend/src/lib/person/genealogy/animateView.test.ts @@ -15,3 +15,28 @@ describe('animateView (reduced motion)', () => { cancel(); }); }); + +describe('animateView (animated path)', () => { + const from = { x: 0, y: 0, z: 1 }; + const to = { x: 100, y: 0, z: 2 }; + + it('tweens across frames and lands exactly on the target', () => { + const frames: { x: number }[] = []; + const callbacks: FrameRequestCallback[] = []; + vi.stubGlobal('performance', { now: () => 0 }); + vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => callbacks.push(cb)); + vi.stubGlobal('cancelAnimationFrame', () => {}); + + animateView(from, to, (v) => frames.push(v), { durationMs: 100 }); + + callbacks[0](50); // t = 0.5 → an interpolated frame + callbacks[callbacks.length - 1](100); // t = 1 → exact target + + expect(frames.length).toBeGreaterThanOrEqual(2); + expect(frames[0].x).toBeGreaterThan(0); + expect(frames[0].x).toBeLessThan(100); + expect(frames.at(-1)).toEqual(to); + + vi.unstubAllGlobals(); + }); +}); diff --git a/frontend/src/routes/stammbaum/page.server.test.ts b/frontend/src/routes/stammbaum/page.server.test.ts index 786ede67..7efa0783 100644 --- a/frontend/src/routes/stammbaum/page.server.test.ts +++ b/frontend/src/routes/stammbaum/page.server.test.ts @@ -16,6 +16,12 @@ function mockNetwork() { } as unknown as ReturnType); } +function mockNetworkResponse(status: number) { + vi.mocked(createApiClient).mockReturnValue({ + GET: vi.fn().mockResolvedValue({ response: { ok: false, status }, error: { code: 'X' } }) + } as unknown as ReturnType); +} + function loadEvent(query = '') { const url = new URL(`http://localhost/stammbaum${query}`); return { @@ -53,4 +59,19 @@ describe('/stammbaum +page.server load — initialView', () => { const result = await load(loadEvent('?z=99') as never); expect(result.initialView.z).toBe(MAX_ZOOM); }); + + it('redirects to /login when the network API returns 401', async () => { + mockNetworkResponse(401); + const { load } = await import('./+page.server'); + await expect(load(loadEvent() as never)).rejects.toMatchObject({ + status: 302, + location: '/login' + }); + }); + + it('throws an HTTP error when the network API fails', async () => { + mockNetworkResponse(500); + const { load } = await import('./+page.server'); + await expect(load(loadEvent() as never)).rejects.toMatchObject({ status: 500 }); + }); });