- lib/user-settings.ts: activeSprints values nullable in Zod-schema. Key-aanwezigheid heeft nu betekenis (key+null = bewust geen sprint; key ontbreekt = fallback-cascade). - lib/active-sprint.ts: nieuwe readStoredActiveSprintState helper + resolveActiveSprint respecteert expliciet 'cleared' state zonder fallback. clearActiveSprintInSettings schrijft null i.p.v. de key te verwijderen. - actions/active-sprint.ts: nieuwe clearActiveSprintAction met auth + membership-check. - components/shared/sprint-switcher.tsx: '— Geen actieve sprint —'-optie in dropdown, disabled wanneer er geen actieve sprint is. - Tests: nieuwe active-sprint.test.ts (resolver-paden + clear), active-sprint-action.test.ts (action-laag), uitbreiding user-settings.test.ts. Plan: docs/plans/PBI-79-backlog-sprint-workflow.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
190 lines
5.2 KiB
TypeScript
190 lines
5.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
sprint: { findFirst: vi.fn() },
|
|
user: {
|
|
findUnique: vi.fn(),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
$executeRaw: vi.fn().mockResolvedValue(1),
|
|
},
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
import type { UserSettings } from '@/lib/user-settings'
|
|
import {
|
|
clearActiveSprintInSettings,
|
|
readStoredActiveSprintState,
|
|
resolveActiveSprint,
|
|
} from '@/lib/active-sprint'
|
|
|
|
const mockPrisma = prisma as unknown as {
|
|
sprint: { findFirst: ReturnType<typeof vi.fn> }
|
|
user: {
|
|
findUnique: ReturnType<typeof vi.fn>
|
|
update: ReturnType<typeof vi.fn>
|
|
}
|
|
$executeRaw: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
function withSettings(settings: UserSettings) {
|
|
mockPrisma.user.findUnique.mockResolvedValueOnce({ settings })
|
|
}
|
|
|
|
describe('readStoredActiveSprintState', () => {
|
|
it('returns unset when activeSprints map is absent', () => {
|
|
expect(readStoredActiveSprintState({}, 'p1')).toEqual({ kind: 'unset' })
|
|
})
|
|
|
|
it('returns unset when productId key is absent', () => {
|
|
const settings: UserSettings = {
|
|
layout: { activeSprints: { p2: 'sprint-2' } },
|
|
}
|
|
expect(readStoredActiveSprintState(settings, 'p1')).toEqual({
|
|
kind: 'unset',
|
|
})
|
|
})
|
|
|
|
it('returns cleared when key is present with null value', () => {
|
|
const settings: UserSettings = {
|
|
layout: { activeSprints: { p1: null } },
|
|
}
|
|
expect(readStoredActiveSprintState(settings, 'p1')).toEqual({
|
|
kind: 'cleared',
|
|
})
|
|
})
|
|
|
|
it('returns set when key is present with string value', () => {
|
|
const settings: UserSettings = {
|
|
layout: { activeSprints: { p1: 'sprint-1' } },
|
|
}
|
|
expect(readStoredActiveSprintState(settings, 'p1')).toEqual({
|
|
kind: 'set',
|
|
sprintId: 'sprint-1',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resolveActiveSprint', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('returns null without fallback when key is explicitly null (cleared)', async () => {
|
|
withSettings({ layout: { activeSprints: { p1: null } } })
|
|
|
|
const result = await resolveActiveSprint('p1', 'user-1')
|
|
|
|
expect(result).toBeNull()
|
|
expect(mockPrisma.sprint.findFirst).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns the stored sprint when key is set and sprint exists', async () => {
|
|
withSettings({ layout: { activeSprints: { p1: 'sprint-1' } } })
|
|
mockPrisma.sprint.findFirst.mockResolvedValueOnce({
|
|
id: 'sprint-1',
|
|
code: 'SP-1',
|
|
status: 'OPEN',
|
|
})
|
|
|
|
const result = await resolveActiveSprint('p1', 'user-1')
|
|
|
|
expect(result).toEqual({ id: 'sprint-1', code: 'SP-1', status: 'OPEN' })
|
|
expect(mockPrisma.sprint.findFirst).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('falls back when stored sprint is not found in DB', async () => {
|
|
withSettings({ layout: { activeSprints: { p1: 'stale-id' } } })
|
|
mockPrisma.sprint.findFirst
|
|
.mockResolvedValueOnce(null) // stored lookup misses
|
|
.mockResolvedValueOnce({ id: 'sprint-open', code: 'SP-O', status: 'OPEN' })
|
|
|
|
const result = await resolveActiveSprint('p1', 'user-1')
|
|
|
|
expect(result).toEqual({
|
|
id: 'sprint-open',
|
|
code: 'SP-O',
|
|
status: 'OPEN',
|
|
})
|
|
})
|
|
|
|
it('falls back to first OPEN sprint when key is absent', async () => {
|
|
withSettings({})
|
|
mockPrisma.sprint.findFirst.mockResolvedValueOnce({
|
|
id: 'sprint-open',
|
|
code: 'SP-O',
|
|
status: 'OPEN',
|
|
})
|
|
|
|
const result = await resolveActiveSprint('p1', 'user-1')
|
|
|
|
expect(result).toEqual({
|
|
id: 'sprint-open',
|
|
code: 'SP-O',
|
|
status: 'OPEN',
|
|
})
|
|
})
|
|
|
|
it('falls back to recent CLOSED sprint when no OPEN exists', async () => {
|
|
withSettings({})
|
|
mockPrisma.sprint.findFirst
|
|
.mockResolvedValueOnce(null) // no OPEN
|
|
.mockResolvedValueOnce({
|
|
id: 'sprint-closed',
|
|
code: 'SP-C',
|
|
status: 'CLOSED',
|
|
})
|
|
|
|
const result = await resolveActiveSprint('p1', 'user-1')
|
|
|
|
expect(result).toEqual({
|
|
id: 'sprint-closed',
|
|
code: 'SP-C',
|
|
status: 'CLOSED',
|
|
})
|
|
})
|
|
|
|
it('returns null when key absent and no sprints exist', async () => {
|
|
withSettings({})
|
|
mockPrisma.sprint.findFirst.mockResolvedValue(null)
|
|
|
|
const result = await resolveActiveSprint('p1', 'user-1')
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('clearActiveSprintInSettings', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('writes null instead of deleting the key', async () => {
|
|
withSettings({
|
|
layout: { activeSprints: { p1: 'sprint-1', p2: 'sprint-2' } },
|
|
})
|
|
|
|
await clearActiveSprintInSettings('user-1', 'p1')
|
|
|
|
expect(mockPrisma.user.update).toHaveBeenCalledTimes(1)
|
|
const updateArg = mockPrisma.user.update.mock.calls[0][0] as {
|
|
data: { settings: UserSettings }
|
|
}
|
|
expect(updateArg.data.settings.layout?.activeSprints).toEqual({
|
|
p1: null,
|
|
p2: 'sprint-2',
|
|
})
|
|
})
|
|
|
|
it('adds the key with null when previously unset', async () => {
|
|
withSettings({})
|
|
|
|
await clearActiveSprintInSettings('user-1', 'p1')
|
|
|
|
const updateArg = mockPrisma.user.update.mock.calls[0][0] as {
|
|
data: { settings: UserSettings }
|
|
}
|
|
expect(updateArg.data.settings.layout?.activeSprints).toEqual({ p1: null })
|
|
})
|
|
})
|