deer-flow/frontend/tests/e2e/user-message-plain-text.spec.ts
Aari 5ffc2d3e27
feat(mcp): complete durable task notifications and chat UI (#4833)
* feat(mcp): add reliable task notifications and cancellation

* feat(mcp): add background task chat UI

* fix(mcp): hide and sanitize task notification prompts

* fix(mcp): sanitize projected task names

* fix(mcp): harden task notifications and details

* fix(mcp): harden task lifecycle recovery

* fix(mcp): gate task UI and isolate cancellations

* test: scope plain-text response locator

* fix(mcp): align task notification boundaries

* fix(mcp): bound task delivery retries

* fix background task notification races
2026-08-22 16:53:32 +08:00

144 lines
4.5 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mockLangGraphAPI, MOCK_THREAD_ID } from "./utils/mock-api";
const C_SOURCE = `#include <stdio.h>
#include <signal.h>
static volatile int connected = 0;
static void daemon_handle_signal(int sig) {
if (sig == SIGTERM) {
connected = 0;
printf("daemon stop requested\\n");
return;
}
printf("ignored signal %d\\n", sig);
}`;
function threadWithMessages(
humanText: string,
aiText = "ack",
): Parameters<typeof mockLangGraphAPI>[1] {
return {
threads: [
{
thread_id: MOCK_THREAD_ID,
title: "Plain text rendering",
updated_at: "2025-06-01T12:00:00Z",
messages: [
{
type: "human",
id: "msg-human-plain-text",
content: [{ type: "text", text: humanText }],
},
{
type: "ai",
id: "msg-ai-plain-text",
content: aiText,
},
],
},
],
};
}
function collectPageErrors(page: Page): string[] {
const errors: string[] = [];
page.on("pageerror", (error) => {
errors.push(`${error.name}: ${error.message}`);
});
return errors;
}
test.describe("User message plain-text rendering", () => {
test("pasted source code renders verbatim as one block", async ({ page }) => {
mockLangGraphAPI(page, threadWithMessages(C_SOURCE));
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await expect(
page.getByTestId("main-message-list").getByText("ack", { exact: true }),
).toBeVisible({ timeout: 15_000 });
// The pasted file must not be split into Markdown code-block widgets.
await expect(
page.locator('[data-code-block-container="true"]'),
).toHaveCount(0);
// Indentation and line structure must be preserved verbatim.
const bubble = page.locator(".is-user");
const text = await bubble.innerText();
expect(text).toContain("#include <stdio.h>");
expect(text).toContain(" if (sig == SIGTERM) {");
expect(text).toContain(' printf("daemon stop requested\\n");');
});
test("dollar signs are not parsed as math", async ({ page }) => {
const message = "this costs $5 and $10 in total";
mockLangGraphAPI(page, threadWithMessages(message));
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await expect(
page.getByTestId("main-message-list").getByText("ack", { exact: true }),
).toBeVisible({ timeout: 15_000 });
await expect(page.locator(".is-user")).toContainText(message);
await expect(page.locator(".is-user .katex")).toHaveCount(0);
});
test("deeply nested blockquote markers in a user message do not crash the page", async ({
page,
}) => {
const pageErrors = collectPageErrors(page);
mockLangGraphAPI(page, threadWithMessages("> ".repeat(3000) + "hi"));
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await expect(
page.getByTestId("main-message-list").getByText("ack", { exact: true }),
).toBeVisible({ timeout: 15_000 });
expect(pageErrors).toEqual([]);
await expect(page.locator(".is-user")).toContainText("> > >");
});
test("deeply nested blockquote markers in an AI message do not crash the page", async ({
page,
}) => {
const pageErrors = collectPageErrors(page);
mockLangGraphAPI(
page,
threadWithMessages("hello", "> ".repeat(3000) + "deep"),
);
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await expect(page.getByText("hello")).toBeVisible({ timeout: 15_000 });
expect(pageErrors).toEqual([]);
// The capped blockquote chain still renders (100 levels of indentation can
// squeeze the innermost element to zero width, so assert presence, not
// visibility).
await expect(page.getByText("deep")).toBeAttached();
});
test("list-prefixed deep nesting in an AI message falls back to plain text instead of crashing", async ({
page,
}) => {
// marked's list and blockquote tokenizers are mutually recursive, so a
// list marker in front of the quote chain bypasses the nesting cap; the
// render error boundary must absorb it.
const pageErrors = collectPageErrors(page);
mockLangGraphAPI(
page,
threadWithMessages("hello", "- " + "> ".repeat(3000) + "deep-list"),
);
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await expect(page.getByText("hello")).toBeVisible({ timeout: 15_000 });
expect(pageErrors).toEqual([]);
await expect(page.getByText("deep-list")).toBeAttached();
});
});