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