Files
deepseek-harness/packages/client/web-react/tests/store.spec.ts
T

136 lines
4.8 KiB
TypeScript
Raw Normal View History

import { afterEach, describe, expect, it, vi } from 'vitest'
import { createSnapshotStore, shallowEqual } from '@deepseek-ai/dsh-client-web-react/store'
interface State {
a: { n: number }
b: { list: string[] }
}
const init = (): State => ({ a: { n: 1 }, b: { list: ['x'] } })
afterEach(() => {
vi.unstubAllGlobals()
})
describe('createSnapshotStore', () => {
it('applies update through a draft and preserves untouched branch references', () => {
const store = createSnapshotStore(init())
const before = store.getSnapshot()
store.update((d) => { d.a.n = 2 })
const after = store.getSnapshot()
expect(after).not.toBe(before)
expect(after.a.n).toBe(2)
expect(after.b).toBe(before.b)
})
it('notifies synchronously per update by default', () => {
const store = createSnapshotStore(init())
const seen: number[] = []
store.subscribe(() => { seen.push(store.getSnapshot().a.n) })
store.update((d) => { d.a.n = 2 })
store.update((d) => { d.a.n = 3 })
expect(seen).toEqual([2, 3])
})
it('coalesces a frame of updates into one notification in raf mode', () => {
const frame: FrameRequestCallback[] = []
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
frame.push(cb)
return frame.length
})
const store = createSnapshotStore(init(), { flush: 'raf' })
const spy = vi.fn()
store.subscribe(spy)
store.update((d) => { d.a.n = 2 })
store.update((d) => { d.a.n = 3 })
store.update((d) => { d.b.list.push('y') })
expect(spy).not.toHaveBeenCalled()
expect(frame).toHaveLength(1)
frame.shift()!(0)
expect(spy).toHaveBeenCalledTimes(1)
expect(store.getSnapshot().a.n).toBe(3)
// Next frame batches independently.
store.update((d) => { d.a.n = 4 })
expect(frame).toHaveLength(1)
frame.shift()!(0)
expect(spy).toHaveBeenCalledTimes(2)
})
it('falls back to microtask batching in raf mode without requestAnimationFrame', async () => {
const store = createSnapshotStore(init(), { flush: 'raf' })
const spy = vi.fn()
store.subscribe(spy)
store.update((d) => { d.a.n = 2 })
store.update((d) => { d.a.n = 3 })
expect(spy).not.toHaveBeenCalled()
await Promise.resolve()
expect(spy).toHaveBeenCalledTimes(1)
})
it('unsubscribes raf-mode listeners', () => {
const frame: FrameRequestCallback[] = []
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
frame.push(cb)
return frame.length
})
const store = createSnapshotStore(init(), { flush: 'raf' })
const spy = vi.fn()
const off = store.subscribe(spy)
store.update((d) => { d.a.n = 2 })
off()
frame.shift()!(0)
expect(spy).not.toHaveBeenCalled()
})
it('replaces state wholesale via set and freezes it outside production', () => {
const store = createSnapshotStore(init())
const next = init()
store.set(next)
expect(store.getSnapshot()).toBe(next)
expect(() => { (store.getSnapshot().a).n = 9 }).toThrow()
})
it('freezes update produce output outside production (immer dev freeze)', () => {
const store = createSnapshotStore(init())
store.update((d) => { d.a.n = 2 })
expect(() => { (store.getSnapshot().a).n = 9 }).toThrow()
})
it('rehydrates primitive state whole, not spread into index keys', () => {
const backing = new Map<string, string>()
vi.stubGlobal('localStorage', {
getItem: (k: string) => backing.get(k) ?? null,
setItem: (k: string, v: string) => { backing.set(k, v) },
removeItem: (k: string) => { backing.delete(k) },
})
const store = createSnapshotStore<string>('', { persist: { name: 'spec-draft' } })
store.set('hello')
const revived = createSnapshotStore<string>('', { persist: { name: 'spec-draft' } })
expect(revived.getSnapshot()).toBe('hello')
})
it('persists to localStorage under the given name and rehydrates', () => {
const backing = new Map<string, string>()
vi.stubGlobal('localStorage', {
getItem: (k: string) => backing.get(k) ?? null,
setItem: (k: string, v: string) => { backing.set(k, v) },
removeItem: (k: string) => { backing.delete(k) },
})
const store = createSnapshotStore(init(), { persist: { name: 'spec-store' } })
store.update((d) => { d.a.n = 42 })
expect(backing.has('spec-store')).toBe(true)
const revived = createSnapshotStore(init(), { persist: { name: 'spec-store' } })
expect(revived.getSnapshot().a.n).toBe(42)
})
})
describe('shallowEqual', () => {
it('matches one-level-equal objects and rejects deeper drift', () => {
const leaf = { deep: 1 }
expect(shallowEqual({ x: 1, y: leaf }, { x: 1, y: leaf })).toBe(true)
expect(shallowEqual({ x: 1, y: { deep: 1 } }, { x: 1, y: { deep: 1 } })).toBe(false)
expect(shallowEqual([1, 2], [1, 2])).toBe(true)
expect(shallowEqual([1, 2], [2, 1])).toBe(false)
})
})