315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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' },
|
|
})
|
|
}
|