Initial commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { LoadingSpinner } from './LoadingSpinner'
|
||||
|
||||
describe('LoadingSpinner', () => {
|
||||
it('renders loading spinner', () => {
|
||||
render(<LoadingSpinner />)
|
||||
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays default message', () => {
|
||||
render(<LoadingSpinner />)
|
||||
expect(screen.getByTestId('spinner-message')).toHaveTextContent('Loading...')
|
||||
})
|
||||
|
||||
it('displays custom message', () => {
|
||||
render(<LoadingSpinner message="Please wait..." />)
|
||||
expect(screen.getByTestId('spinner-message')).toHaveTextContent('Please wait...')
|
||||
})
|
||||
|
||||
it('has correct aria attributes for accessibility', () => {
|
||||
render(<LoadingSpinner ariaLabel="Loading data" />)
|
||||
const spinner = screen.getByTestId('loading-spinner')
|
||||
expect(spinner).toHaveAttribute('role', 'status')
|
||||
expect(spinner).toHaveAttribute('aria-label', 'Loading data')
|
||||
expect(spinner).toHaveAttribute('aria-busy', 'true')
|
||||
})
|
||||
|
||||
it('uses default aria label', () => {
|
||||
render(<LoadingSpinner />)
|
||||
const spinner = screen.getByTestId('loading-spinner')
|
||||
expect(spinner).toHaveAttribute('aria-label', 'Loading')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
message?: string
|
||||
ariaLabel?: string
|
||||
}
|
||||
|
||||
export function LoadingSpinner({ message = 'Loading...', ariaLabel = 'Loading' }: LoadingSpinnerProps): ReactNode {
|
||||
return (
|
||||
<div role="status" aria-label={ariaLabel} aria-busy={true} data-testid="loading-spinner">
|
||||
<div data-testid="spinner-icon" role="presentation">⊙</div>
|
||||
<p data-testid="spinner-message">{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { OverviewMetrics } from './OverviewMetrics'
|
||||
|
||||
describe('OverviewMetrics', () => {
|
||||
it('renders metrics section', () => {
|
||||
render(<OverviewMetrics metrics={[]} />)
|
||||
expect(screen.getByTestId('overview-metrics')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders default title', () => {
|
||||
render(<OverviewMetrics metrics={[]} />)
|
||||
expect(screen.getByTestId('metrics-title')).toHaveTextContent('Overview')
|
||||
})
|
||||
|
||||
it('renders custom title', () => {
|
||||
render(<OverviewMetrics metrics={[]} title="Custom Title" />)
|
||||
expect(screen.getByTestId('metrics-title')).toHaveTextContent('Custom Title')
|
||||
})
|
||||
|
||||
it('renders metrics list with items', () => {
|
||||
const metrics = [
|
||||
{ label: 'Days Used', value: '10', testId: 'days-used' },
|
||||
{ label: 'Days Left', value: '15', testId: 'days-left' },
|
||||
]
|
||||
render(<OverviewMetrics metrics={metrics} />)
|
||||
|
||||
expect(screen.getByTestId('metric-label-0')).toHaveTextContent('Days Used:')
|
||||
expect(screen.getByTestId('metric-value-0')).toHaveTextContent('10')
|
||||
expect(screen.getByTestId('metric-label-1')).toHaveTextContent('Days Left:')
|
||||
expect(screen.getByTestId('metric-value-1')).toHaveTextContent('15')
|
||||
})
|
||||
|
||||
it('renders empty metrics list', () => {
|
||||
render(<OverviewMetrics metrics={[]} />)
|
||||
expect(screen.getByTestId('metrics-list')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses custom test ids when provided', () => {
|
||||
const metrics = [{ label: 'Test', value: '100', testId: 'custom-test-id' }]
|
||||
render(<OverviewMetrics metrics={metrics} />)
|
||||
expect(screen.getByTestId('custom-test-id')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has proper accessibility structure', () => {
|
||||
const metrics = [{ label: 'Metric', value: '50' }]
|
||||
render(<OverviewMetrics metrics={metrics} title="Metrics Section" />)
|
||||
|
||||
const section = screen.getByTestId('overview-metrics')
|
||||
expect(section).toHaveAttribute('role', 'region')
|
||||
expect(section).toHaveAttribute('aria-label', 'Metrics Section')
|
||||
|
||||
expect(screen.getByTestId('metrics-list')).toHaveAttribute('role', 'list')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface Metric {
|
||||
label: string
|
||||
value: string | number
|
||||
testId?: string
|
||||
}
|
||||
|
||||
interface OverviewMetricsProps {
|
||||
metrics: Metric[]
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function OverviewMetrics({ metrics, title = 'Overview' }: OverviewMetricsProps): ReactNode {
|
||||
return (
|
||||
<section role="region" aria-label={title} data-testid="overview-metrics">
|
||||
<h2 data-testid="metrics-title">{title}</h2>
|
||||
<div role="list" data-testid="metrics-list">
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
role="listitem"
|
||||
data-testid={metric.testId || `metric-${index}`}
|
||||
data-label={metric.label}
|
||||
>
|
||||
<span data-testid={`metric-label-${index}`}>{metric.label}:</span>
|
||||
<span data-testid={`metric-value-${index}`}>{metric.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ChildBirthdayInput } from './ChildBirthdayInput'
|
||||
|
||||
describe('ChildBirthdayInput', () => {
|
||||
it('renders birth date input', () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('birth-date-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders custom label', () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
label="Custom Label"
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('birthday-legend')).toHaveTextContent('Custom Label')
|
||||
})
|
||||
|
||||
it('calls onBirthDateChange when birth date changes', async () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
/>
|
||||
)
|
||||
const input = screen.getByTestId('birth-date-input')
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, '2024-07-20')
|
||||
expect(onBirthDateChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders preschool date input when callback provided', () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
const onPreschoolStartDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
onPreschoolStartDateChange={onPreschoolStartDateChange}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('preschool-start-date-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render preschool date input when callback not provided', () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByTestId('preschool-start-date-input')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onPreschoolStartDateChange when preschool date changes', async () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
const onPreschoolStartDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
preschoolStartDate="2027-08-15"
|
||||
onPreschoolStartDateChange={onPreschoolStartDateChange}
|
||||
/>
|
||||
)
|
||||
const input = screen.getByTestId('preschool-start-date-input')
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, '2027-09-01')
|
||||
expect(onPreschoolStartDateChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('has proper fieldset structure', () => {
|
||||
const onBirthDateChange = vi.fn()
|
||||
render(
|
||||
<ChildBirthdayInput
|
||||
birthDate="2024-06-15"
|
||||
onBirthDateChange={onBirthDateChange}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('birthday-input-fieldset')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ReactNode, ChangeEvent } from 'react'
|
||||
import { formatDateForInput } from '@/utils/dates'
|
||||
|
||||
interface ChildBirthdayInputProps {
|
||||
birthDate: string
|
||||
onBirthDateChange: (date: string) => void
|
||||
preschoolStartDate?: string | null
|
||||
onPreschoolStartDateChange?: (date: string | null) => void
|
||||
label?: string
|
||||
}
|
||||
|
||||
export function ChildBirthdayInput({
|
||||
birthDate,
|
||||
onBirthDateChange,
|
||||
preschoolStartDate,
|
||||
onPreschoolStartDateChange,
|
||||
label = 'Child Birthday',
|
||||
}: ChildBirthdayInputProps): ReactNode {
|
||||
const handleBirthDateChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
onBirthDateChange(e.target.value)
|
||||
}
|
||||
|
||||
const handlePreschoolDateChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
onPreschoolStartDateChange?.(e.target.value || null)
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset data-testid="birthday-input-fieldset">
|
||||
<legend data-testid="birthday-legend">{label}</legend>
|
||||
<div data-testid="birth-date-group">
|
||||
<label htmlFor="birth-date">Birth Date</label>
|
||||
<input
|
||||
id="birth-date"
|
||||
type="date"
|
||||
value={formatDateForInput(birthDate)}
|
||||
onChange={handleBirthDateChange}
|
||||
data-testid="birth-date-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{onPreschoolStartDateChange && (
|
||||
<div data-testid="preschool-date-group">
|
||||
<label htmlFor="preschool-start-date">Preschool Start Date (optional)</label>
|
||||
<input
|
||||
id="preschool-start-date"
|
||||
type="date"
|
||||
value={preschoolStartDate ? formatDateForInput(preschoolStartDate) : ''}
|
||||
onChange={handlePreschoolDateChange}
|
||||
data-testid="preschool-start-date-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { Footer } from './Footer'
|
||||
|
||||
describe('Footer', () => {
|
||||
it('renders footer', () => {
|
||||
render(<Footer />)
|
||||
expect(screen.getByTestId('footer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays current year', () => {
|
||||
render(<Footer />)
|
||||
const currentYear = new Date().getFullYear()
|
||||
expect(screen.getByTestId('footer-text')).toHaveTextContent(currentYear.toString())
|
||||
})
|
||||
|
||||
it('displays copyright text', () => {
|
||||
render(<Footer />)
|
||||
expect(screen.getByTestId('footer-text')).toHaveTextContent('© ')
|
||||
expect(screen.getByTestId('footer-text')).toHaveTextContent('Föräldraledigplannerare')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export function Footer(): ReactNode {
|
||||
const currentYear = new Date().getFullYear()
|
||||
return (
|
||||
<footer role="contentinfo" data-testid="footer">
|
||||
<p data-testid="footer-text">© {currentYear} Föräldraledigplannerare. All rights reserved.</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Header } from './Header'
|
||||
|
||||
describe('Header', () => {
|
||||
it('renders header with default title', () => {
|
||||
render(<Header />)
|
||||
expect(screen.getByTestId('header-title')).toHaveTextContent('Föräldraledigplannerare')
|
||||
})
|
||||
|
||||
it('renders header with custom title', () => {
|
||||
render(<Header title="Custom Title" />)
|
||||
expect(screen.getByTestId('header-title')).toHaveTextContent('Custom Title')
|
||||
})
|
||||
|
||||
it('renders logout button when onLogout provided', () => {
|
||||
const onLogout = vi.fn()
|
||||
render(<Header onLogout={onLogout} />)
|
||||
const button = screen.getByTestId('logout-button')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onLogout when logout button clicked', async () => {
|
||||
const onLogout = vi.fn()
|
||||
render(<Header onLogout={onLogout} />)
|
||||
const button = screen.getByTestId('logout-button')
|
||||
await userEvent.click(button)
|
||||
expect(onLogout).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders workspace selector button when onWorkspaceSelect provided', () => {
|
||||
const onWorkspaceSelect = vi.fn()
|
||||
render(<Header onWorkspaceSelect={onWorkspaceSelect} />)
|
||||
expect(screen.getByTestId('workspace-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onWorkspaceSelect when workspace button clicked', async () => {
|
||||
const onWorkspaceSelect = vi.fn()
|
||||
render(<Header onWorkspaceSelect={onWorkspaceSelect} />)
|
||||
const button = screen.getByTestId('workspace-selector')
|
||||
await userEvent.click(button)
|
||||
expect(onWorkspaceSelect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not render buttons when no handlers provided', () => {
|
||||
render(<Header />)
|
||||
expect(screen.queryByTestId('logout-button')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('workspace-selector')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface HeaderProps {
|
||||
title?: string
|
||||
onLogout?: () => void
|
||||
onWorkspaceSelect?: () => void
|
||||
}
|
||||
|
||||
export function Header({ title = 'Föräldraledigplannerare', onLogout, onWorkspaceSelect }: HeaderProps): ReactNode {
|
||||
return (
|
||||
<header role="banner" data-testid="header">
|
||||
<div data-testid="header-title">{title}</div>
|
||||
<nav role="navigation" data-testid="header-nav">
|
||||
{onWorkspaceSelect && (
|
||||
<button
|
||||
data-testid="workspace-selector"
|
||||
onClick={onWorkspaceSelect}
|
||||
aria-label="Select workspace"
|
||||
>
|
||||
Select Workspace
|
||||
</button>
|
||||
)}
|
||||
{onLogout && (
|
||||
<button
|
||||
data-testid="logout-button"
|
||||
onClick={onLogout}
|
||||
aria-label="Log out"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Header, HeaderProps } from './Header'
|
||||
import { Footer } from './Footer'
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode
|
||||
header?: HeaderProps
|
||||
}
|
||||
|
||||
export function Layout({ children, header }: LayoutProps): ReactNode {
|
||||
return (
|
||||
<div role="main" data-testid="layout">
|
||||
<Header {...(header || {})} />
|
||||
<main data-testid="layout-main">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* REFERENCE TEST — for the VacationDaysList component + useVacationDays hook.
|
||||
*
|
||||
* Pattern to copy for other component tests:
|
||||
* - vi.mock('@/services/api') to isolate from the real HTTP layer.
|
||||
* - Use @testing-library/react renderHook for hooks.
|
||||
* - Use render + waitFor for async component tests.
|
||||
* - Always cover: loading state, error state, empty state, and the happy path.
|
||||
*/
|
||||
|
||||
import { render, renderHook, act, screen, waitFor } from '@testing-library/react'
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
import { VacationDaysList } from './VacationDaysList'
|
||||
import { useVacationDays } from '@/hooks/useVacationDays'
|
||||
import * as api from '@/services/api'
|
||||
import type { VacationDays } from '@/types'
|
||||
|
||||
vi.mock('@/services/api')
|
||||
|
||||
const MOCK_WS = 'workspace-1'
|
||||
const MOCK_VACATION_DAYS: VacationDays[] = [
|
||||
{
|
||||
id: 'vd-1',
|
||||
personalDataUuid: 'pd-1',
|
||||
workspaceId: MOCK_WS,
|
||||
year: 2026,
|
||||
totalDays: 25,
|
||||
savedDaysIn: 5,
|
||||
createdAt: '2026-01-01T00:00:00',
|
||||
updatedAt: '2026-01-01T00:00:00',
|
||||
},
|
||||
{
|
||||
id: 'vd-2',
|
||||
personalDataUuid: 'pd-2',
|
||||
workspaceId: MOCK_WS,
|
||||
year: 2025,
|
||||
totalDays: 25,
|
||||
savedDaysIn: 0,
|
||||
createdAt: '2025-01-01T00:00:00',
|
||||
updatedAt: '2025-01-01T00:00:00',
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('VacationDaysList', () => {
|
||||
it('shows loading state while fetching', () => {
|
||||
vi.mocked(api.getVacationDaysByWorkspace).mockReturnValue(new Promise(() => {}))
|
||||
render(<VacationDaysList workspaceId={MOCK_WS} />)
|
||||
expect(screen.getByRole('status')).toHaveTextContent('Laddar')
|
||||
})
|
||||
|
||||
it('renders vacation days list on success', async () => {
|
||||
vi.mocked(api.getVacationDaysByWorkspace).mockResolvedValue(MOCK_VACATION_DAYS)
|
||||
render(<VacationDaysList workspaceId={MOCK_WS} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('vacation-days-list')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByTestId('vacation-days-item-vd-1')).toHaveTextContent('2026')
|
||||
expect(screen.getByTestId('vacation-days-item-vd-1')).toHaveTextContent('+5 sparade')
|
||||
expect(screen.getByTestId('vacation-days-item-vd-2')).toHaveTextContent('2025')
|
||||
})
|
||||
|
||||
it('shows error message on API failure', async () => {
|
||||
vi.mocked(api.getVacationDaysByWorkspace).mockRejectedValue(new Error('Network error'))
|
||||
render(<VacationDaysList workspaceId={MOCK_WS} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows empty state when no vacation days exist', async () => {
|
||||
vi.mocked(api.getVacationDaysByWorkspace).mockResolvedValue([])
|
||||
render(<VacationDaysList workspaceId={MOCK_WS} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/inga semesterdagar/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('useVacationDays', () => {
|
||||
it('fetches vacation days on mount', async () => {
|
||||
vi.mocked(api.getVacationDaysByWorkspace).mockResolvedValue(MOCK_VACATION_DAYS)
|
||||
const { result } = renderHook(() => useVacationDays(MOCK_WS))
|
||||
|
||||
expect(result.current.isLoading).toBe(true)
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||
expect(result.current.data).toEqual(MOCK_VACATION_DAYS)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
|
||||
it('sets error on fetch failure', async () => {
|
||||
vi.mocked(api.getVacationDaysByWorkspace).mockRejectedValue(new Error('Timeout'))
|
||||
const { result } = renderHook(() => useVacationDays(MOCK_WS))
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||
expect(result.current.error).toBe('Timeout')
|
||||
expect(result.current.data).toEqual([])
|
||||
})
|
||||
|
||||
it('upsert calls api and refreshes list', async () => {
|
||||
const updated: VacationDays = { ...MOCK_VACATION_DAYS[0]!, totalDays: 30 }
|
||||
vi.mocked(api.getVacationDaysByWorkspace)
|
||||
.mockResolvedValueOnce(MOCK_VACATION_DAYS)
|
||||
.mockResolvedValueOnce([updated])
|
||||
vi.mocked(api.upsertVacationDays).mockResolvedValue(updated)
|
||||
|
||||
const { result } = renderHook(() => useVacationDays(MOCK_WS))
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsert({
|
||||
personalDataUuid: 'pd-1',
|
||||
workspaceId: MOCK_WS,
|
||||
year: 2026,
|
||||
totalDays: 30,
|
||||
})
|
||||
})
|
||||
|
||||
expect(api.upsertVacationDays).toHaveBeenCalledOnce()
|
||||
expect(result.current.data[0]?.totalDays).toBe(30)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* REFERENCE COMPONENT — renders the VacationDays list.
|
||||
*
|
||||
* Shows the established pattern:
|
||||
* - Accept workspaceId as a prop (not from context directly — easier to test).
|
||||
* - Delegate data fetching to the hook.
|
||||
* - Handle loading / error / empty states explicitly.
|
||||
*
|
||||
* Copy this structure for other list components.
|
||||
*/
|
||||
|
||||
import { useVacationDays } from '@/hooks/useVacationDays'
|
||||
import type { VacationDays } from '@/types'
|
||||
|
||||
interface Props {
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
export function VacationDaysList({ workspaceId }: Props) {
|
||||
const { data, isLoading, error } = useVacationDays(workspaceId)
|
||||
|
||||
if (isLoading) return <p role="status">Laddar semesterdagar…</p>
|
||||
if (error) return <p role="alert">{error}</p>
|
||||
if (data.length === 0) return <p>Inga semesterdagar registrerade.</p>
|
||||
|
||||
return (
|
||||
<ul data-testid="vacation-days-list">
|
||||
{data.map((vd) => (
|
||||
<VacationDaysItem key={vd.id} vacationDays={vd} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function VacationDaysItem({ vacationDays: vd }: { vacationDays: VacationDays }) {
|
||||
return (
|
||||
<li data-testid={`vacation-days-item-${vd.id}`}>
|
||||
<strong>{vd.year}</strong>: {vd.totalDays} dagar
|
||||
{vd.savedDaysIn > 0 && <span> (+{vd.savedDaysIn} sparade)</span>}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user