From d48a89ba5c0c66cbd55ad72721e9b757cc633b77 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Jun 2026 00:33:01 +0200 Subject: [PATCH] =?UTF-8?q?test(timeline):=20assert=20submit-disabled=20tr?= =?UTF-8?q?ansition=20and=20=E2=89=A544px=20chip=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tighten the review-flagged test gaps (no production change): - submitting state: the old test only asserted the button was initially enabled (a tautology). Now stub a never-resolving fetch, click Speichern, and assert the button gains `disabled` — exercising the double-submit guard (Decision 8). - the two redirect-throwing save tests now use `await expect(...).rejects` so a future missing redirect fails loudly instead of being swallowed by try/catch. - the YEAR end-date-hide assertion uses getByLabelText('Enddatum') not-present, symmetric with the RANGE reveal, instead of a raw #eventDateEnd querySelector. - PersonMultiSelect + DocumentMultiSelect: assert the chip remove button carries the min-h-[44px]/min-w-[44px] target the rtm cites for REQ-017. Addresses PR #832 review (Tester + Requirements Engineer test-coverage concerns). Co-Authored-By: Claude Opus 4.8 --- .../DocumentMultiSelect.svelte.spec.ts | 10 +++++++++ .../person/PersonMultiSelect.svelte.spec.ts | 13 +++++++++++ .../src/lib/timeline/EventForm.svelte.spec.ts | 22 ++++++++++++++----- .../zeitstrahl/events/new/page.server.spec.ts | 20 +++++++---------- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib/document/DocumentMultiSelect.svelte.spec.ts b/frontend/src/lib/document/DocumentMultiSelect.svelte.spec.ts index b8af8aac..b9c7d126 100644 --- a/frontend/src/lib/document/DocumentMultiSelect.svelte.spec.ts +++ b/frontend/src/lib/document/DocumentMultiSelect.svelte.spec.ts @@ -157,4 +157,14 @@ describe('DocumentMultiSelect — remove', () => { document.querySelector('input[type="hidden"][name="documentIds"]') ).toBeNull(); }); + + // REQ-017 (#781): chip remove targets must be ≥44px for the 60+ audience. + it('renders a ≥44px touch target on the chip remove button', async () => { + render(DocumentMultiSelect, { + selectedDocuments: [docFactory('d1', 'Brief A')] + }); + const removeBtn = (await page.getByLabelText('Entfernen').element()) as HTMLElement; + expect(removeBtn.className).toContain('min-h-[44px]'); + expect(removeBtn.className).toContain('min-w-[44px]'); + }); }); diff --git a/frontend/src/lib/person/PersonMultiSelect.svelte.spec.ts b/frontend/src/lib/person/PersonMultiSelect.svelte.spec.ts index 86c26274..16f6c7a7 100644 --- a/frontend/src/lib/person/PersonMultiSelect.svelte.spec.ts +++ b/frontend/src/lib/person/PersonMultiSelect.svelte.spec.ts @@ -258,6 +258,19 @@ describe('PersonMultiSelect – removing persons', () => { await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument(); }); + // REQ-017 (#781): chip remove targets must be ≥44px for the 60+ audience. + it('renders a ≥44px touch target on the chip remove button', async () => { + render(PersonMultiSelect, { + selectedPersons: [{ id: '1', displayName: 'Max Mustermann' }] + }); + const removeBtn = (await page + .getByRole('button', { name: 'Entfernen' }) + .first() + .element()) as HTMLElement; + expect(removeBtn.className).toContain('min-h-[44px]'); + expect(removeBtn.className).toContain('min-w-[44px]'); + }); + it('removes the corresponding hidden input when a chip is removed', async () => { render(PersonMultiSelect, { selectedPersons: [ diff --git a/frontend/src/lib/timeline/EventForm.svelte.spec.ts b/frontend/src/lib/timeline/EventForm.svelte.spec.ts index 4a3d9998..4e3a0d96 100644 --- a/frontend/src/lib/timeline/EventForm.svelte.spec.ts +++ b/frontend/src/lib/timeline/EventForm.svelte.spec.ts @@ -1,10 +1,13 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import EventForm from './EventForm.svelte'; import type { components } from '$lib/generated/api'; -afterEach(() => cleanup()); +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); type TimelineEventView = components['schemas']['TimelineEventView']; @@ -39,7 +42,7 @@ describe('EventForm — date precision RANGE reveal (headline AC, REQ-008/009)', it('hides the end-date field when precision is YEAR', async () => { render(EventForm, { event: makeEvent({ precision: 'YEAR' }) }); await expect.element(page.getByTestId('end-date-region')).toBeInTheDocument(); - expect(document.querySelector('#eventDateEnd')).toBeNull(); + await expect.element(page.getByLabelText('Enddatum')).not.toBeInTheDocument(); }); }); @@ -82,11 +85,20 @@ describe('EventForm — server date error wired per-field (REQ-011)', () => { }); }); -describe('EventForm — submitting state (named AC)', () => { - it('renders an enabled submit button initially', async () => { +describe('EventForm — submitting state (named AC, Decision 8)', () => { + it('disables the submit button while submitting', async () => { + // A never-resolving fetch keeps use:enhance in flight so the disabled + // transition (the double-submit guard) is observable rather than racing the + // reset in the result callback. + vi.stubGlobal( + 'fetch', + vi.fn(() => new Promise(() => {})) + ); render(EventForm, { event: makeEvent() }); const btn = page.getByRole('button', { name: 'Speichern' }); await expect.element(btn).not.toBeDisabled(); + await btn.click(); + await expect.element(btn).toBeDisabled(); }); }); diff --git a/frontend/src/routes/zeitstrahl/events/new/page.server.spec.ts b/frontend/src/routes/zeitstrahl/events/new/page.server.spec.ts index 83a6482d..208dabdf 100644 --- a/frontend/src/routes/zeitstrahl/events/new/page.server.spec.ts +++ b/frontend/src/routes/zeitstrahl/events/new/page.server.spec.ts @@ -113,8 +113,8 @@ describe('zeitstrahl/events/new save action (REQ-004/009/010/015)', () => { .mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } }); vi.mocked(createApiClient).mockReturnValue({ POST: post } as never); - try { - await actions.save( + await expect( + actions.save( saveEvent({ title: 'Umzug', type: 'PERSONAL', @@ -122,10 +122,8 @@ describe('zeitstrahl/events/new save action (REQ-004/009/010/015)', () => { precision: 'YEAR', eventDateEnd: '1925-05-01' }) - ); - } catch { - // redirect throws on success - } + ) + ).rejects.toMatchObject({ status: 303 }); expect(post.mock.calls[0][1].body.eventDateEnd).toBeNull(); }); @@ -135,18 +133,16 @@ describe('zeitstrahl/events/new save action (REQ-004/009/010/015)', () => { .mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } }); vi.mocked(createApiClient).mockReturnValue({ POST: post } as never); - try { - await actions.save( + await expect( + actions.save( saveEvent({ title: 'Umzug', type: 'PERSONAL', eventDate: '1925-04-01', precision: 'NOT_A_REAL_PRECISION' }) - ); - } catch { - // redirect throws on success - } + ) + ).rejects.toMatchObject({ status: 303 }); expect(post.mock.calls[0][1].body.precision).toBe('DAY'); });