test(timeline): assert submit-disabled transition and ≥44px chip targets

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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 00:33:01 +02:00
parent 4dc5e3278f
commit d48a89ba5c
4 changed files with 48 additions and 17 deletions

View File

@@ -157,4 +157,14 @@ describe('DocumentMultiSelect — remove', () => {
document.querySelector<HTMLInputElement>('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]');
});
});

View File

@@ -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: [

View File

@@ -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();
});
});

View File

@@ -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');
});