- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
98 lines
3.9 KiB
TypeScript
98 lines
3.9 KiB
TypeScript
import { readFileSync } from "node:fs";
|
||
import { expect, test } from "@playwright/test";
|
||
test.use({ trace: "off" });
|
||
// @muse-case {"case_id":"O08-browser-branch-buffer-lifecycle","then":"真实浏览器中同章两分支草稿刷新不串用,未知保存刷新重放原请求;终态任务没有持续事件请求"}
|
||
test("O08-browser-branch-buffer-lifecycle:分支缓冲与终态读取旅程", async ({
|
||
page,
|
||
}, info) => {
|
||
test.setTimeout(120_000);
|
||
const work = process.env.MUSE_WORK_ID,
|
||
chapter = process.env.MUSE_CHAPTER_ID,
|
||
taskId = process.env.MUSE_TERMINAL_TASK_ID;
|
||
const branchA = process.env.MUSE_BRANCH_A,
|
||
branchB = process.env.MUSE_BRANCH_B;
|
||
if (
|
||
!work ||
|
||
!chapter ||
|
||
!taskId ||
|
||
!branchA ||
|
||
!branchB ||
|
||
branchA === branchB
|
||
)
|
||
throw new Error(
|
||
"需要预置合成作品、同章两个独立分支与已completed/cancelled任务身份。",
|
||
);
|
||
await page.goto("/");
|
||
await page
|
||
.getByLabel("访问口令")
|
||
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE!, "utf8").trim());
|
||
await page.getByRole("button", { name: "进入工作台" }).click();
|
||
const route = (branch: string) =>
|
||
`/works/${encodeURIComponent(work)}/candidates/${encodeURIComponent(chapter)}?branch=${encodeURIComponent(branch)}`;
|
||
await page.goto(route(branchA));
|
||
await page.getByRole("button", { name: "另拟改稿" }).click();
|
||
const aText = `A分支合成稿-${Date.now()}`;
|
||
await page.getByRole("textbox", { name: "候选改稿" }).fill(aText);
|
||
await page.goto(route(branchB));
|
||
await page.getByRole("button", { name: "另拟改稿" }).click();
|
||
await expect(
|
||
page.getByRole("textbox", { name: "候选改稿" }),
|
||
).not.toContainText(aText);
|
||
const bText = `B分支合成稿-${Date.now()}`;
|
||
await page.getByRole("textbox", { name: "候选改稿" }).fill(bText);
|
||
await page.reload();
|
||
await page.getByRole("button", { name: "另拟改稿" }).click();
|
||
await expect(page.getByRole("textbox", { name: "候选改稿" })).toContainText(
|
||
bText,
|
||
);
|
||
await page.goto(route(branchA));
|
||
await page.getByRole("button", { name: "另拟改稿" }).click();
|
||
await expect(page.getByRole("textbox", { name: "候选改稿" })).toContainText(
|
||
aText,
|
||
);
|
||
const requests: unknown[] = [];
|
||
let lose = true;
|
||
await page.route(
|
||
`**/api/v1/chapters/${encodeURIComponent(chapter)}/writing-candidates`,
|
||
async (intercepted) => {
|
||
if (intercepted.request().method() !== "POST") {
|
||
await intercepted.continue();
|
||
return;
|
||
}
|
||
requests.push(intercepted.request().postDataJSON());
|
||
const response = await intercepted.fetch();
|
||
if (lose) {
|
||
lose = false;
|
||
await intercepted.abort("failed");
|
||
} else await intercepted.fulfill({ response });
|
||
},
|
||
);
|
||
await page.getByRole("button", { name: "保存候选版本", exact: true }).click();
|
||
await expect(
|
||
page.getByRole("button", { name: "重试候选保存" }),
|
||
).toBeVisible();
|
||
await page.reload();
|
||
await page.getByRole("button", { name: "另拟改稿" }).click();
|
||
await page.getByRole("button", { name: "重试候选保存" }).click();
|
||
await expect(page.getByText(/候选新版本已保存/)).toBeVisible();
|
||
expect(requests).toHaveLength(2);
|
||
expect(requests[1]).toEqual(requests[0]);
|
||
const streamRequests: string[] = [],
|
||
taskReads: string[] = [];
|
||
page.on("request", (request) => {
|
||
if (request.url().includes("/events/stream"))
|
||
streamRequests.push(request.url());
|
||
if (new URL(request.url()).pathname === `/api/v1/tasks/${taskId}`)
|
||
taskReads.push(request.url());
|
||
});
|
||
await page.goto(`/tasks?task=${encodeURIComponent(taskId)}`);
|
||
await expect(page.getByText("执行已收尾")).toBeVisible();
|
||
await page.waitForTimeout(60_000);
|
||
expect(streamRequests).toHaveLength(0);
|
||
expect(taskReads).toHaveLength(1);
|
||
await page.screenshot({
|
||
path: info.outputPath("终态执行与独立账目.png"),
|
||
fullPage: true,
|
||
});
|
||
});
|