import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { DevFetch } from './DevFetch' import * as api from '@/services/api' import * as errors from '@/services/errors' import { useScraperFetch } from '@/hooks' vi.mock('@/services/api') vi.mock('@/services/errors') vi.mock('@/hooks', () => ({ useScraperFetch: vi.fn(), })) describe('DevFetch', () => { const mockUserContext: api.UserContext = { workspace_id: 'ws-test-123', personal_data_uuid: 'pd-test-456', parent_name: 'Test Parent', children: [ { id: 'child-1', name: 'Test Child', birth_date: '2022-01-15', }, ], } const emptyUserContext: api.UserContext = { workspace_id: 'ws-test-123', personal_data_uuid: 'pd-test-456', parent_name: 'Test Parent', children: [], } const mockScraperFetchResult = { sessionId: null, pollData: null, resolvedIds: null, error: null, isUnauthorized: false, start: vi.fn(), cancel: vi.fn(), isRunning: false, isTerminal: false, } beforeEach(() => { vi.clearAllMocks() vi.mocked(api.getUserContext).mockResolvedValue(mockUserContext) vi.mocked(errors.isUnauthorized).mockReturnValue(false) vi.mocked(useScraperFetch).mockReturnValue(mockScraperFetchResult) }) describe('initial load', () => { it('loads user context on mount', async () => { render() await waitFor(() => { expect(api.getUserContext).toHaveBeenCalled() }) }) it('displays workspace and personal data when context loads', async () => { render() await waitFor(() => { expect(screen.getByText(/ws-test-123/)).toBeInTheDocument() expect(screen.getByText(/pd-test-456/)).toBeInTheDocument() }) }) it('displays children when context includes them', async () => { render() await waitFor(() => { expect(screen.getByText(/Test Child \(2022-01-15\)/)).toBeInTheDocument() }) }) it('displays no children message when context has no children', async () => { vi.mocked(api.getUserContext).mockResolvedValue(emptyUserContext) render() await waitFor(() => { expect(screen.getByText(/Inga barn registrerade/)).toBeInTheDocument() }) }) }) describe('error handling', () => { it('displays unauthorized error when getUserContext fails with 401', async () => { const unauthorizedError = new Error('Unauthorized') vi.mocked(api.getUserContext).mockRejectedValue(unauthorizedError) vi.mocked(errors.isUnauthorized).mockReturnValue(true) render() await waitFor(() => { expect(screen.getByText(/Setup-fel:/)).toBeInTheDocument() expect(screen.getByText(/Not authenticated/)).toBeInTheDocument() }) }) it('displays login link when unauthorized error occurs', async () => { const unauthorizedError = new Error('Unauthorized') vi.mocked(api.getUserContext).mockRejectedValue(unauthorizedError) vi.mocked(errors.isUnauthorized).mockReturnValue(true) vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, isUnauthorized: true, }) render() await waitFor(() => { const loginLink = screen.getByText(/Logga in först/) expect(loginLink).toBeInTheDocument() expect(loginLink).toHaveAttribute('href', '/auth') }) }) it('displays generic error when getUserContext fails with other errors', async () => { const genericError = new Error('Network error') vi.mocked(api.getUserContext).mockRejectedValue(genericError) vi.mocked(errors.isUnauthorized).mockReturnValue(false) render() await waitFor(() => { expect(screen.getByText(/Setup-fel:/)).toBeInTheDocument() expect(screen.getByText(/Network error/)).toBeInTheDocument() }) }) it('handles non-Error exception in getUserContext', async () => { vi.mocked(api.getUserContext).mockRejectedValue('Unknown error') vi.mocked(errors.isUnauthorized).mockReturnValue(false) render() await waitFor(() => { expect(screen.getByText(/Setup-fel:/)).toBeInTheDocument() expect(screen.getByText(/Failed to load context/)).toBeInTheDocument() }) }) }) describe('refresh button', () => { it('disables refresh button when no children', async () => { vi.mocked(api.getUserContext).mockResolvedValue(emptyUserContext) render() await waitFor(() => { const refreshButton = screen.getByRole('button', { name: /Refresh/ }) expect(refreshButton).toBeDisabled() }) }) it('enables refresh button when children exist', async () => { render() await waitFor(() => { const refreshButton = screen.getByRole('button', { name: /Refresh/ }) expect(refreshButton).not.toBeDisabled() }) }) it('calls scraper.start when refresh is clicked', async () => { const mockStart = vi.fn() vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, start: mockStart, }) render() const refreshButton = await screen.findByRole('button', { name: /Refresh/ }) fireEvent.click(refreshButton) expect(mockStart).toHaveBeenCalled() }) }) describe('create child', () => { beforeEach(() => { vi.mocked(api.getUserContext).mockResolvedValue(emptyUserContext) }) it('shows child creation form when no children exist', async () => { render() await waitFor(() => { expect(screen.getByPlaceholderText(/Barnets namn/)).toBeInTheDocument() expect(screen.getByPlaceholderText(/Födelsedatum/)).toBeInTheDocument() }) }) it('hides child creation form when children exist', async () => { render() await waitFor(() => { expect(screen.queryByPlaceholderText(/Barnets namn/)).not.toBeInTheDocument() }) }) it('disables create button when name or date is empty', async () => { render() await waitFor(() => { const createButton = screen.getByRole('button', { name: /Skapa barn/ }) expect(createButton).toBeDisabled() }) }) it('enables create button when both name and date are filled', async () => { render() await waitFor(() => { const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) expect(createButton).not.toBeDisabled() }) it('calls api.createChild with correct parameters', async () => { const mockCreateChild = vi.fn().mockResolvedValue({ id: 'new-child', name: 'New Child' }) vi.mocked(api.createChild).mockImplementation(mockCreateChild) render() await waitFor(() => { const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) fireEvent.click(createButton) await waitFor(() => { expect(mockCreateChild).toHaveBeenCalledWith({ workspace_id: 'ws-test-123', name: 'New Child', birth_date: '2024-01-01', }) }) }) it('shows loading state while creating child', async () => { vi.mocked(api.createChild).mockImplementation( () => new Promise(resolve => setTimeout(() => resolve({ id: 'new-child', name: 'New Child' }), 100)) ) render() await waitFor(() => { const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) fireEvent.click(createButton) await waitFor(() => { expect(screen.getByRole('button', { name: /Skapar…/ })).toBeInTheDocument() }) }) it('reloads context after successful child creation', async () => { vi.mocked(api.createChild).mockResolvedValue({ id: 'new-child', name: 'New Child' }) render() await waitFor(() => { const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) fireEvent.click(createButton) await waitFor(() => { expect(api.getUserContext).toHaveBeenCalledTimes(2) }) }) it('clears form inputs and hides form after successful child creation', async () => { vi.mocked(api.getUserContext) .mockResolvedValueOnce(emptyUserContext) .mockResolvedValueOnce(mockUserContext) // Return non-empty after create vi.mocked(api.createChild).mockResolvedValue({ id: 'new-child', name: 'New Child' }) render() await waitFor(() => { expect(screen.getByPlaceholderText(/Barnets namn/)).toBeInTheDocument() }) const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) fireEvent.click(createButton) // After creation succeeds, the form should be hidden because context now includes children await waitFor(() => { expect(screen.queryByPlaceholderText(/Barnets namn/)).not.toBeInTheDocument() }) }) it('handles error when child creation fails', async () => { vi.mocked(api.createChild).mockRejectedValue(new Error('Creation failed')) render() await waitFor(() => { const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) fireEvent.click(createButton) await waitFor(() => { expect(screen.getByText(/Setup-fel:/)).toBeInTheDocument() expect(screen.getByText(/Creation failed/)).toBeInTheDocument() }) }) it('handles non-Error exception in child creation', async () => { vi.mocked(api.createChild).mockRejectedValue('Unknown error') render() await waitFor(() => { const nameInput = screen.getByPlaceholderText(/Barnets namn/) as HTMLInputElement const dateInput = screen.getByPlaceholderText(/Födelsedatum/) as HTMLInputElement fireEvent.change(nameInput, { target: { value: 'New Child' } }) fireEvent.change(dateInput, { target: { value: '2024-01-01' } }) }) const createButton = screen.getByRole('button', { name: /Skapa barn/ }) fireEvent.click(createButton) await waitFor(() => { expect(screen.getByText(/Setup-fel:/)).toBeInTheDocument() expect(screen.getByText(/Failed to create child/)).toBeInTheDocument() }) }) }) describe('scraper integration', () => { it('displays resolved IDs when scraper has them', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, resolvedIds: { workspace_id: 'ws-resolved', personal_data_uuid: 'pd-resolved', child_id: 'c-resolved', }, }) render() await waitFor(() => { expect(screen.getByText(/workspace_id: ws-resolved/)).toBeInTheDocument() expect(screen.getByText(/personal_data_uuid: pd-resolved/)).toBeInTheDocument() expect(screen.getByText(/child_id: c-resolved/)).toBeInTheDocument() }) }) it('displays scraper error when present', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, error: 'Scraper session failed', }) render() await waitFor(() => { expect(screen.getByText(/Fel:/)).toBeInTheDocument() expect(screen.getByText(/Scraper session failed/)).toBeInTheDocument() }) }) it('displays polling status when scraper is active', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'authenticating', message: 'Scanning BankID...', }, isRunning: true, }) render() await waitFor(() => { expect(screen.getByText(/Status:/)).toBeInTheDocument() expect(screen.getByText(/authenticating/)).toBeInTheDocument() }) }) it('displays QR code when awaiting scan', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'awaiting_scan', qr_data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', }, isRunning: true, }) render() await waitFor(() => { const qrImage = screen.getByAltText(/BankID QR-kod/) as HTMLImageElement expect(qrImage).toBeInTheDocument() expect(qrImage.src).toContain('data:image/png;base64,') }) }) it('colors the status red when the scraper session errored', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'error', message: 'Something broke', }, }) render() await waitFor(() => { const statusValue = screen.getByText('error') expect(statusValue).toHaveStyle({ color: 'rgb(255, 0, 0)' }) }) }) it('displays fetching message during data fetch', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'fetching', }, isRunning: true, }) render() await waitFor(() => { expect(screen.getByText(/Hämtar data från FK…/)).toBeInTheDocument() }) }) it('displays success state with result data', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'done', result: { children: [ { name: 'Scraped Child', birth_date: '2023-06-15', }, ], }, }, }) render() await waitFor(() => { expect(screen.getByText(/Klar! Hämtad data:/)).toBeInTheDocument() expect(screen.getByText(/Scraped Child/)).toBeInTheDocument() }) }) it('displays cancel button when scraper is running', async () => { const mockCancel = vi.fn() vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'authenticating', }, isRunning: true, cancel: mockCancel, }) render() await waitFor(() => { const cancelButton = screen.getByRole('button', { name: /Avbryt/ }) expect(cancelButton).toBeInTheDocument() fireEvent.click(cancelButton) expect(mockCancel).toHaveBeenCalled() }) }) it('displays cancelled message when scraper session is cancelled', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'cancelled', }, }) render() await waitFor(() => { expect(screen.getByText(/Sessionen avbröts./)).toBeInTheDocument() }) }) it('does not display cancel button when scraper is not running', async () => { vi.mocked(useScraperFetch).mockReturnValue({ ...mockScraperFetchResult, pollData: { status: 'done', }, isRunning: false, }) render() await waitFor(() => { expect(screen.queryByRole('button', { name: /Avbryt/ })).not.toBeInTheDocument() }) }) }) describe('page structure and styling', () => { it('renders the header with tool name', async () => { render() await waitFor(() => { expect(screen.getByText(/🔧 FK Scraper Test Harness/)).toBeInTheDocument() }) }) it('renders the description text', async () => { render() await waitFor(() => { expect(screen.getByText(/Dev-only./)).toBeInTheDocument() }) }) it('renders context information box when context is loaded', async () => { render() await waitFor(() => { expect(screen.getByText(/Workspace:/)).toBeInTheDocument() expect(screen.getByText(/PersonalData:/)).toBeInTheDocument() expect(screen.getByText(/Barn:/)).toBeInTheDocument() }) }) }) })