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 配置/既有文件规模)
This commit is contained in:
161
apps/admin/src/components/AiChat/useSubmissionState.test.tsx
Normal file
161
apps/admin/src/components/AiChat/useSubmissionState.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user