diff --git a/tests/e2e/copy-paste.spec.ts b/tests/e2e/copy-paste.spec.ts
index 191824d..5a3326c 100644
--- a/tests/e2e/copy-paste.spec.ts
+++ b/tests/e2e/copy-paste.spec.ts
@@ -1,36 +1,5 @@
import { expect, test } from "@playwright/test"
-
-// Helper to create SSE response
-function createMockSSEResponse(xml: string, text: string) {
- const messageId = `msg_${Date.now()}`
- const toolCallId = `call_${Date.now()}`
- const textId = `text_${Date.now()}`
-
- const events = [
- { type: "start", messageId },
- { type: "text-start", id: textId },
- { type: "text-delta", id: textId, delta: text },
- { type: "text-end", id: textId },
- { type: "tool-input-start", toolCallId, toolName: "display_diagram" },
- {
- type: "tool-input-available",
- toolCallId,
- toolName: "display_diagram",
- input: { xml },
- },
- {
- type: "tool-output-available",
- toolCallId,
- output: "Successfully displayed the diagram",
- },
- { type: "finish" },
- ]
-
- return (
- events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
- "data: [DONE]\n\n"
- )
-}
+import { createMockSSEResponse } from "./lib/helpers"
test.describe("Copy/Paste Functionality", () => {
test("can paste text into chat input", async ({ page }) => {
diff --git a/tests/e2e/diagram-generation.spec.ts b/tests/e2e/diagram-generation.spec.ts
index fdf7e84..5d12c5e 100644
--- a/tests/e2e/diagram-generation.spec.ts
+++ b/tests/e2e/diagram-generation.spec.ts
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test"
+import { createMockSSEResponse } from "./lib/helpers"
// Simple cat diagram XML for testing (mxCell elements only, no wrapper)
const CAT_DIAGRAM_XML = `
@@ -19,52 +20,14 @@ 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)
+ const response = createMockSSEResponse(
+ CAT_DIAGRAM_XML,
+ "I'll create a diagram for you.",
+ )
await route.fulfill({
status: 200,
contentType: "text/event-stream",
@@ -147,7 +110,10 @@ test.describe("Diagram Edit", () => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
- body: createMockSSEResponse(FLOWCHART_XML),
+ body: createMockSSEResponse(
+ FLOWCHART_XML,
+ "I'll create a diagram for you.",
+ ),
})
} else {
// Edit response - replaces the diagram
@@ -158,7 +124,10 @@ test.describe("Diagram Edit", () => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
- body: createMockSSEResponse(editedXml),
+ body: createMockSSEResponse(
+ editedXml,
+ "I'll create a diagram for you.",
+ ),
})
}
})
@@ -200,7 +169,10 @@ test.describe("Diagram Append", () => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
- body: createMockSSEResponse(FLOWCHART_XML),
+ body: createMockSSEResponse(
+ FLOWCHART_XML,
+ "I'll create a diagram for you.",
+ ),
})
} else {
// Append response - adds new element
@@ -210,7 +182,11 @@ test.describe("Diagram Append", () => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
- body: createMockSSEResponse(appendXml, "append_diagram"),
+ body: createMockSSEResponse(
+ appendXml,
+ "I'll create a diagram for you.",
+ "append_diagram",
+ ),
})
}
})
diff --git a/tests/e2e/error-handling.spec.ts b/tests/e2e/error-handling.spec.ts
index 268220a..d129003 100644
--- a/tests/e2e/error-handling.spec.ts
+++ b/tests/e2e/error-handling.spec.ts
@@ -21,8 +21,14 @@ test.describe("Error Handling", () => {
await chatInput.fill("Draw a cat")
await chatInput.press("ControlOrMeta+Enter")
- // After error, user should be able to type again (input still functional)
- await page.waitForTimeout(2000) // Wait for error response
+ // Should show error indication (toast, alert, or error text)
+ const errorIndicator = page
+ .locator('[role="alert"]')
+ .or(page.locator("[data-sonner-toast]"))
+ .or(page.locator("text=/error|failed|something went wrong/i"))
+ await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
+
+ // User should be able to type again (input still functional)
await chatInput.fill("Retry message")
await expect(chatInput).toHaveValue("Retry message")
})
@@ -49,8 +55,14 @@ test.describe("Error Handling", () => {
await chatInput.fill("Draw a cat")
await chatInput.press("ControlOrMeta+Enter")
- // After rate limit error, user should be able to type again
- await page.waitForTimeout(2000) // Wait for error response
+ // Should show error indication for rate limit
+ const errorIndicator = page
+ .locator('[role="alert"]')
+ .or(page.locator("[data-sonner-toast]"))
+ .or(page.locator("text=/rate limit|too many|try again/i"))
+ await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
+
+ // User should be able to type again
await chatInput.fill("Retry after rate limit")
await expect(chatInput).toHaveValue("Retry after rate limit")
})
@@ -73,8 +85,12 @@ test.describe("Error Handling", () => {
await chatInput.fill("Draw a cat")
await chatInput.press("ControlOrMeta+Enter")
- // Wait for timeout error to occur
- await page.waitForTimeout(3000)
+ // Should show error indication for network failure
+ const errorIndicator = page
+ .locator('[role="alert"]')
+ .or(page.locator("[data-sonner-toast]"))
+ .or(page.locator("text=/error|failed|network|timeout/i"))
+ await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
// After timeout, user should be able to type again
await chatInput.fill("Try again after timeout")
diff --git a/tests/e2e/file-upload.spec.ts b/tests/e2e/file-upload.spec.ts
index 740a445..46a9d50 100644
--- a/tests/e2e/file-upload.spec.ts
+++ b/tests/e2e/file-upload.spec.ts
@@ -1,37 +1,5 @@
-import path from "node:path"
import { expect, test } from "@playwright/test"
-
-// Helper to create SSE response
-function createMockSSEResponse(xml: string, text: string) {
- const messageId = `msg_${Date.now()}`
- const toolCallId = `call_${Date.now()}`
- const textId = `text_${Date.now()}`
-
- const events = [
- { type: "start", messageId },
- { type: "text-start", id: textId },
- { type: "text-delta", id: textId, delta: text },
- { type: "text-end", id: textId },
- { type: "tool-input-start", toolCallId, toolName: "display_diagram" },
- {
- type: "tool-input-available",
- toolCallId,
- toolName: "display_diagram",
- input: { xml },
- },
- {
- type: "tool-output-available",
- toolCallId,
- output: "Successfully displayed the diagram",
- },
- { type: "finish" },
- ]
-
- return (
- events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
- "data: [DONE]\n\n"
- )
-}
+import { createMockSSEResponse } from "./lib/helpers"
test.describe("File Upload", () => {
test("upload button opens file picker", async ({ page }) => {
diff --git a/tests/e2e/history-restore.spec.ts b/tests/e2e/history-restore.spec.ts
index a5da1fa..f217747 100644
--- a/tests/e2e/history-restore.spec.ts
+++ b/tests/e2e/history-restore.spec.ts
@@ -1,36 +1,5 @@
import { expect, test } from "@playwright/test"
-
-// Helper to create SSE response
-function createMockSSEResponse(xml: string, text: string) {
- const messageId = `msg_${Date.now()}`
- const toolCallId = `call_${Date.now()}`
- const textId = `text_${Date.now()}`
-
- const events = [
- { type: "start", messageId },
- { type: "text-start", id: textId },
- { type: "text-delta", id: textId, delta: text },
- { type: "text-end", id: textId },
- { type: "tool-input-start", toolCallId, toolName: "display_diagram" },
- {
- type: "tool-input-available",
- toolCallId,
- toolName: "display_diagram",
- input: { xml },
- },
- {
- type: "tool-output-available",
- toolCallId,
- output: "Successfully displayed the diagram",
- },
- { type: "finish" },
- ]
-
- return (
- events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
- "data: [DONE]\n\n"
- )
-}
+import { createMockSSEResponse } from "./lib/helpers"
test.describe("History and Session Restore", () => {
test("new chat button clears conversation", async ({ page }) => {
diff --git a/tests/e2e/iframe-interaction.spec.ts b/tests/e2e/iframe-interaction.spec.ts
index d8f7a02..fc5c26d 100644
--- a/tests/e2e/iframe-interaction.spec.ts
+++ b/tests/e2e/iframe-interaction.spec.ts
@@ -1,36 +1,5 @@
import { expect, test } from "@playwright/test"
-
-// Helper to create SSE response
-function createMockSSEResponse(xml: string, text: string) {
- const messageId = `msg_${Date.now()}`
- const toolCallId = `call_${Date.now()}`
- const textId = `text_${Date.now()}`
-
- const events = [
- { type: "start", messageId },
- { type: "text-start", id: textId },
- { type: "text-delta", id: textId, delta: text },
- { type: "text-end", id: textId },
- { type: "tool-input-start", toolCallId, toolName: "display_diagram" },
- {
- type: "tool-input-available",
- toolCallId,
- toolName: "display_diagram",
- input: { xml },
- },
- {
- type: "tool-output-available",
- toolCallId,
- output: "Successfully displayed the diagram",
- },
- { type: "finish" },
- ]
-
- return (
- events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
- "data: [DONE]\n\n"
- )
-}
+import { createMockSSEResponse } from "./lib/helpers"
test.describe("Iframe Interaction", () => {
test("draw.io iframe loads successfully", async ({ page }) => {
diff --git a/tests/e2e/lib/helpers.ts b/tests/e2e/lib/helpers.ts
new file mode 100644
index 0000000..70cbaed
--- /dev/null
+++ b/tests/e2e/lib/helpers.ts
@@ -0,0 +1,88 @@
+/**
+ * Shared test helpers for E2E tests
+ */
+
+/**
+ * Creates a mock SSE response for the chat API
+ * Format matches AI SDK UI message stream protocol
+ */
+export function createMockSSEResponse(
+ xml: string,
+ text: string,
+ toolName = "display_diagram",
+) {
+ const messageId = `msg_${Date.now()}`
+ const toolCallId = `call_${Date.now()}`
+ const textId = `text_${Date.now()}`
+
+ const events = [
+ { type: "start", messageId },
+ { type: "text-start", id: textId },
+ { type: "text-delta", id: textId, delta: text },
+ { type: "text-end", id: textId },
+ { type: "tool-input-start", toolCallId, toolName },
+ { type: "tool-input-available", toolCallId, toolName, input: { xml } },
+ {
+ type: "tool-output-available",
+ toolCallId,
+ output: "Successfully displayed the diagram",
+ },
+ { type: "finish" },
+ ]
+
+ return (
+ events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
+ "data: [DONE]\n\n"
+ )
+}
+
+/**
+ * Creates a text-only SSE response (no tool call)
+ */
+export function createTextOnlyResponse(text: string) {
+ const messageId = `msg_${Date.now()}`
+ const textId = `text_${Date.now()}`
+
+ const events = [
+ { type: "start", messageId },
+ { type: "text-start", id: textId },
+ { type: "text-delta", id: textId, delta: text },
+ { type: "text-end", id: textId },
+ { type: "finish" },
+ ]
+
+ return (
+ events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
+ "data: [DONE]\n\n"
+ )
+}
+
+/**
+ * Creates a mock SSE response with a tool error
+ */
+export function createToolErrorResponse(text: string, errorMessage: string) {
+ const messageId = `msg_${Date.now()}`
+ const toolCallId = `call_${Date.now()}`
+ const textId = `text_${Date.now()}`
+
+ const events = [
+ { type: "start", messageId },
+ { type: "text-start", id: textId },
+ { type: "text-delta", id: textId, delta: text },
+ { type: "text-end", id: textId },
+ { type: "tool-input-start", toolCallId, toolName: "display_diagram" },
+ {
+ type: "tool-input-available",
+ toolCallId,
+ toolName: "display_diagram",
+ input: { xml: "" },
+ },
+ { type: "tool-output-error", toolCallId, error: errorMessage },
+ { type: "finish" },
+ ]
+
+ return (
+ events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
+ "data: [DONE]\n\n"
+ )
+}
diff --git a/tests/e2e/multi-turn.spec.ts b/tests/e2e/multi-turn.spec.ts
index 7e7a1e4..e722eb5 100644
--- a/tests/e2e/multi-turn.spec.ts
+++ b/tests/e2e/multi-turn.spec.ts
@@ -1,54 +1,5 @@
import { expect, test } from "@playwright/test"
-
-// Helper to create SSE response
-function createMockSSEResponse(
- xml: string,
- text: string,
- toolName = "display_diagram",
-) {
- const messageId = `msg_${Date.now()}`
- const toolCallId = `call_${Date.now()}`
- const textId = `text_${Date.now()}`
-
- const events = [
- { type: "start", messageId },
- { type: "text-start", id: textId },
- { type: "text-delta", id: textId, delta: text },
- { type: "text-end", id: textId },
- { type: "tool-input-start", toolCallId, toolName },
- { type: "tool-input-available", toolCallId, toolName, input: { xml } },
- {
- type: "tool-output-available",
- toolCallId,
- output: "Successfully displayed the diagram",
- },
- { type: "finish" },
- ]
-
- return (
- events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
- "data: [DONE]\n\n"
- )
-}
-
-// Helper for text-only response
-function createTextOnlyResponse(text: string) {
- const messageId = `msg_${Date.now()}`
- const textId = `text_${Date.now()}`
-
- const events = [
- { type: "start", messageId },
- { type: "text-start", id: textId },
- { type: "text-delta", id: textId, delta: text },
- { type: "text-end", id: textId },
- { type: "finish" },
- ]
-
- return (
- events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") +
- "data: [DONE]\n\n"
- )
-}
+import { createMockSSEResponse, createTextOnlyResponse } from "./lib/helpers"
test.describe("Multi-turn Conversation", () => {
test("handles multiple diagram requests in sequence", async ({ page }) => {