Scrum4Me/__tests__/lib/active-sprint.test.ts
Madhura68 2af6f24598 feat(PBI-79/ST-1333): active-sprint null-contract + clearActiveSprintAction
- 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>
2026-05-11 13:35:32 +02:00

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 })
})
})