Files
waoowaoo/tests/unit/optimistic/project-asset-mutations.test.ts

158 lines
5.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Character, Location, Project } from '@/types/project'
import type { ProjectAssetsData } from '@/lib/query/hooks/useProjectAssets'
import { queryKeys } from '@/lib/query/keys'
import { MockQueryClient } from '../../helpers/mock-query-client'
let queryClient = new MockQueryClient()
const useQueryClientMock = vi.fn(() => queryClient)
const useMutationMock = vi.fn((options: unknown) => options)
vi.mock('react', async () => {
const actual = await vi.importActual<typeof import('react')>('react')
return {
...actual,
useRef: <T,>(value: T) => ({ current: value }),
}
})
vi.mock('@tanstack/react-query', () => ({
useQueryClient: () => useQueryClientMock(),
useMutation: (options: unknown) => useMutationMock(options),
}))
vi.mock('@/lib/query/mutations/mutation-shared', async () => {
const actual = await vi.importActual<typeof import('@/lib/query/mutations/mutation-shared')>(
'@/lib/query/mutations/mutation-shared',
)
return {
...actual,
requestJsonWithError: vi.fn(),
requestVoidWithError: vi.fn(),
invalidateQueryTemplates: vi.fn(),
}
})
import {
useDeleteProjectCharacter,
useSelectProjectCharacterImage,
} from '@/lib/query/mutations/character-base-mutations'
interface SelectProjectCharacterMutation {
onMutate: (variables: {
characterId: string
appearanceId: string
imageIndex: number | null
}) => Promise<unknown>
onError: (error: unknown, variables: unknown, context: unknown) => void
}
interface DeleteProjectCharacterMutation {
onMutate: (characterId: string) => Promise<unknown>
onError: (error: unknown, characterId: string, context: unknown) => void
}
function buildCharacter(selectedIndex: number | null): Character {
return {
id: 'character-1',
name: 'Hero',
appearances: [{
id: 'appearance-1',
appearanceIndex: 0,
changeReason: 'default',
description: null,
descriptions: null,
imageUrl: selectedIndex === null ? null : `img-${selectedIndex}`,
imageUrls: ['img-0', 'img-1', 'img-2'],
previousImageUrl: null,
previousImageUrls: [],
previousDescription: null,
previousDescriptions: null,
selectedIndex,
}],
}
}
function buildAssets(selectedIndex: number | null): ProjectAssetsData {
return {
characters: [buildCharacter(selectedIndex)],
locations: [] as Location[],
}
}
function buildProject(selectedIndex: number | null): Project {
return {
novelPromotionData: {
characters: [buildCharacter(selectedIndex)],
locations: [],
},
} as unknown as Project
}
describe('project asset optimistic mutations', () => {
beforeEach(() => {
queryClient = new MockQueryClient()
useQueryClientMock.mockClear()
useMutationMock.mockClear()
})
it('optimistically selects project character image and ignores stale rollback', async () => {
const projectId = 'project-1'
const assetsKey = queryKeys.projectAssets.all(projectId)
const projectKey = queryKeys.projectData(projectId)
queryClient.seedQuery(assetsKey, buildAssets(0))
queryClient.seedQuery(projectKey, buildProject(0))
const mutation = useSelectProjectCharacterImage(projectId) as unknown as SelectProjectCharacterMutation
const firstVariables = {
characterId: 'character-1',
appearanceId: 'appearance-1',
imageIndex: 1,
}
const secondVariables = {
characterId: 'character-1',
appearanceId: 'appearance-1',
imageIndex: 2,
}
const firstContext = await mutation.onMutate(firstVariables)
const afterFirst = queryClient.getQueryData<ProjectAssetsData>(assetsKey)
expect(afterFirst?.characters[0]?.appearances[0]?.selectedIndex).toBe(1)
const secondContext = await mutation.onMutate(secondVariables)
const afterSecond = queryClient.getQueryData<ProjectAssetsData>(assetsKey)
expect(afterSecond?.characters[0]?.appearances[0]?.selectedIndex).toBe(2)
mutation.onError(new Error('first failed'), firstVariables, firstContext)
const afterStaleError = queryClient.getQueryData<ProjectAssetsData>(assetsKey)
expect(afterStaleError?.characters[0]?.appearances[0]?.selectedIndex).toBe(2)
mutation.onError(new Error('second failed'), secondVariables, secondContext)
const afterLatestRollback = queryClient.getQueryData<ProjectAssetsData>(assetsKey)
expect(afterLatestRollback?.characters[0]?.appearances[0]?.selectedIndex).toBe(1)
})
it('optimistically deletes project character and restores on error', async () => {
const projectId = 'project-1'
const assetsKey = queryKeys.projectAssets.all(projectId)
const projectKey = queryKeys.projectData(projectId)
queryClient.seedQuery(assetsKey, buildAssets(0))
queryClient.seedQuery(projectKey, buildProject(0))
const mutation = useDeleteProjectCharacter(projectId) as unknown as DeleteProjectCharacterMutation
const context = await mutation.onMutate('character-1')
const afterDeleteAssets = queryClient.getQueryData<ProjectAssetsData>(assetsKey)
expect(afterDeleteAssets?.characters).toHaveLength(0)
const afterDeleteProject = queryClient.getQueryData<Project>(projectKey)
expect(afterDeleteProject?.novelPromotionData?.characters ?? []).toHaveLength(0)
mutation.onError(new Error('delete failed'), 'character-1', context)
const rolledBackAssets = queryClient.getQueryData<ProjectAssetsData>(assetsKey)
expect(rolledBackAssets?.characters).toHaveLength(1)
expect(rolledBackAssets?.characters[0]?.id).toBe('character-1')
})
})