import React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { SegmentedOtpInput } from '../components/segmented-otp-input'; import { VerifyEmailForm } from '../components/verify-email-form'; import { RecoveryForm } from '../components/recovery-form'; describe('SegmentedOtpInput Component (WEB-22 / WEB-21)', () => { it('renders 6 distinct numeric input elements', () => { const onChange = vi.fn(); render(); const inputs = screen.getAllByRole('textbox'); expect(inputs).toHaveLength(6); expect(inputs[0].getAttribute('inputmode')).toBe('numeric'); }); it('populates initial digits from value prop', () => { render(); const inputs = screen.getAllByRole('textbox') as HTMLInputElement[]; expect(inputs.map((i) => i.value)).toEqual(['1', '2', '3', '4', '5', '6']); }); it('updates value and advances focus on typing', () => { const onChange = vi.fn(); render(); const inputs = screen.getAllByRole('textbox') as HTMLInputElement[]; fireEvent.change(inputs[0], { target: { value: '9' } }); expect(onChange).toHaveBeenCalledWith('9'); }); it('handles paste event correctly across all 6 inputs', () => { const onChange = vi.fn(); render(); const inputs = screen.getAllByRole('textbox'); fireEvent.paste(inputs[0], { clipboardData: { getData: () => '849201', }, }); expect(onChange).toHaveBeenCalledWith('849201'); }); }); describe('VerifyEmailForm Component (WEB-22)', () => { const originalFetch = global.fetch; beforeEach(() => { vi.restoreAllMocks(); }); afterEach(() => { global.fetch = originalFetch; }); it('renders tactical header with Verify Telemetry Signature and 6-digit OTP challenge', () => { render(); expect(screen.getByRole('heading', { name: /verify telemetry signature/i })).toBeDefined(); const inputs = screen.getAllByRole('textbox'); expect(inputs).toHaveLength(6); expect(screen.getByRole('button', { name: /confirm telemetry signature/i })).toBeDefined(); }); it('disables submit button until 6 digits are provided, then submits successfully', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}), }); render(); const submitBtn = screen.getByRole('button', { name: /confirm telemetry signature/i }) as HTMLButtonElement; expect(submitBtn.disabled).toBe(true); const inputs = screen.getAllByRole('textbox') as HTMLInputElement[]; fireEvent.paste(inputs[0], { clipboardData: { getData: () => '654321', }, }); await waitFor(() => { expect(submitBtn.disabled).toBe(false); }); fireEvent.click(submitBtn); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('/api/auth/verify-email', expect.objectContaining({ method: 'POST', body: JSON.stringify({ token: '654321' }), })); expect(screen.getByText(/telemetry signature verified successfully/i)).toBeDefined(); }); }); it('renders resend cooldown timer and handles resend verification code request', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ detail: 'Dispatched' }), }); render(); expect(screen.getByText(/resend code in/i)).toBeDefined(); }); }); describe('RecoveryForm Component (WEB-21)', () => { const originalFetch = global.fetch; beforeEach(() => { vi.restoreAllMocks(); }); afterEach(() => { global.fetch = originalFetch; }); it('renders step 1 with Recover Account Access and email input', () => { render(); expect(screen.getByRole('heading', { name: /recover account access/i })).toBeDefined(); expect(screen.getByPlaceholderText('rider@example.com')).toBeDefined(); expect(screen.getByRole('button', { name: /send recovery code/i })).toBeDefined(); }); it('submits email in step 1 and advances to step 2 OTP challenge', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ detail: 'Sent' }), }); render(); const emailInput = screen.getByPlaceholderText('rider@example.com'); fireEvent.change(emailInput, { target: { value: 'speed@motorsignal.app' } }); const sendBtn = screen.getByRole('button', { name: /send recovery code/i }); fireEvent.click(sendBtn); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('/api/auth/password-reset-request', expect.objectContaining({ method: 'POST', body: JSON.stringify({ email: 'speed@motorsignal.app' }), })); expect(screen.getByText(/enter your 6-digit verification code/i)).toBeDefined(); expect(screen.getAllByPlaceholderText('••••••••••••')).toHaveLength(2); }); }); it('validates password requirements and submits password reset', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}), }); render(); expect(screen.getByText(/6-digit recovery code/i)).toBeDefined(); const passwordInputs = screen.getAllByPlaceholderText('••••••••••••') as HTMLInputElement[]; expect(passwordInputs).toHaveLength(2); // Enter short password fireEvent.change(passwordInputs[0], { target: { value: 'short' } }); fireEvent.change(passwordInputs[1], { target: { value: 'short' } }); const submitBtn = screen.getByRole('button', { name: /set new password/i }); fireEvent.click(submitBtn); await waitFor(() => { expect(screen.getByText(/password must be at least 12 characters/i)).toBeDefined(); }); // Enter matching 12+ char password fireEvent.change(passwordInputs[0], { target: { value: 'SuperSecret1234!' } }); fireEvent.change(passwordInputs[1], { target: { value: 'SuperSecret1234!' } }); fireEvent.click(submitBtn); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('/api/auth/password-reset', expect.objectContaining({ method: 'POST', body: JSON.stringify({ token: '123456', password: 'SuperSecret1234!' }), })); expect(screen.getByText(/password reset complete/i)).toBeDefined(); }); }); });