import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const updateAction = vi.fn() const setDraftAction = vi.fn() const clearDraftAction = vi.fn() vi.mock('@/actions/user-settings', () => ({ updateUserSettingsAction: (...args: unknown[]) => updateAction(...args), })) vi.mock('@/actions/sprint-draft', () => ({ setPendingSprintDraftAction: (...args: unknown[]) => setDraftAction(...args), clearPendingSprintDraftAction: (...args: unknown[]) => clearDraftAction(...args), })) import { useUserSettingsStore } from '@/stores/user-settings/store' import type { PendingSprintDraft } from '@/lib/user-settings' function resetStore() { useUserSettingsStore.setState((s) => { s.entities.settings = {} s.context.hydrated = false s.context.isDemo = false s.pendingMutations = {} }) } beforeEach(() => { resetStore() updateAction.mockReset() setDraftAction.mockReset() clearDraftAction.mockReset() }) afterEach(() => { resetStore() }) describe('useUserSettingsStore', () => { it('hydrate sets entities and context', () => { useUserSettingsStore.getState().hydrate( { views: { sprintBacklog: { sort: 'code' } } }, false, ) const s = useUserSettingsStore.getState() expect(s.entities.settings.views?.sprintBacklog?.sort).toBe('code') expect(s.context.hydrated).toBe(true) expect(s.context.isDemo).toBe(false) }) it('setPref updates state optimistically and settles on success', async () => { useUserSettingsStore.getState().hydrate({}, false) updateAction.mockResolvedValueOnce({ success: true, settings: { views: { sprintBacklog: { filterStatus: 'all' } } }, }) await useUserSettingsStore .getState() .setPref(['views', 'sprintBacklog', 'filterStatus'], 'all') const s = useUserSettingsStore.getState() expect(s.entities.settings.views?.sprintBacklog?.filterStatus).toBe('all') expect(Object.keys(s.pendingMutations)).toHaveLength(0) expect(updateAction).toHaveBeenCalledWith({ views: { sprintBacklog: { filterStatus: 'all' } }, }) }) it('setPref rolls back on server error', async () => { useUserSettingsStore.getState().hydrate( { views: { sprintBacklog: { sort: 'code' } } }, false, ) updateAction.mockResolvedValueOnce({ error: 'boom', code: 422 }) await useUserSettingsStore .getState() .setPref(['views', 'sprintBacklog', 'sort'], 'priority') const s = useUserSettingsStore.getState() expect(s.entities.settings.views?.sprintBacklog?.sort).toBe('code') expect(Object.keys(s.pendingMutations)).toHaveLength(0) }) it('setPref skips server-call for demo accounts', async () => { useUserSettingsStore.getState().hydrate({}, true) await useUserSettingsStore .getState() .setPref(['devTools', 'debugMode'], true) const s = useUserSettingsStore.getState() expect(s.entities.settings.devTools?.debugMode).toBe(true) expect(updateAction).not.toHaveBeenCalled() }) it('setPendingSprintDraft persists draft lokaal (session-only, geen server-call)', async () => { useUserSettingsStore.getState().hydrate({}, false) const draft: PendingSprintDraft = { goal: 'Sprint 1', pbiIntent: { pbiA: 'all' }, storyOverrides: {}, } await useUserSettingsStore .getState() .setPendingSprintDraft('product-1', draft) const s = useUserSettingsStore.getState() expect( s.entities.settings.workflow?.pendingSprintDraft?.['product-1'], ).toMatchObject({ goal: 'Sprint 1' }) expect(setDraftAction).not.toHaveBeenCalled() }) it('hydrate strips workflow.pendingSprintDraft uit legacy server-state', () => { useUserSettingsStore.getState().hydrate( { workflow: { pendingSprintDraft: { 'product-1': { goal: 'Legacy draft', pbiIntent: {}, storyOverrides: {}, }, }, }, }, false, ) const s = useUserSettingsStore.getState() expect(s.entities.settings.workflow?.pendingSprintDraft).toBeUndefined() }) it('clearPendingSprintDraft verwijdert de key lokaal zonder server-call', async () => { useUserSettingsStore.getState().hydrate({}, false) await useUserSettingsStore.getState().setPendingSprintDraft('product-1', { goal: 'Old', pbiIntent: {}, storyOverrides: {}, }) await useUserSettingsStore .getState() .clearPendingSprintDraft('product-1') const s = useUserSettingsStore.getState() expect( s.entities.settings.workflow?.pendingSprintDraft?.['product-1'], ).toBeUndefined() expect(clearDraftAction).not.toHaveBeenCalled() }) it('upsertPbiIntent updates intent and wipes storyOverrides for that PBI', async () => { useUserSettingsStore.getState().hydrate({}, false) await useUserSettingsStore.getState().setPendingSprintDraft('product-1', { goal: 'g', pbiIntent: { pbiA: 'none' }, storyOverrides: { pbiA: { add: ['s-1'], remove: [] }, pbiB: { add: [], remove: ['s-2'] }, }, }) await useUserSettingsStore .getState() .upsertPbiIntent('product-1', 'pbiA', 'all') const draft = useUserSettingsStore.getState().entities.settings.workflow ?.pendingSprintDraft?.['product-1'] expect(draft?.pbiIntent.pbiA).toBe('all') expect(draft?.storyOverrides.pbiA).toBeUndefined() expect(draft?.storyOverrides.pbiB).toEqual({ add: [], remove: ['s-2'] }) }) it('upsertStoryOverride add adds to add[] and removes from remove[]', async () => { useUserSettingsStore.getState().hydrate({}, false) await useUserSettingsStore.getState().setPendingSprintDraft('product-1', { goal: 'g', pbiIntent: {}, storyOverrides: { pbiA: { add: [], remove: ['story-1'] }, }, }) await useUserSettingsStore .getState() .upsertStoryOverride('product-1', 'pbiA', 'story-1', 'add') const draft = useUserSettingsStore.getState().entities.settings.workflow ?.pendingSprintDraft?.['product-1'] expect(draft?.storyOverrides.pbiA).toEqual({ add: ['story-1'], remove: [], }) }) it('upsertStoryOverride clear removes from both arrays and drops empty entry', async () => { useUserSettingsStore.getState().hydrate({}, false) await useUserSettingsStore.getState().setPendingSprintDraft('product-1', { goal: 'g', pbiIntent: {}, storyOverrides: { pbiA: { add: ['story-1'], remove: [] }, }, }) await useUserSettingsStore .getState() .upsertStoryOverride('product-1', 'pbiA', 'story-1', 'clear') const draft = useUserSettingsStore.getState().entities.settings.workflow ?.pendingSprintDraft?.['product-1'] expect(draft?.storyOverrides.pbiA).toBeUndefined() }) it('applyServerPatch merges without optimistic state', () => { useUserSettingsStore.getState().hydrate( { views: { sprintBacklog: { sort: 'code' } } }, false, ) useUserSettingsStore.getState().applyServerPatch({ views: { sprintBacklog: { sortDir: 'desc' } }, }) const s = useUserSettingsStore.getState() expect(s.entities.settings.views?.sprintBacklog).toEqual({ sort: 'code', sortDir: 'desc', }) expect(Object.keys(s.pendingMutations)).toHaveLength(0) }) })