diff --git a/tests/e2e/diagram-generation.spec.ts b/tests/e2e/diagram-generation.spec.ts
new file mode 100644
index 0000000..fdf7e84
--- /dev/null
+++ b/tests/e2e/diagram-generation.spec.ts
@@ -0,0 +1,244 @@
+import { expect, test } from "@playwright/test"
+
+// Simple cat diagram XML for testing (mxCell elements only, no wrapper)
+const CAT_DIAGRAM_XML = `
+
+
+
+
+`
+
+// Simple flowchart XML for testing edits
+const FLOWCHART_XML = `
+
+
+
+
+
+
+
+`
+
+// Helper to create SSE-formatted UI message stream response
+function createMockSSEResponse(xml: string, toolName = "display_diagram") {
+ const messageId = `msg_${Date.now()}`
+ const toolCallId = `call_${Date.now()}`
+ const textId = `text_${Date.now()}`
+
+ // SSE format: each event is "data: \n\n"
+ const events = [
+ // Message start
+ { type: "start", messageId },
+ // Text content
+ { type: "text-start", id: textId },
+ {
+ type: "text-delta",
+ id: textId,
+ delta: "I'll create a diagram for you.",
+ },
+ { type: "text-end", id: textId },
+ // Tool call
+ { type: "tool-input-start", toolCallId, toolName },
+ {
+ type: "tool-input-available",
+ toolCallId,
+ toolName,
+ input: { xml },
+ },
+ {
+ type: "tool-output-available",
+ toolCallId,
+ output: "Successfully displayed the diagram",
+ },
+ // Finish
+ { type: "finish" },
+ ]
+
+ return (
+ events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
+ "data: [DONE]\n\n"
+ )
+}
+
+test.describe("Diagram Generation", () => {
+ test.beforeEach(async ({ page }) => {
+ // Mock the chat API to return our test XML
+ await page.route("**/api/chat", async (route) => {
+ const response = createMockSSEResponse(CAT_DIAGRAM_XML)
+ await route.fulfill({
+ status: 200,
+ contentType: "text/event-stream",
+ body: response,
+ })
+ })
+
+ await page.goto("/", { waitUntil: "networkidle" })
+ await page
+ .locator("iframe")
+ .waitFor({ state: "visible", timeout: 30000 })
+ })
+
+ test("generates and displays a diagram", async ({ page }) => {
+ // Find the chat input by aria-label
+ const chatInput = page.locator('textarea[aria-label="Chat input"]')
+ await expect(chatInput).toBeVisible({ timeout: 10000 })
+
+ // Type a prompt
+ await chatInput.fill("Draw a cat")
+
+ // Submit using Cmd/Ctrl+Enter
+ await chatInput.press("ControlOrMeta+Enter")
+
+ // Wait for the tool card with "Generate Diagram" header and "Complete" badge
+ await expect(page.locator('text="Generate Diagram"')).toBeVisible({
+ timeout: 15000,
+ })
+ await expect(page.locator('text="Complete"')).toBeVisible({
+ timeout: 15000,
+ })
+ })
+
+ test("chat input clears after sending", async ({ page }) => {
+ const chatInput = page.locator('textarea[aria-label="Chat input"]')
+ await expect(chatInput).toBeVisible({ timeout: 10000 })
+
+ await chatInput.fill("Draw a cat")
+ await chatInput.press("ControlOrMeta+Enter")
+
+ // Input should clear after sending
+ await expect(chatInput).toHaveValue("", { timeout: 5000 })
+ })
+
+ test("user message appears in chat", async ({ page }) => {
+ const chatInput = page.locator('textarea[aria-label="Chat input"]')
+ await expect(chatInput).toBeVisible({ timeout: 10000 })
+
+ await chatInput.fill("Draw a cute cat")
+ await chatInput.press("ControlOrMeta+Enter")
+
+ // User message should appear
+ await expect(page.locator('text="Draw a cute cat"')).toBeVisible({
+ timeout: 10000,
+ })
+ })
+
+ test("assistant text message appears in chat", async ({ page }) => {
+ const chatInput = page.locator('textarea[aria-label="Chat input"]')
+ await expect(chatInput).toBeVisible({ timeout: 10000 })
+
+ await chatInput.fill("Draw a cat")
+ await chatInput.press("ControlOrMeta+Enter")
+
+ // Assistant message should appear
+ await expect(
+ page.locator('text="I\'ll create a diagram for you."'),
+ ).toBeVisible({ timeout: 10000 })
+ })
+})
+
+test.describe("Diagram Edit", () => {
+ test.beforeEach(async ({ page }) => {
+ // First request: display initial diagram
+ // Second request: edit diagram
+ let requestCount = 0
+ await page.route("**/api/chat", async (route) => {
+ requestCount++
+ if (requestCount === 1) {
+ await route.fulfill({
+ status: 200,
+ contentType: "text/event-stream",
+ body: createMockSSEResponse(FLOWCHART_XML),
+ })
+ } else {
+ // Edit response - replaces the diagram
+ const editedXml = FLOWCHART_XML.replace(
+ "Process",
+ "Updated Process",
+ )
+ await route.fulfill({
+ status: 200,
+ contentType: "text/event-stream",
+ body: createMockSSEResponse(editedXml),
+ })
+ }
+ })
+
+ await page.goto("/", { waitUntil: "networkidle" })
+ await page
+ .locator("iframe")
+ .waitFor({ state: "visible", timeout: 30000 })
+ })
+
+ test("can edit an existing diagram", async ({ page }) => {
+ const chatInput = page.locator('textarea[aria-label="Chat input"]')
+ await expect(chatInput).toBeVisible({ timeout: 10000 })
+
+ // First: create initial diagram
+ await chatInput.fill("Create a flowchart")
+ await chatInput.press("ControlOrMeta+Enter")
+ await expect(page.locator('text="Complete"').first()).toBeVisible({
+ timeout: 15000,
+ })
+
+ // Second: edit the diagram
+ await chatInput.fill("Change Process to Updated Process")
+ await chatInput.press("ControlOrMeta+Enter")
+
+ // Should see second "Complete" badge
+ await expect(page.locator('text="Complete"')).toHaveCount(2, {
+ timeout: 15000,
+ })
+ })
+})
+
+test.describe("Diagram Append", () => {
+ test.beforeEach(async ({ page }) => {
+ let requestCount = 0
+ await page.route("**/api/chat", async (route) => {
+ requestCount++
+ if (requestCount === 1) {
+ await route.fulfill({
+ status: 200,
+ contentType: "text/event-stream",
+ body: createMockSSEResponse(FLOWCHART_XML),
+ })
+ } else {
+ // Append response - adds new element
+ const appendXml = `
+
+`
+ await route.fulfill({
+ status: 200,
+ contentType: "text/event-stream",
+ body: createMockSSEResponse(appendXml, "append_diagram"),
+ })
+ }
+ })
+
+ await page.goto("/", { waitUntil: "networkidle" })
+ await page
+ .locator("iframe")
+ .waitFor({ state: "visible", timeout: 30000 })
+ })
+
+ test("can append to an existing diagram", async ({ page }) => {
+ const chatInput = page.locator('textarea[aria-label="Chat input"]')
+ await expect(chatInput).toBeVisible({ timeout: 10000 })
+
+ // First: create initial diagram
+ await chatInput.fill("Create a flowchart")
+ await chatInput.press("ControlOrMeta+Enter")
+ await expect(page.locator('text="Complete"').first()).toBeVisible({
+ timeout: 15000,
+ })
+
+ // Second: append to diagram
+ await chatInput.fill("Add a new node to the right")
+ await chatInput.press("ControlOrMeta+Enter")
+
+ // Should see second "Complete" badge
+ await expect(page.locator('text="Complete"')).toHaveCount(2, {
+ timeout: 15000,
+ })
+ })
+})