import { describe, expect, it, vi } from 'vitest' import { createMutationReconciler, reconcileCurrentTaskView } from './lib/mvp-utils' type ViewContext = { navigation: number; view: 'tasks' | 'today' | 'upcoming' | 'trash' | 'habits'; listId: string } type State = { tasks: string[]; total: number; loading: boolean; error: string } function deferred() { let resolve!: (value: T) => void const promise = new Promise((res) => { resolve = res }) return { promise, resolve } } const sameContext = (left: ViewContext, right: ViewContext) => left.navigation === right.navigation && left.view === right.view && left.listId === right.listId function mutationHarness() { let current: ViewContext = { navigation: 1, view: 'tasks', listId: 'inbox' } const state: State = { tasks: ['inbox task'], total: 1, loading: false, error: '' } const loadTaskView = vi.fn(async (context: ViewContext, ownership: { current: () => boolean }) => { if (ownership.current()) Object.assign(state, { tasks: [`${context.listId} task`], total: 1, loading: false, error: '' }) }) const loadTrash = vi.fn(async (ownership: { current: () => boolean }) => { if (ownership.current()) Object.assign(state, { tasks: ['trashed task'], total: 7, loading: false, error: '' }) }) const reconcileCurrentView = (options: { affectsTrash?: boolean; affectsTaskView?: boolean } = {}) => reconcileCurrentTaskView({ capture: () => current, isTaskBacked: context => ['tasks', 'today', 'upcoming'].includes(context.view), isTrash: context => context.view === 'trash', sameContext, loadTaskView, loadTrash, affectsTrash: options.affectsTrash, affectsTaskView: options.affectsTaskView, }) const reconciler = createMutationReconciler( () => current, sameContext, reconcileCurrentView, ) return { state, loadTaskView, loadTrash, reconciler, navigate(next: ViewContext, nextState: State) { current = next Object.assign(state, nextState) }, } } describe('current-view-aware task mutation reconciliation', () => { it.each([ { mutation: 'create', affectsTrash: false }, { mutation: 'add subtask', affectsTrash: false }, { mutation: 'delete', affectsTrash: true }, { mutation: 'remove subtask', affectsTrash: true }, ])('keeps resolved Trash authoritative after a stale $mutation settles', async ({ affectsTrash }) => { const harness = mutationHarness() const request = deferred() const pending = harness.reconciler.run( () => request.promise, () => undefined, undefined, undefined, { affectsTrash }, ) harness.navigate( { navigation: 2, view: 'trash', listId: 'inbox' }, { tasks: ['target trash'], total: 9, loading: false, error: 'target error' }, ) request.resolve() await pending expect(harness.loadTaskView).not.toHaveBeenCalled() if (affectsTrash) { expect(harness.loadTrash).toHaveBeenCalledOnce() expect(harness.state).toEqual({ tasks: ['trashed task'], total: 7, loading: false, error: '' }) } else { expect(harness.loadTrash).not.toHaveBeenCalled() expect(harness.state).toEqual({ tasks: ['target trash'], total: 9, loading: false, error: 'target error' }) } }) it.each(['create', 'delete', 'add subtask', 'remove subtask'])('keeps a non-task view authoritative after a stale %s settles', async () => { const harness = mutationHarness() const request = deferred() const pending = harness.reconciler.run( () => request.promise, () => undefined, undefined, undefined, { affectsTrash: true }, ) harness.navigate( { navigation: 2, view: 'habits', listId: 'inbox' }, { tasks: ['habit-owned state'], total: 4, loading: false, error: 'habit error' }, ) request.resolve() await pending expect(harness.loadTaskView).not.toHaveBeenCalled() expect(harness.loadTrash).not.toHaveBeenCalled() expect(harness.state).toEqual({ tasks: ['habit-owned state'], total: 4, loading: false, error: 'habit error' }) }) it('does not reload Trash when a stale subtask removal settles', async () => { const harness = mutationHarness() const request = deferred() const pending = harness.reconciler.run( () => request.promise, () => undefined, undefined, undefined, { affectsTrash: false }, ) harness.navigate( { navigation: 2, view: 'trash', listId: 'inbox' }, { tasks: ['target trash'], total: 9, loading: false, error: 'target error' }, ) request.resolve() await pending expect(harness.loadTaskView).not.toHaveBeenCalled() expect(harness.loadTrash).not.toHaveBeenCalled() expect(harness.state).toEqual({ tasks: ['target trash'], total: 9, loading: false, error: 'target error' }) }) it.each([ { mutation: 'restore', affectsTaskView: true, expectedTaskLoads: 1 }, { mutation: 'purge', affectsTaskView: false, expectedTaskLoads: 0 }, ])('reconciles stale Trash $mutation only when it can affect the new task-backed view', async ({ affectsTaskView, expectedTaskLoads }) => { const harness = mutationHarness() const request = deferred() const pending = harness.reconciler.run( () => request.promise, () => undefined, undefined, undefined, { affectsTrash: true, affectsTaskView }, ) harness.navigate( { navigation: 2, view: 'tasks', listId: 'work' }, { tasks: ['target work'], total: 3, loading: false, error: '' }, ) request.resolve() await pending expect(harness.loadTaskView).toHaveBeenCalledTimes(expectedTaskLoads) expect(harness.loadTrash).not.toHaveBeenCalled() expect(harness.state).toEqual(expectedTaskLoads ? { tasks: ['work task'], total: 1, loading: false, error: '' } : { tasks: ['target work'], total: 3, loading: false, error: '' }) }) it.each(['restore', 'purge'])('leaves a non-task view untouched when stale Trash %s settles', async () => { const harness = mutationHarness() const request = deferred() const pending = harness.reconciler.run( () => request.promise, () => undefined, undefined, undefined, { affectsTrash: true, affectsTaskView: true }, ) harness.navigate( { navigation: 2, view: 'habits', listId: 'inbox' }, { tasks: ['habit-owned state'], total: 4, loading: false, error: 'habit error' }, ) request.resolve() await pending expect(harness.loadTaskView).not.toHaveBeenCalled() expect(harness.loadTrash).not.toHaveBeenCalled() expect(harness.state).toEqual({ tasks: ['habit-owned state'], total: 4, loading: false, error: 'habit error' }) }) it('refreshes the currently visible task-backed list after a stale mutation settles', async () => { const harness = mutationHarness() const request = deferred() const pending = harness.reconciler.run(() => request.promise, () => undefined) harness.navigate( { navigation: 2, view: 'tasks', listId: 'work' }, { tasks: ['target work'], total: 3, loading: false, error: '' }, ) request.resolve() await pending expect(harness.loadTaskView).toHaveBeenCalledOnce() expect(harness.loadTrash).not.toHaveBeenCalled() expect(harness.state).toEqual({ tasks: ['work task'], total: 1, loading: false, error: '' }) }) })