Compare commits

...

7 Commits

Author SHA1 Message Date
7c66dcad3a refactor(onboarding): clarify test comment and remove unused response mock
HouseholdSetupForm.test.ts: explain that touched+empty drives the $derived
error, not a submit event on the disabled button.
page.server.test.ts: remove unused response key from mockSuccess() —
household creation doesn't set a session cookie.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:32:44 +02:00
01a321caa9 test(onboarding): add ProgressSidebar test for currentStep=3 (all prior steps completed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:31:56 +02:00
2d1604492d feat(onboarding): add max-length validation for household name (100 chars)
Fails fast before the API call with a clear German error message.
Tests boundary: 100 chars accepted, 101 rejected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:31:13 +02:00
3742364956 fix(onboarding): make HouseholdSetupForm subtitle responsive (12px mobile, 14px desktop)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:30:23 +02:00
36dfea34cc fix(onboarding): make HouseholdSetupForm heading responsive and use font-medium
text-[18px] md:text-[28px] matches auth form pattern.
font-medium (500) replaces font-semibold (600) per design system rules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:29:52 +02:00
66525484a6 fix(onboarding): correct Tailwind arbitrary font-family syntax in HouseholdSetupForm
font-['var(--font-display)'] → font-[var(--font-display)] so Fraunces
display font is applied correctly to the h1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:29:21 +02:00
e5614ccf30 refactor(onboarding): remove aria-hidden workaround from progress sidebar
Replace getByText with getByRole(heading) in page test to disambiguate
the duplicate "Haushalt benennen" text between sidebar and form.
Revert defaultIgnore change in test-setup.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:28:46 +02:00
8 changed files with 40 additions and 13 deletions

View File

@@ -53,4 +53,11 @@ describe('ProgressSidebar', () => {
expect(screen.getByTestId('step-2')).not.toHaveAttribute('aria-current');
expect(screen.getByTestId('step-3')).not.toHaveAttribute('aria-current');
});
it('step 3 active: steps 1 and 2 are both completed', () => {
render(ProgressSidebar, { props: { currentStep: 3 } });
expect(screen.getByTestId('step-1')).toHaveAttribute('data-state', 'completed');
expect(screen.getByTestId('step-2')).toHaveAttribute('data-state', 'completed');
expect(screen.getByTestId('step-3')).toHaveAttribute('aria-current', 'step');
});
});

View File

@@ -38,8 +38,8 @@
</script>
<form method="POST" novalidate use:enhance onsubmit={handleSubmit}>
<h1 class="mb-[8px] font-['var(--font-display)'] text-[24px] font-semibold">Haushalt benennen</h1>
<p class="mb-[24px] text-[14px] text-[var(--color-text-muted)]">
<h1 class="mb-[8px] font-[var(--font-display)] text-[18px] font-medium md:text-[28px]">Haushalt benennen</h1>
<p class="mb-[24px] text-[12px] text-[var(--color-text-muted)] md:text-[14px]">
Gib deinem Haushalt einen Namen, damit du ihn leicht wiederfindest.
</p>

View File

@@ -41,7 +41,9 @@ describe('HouseholdSetupForm', () => {
const user = userEvent.setup();
render(HouseholdSetupForm);
// override disabled to allow submit attempt by typing then clearing
// Type then clear: sets touched=true, which makes the $derived error visible
// as soon as the field is empty. The button is disabled so the click is a no-op,
// but the error is already shown from the touched+empty state.
const input = screen.getByLabelText('Haushaltsname');
await user.type(input, 'a');
await user.clear(input);

View File

@@ -18,6 +18,10 @@ export const actions = {
return fail(400, { errors: { name: 'Haushaltsname ist erforderlich' }, name: '' });
}
if (name.length > 100) {
return fail(400, { errors: { name: 'Haushaltsname darf maximal 100 Zeichen lang sein' }, name });
}
const api = apiClient(fetch);
const { data, error } = await api.POST('/v1/households', {
body: { name }

View File

@@ -18,7 +18,6 @@
<!-- Desktop progress sidebar — hidden on mobile -->
<aside
class="hidden md:flex w-[300px] flex-shrink-0 flex-col bg-[var(--color-surface)] border-r border-[var(--color-border)] p-[40px_28px]"
aria-hidden="true"
>
<ProgressSidebar currentStep={1} />
</aside>

View File

@@ -71,8 +71,7 @@ describe('household setup — form action', () => {
function mockSuccess() {
return {
data: { data: { id: 'hh-123', name: 'Smith family', members: [] } },
error: undefined,
response: { headers: { get: vi.fn().mockReturnValue(null) } }
error: undefined
};
}
@@ -123,6 +122,28 @@ describe('household setup — form action', () => {
expect(result.data.name).toBe('');
});
it('returns fail(400) when name exceeds 100 characters', async () => {
const longName = 'a'.repeat(101);
const result = await actions.default(createRequest({ name: longName }));
expect(result.status).toBe(400);
expect(result.data.errors.name).toBeTruthy();
expect(mockPost).not.toHaveBeenCalled();
});
it('accepts name at exactly 100 characters', async () => {
mockPost.mockResolvedValue(mockSuccess());
const maxName = 'a'.repeat(100);
try {
await actions.default(createRequest({ name: maxName }));
} catch {
// redirect throws
}
expect(mockPost).toHaveBeenCalled();
});
it('returns fail with form error on API failure', async () => {
mockPost.mockResolvedValue({
data: undefined,

View File

@@ -9,7 +9,7 @@ vi.mock('$app/forms', () => ({
describe('household setup page', () => {
it('renders the form heading', () => {
render(Page);
expect(screen.getByText('Haushalt benennen')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Haushalt benennen' })).toBeInTheDocument();
});
it('renders the household name input', () => {

View File

@@ -1,7 +1 @@
import '@testing-library/jest-dom/vitest';
import { configure } from '@testing-library/dom';
// Exclude elements inside aria-hidden containers from text queries,
// so that visually-hidden sidebars (e.g. ProgressSidebar in onboarding pages)
// don't create duplicate text matches when the same text appears in the main content.
configure({ defaultIgnore: 'script, style, [aria-hidden="true"] *' });