feat: add task creation board

This commit is contained in:
Urban Modig
2026-07-25 10:31:37 +02:00
parent 050f248857
commit 3f152eeccc
16 changed files with 962 additions and 29 deletions

View File

@ -1,4 +1,4 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
import App from './App'
@ -15,6 +15,30 @@ const users = [
},
]
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()
})
@ -42,13 +66,14 @@ test('befintliga användare utan lokalt val visar Vem är du', async () => {
expect(screen.getByRole('button', { name: 'Anna' })).toBeInTheDocument()
})
test('val av användare lagrar id och visar startsidan', async () => {
mockJsonResponse(users)
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(screen.getByRole('heading', { name: 'Hej Urban' })).toBeInTheDocument()
expect(await screen.findByRole('heading', { name: 'Uppgifter' })).toBeInTheDocument()
expect(screen.getByText('Urban')).toBeInTheDocument()
expect(window.localStorage.getItem('hemhub.activeUserId')).toBe(users[0].id)
})
@ -57,22 +82,23 @@ test('skapad användare blir automatiskt aktiv', async () => {
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: 'Hej Urban' })).toBeInTheDocument()
expect(await screen.findByRole('heading', { name: 'Uppgifter' })).toBeInTheDocument()
expect(window.localStorage.getItem('hemhub.activeUserId')).toBe(createdUser.id)
expect(fetchMock).toHaveBeenLastCalledWith('/api/users', {
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Urban' }),
})
})
test('skapandefel visas utan att namnet försvinner', async () => {
test('skapandefel för användare visas utan att namnet försvinner', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch')
fetchMock.mockResolvedValueOnce(jsonResponse([]))
fetchMock.mockResolvedValueOnce(
@ -97,18 +123,20 @@ test('skapandefel visas utan att namnet försvinner', async () => {
expect(input).toHaveValue('Urban')
})
test('Byt användare rensar valt id och visar användarvalet', async () => {
test('Logga ut rensar valt id och väljer inte användaren automatiskt', async () => {
window.localStorage.setItem('hemhub.activeUserId', users[0].id)
mockJsonResponse(users)
mockUsersAndTasks(users, [])
render(<App />)
fireEvent.click(await screen.findByRole('button', { name: 'Byt användare' }))
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 visas som fel och kan återförsökas', async () => {
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))
@ -133,6 +161,147 @@ test('ogiltigt lagrat användar-id rensas', async () => {
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))
}