deer-flow/frontend/tests/e2e/artifact-draft-navigation.spec.ts
Lucien c5fd453c37
fix(artifacts): preserve account-scoped drafts across navigation (#6700)
* fix(artifacts): preserve account-scoped drafts across navigation

* docs(artifacts): keep draft lifecycle guidance within its budget

* fix(artifacts): retain drafts through unknown auth and isolate typing updates

* fix(artifacts): preserve drafts when visiting public showcases

* fix(artifacts): preserve edits after pending save
2026-10-13 15:44:20 +08:00

127 lines
4.3 KiB
TypeScript

import { createHash } from "node:crypto";
import { expect, test, type Page } from "@playwright/test";
import { mockLangGraphAPI } from "./utils/mock-api";
const threadA = "00000000-0000-0000-0000-000000006696";
const threadB = "00000000-0000-0000-0000-000000006697";
const filepath = "/mnt/user-data/outputs/navigation-report.txt";
const original = "Original saved report";
const draft = "My draft survives chat navigation";
async function navigateToChat(page: Page, threadId: string) {
const link = page.locator(`a[href="/workspace/chats/${threadId}"]`).first();
if (!(await link.isVisible())) {
await page.getByRole("button", { name: "Toggle Sidebar" }).first().click();
}
await link.click();
await expect(page).toHaveURL(new RegExp(`/workspace/chats/${threadId}$`));
}
test("retains drafts through chat navigation and keeps Save and Discard final", async ({
page,
}) => {
mockLangGraphAPI(page, {
threads: [
{
thread_id: threadA,
title: "Draft navigation A",
artifacts: [filepath],
messages: [
{ type: "human", id: "draft-human", content: "Write a report" },
{
type: "ai",
id: "draft-ai",
content: "Here is the report",
tool_calls: [
{
id: "draft-present",
name: "present_files",
args: { filepaths: [filepath] },
},
],
},
],
},
{ thread_id: threadB, title: "Draft navigation B", messages: [] },
],
});
let saved = original;
let putCount = 0;
const sha256 = () => createHash("sha256").update(saved).digest("hex");
await page.route(
`**/api/threads/${threadA}/artifacts${filepath}`,
(route) => {
if (route.request().method() === "PUT") {
const body = route.request().postDataJSON() as {
content: string;
expected_sha256: string;
};
expect(body.expected_sha256).toBe(sha256());
saved = body.content;
putCount++;
return route.fulfill({
contentType: "application/json",
body: JSON.stringify({
path: filepath,
sha256: sha256(),
size: Buffer.byteLength(saved),
}),
});
}
return route.fulfill({
contentType: "text/plain; charset=utf-8",
headers: { ETag: `"${sha256()}"` },
body: saved,
});
},
);
await page.goto(`/workspace/chats/${threadA}`);
await page
.getByText("navigation-report.txt", { exact: true })
.first()
.click();
const panel = page.locator("#artifacts");
await expect(panel.locator(".cm-content")).toHaveText(original);
await panel.getByRole("button", { name: "Edit", exact: true }).click();
await panel.locator('.cm-content[contenteditable="true"]').fill(draft);
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(1);
await page.screenshot({
path: "output/playwright/artifact-draft-before-navigation.png",
});
await navigateToChat(page, threadB);
await navigateToChat(page, threadA);
await expect(panel.locator(".cm-content")).toHaveText(draft);
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(1);
await panel.getByRole("button", { name: "Edit", exact: true }).click();
await panel.getByRole("button", { name: "Save", exact: true }).click();
await expect(
panel.getByRole("button", { name: "Save", exact: true }),
).toBeDisabled();
expect(putCount).toBe(1);
expect(saved).toBe(draft);
await navigateToChat(page, threadB);
await navigateToChat(page, threadA);
await expect(panel.locator(".cm-content")).toHaveText(draft);
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(0);
await panel.getByRole("button", { name: "Edit", exact: true }).click();
await panel
.locator('.cm-content[contenteditable="true"]')
.fill("Discard this edit");
page.once("dialog", (dialog) => dialog.accept());
await panel
.getByRole("button", { name: "Discard changes", exact: true })
.click();
await navigateToChat(page, threadB);
await navigateToChat(page, threadA);
await expect(panel.locator(".cm-content")).toHaveText(draft);
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(0);
await page.screenshot({
path: "output/playwright/artifact-draft-after-save-return.png",
});
});