import { describe, it, expect, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import RegisterPage from './+page.svelte'; afterEach(cleanup); const baseData = (overrides: Record = {}) => ({ code: 'invite-token', prefill: null as { firstName?: string; lastName?: string; email?: string } | null, codeError: null as string | null, ...overrides }); describe('register page', () => { it('renders the hero headline when there is no codeError', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); await expect .element(page.getByRole('heading', { name: /schön, dass du da bist/i })) .toBeVisible(); }); it('renders the NO_INVITE_CODE error card with invite-only heading', async () => { render(RegisterPage, { props: { data: baseData({ codeError: 'NO_INVITE_CODE' }), form: undefined } }); await expect.element(page.getByRole('heading', { name: /nur auf einladung/i })).toBeVisible(); }); it('renders the invalid-code error card for any other codeError value', async () => { render(RegisterPage, { props: { data: baseData({ codeError: 'EXPIRED' }), form: undefined } }); await expect .element(page.getByRole('heading', { name: /ungültiger einladungslink/i })) .toBeVisible(); }); it('hides the form when there is a codeError', async () => { render(RegisterPage, { props: { data: baseData({ codeError: 'NO_INVITE_CODE' }), form: undefined } }); await expect.element(page.getByLabelText(/^vorname$/i)).not.toBeInTheDocument(); }); it('shows the back-to-login link in the codeError card', async () => { render(RegisterPage, { props: { data: baseData({ codeError: 'NO_INVITE_CODE' }), form: undefined } }); await expect .element(page.getByRole('link', { name: /zurück zum login/i })) .toHaveAttribute('href', '/login'); }); it('renders the form sections when there is no codeError', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); await expect.element(page.getByText('Über dich')).toBeVisible(); await expect.element(page.getByLabelText(/vorname/i)).toBeVisible(); await expect.element(page.getByLabelText(/^e-mail-adresse$/i)).toBeVisible(); }); it('hydrates the form fields from prefill', async () => { render(RegisterPage, { props: { data: baseData({ prefill: { firstName: 'Anna', lastName: 'Schmidt', email: 'anna@example.com' } }), form: undefined } }); const firstName = (await page.getByLabelText(/vorname/i).element()) as HTMLInputElement; const lastName = (await page.getByLabelText(/nachname/i).element()) as HTMLInputElement; const email = (await page.getByLabelText(/^e-mail-adresse$/i).element()) as HTMLInputElement; expect(firstName.value).toBe('Anna'); expect(lastName.value).toBe('Schmidt'); expect(email.value).toBe('anna@example.com'); }); it('shows the prefill hint when an email is prefilled', async () => { render(RegisterPage, { props: { data: baseData({ prefill: { email: 'anna@example.com' } }), form: undefined } }); await expect .element(page.getByText('Von deiner Einladung übernommen – du kannst es ändern')) .toBeVisible(); }); it('omits the prefill hint when no email is prefilled', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); await expect .element(page.getByText('Von deiner Einladung übernommen – du kannst es ändern')) .not.toBeInTheDocument(); }); it('puts the invite code in a hidden input', async () => { render(RegisterPage, { props: { data: baseData({ code: 'tok-99' }), form: undefined } }); const codeInput = document.querySelector('input[name="code"]') as HTMLInputElement; expect(codeInput.type).toBe('hidden'); expect(codeInput.value).toBe('tok-99'); }); it('renders an empty hidden code input when data.code is null', async () => { render(RegisterPage, { props: { data: baseData({ code: null }), form: undefined } }); const codeInput = document.querySelector('input[name="code"]') as HTMLInputElement; expect(codeInput.value).toBe(''); }); it('toggles password visibility when the show/hide button is clicked', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const pw = document.querySelector('input[name="password"]') as HTMLInputElement; expect(pw.type).toBe('password'); const toggle = pw.parentElement?.querySelector('button[type="button"]') as HTMLButtonElement; toggle.click(); await vi.waitFor(() => expect(pw.type).toBe('text')); toggle.click(); await vi.waitFor(() => expect(pw.type).toBe('password')); }); it('toggles passwordConfirm visibility independently from password', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const confirm = document.querySelector('#passwordConfirm') as HTMLInputElement; expect(confirm.type).toBe('password'); const toggle = confirm.parentElement?.querySelector( 'button[type="button"]' ) as HTMLButtonElement; toggle.click(); await vi.waitFor(() => expect(confirm.type).toBe('text')); }); it('shows the password length hint after typing a short password', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const pw = document.querySelector('input[name="password"]') as HTMLInputElement; pw.value = 'abc'; pw.dispatchEvent(new Event('input', { bubbles: true })); await expect.element(page.getByText(/mindestens 8 zeichen/i)).toBeVisible(); }); it('shows the password OK hint when password is at least 8 chars', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const pw = document.querySelector('input[name="password"]') as HTMLInputElement; pw.value = 'longenoughpw'; pw.dispatchEvent(new Event('input', { bubbles: true })); await vi.waitFor(() => { expect(document.querySelectorAll('.text-green-700').length).toBeGreaterThan(0); }); }); it('shows the password mismatch error when confirm differs from password', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const pw = document.querySelector('input[name="password"]') as HTMLInputElement; const confirm = document.querySelector('#passwordConfirm') as HTMLInputElement; pw.value = 'longenoughpw'; pw.dispatchEvent(new Event('input', { bubbles: true })); confirm.value = 'differentpw'; confirm.dispatchEvent(new Event('input', { bubbles: true })); await vi.waitFor(() => { expect(document.querySelectorAll('.text-red-600').length).toBeGreaterThan(0); }); }); it('shows the password match success when confirm equals password', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const pw = document.querySelector('input[name="password"]') as HTMLInputElement; const confirm = document.querySelector('#passwordConfirm') as HTMLInputElement; pw.value = 'longenoughpw'; pw.dispatchEvent(new Event('input', { bubbles: true })); confirm.value = 'longenoughpw'; confirm.dispatchEvent(new Event('input', { bubbles: true })); await vi.waitFor(() => { // Both "pw OK" and "match OK" hints render with text-green-700. expect(document.querySelectorAll('.text-green-700').length).toBeGreaterThanOrEqual(2); }); }); it('renders the form error from the form prop', async () => { render(RegisterPage, { props: { data: baseData(), form: { error: 'EMAIL_TAKEN' } } }); const errorBox = document.querySelector('.text-red-600.font-medium'); expect(errorBox).not.toBeNull(); }); it('toggles the notifyOnMention checkbox', async () => { render(RegisterPage, { props: { data: baseData(), form: undefined } }); const cb = document.querySelector('input[name="notifyOnMention"]') as HTMLInputElement; expect(cb.checked).toBe(true); cb.click(); await vi.waitFor(() => expect(cb.checked).toBe(false)); }); });