import { expect, type Page, test } from "@playwright/test"
import { getIframe, sendMessage, waitForCompleteCount } from "./lib/fixtures"
/**
* Checks what draw.io actually shows after the diagram tools, not only the
* tool card. The tool input is streamed in chunks like a real model, and
* the browser tool handler (not the server) completes the tool call.
*/
function streamedToolCall(toolName: string, input: unknown) {
const toolCallId = `call_${Math.random().toString(36).slice(2)}`
const chunks = JSON.stringify(input).match(/[\s\S]{1,40}/g) ?? []
const events = [
{ type: "start", messageId: `msg_${toolCallId}` },
{ type: "tool-input-start", toolCallId, toolName },
...chunks.map((inputTextDelta) => ({
type: "tool-input-delta",
toolCallId,
inputTextDelta,
})),
{ type: "tool-input-available", toolCallId, toolName, input },
{ type: "finish" },
]
return `${events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("")}data: [DONE]\n\n`
}
const END_TURN =
'data: {"type":"start"}\n\ndata: {"type":"finish"}\n\ndata: [DONE]\n\n'
/** Answer each chat request with the next reply, then end the turn */
async function mockReplies(p: Page, replies: string[]) {
await p.route("**/api/chat", async (route) => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
body: replies.shift() ?? END_TURN,
})
})
await p.goto("/", { waitUntil: "networkidle" })
await getIframe(p).waitFor({ state: "visible", timeout: 30000 })
return p.frameLocator("iframe")
}
const cell = (id: string, label: string, x: number) =>
``
const page = (id: string, cells: string) =>
`${cells}`
const TWO_PAGES = `${page("First", cell("a", "Old A", 40))}${page("Second", cell("b", "Old B", 40))}`
// Bare cells with a duplicate id and an unescaped &, which get fixed, and
// a linked cell whose label lives on its UserObject wrapper
const NEW_CELLS =
cell("2", "Alpha", 40) +
cell("2", "Beta", 220) +
cell("3", "R&D", 400) +
``
test("display_diagram replaces the document with the fixed diagram", async ({
page: p,
}) => {
const canvas = await mockReplies(p, [
streamedToolCall("display_diagram", { xml: TWO_PAGES }),
streamedToolCall("display_diagram", { xml: NEW_CELLS }),
])
await sendMessage(p, "Draw two pages")
await waitForCompleteCount(p, 1)
await expect(canvas.getByText("Old A")).toBeVisible({ timeout: 15000 })
await expect(canvas.getByText("Second", { exact: true })).toBeVisible()
await sendMessage(p, "Start over with three boxes")
await waitForCompleteCount(p, 2)
// Give a late preview time to redraw the raw cells, as it used to
await p.waitForTimeout(1000)
for (const label of ["Alpha", "Beta", "R&D", "Docs"]) {
await expect(canvas.getByText(label, { exact: true })).toBeVisible({
timeout: 15000,
})
}
// The old pages are gone
await expect(canvas.getByText("Old A")).toHaveCount(0)
await expect(canvas.getByText("Second", { exact: true })).toHaveCount(0)
})
test("edit_diagram applies all operations or none", async ({ page: p }) => {
const canvas = await mockReplies(p, [
streamedToolCall("display_diagram", {
xml: cell("a", "Alpha", 40) + cell("b", "Beta", 220),
}),
streamedToolCall("edit_diagram", {
operations: [
{ operation: "delete", cell_id: "a" },
{
operation: "update",
cell_id: "b",
new_xml: cell("b", "Beta two", 220),
},
{
operation: "add",
cell_id: "c",
new_xml: cell("c", "Gamma", 400),
},
],
}),
// The first operation is fine, the second fails: nothing is kept
streamedToolCall("edit_diagram", {
operations: [
{
operation: "update",
cell_id: "c",
new_xml: cell("c", "Broken", 400),
},
{ operation: "delete", cell_id: "missing" },
],
}),
])
await sendMessage(p, "Draw two boxes")
await waitForCompleteCount(p, 1)
await expect(canvas.getByText("Alpha", { exact: true })).toBeVisible({
timeout: 15000,
})
await sendMessage(p, "Change them")
await waitForCompleteCount(p, 2)
await expect(canvas.getByText("Gamma", { exact: true })).toBeVisible({
timeout: 15000,
})
await expect(canvas.getByText("Beta two", { exact: true })).toBeVisible()
await expect(canvas.getByText("Alpha", { exact: true })).toHaveCount(0)
await sendMessage(p, "Change again")
await expect(p.getByText(/No changes were made/).first()).toBeAttached({
timeout: 15000,
})
await p.waitForTimeout(1000)
await expect(canvas.getByText("Gamma", { exact: true })).toBeVisible()
await expect(canvas.getByText("Broken", { exact: true })).toHaveCount(0)
})