- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
/** 组件传输替身只验证迟到响应,不作为真实后端旅程证据。 */
|
||
import { afterEach, expect, it, vi } from "vitest";
|
||
import {
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
cleanup,
|
||
} from "@testing-library/react";
|
||
import { Ui依赖装配 } from "../../src/应用/依赖装配";
|
||
import { Ui任务列表 } from "../../src/功能/任务运行/任务列表";
|
||
import { MemoryRouter } from "react-router-dom";
|
||
|
||
const taskA = {
|
||
task_id: "task-A",
|
||
work_id: "A",
|
||
task_type: "甲的任务",
|
||
state: "queued",
|
||
last_sequence: 1,
|
||
steps: [],
|
||
};
|
||
const taskB = {
|
||
...taskA,
|
||
task_id: "task-B",
|
||
work_id: "B",
|
||
task_type: "乙的任务",
|
||
};
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-work-scope","environment":"离线组件,传输替身","given":"作品A响应尚未到达","when":"切到B并交付迟到的A响应","then":["页面和请求范围保持B"],"contract":".agent/rules/测试隔离.md"}
|
||
it("NC-ui-work-scope:切作品后迟到响应不覆盖当前作品", async () => {
|
||
let finishA!: (response: Response) => void;
|
||
const delayed = new Promise<Response>((resolve) => {
|
||
finishA = resolve;
|
||
});
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const work = new URL(request.url).searchParams.get("work_id");
|
||
if (work === "A") return delayed;
|
||
return new Response(
|
||
JSON.stringify(work === "B" ? [taskB] : [taskA, taskB]),
|
||
{ headers: { "Content-Type": "application/json" } },
|
||
);
|
||
}),
|
||
);
|
||
render(
|
||
<Ui依赖装配>
|
||
<MemoryRouter>
|
||
<Ui任务列表 />
|
||
</MemoryRouter>
|
||
</Ui依赖装配>,
|
||
);
|
||
await screen.findByRole("button", { name: /甲的任务/ });
|
||
fireEvent.change(screen.getByLabelText("作品范围"), {
|
||
target: { value: "A" },
|
||
});
|
||
await waitFor(() =>
|
||
expect(fetch).toHaveBeenCalledWith(
|
||
expect.objectContaining({ url: expect.stringContaining("work_id=A") }),
|
||
),
|
||
);
|
||
fireEvent.change(screen.getByLabelText("作品范围"), {
|
||
target: { value: "B" },
|
||
});
|
||
await screen.findByRole("button", { name: /乙的任务/ });
|
||
finishA(
|
||
new Response(JSON.stringify([taskA]), {
|
||
headers: { "Content-Type": "application/json" },
|
||
}),
|
||
);
|
||
await waitFor(() =>
|
||
expect(screen.queryByRole("button", { name: /甲的任务/ })).toBeNull(),
|
||
);
|
||
expect(screen.getByRole("button", { name: /乙的任务/ })).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"O08-task-list-global-dedupe","then":"全部作品列表与筛选来源共用相同查询键,只发一次同范围GET"}
|
||
it("O08-task-list-global-dedupe:全局列表取数复用", async () => {
|
||
let reads = 0;
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
if (new URL(request.url).pathname === "/api/v1/tasks") reads++;
|
||
return new Response(JSON.stringify([taskA, taskB]), {
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
}),
|
||
);
|
||
render(
|
||
<Ui依赖装配>
|
||
<MemoryRouter>
|
||
<Ui任务列表 />
|
||
</MemoryRouter>
|
||
</Ui依赖装配>,
|
||
);
|
||
await screen.findByRole("button", { name: /甲的任务/ });
|
||
expect(reads).toBe(1);
|
||
});
|