import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { afterEach, beforeEach, expect, test, vi } from 'vitest' import App from './App' const users = [ { id: 'd56b54dd-31b0-4d71-8a10-82464be59a61', name: 'Urban', createdAt: '2026-07-24T10:15:30Z', }, { id: '2213a673-c859-462b-a031-7b68b6f01c73', name: 'Anna', createdAt: '2026-07-24T10:16:30Z', }, ] const tasks = [ { id: '00000000-0000-0000-0000-000000000001', title: 'Dammsuga', description: 'Bottenvåningen', status: 'WAITING', createdAt: '2026-07-24T10:00:00Z', }, { id: '00000000-0000-0000-0000-000000000002', title: 'Diska', description: null, status: 'IN_PROGRESS', createdAt: '2026-07-24T10:01:00Z', }, { id: '00000000-0000-0000-0000-000000000003', title: 'Vattna blommor', description: null, status: 'COMPLETED', createdAt: '2026-07-24T10:02:00Z', }, ] beforeEach(() => { window.localStorage.clear() }) afterEach(() => { cleanup() vi.restoreAllMocks() }) test('tom användarlista visar Skapa användare', async () => { mockJsonResponse([]) render() expect(await screen.findByRole('heading', { name: 'Skapa användare' })).toBeInTheDocument() }) test('befintliga användare utan lokalt val visar Vem är du', async () => { mockJsonResponse(users) render() expect(await screen.findByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Urban' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Anna' })).toBeInTheDocument() }) test('val av användare lagrar id och visar brädan', async () => { mockUsersAndTasks(users, []) render() fireEvent.click(await screen.findByRole('button', { name: 'Urban' })) expect(await screen.findByRole('heading', { name: 'Uppgifter' })).toBeInTheDocument() expect(screen.getByText('Urban')).toBeInTheDocument() expect(window.localStorage.getItem('hemhub.activeUserId')).toBe(users[0].id) }) test('skapad användare blir automatiskt aktiv', async () => { const createdUser = users[0] const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockResolvedValueOnce(jsonResponse([])) fetchMock.mockResolvedValueOnce(jsonResponse(createdUser, 201)) fetchMock.mockResolvedValueOnce(jsonResponse([])) render() fireEvent.change(await screen.findByLabelText('Namn'), { target: { value: ' Urban ' } }) fireEvent.click(screen.getByRole('button', { name: 'Skapa användare' })) expect(await screen.findByRole('heading', { name: 'Uppgifter' })).toBeInTheDocument() expect(window.localStorage.getItem('hemhub.activeUserId')).toBe(createdUser.id) expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Urban' }), }) }) test('skapandefel för användare visas utan att namnet försvinner', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockResolvedValueOnce(jsonResponse([])) fetchMock.mockResolvedValueOnce( jsonResponse( { code: 'USER_NAME_ALREADY_EXISTS', message: 'En användare med det namnet finns redan.', }, 409, ), ) render() const input = await screen.findByLabelText('Namn') fireEvent.change(input, { target: { value: 'Urban' } }) fireEvent.click(screen.getByRole('button', { name: 'Skapa användare' })) expect(await screen.findByRole('alert')).toHaveTextContent( 'En användare med det namnet finns redan.', ) expect(input).toHaveValue('Urban') }) test('Logga ut rensar valt id och väljer inte användaren automatiskt', async () => { window.localStorage.setItem('hemhub.activeUserId', users[0].id) mockUsersAndTasks(users, []) render() fireEvent.click(await screen.findByRole('button', { name: 'Logga ut' })) expect(screen.getByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Urban' })).toBeInTheDocument() expect(screen.queryByRole('heading', { name: 'Uppgifter' })).not.toBeInTheDocument() expect(window.localStorage.getItem('hemhub.activeUserId')).toBeNull() }) test('hämtningsfel för användare visas som fel och kan återförsökas', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockRejectedValueOnce(new Error('Nätverksfel')) fetchMock.mockResolvedValueOnce(jsonResponse(users)) render() expect(await screen.findByText('Kunde inte ansluta till HemHub.')).toBeInTheDocument() expect(screen.queryByRole('heading', { name: 'Skapa användare' })).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'Försök igen' })) expect(await screen.findByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument() expect(fetchMock).toHaveBeenCalledTimes(2) }) test('ogiltigt lagrat användar-id rensas', async () => { window.localStorage.setItem('hemhub.activeUserId', 'finns-inte') mockJsonResponse(users) render() expect(await screen.findByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument() expect(window.localStorage.getItem('hemhub.activeUserId')).toBeNull() }) test('brädan visar tre kolumner och grupperar hämtade uppgifter', async () => { window.localStorage.setItem('hemhub.activeUserId', users[0].id) mockUsersAndTasks(users, tasks) render() const waiting = await screen.findByRole('region', { name: 'Väntande' }) const inProgress = screen.getByRole('region', { name: 'Pågående' }) const completed = screen.getByRole('region', { name: 'Klart' }) expect(within(waiting).getByText('Dammsuga')).toBeInTheDocument() expect(within(waiting).getByText('Bottenvåningen')).toBeInTheDocument() expect(within(inProgress).getByText('Diska')).toBeInTheDocument() expect(within(completed).getByText('Vattna blommor')).toBeInTheDocument() expect(screen.queryByText(/Inga uppgifter/i)).not.toBeInTheDocument() }) test('Ny uppgift öppnar modalen med fokus i titelfältet', async () => { window.localStorage.setItem('hemhub.activeUserId', users[0].id) mockUsersAndTasks(users, []) render() fireEvent.click(await screen.findByRole('button', { name: 'Ny uppgift' })) expect(screen.getByRole('dialog', { name: 'Skapa ny uppgift' })).toBeInTheDocument() expect(screen.getByLabelText('Titel')).toHaveFocus() }) test.each([ { sätt: 'stängningskrysset', close: () => fireEvent.click(screen.getByRole('button', { name: 'Stäng' })), }, { sätt: 'Escape', close: () => fireEvent.keyDown(window, { key: 'Escape' }), }, { sätt: 'modalens bakgrund', close: () => { const backdrop = screen.getByRole('dialog', { name: 'Skapa ny uppgift' }).parentElement if (!backdrop) { throw new Error('Modalens bakgrund saknas') } fireEvent.mouseDown(backdrop) }, }, ])('modalen stängs med $sätt och rensar formuläret', async ({ close }) => { window.localStorage.setItem('hemhub.activeUserId', users[0].id) mockUsersAndTasks(users, []) render() fireEvent.click(await screen.findByRole('button', { name: 'Ny uppgift' })) fireEvent.change(screen.getByLabelText('Titel'), { target: { value: 'Dammsuga' } }) fireEvent.change(screen.getByLabelText('Beskrivning (valfri)'), { target: { value: 'Bottenvåningen' }, }) close() expect(screen.queryByRole('dialog', { name: 'Skapa ny uppgift' })).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' })) expect(screen.getByLabelText('Titel')).toHaveValue('') expect(screen.getByLabelText('Beskrivning (valfri)')).toHaveValue('') }) test('en skapad uppgift visas längst ned i Väntande och modalen stängs', async () => { const createdTask = { id: '00000000-0000-0000-0000-000000000004', title: 'Putsa fönster', description: 'Köket', status: 'WAITING', createdAt: '2026-07-24T10:03:00Z', } window.localStorage.setItem('hemhub.activeUserId', users[0].id) const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockResolvedValueOnce(jsonResponse(users)) fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0]])) fetchMock.mockResolvedValueOnce(jsonResponse(createdTask, 201)) render() await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)) fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' })) fireEvent.change(screen.getByLabelText('Titel'), { target: { value: ' Putsa fönster ' } }) fireEvent.change(screen.getByLabelText('Beskrivning (valfri)'), { target: { value: ' Köket ' }, }) fireEvent.click(screen.getByRole('button', { name: 'Skapa uppgift' })) await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Skapa ny uppgift' })).not.toBeInTheDocument(), ) const waiting = screen.getByRole('region', { name: 'Väntande' }) expect(within(waiting).getAllByRole('article').map((card) => card.textContent)).toEqual([ 'DammsugaBottenvåningen', 'Putsa fönsterKöket', ]) expect(fetchMock).toHaveBeenLastCalledWith('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Putsa fönster', description: 'Köket' }), }) }) test('formulärdata bevaras när skapande av uppgift misslyckas', async () => { window.localStorage.setItem('hemhub.activeUserId', users[0].id) const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockResolvedValueOnce(jsonResponse(users)) fetchMock.mockResolvedValueOnce(jsonResponse([])) fetchMock.mockResolvedValueOnce( jsonResponse({ code: 'INVALID_TASK', message: 'Uppgiften är ogiltig.' }, 400), ) render() await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)) fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' })) const title = screen.getByLabelText('Titel') const description = screen.getByLabelText('Beskrivning (valfri)') fireEvent.change(title, { target: { value: 'Dammsuga' } }) fireEvent.change(description, { target: { value: 'Bottenvåningen' } }) fireEvent.click(screen.getByRole('button', { name: 'Skapa uppgift' })) expect(await screen.findByRole('alert')).toHaveTextContent('Uppgiften är ogiltig.') expect(screen.getByRole('dialog', { name: 'Skapa ny uppgift' })).toBeInTheDocument() expect(title).toHaveValue('Dammsuga') expect(description).toHaveValue('Bottenvåningen') }) function mockUsersAndTasks(userResponse: unknown, taskResponse: unknown) { const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockResolvedValueOnce(jsonResponse(userResponse)) fetchMock.mockResolvedValueOnce(jsonResponse(taskResponse)) return fetchMock } function mockJsonResponse(body: unknown, status = 200) { return vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse(body, status)) } function jsonResponse(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }) }