refactor(client): name the compile face in every client test filename
A test file under packages/client now says which face it covers:
`*.client.spec.{ts,tsx}` and its `*.client.{ts,tsx}` helpers belong to the
Client aggregate, `*.host.spec.ts` to the host aggregate. The carrier's four
node-half specs take the Host suffix.
The two suffixes are mutually exclusive, so each aggregate excludes the
other's and both keep one broad test glob: `exclude` wins over `include`, and
`packages/client/**` no longer has to be excluded wholesale from the host
program with per-file `files` entries carved back out of it. A Host-face spec
that reaches only Host source therefore needs no cross-face project
reference, which the split-project rule rejects.
vitest still discovers every file through `**/*.spec.{ts,tsx}`.
This commit is contained in:
84
packages/client/web-react/tests/use-invoke.client.spec.tsx
Normal file
84
packages/client/web-react/tests/use-invoke.client.spec.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import { useInvoke } from '@deepseek-ai/dsh-client-web-react'
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void
|
||||
let reject!: (e: unknown) => void
|
||||
const promise = new Promise<void>((res, rej) => { resolve = res; reject = rej })
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
interface Probe {
|
||||
invoke: () => void
|
||||
pending: boolean
|
||||
renders: number
|
||||
}
|
||||
|
||||
function Harness({ fn, probe }: { fn: () => Promise<unknown>; probe: Probe }) {
|
||||
const [invoke, pending] = useInvoke(fn)
|
||||
probe.invoke = invoke
|
||||
probe.pending = pending
|
||||
probe.renders += 1
|
||||
return null
|
||||
}
|
||||
|
||||
const newProbe = (): Probe => ({ invoke: () => {}, pending: false, renders: 0 })
|
||||
|
||||
describe('useInvoke', () => {
|
||||
it('tracks pending across the action lifecycle', async () => {
|
||||
const d = deferred()
|
||||
const probe = newProbe()
|
||||
render(<Harness fn={() => d.promise} probe={probe} />)
|
||||
expect(probe.pending).toBe(false)
|
||||
act(() => { probe.invoke() })
|
||||
expect(probe.pending).toBe(true)
|
||||
await act(async () => { d.resolve(); await d.promise })
|
||||
expect(probe.pending).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps pending true until the last concurrent call settles', async () => {
|
||||
const d1 = deferred()
|
||||
const d2 = deferred()
|
||||
const queue = [d1, d2]
|
||||
const probe = newProbe()
|
||||
render(<Harness fn={() => queue.shift()!.promise} probe={probe} />)
|
||||
act(() => { probe.invoke() })
|
||||
act(() => { probe.invoke() })
|
||||
expect(probe.pending).toBe(true)
|
||||
await act(async () => { d1.resolve(); await d1.promise })
|
||||
expect(probe.pending).toBe(true)
|
||||
await act(async () => { d2.resolve(); await d2.promise })
|
||||
expect(probe.pending).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the invoke reference stable while fn changes, and calls the latest fn', async () => {
|
||||
const first = vi.fn(() => Promise.resolve())
|
||||
const second = vi.fn(() => Promise.resolve())
|
||||
const probe = newProbe()
|
||||
const { rerender } = render(<Harness fn={first} probe={probe} />)
|
||||
const invokeBefore = probe.invoke
|
||||
rerender(<Harness fn={second} probe={probe} />)
|
||||
expect(probe.invoke).toBe(invokeBefore)
|
||||
await act(async () => { probe.invoke() })
|
||||
expect(first).not.toHaveBeenCalled()
|
||||
expect(second).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('resets pending and logs when the action rejects', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const d = deferred()
|
||||
const probe = newProbe()
|
||||
render(<Harness fn={() => d.promise} probe={probe} />)
|
||||
act(() => { probe.invoke() })
|
||||
expect(probe.pending).toBe(true)
|
||||
await act(async () => {
|
||||
d.reject(new Error('boom'))
|
||||
await d.promise.catch(() => {})
|
||||
})
|
||||
expect(probe.pending).toBe(false)
|
||||
expect(consoleError).toHaveBeenCalledWith('useInvoke action failed:', expect.any(Error))
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user