import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it } from 'vitest'; import { useSubmissionState, useXCardSurface } from './useSubmissionState'; // 项目未安装 @testing-library/react,用 createRoot + harness 组件暴露 hook API let container: HTMLDivElement | null = null; let root: ReturnType | null = null; let api: ReturnType | null = null; let surface: ReturnType | null = null; let surfaceId = 'surface-test'; function Harness() { api = useSubmissionState(); surface = useXCardSurface(surfaceId); return null; } function renderHarness(): void { container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); act(() => { root?.render(); }); } afterEach(async () => { if (root) await act(async () => root?.unmount()); container?.remove(); root = null; container = null; api = null; surface = null; surfaceId = 'surface-test'; }); describe('useSubmissionState', () => { it('tracks submitting during the task and succeeds afterwards', async () => { renderHarness(); let resolveTask: () => void = () => undefined; const task = () => new Promise((resolve) => { resolveTask = resolve; }); let promise: Promise | undefined; act(() => { promise = api?.run(task); }); expect(api?.submitting).toBe(true); expect(api?.error).toBeNull(); await act(async () => { resolveTask(); await promise; }); expect(api?.submitting).toBe(false); expect(api?.submitted).toBe(true); expect(api?.error).toBeNull(); }); it('captures the error message and keeps submitted false on failure', async () => { renderHarness(); const failing = () => { throw new Error('接口 500'); }; await act(async () => { await api?.run(failing); }); expect(api?.submitting).toBe(false); expect(api?.submitted).toBe(false); expect(api?.error).toBe('接口 500'); }); it('normalizes non-Error rejections to a generic message', async () => { renderHarness(); const failing = () => Promise.reject('raw string'); await act(async () => { await api?.run(failing); }); expect(api?.error).toBe('提交失败,请稍后重试'); }); it('ignores re-entrant calls while a task is in flight', async () => { renderHarness(); let resolveTask: () => void = () => undefined; const task = () => new Promise((resolve) => { resolveTask = resolve; }); let secondRan = false; act(() => { void api?.run(task); void api?.run(() => { secondRan = true; }); }); expect(secondRan).toBe(false); await act(async () => { resolveTask(); }); expect(api?.submitted).toBe(true); }); it('reset clears submitted and error states', async () => { renderHarness(); await act(async () => { await api?.run(() => undefined); }); expect(api?.submitted).toBe(true); act(() => { api?.reset(); }); expect(api?.submitted).toBe(false); expect(api?.error).toBeNull(); }); }); describe('useXCardSurface', () => { it('deduplicates createSurface commands for the same surface id', () => { renderHarness(); act(() => { surface?.pushCommands([ { version: 'v0.9', createSurface: { surfaceId: 'surface-test', catalogId: 'catalog' } }, { version: 'v0.9', updateDataModel: { surfaceId: 'surface-test', path: '/x', value: 1 } }, ]); surface?.pushCommands([ { version: 'v0.9', createSurface: { surfaceId: 'surface-test', catalogId: 'catalog' } }, { version: 'v0.9', updateDataModel: { surfaceId: 'surface-test', path: '/x', value: 2 } }, ]); }); const createCommands = surface?.commands.filter((command) => 'createSurface' in command); expect(createCommands).toHaveLength(1); expect(surface?.commands).toHaveLength(3); }); it('resets the command stream when the surface id changes', () => { renderHarness(); act(() => { surface?.pushCommands([ { version: 'v0.9', createSurface: { surfaceId: 'surface-test', catalogId: 'c' } }, ]); }); expect(surface?.commands).toHaveLength(1); surfaceId = 'surface-other'; act(() => { root?.render(); surface?.pushCommands([ { version: 'v0.9', createSurface: { surfaceId: 'surface-other', catalogId: 'c' } }, ]); }); expect(surface?.commands).toHaveLength(1); }); });