Files
deepseek-harness/packages/client/web-react/tests/use-invoke.spec.tsx
T
imccyu cdd4d59ea0 chore(lint): clear the semantic .tsx backlog
Hand fixes for the findings --fix cannot touch, mirroring the fixes
already applied on the fe-docs feature branch (same file, same shape)
so its eventual rebase resolves cleanly:

- restore the return the no-confusing-void-expression autofix ate in
  useAbsentSnapshot (typed S | undefined; hook call kept for hook-order
  stability, undefined returned explicitly);
- re-type DOM queries the no-unnecessary-type-assertion autofix broke:
  getByRole<HTMLButtonElement>(...) generics instead of the removed
  as-casts (the eslint program and the client tsconfig aggregate
  disagree about these casts; the generic form satisfies both);
- justified eslint-disable for the deliberate legacy paths: keyCode 229
  IME-composition detection, execCommand clipboard fallbacks, lib.dom
  clipboard optionality, and the any-typed Reflect.get/this probes in
  test fakes;
- drop the dead react/no-danger directive (eslint-plugin-react is not
  loaded, so the rule never applied) keeping its shiki rationale;
- delete the tautological 'Z' comparison and the renameTarget null
  check already implied by renameBlocked;
- css-module non-null assertions replaced by type widening
  (Button className, TAG_CLASS Record) per the established pattern;
- misc: max-len comment wraps, void generic drop in the deferred test
  helper, unused type imports, floating selectWorkspace promises voided,
  member-delimiter newlines in inline type literals.
2026-07-27 22:23:41 +08:00

85 lines
2.9 KiB
TypeScript

// @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()
})
})