Files
gongxue-base/apps/admin/src/components/AiChat/useSubmissionState.test.tsx
wangziqi 4b60e0c018 fix: code-review 审查问题修复 + A2UI 测试补齐
Standards 轴:
- 移除 uiArtifacts.ts 的 payloadOf 死代码残留
- AttendanceDevices 残留 any 类型化(补 ClassroomOption.status 字段)
- 批量考勤纠错区分业务失败(已结算/无权限)与系统错误,
  前端提示精确到两类数量
- Dashboard queryFn 六段重复校验块收敛为 safeValidate 助手

Spec 轴:
- 补齐阶段 3.4 A2UI 测试:图表空数据占位、ArtifactErrorBoundary
  降级隔离、useSubmissionState/useXCardSurface 单测(7 用例)
- 阶段 2.2 补两处引导:教师工作台区分「今日无课」与「未分配班级」、
  班级花名册空态带「添加学员」动作
- 契约文档修正 DynamicReview 状态管理描述(多提交点如实说明)

aislop 剩余 16 警告均为必要豁免(类型边界/声明式 SQL 配置/既有文件规模)
2026-08-08 09:43:48 +08:00

162 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<typeof createRoot> | null = null;
let api: ReturnType<typeof useSubmissionState> | null = null;
let surface: ReturnType<typeof useXCardSurface> | 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(<Harness />);
});
}
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<void>((resolve) => {
resolveTask = resolve;
});
let promise: Promise<void> | 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<void>((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(<Harness />);
surface?.pushCommands([
{ version: 'v0.9', createSurface: { surfaceId: 'surface-other', catalogId: 'c' } },
]);
});
expect(surface?.commands).toHaveLength(1);
});
});