From 6ce7e3378b917a23a1b86e1b282c1588b1579b24 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 4 Jan 2026 21:14:08 +0900 Subject: [PATCH] refactor: extract shared test helpers and improve error assertions - Create tests/e2e/lib/helpers.ts with shared SSE mock functions - Add proper error UI assertions to error-handling.spec.ts - Remove waitForTimeout calls in favor of real assertions - Update 6 test files to use shared helpers --- tests/e2e/copy-paste.spec.ts | 33 +---------- tests/e2e/diagram-generation.spec.ts | 68 +++++++-------------- tests/e2e/error-handling.spec.ts | 28 +++++++-- tests/e2e/file-upload.spec.ts | 34 +---------- tests/e2e/history-restore.spec.ts | 33 +---------- tests/e2e/iframe-interaction.spec.ts | 33 +---------- tests/e2e/lib/helpers.ts | 88 ++++++++++++++++++++++++++++ tests/e2e/multi-turn.spec.ts | 51 +--------------- 8 files changed, 137 insertions(+), 231 deletions(-) create mode 100644 tests/e2e/lib/helpers.ts 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 }) => {