test(stammbaum): prove the AC8 mobile-centre wiring at the route layer (#703)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m38s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m36s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m38s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m36s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
Sara/Elicit noted AC8 was proven only as recentreAbove geometry, never as wired behaviour. Add route-level tests that mock window.matchMedia: a tap recentres the canvas (mirror effect re-fires) when the mobile breakpoint matches, and leaves the view untouched on desktop where the side panel is a flex sibling that never overlaps the canvas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,28 @@ vi.mock('$app/navigation', () => ({
|
|||||||
goto: vi.fn()
|
goto: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(cleanup);
|
// The page reads window.matchMedia('(max-width: 767px)').matches at init to
|
||||||
|
// decide whether to centre a tapped person above the bottom sheet (#703 AC8).
|
||||||
|
// Make the mock query-aware so only the mobile breakpoint flips; other media
|
||||||
|
// queries (e.g. prefers-reduced-motion) keep their benign default.
|
||||||
|
const originalMatchMedia = window.matchMedia;
|
||||||
|
function mockMatchMedia(isMobile: boolean) {
|
||||||
|
window.matchMedia = ((query: string) => ({
|
||||||
|
matches: query.includes('max-width') ? isMobile : false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
addListener: () => {},
|
||||||
|
removeListener: () => {},
|
||||||
|
dispatchEvent: () => false
|
||||||
|
})) as unknown as typeof window.matchMedia;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
window.matchMedia = originalMatchMedia;
|
||||||
|
});
|
||||||
|
|
||||||
async function loadComponent() {
|
async function loadComponent() {
|
||||||
return (await import('./+page.svelte')).default;
|
return (await import('./+page.svelte')).default;
|
||||||
@@ -35,6 +56,12 @@ const sampleNodes = [
|
|||||||
{ id: 'p-2', firstName: 'Bert', lastName: 'Schmidt', displayName: 'Bert Schmidt' }
|
{ id: 'p-2', firstName: 'Bert', lastName: 'Schmidt', displayName: 'Bert Schmidt' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Typed family nodes for the AC8 tests — familyMember is required on the DTO.
|
||||||
|
const familyNodes = [
|
||||||
|
{ id: 'p-1', displayName: 'Anna Schmidt', familyMember: true },
|
||||||
|
{ id: 'p-2', displayName: 'Bert Schmidt', familyMember: true }
|
||||||
|
];
|
||||||
|
|
||||||
describe('stammbaum page', () => {
|
describe('stammbaum page', () => {
|
||||||
it('shows the empty state when there are no family nodes', async () => {
|
it('shows the empty state when there are no family nodes', async () => {
|
||||||
mockPage.url = new URL('http://localhost/stammbaum');
|
mockPage.url = new URL('http://localhost/stammbaum');
|
||||||
@@ -125,4 +152,44 @@ describe('stammbaum page', () => {
|
|||||||
expect(url.searchParams.has('cx')).toBe(true);
|
expect(url.searchParams.has('cx')).toBe(true);
|
||||||
expect(url.searchParams.has('cy')).toBe(true);
|
expect(url.searchParams.has('cy')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AC8 — the tapped person must clear the bottom sheet on a phone, but the
|
||||||
|
// desktop side panel is a flex sibling that never overlaps the canvas, so no
|
||||||
|
// centring should fire there. These tests prove the matchMedia gate around
|
||||||
|
// selectPerson, not just the recentreAbove geometry (covered in panZoom.test).
|
||||||
|
it('recentres the tapped person when matchMedia reports mobile (#703 AC8)', async () => {
|
||||||
|
mockMatchMedia(true);
|
||||||
|
mockPage.url = new URL('http://localhost/stammbaum');
|
||||||
|
const Stammbaum = await loadComponent();
|
||||||
|
render(Stammbaum, {
|
||||||
|
props: { data: { nodes: familyNodes, edges: [], initialView: DEFAULT_VIEW } }
|
||||||
|
});
|
||||||
|
// Let the mount-time URL mirror settle, then isolate the tap's effect.
|
||||||
|
await vi.waitFor(() => expect(replaceState).toHaveBeenCalled());
|
||||||
|
replaceState.mockClear();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Anna Schmidt' }).click();
|
||||||
|
|
||||||
|
// The mobile tap recentres the canvas → the view changes → the ?cx&cy&z
|
||||||
|
// mirror effect re-fires. (Desktop, below, leaves the view untouched.)
|
||||||
|
await vi.waitFor(() => expect(replaceState).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not recentre on tap when matchMedia reports desktop (#703 AC8)', async () => {
|
||||||
|
mockMatchMedia(false);
|
||||||
|
mockPage.url = new URL('http://localhost/stammbaum');
|
||||||
|
const Stammbaum = await loadComponent();
|
||||||
|
render(Stammbaum, {
|
||||||
|
props: { data: { nodes: familyNodes, edges: [], initialView: DEFAULT_VIEW } }
|
||||||
|
});
|
||||||
|
await vi.waitFor(() => expect(replaceState).toHaveBeenCalled());
|
||||||
|
replaceState.mockClear();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Anna Schmidt' }).click();
|
||||||
|
|
||||||
|
// The tap registers — the desktop side panel opens — but no recentre fires,
|
||||||
|
// so the view never changes and the mirror effect stays silent.
|
||||||
|
await expect.element(page.getByRole('complementary')).toBeVisible();
|
||||||
|
expect(replaceState).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user