From 828bf43e31f47890e02a2ddd8e63ab4077105262 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Mon, 5 Jan 2026 00:37:40 +0900 Subject: [PATCH] refactor: add shared fixtures and test.step() patterns - Add tests/e2e/lib/fixtures.ts with shared test helpers - Add tests/e2e/fixtures/diagrams.ts with XML test data - Add expectBeforeAndAfterReload() helper for persistence tests - Add test.step() for better test reporting in complex tests - Consolidate mock helpers into fixtures module - Reduce code duplication across 17 test files --- tests/e2e/chat.spec.ts | 10 +- tests/e2e/copy-paste.spec.ts | 84 ++++------ tests/e2e/diagram-generation.spec.ts | 196 +++++++---------------- tests/e2e/error-handling.spec.ts | 86 ++++------- tests/e2e/file-upload.spec.ts | 70 +++------ tests/e2e/fixtures/diagrams.ts | 50 ++++++ tests/e2e/history-restore.spec.ts | 222 +++++++++------------------ tests/e2e/history.spec.ts | 6 +- tests/e2e/iframe-interaction.spec.ts | 75 ++++----- tests/e2e/keyboard.spec.ts | 16 +- tests/e2e/language.spec.ts | 136 +++++++--------- tests/e2e/lib/fixtures.ts | 208 +++++++++++++++++++++++++ tests/e2e/model-config.spec.ts | 16 +- tests/e2e/multi-turn.spec.ts | 136 +++++++--------- tests/e2e/save.spec.ts | 7 +- tests/e2e/settings.spec.ts | 32 ++-- tests/e2e/smoke.spec.ts | 23 +-- tests/e2e/theme.spec.ts | 96 +++++------- tests/e2e/upload.spec.ts | 8 +- 19 files changed, 662 insertions(+), 815 deletions(-) create mode 100644 tests/e2e/fixtures/diagrams.ts create mode 100644 tests/e2e/lib/fixtures.ts diff --git a/tests/e2e/chat.spec.ts b/tests/e2e/chat.spec.ts index bdd9147..b2d125a 100644 --- a/tests/e2e/chat.spec.ts +++ b/tests/e2e/chat.spec.ts @@ -1,25 +1,21 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, test } from "./lib/fixtures" test.describe("Chat Panel", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("page has interactive elements", async ({ page }) => { - // Verify buttons exist (settings, etc.) const buttons = page.locator("button") const count = await buttons.count() expect(count).toBeGreaterThan(0) }) test("draw.io iframe is interactive", async ({ page }) => { - const iframe = page.locator("iframe") + const iframe = getIframe(page) await expect(iframe).toBeVisible() - // Iframe should have loaded draw.io const src = await iframe.getAttribute("src") expect(src).toBeTruthy() }) diff --git a/tests/e2e/copy-paste.spec.ts b/tests/e2e/copy-paste.spec.ts index 57202b0..5e9e216 100644 --- a/tests/e2e/copy-paste.spec.ts +++ b/tests/e2e/copy-paste.spec.ts @@ -1,17 +1,21 @@ -import { expect, test } from "@playwright/test" +import { SINGLE_BOX_XML } from "./fixtures/diagrams" +import { + expect, + getChatInput, + getIframe, + sendMessage, + test, +} from "./lib/fixtures" import { createMockSSEResponse } from "./lib/helpers" test.describe("Copy/Paste Functionality", () => { test("can paste text into chat input", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) - // Focus and paste text await chatInput.focus() await page.keyboard.insertText("Create a flowchart diagram") @@ -20,11 +24,9 @@ test.describe("Copy/Paste Functionality", () => { test("can paste multiline text into chat input", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) await chatInput.focus() @@ -40,23 +42,16 @@ test.describe("Copy/Paste Functionality", () => { status: 200, contentType: "text/event-stream", body: createMockSSEResponse( - ``, + SINGLE_BOX_XML, "Here is your diagram with a test box.", ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) - - // Send a message - await chatInput.fill("Create a test box") - await chatInput.press("ControlOrMeta+Enter") + await sendMessage(page, "Create a test box") // Wait for response await expect( @@ -68,7 +63,7 @@ test.describe("Copy/Paste Functionality", () => { '[data-testid="copy-button"], button[aria-label*="Copy"], button:has(svg.lucide-copy), button:has(svg.lucide-clipboard)', ) - // Copy button feature may not exist in all versions - skip if not available + // Copy button feature may not exist - skip if not available const buttonCount = await copyButton.count() if (buttonCount === 0) { test.skip() @@ -76,7 +71,6 @@ test.describe("Copy/Paste Functionality", () => { } await copyButton.first().click() - // Should show copied confirmation (toast or button state change) await expect( page.locator('text="Copied"').or(page.locator("svg.lucide-check")), ).toBeVisible({ timeout: 3000 }) @@ -84,16 +78,14 @@ test.describe("Copy/Paste Functionality", () => { test("paste XML into XML input works", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Find XML input textarea (different from chat input) + // Find XML input textarea const xmlInput = page.locator( 'textarea[placeholder*="XML"], textarea[placeholder*="mxCell"]', ) - // XML input might be in a collapsed section - try to expand it + // Try to expand XML section if collapsed const xmlToggle = page.locator( 'button:has-text("XML"), [data-testid*="xml"], details summary', ) @@ -101,7 +93,7 @@ test.describe("Copy/Paste Functionality", () => { await xmlToggle.first().click() } - // XML input feature may not exist in all versions - skip if not available + // Skip if XML input not available const xmlInputCount = await xmlInput.count() const isXmlVisible = xmlInputCount > 0 && (await xmlInput.first().isVisible()) @@ -120,20 +112,13 @@ test.describe("Copy/Paste Functionality", () => { test("keyboard shortcuts work in chat input", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) - // Type some text await chatInput.fill("Hello world") - - // Select all with Ctrl+A await chatInput.press("ControlOrMeta+a") - - // Type replacement text await chatInput.fill("New text") await expect(chatInput).toHaveValue("New text") @@ -141,21 +126,16 @@ test.describe("Copy/Paste Functionality", () => { test("can undo/redo in chat input", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) - // Type text await chatInput.fill("First text") - await chatInput.press("Tab") // Blur to register change + await chatInput.press("Tab") await chatInput.focus() await chatInput.fill("Second text") - - // Undo with Ctrl+Z await chatInput.press("ControlOrMeta+z") // Verify page is still functional after undo @@ -164,11 +144,9 @@ test.describe("Copy/Paste Functionality", () => { test("chat input handles special characters", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) const specialText = "Test <>&\"' special chars 日本語 中文 🎉" @@ -179,18 +157,14 @@ test.describe("Copy/Paste Functionality", () => { test("long text in chat input scrolls", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) - // Very long text const longText = "This is a very long text. ".repeat(50) await chatInput.fill(longText) - // Input should handle it without error const value = await chatInput.inputValue() expect(value.length).toBeGreaterThan(500) }) diff --git a/tests/e2e/diagram-generation.spec.ts b/tests/e2e/diagram-generation.spec.ts index 5d12c5e..8162964 100644 --- a/tests/e2e/diagram-generation.spec.ts +++ b/tests/e2e/diagram-generation.spec.ts @@ -1,37 +1,29 @@ -import { expect, test } from "@playwright/test" +import { + CAT_DIAGRAM_XML, + FLOWCHART_XML, + NEW_NODE_XML, +} from "./fixtures/diagrams" +import { + createMultiTurnMock, + expect, + getChatInput, + sendMessage, + test, + waitForComplete, + waitForCompleteCount, +} from "./lib/fixtures" import { createMockSSEResponse } from "./lib/helpers" -// Simple cat diagram XML for testing (mxCell elements only, no wrapper) -const CAT_DIAGRAM_XML = ` - - - - -` - -// Simple flowchart XML for testing edits -const FLOWCHART_XML = ` - - - - - - - -` - 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, - "I'll create a diagram for you.", - ) await route.fulfill({ status: 200, contentType: "text/event-stream", - body: response, + body: createMockSSEResponse( + CAT_DIAGRAM_XML, + "I'll create a diagram for you.", + ), }) }) @@ -42,57 +34,32 @@ test.describe("Diagram Generation", () => { }) 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 sendMessage(page, "Draw a cat") await expect(page.locator('text="Generate Diagram"')).toBeVisible({ timeout: 15000, }) - await expect(page.locator('text="Complete"')).toBeVisible({ - timeout: 15000, - }) + await waitForComplete(page) }) test("chat input clears after sending", async ({ page }) => { - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) 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 sendMessage(page, "Draw a cute cat") 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 sendMessage(page, "Draw a cat") await expect( page.locator('text="I\'ll create a diagram for you."'), ).toBeVisible({ timeout: 10000 }) @@ -101,36 +68,16 @@ test.describe("Diagram Generation", () => { 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, - "I'll create a diagram for you.", - ), - }) - } 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, - "I'll create a diagram for you.", - ), - }) - } - }) + await page.route( + "**/api/chat", + createMultiTurnMock([ + { xml: FLOWCHART_XML, text: "I'll create a diagram for you." }, + { + xml: FLOWCHART_XML.replace("Process", "Updated Process"), + text: "I'll create a diagram for you.", + }, + ]), + ) await page.goto("/", { waitUntil: "networkidle" }) await page @@ -139,57 +86,29 @@ test.describe("Diagram Edit", () => { }) 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, - }) + await sendMessage(page, "Create a flowchart") + await waitForComplete(page) // 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, - }) + await sendMessage(page, "Change Process to Updated Process") + await waitForCompleteCount(page, 2) }) }) 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, - "I'll create a diagram for you.", - ), - }) - } else { - // Append response - adds new element - const appendXml = ` - -` - await route.fulfill({ - status: 200, - contentType: "text/event-stream", - body: createMockSSEResponse( - appendXml, - "I'll create a diagram for you.", - "append_diagram", - ), - }) - } - }) + await page.route( + "**/api/chat", + createMultiTurnMock([ + { xml: FLOWCHART_XML, text: "I'll create a diagram for you." }, + { + xml: NEW_NODE_XML, + text: "I'll create a diagram for you.", + toolName: "append_diagram", + }, + ]), + ) await page.goto("/", { waitUntil: "networkidle" }) await page @@ -198,23 +117,12 @@ test.describe("Diagram Append", () => { }) 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, - }) + await sendMessage(page, "Create a flowchart") + await waitForComplete(page) // 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, - }) + await sendMessage(page, "Add a new node to the right") + await waitForCompleteCount(page, 2) }) }) diff --git a/tests/e2e/error-handling.spec.ts b/tests/e2e/error-handling.spec.ts index d129003..e5dd5f2 100644 --- a/tests/e2e/error-handling.spec.ts +++ b/tests/e2e/error-handling.spec.ts @@ -1,34 +1,34 @@ -import { expect, test } from "@playwright/test" +import { TRUNCATED_XML } from "./fixtures/diagrams" +import { + createErrorMock, + expect, + getChatInput, + getIframe, + sendMessage, + test, +} from "./lib/fixtures" test.describe("Error Handling", () => { test("displays error message when API returns 500", async ({ page }) => { - await page.route("**/api/chat", async (route) => { - await route.fulfill({ - status: 500, - contentType: "application/json", - body: JSON.stringify({ error: "Internal server error" }), - }) - }) + await page.route( + "**/api/chat", + createErrorMock(500, "Internal server error"), + ) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await sendMessage(page, "Draw a cat") - await chatInput.fill("Draw a cat") - await chatInput.press("ControlOrMeta+Enter") - - // Should show error indication (toast, alert, or error text) + // Should show error indication 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) + // User should be able to type again + const chatInput = getChatInput(page) await chatInput.fill("Retry message") await expect(chatInput).toHaveValue("Retry message") }) @@ -36,24 +36,15 @@ test.describe("Error Handling", () => { test("displays error message when API returns 429 rate limit", async ({ page, }) => { - await page.route("**/api/chat", async (route) => { - await route.fulfill({ - status: 429, - contentType: "application/json", - body: JSON.stringify({ error: "Rate limit exceeded" }), - }) - }) + await page.route( + "**/api/chat", + createErrorMock(429, "Rate limit exceeded"), + ) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - 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") + await sendMessage(page, "Draw a cat") // Should show error indication for rate limit const errorIndicator = page @@ -63,27 +54,21 @@ test.describe("Error Handling", () => { await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 }) // User should be able to type again + const chatInput = getChatInput(page) await chatInput.fill("Retry after rate limit") await expect(chatInput).toHaveValue("Retry after rate limit") }) test("handles network timeout gracefully", async ({ page }) => { await page.route("**/api/chat", async (route) => { - // Simulate timeout by not responding for a short time then aborting await new Promise((resolve) => setTimeout(resolve, 2000)) await route.abort("timedout") }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - 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") + await sendMessage(page, "Draw a cat") // Should show error indication for network failure const errorIndicator = page @@ -93,6 +78,7 @@ test.describe("Error Handling", () => { await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 }) // After timeout, user should be able to type again + const chatInput = getChatInput(page) await chatInput.fill("Try again after timeout") await expect(chatInput).toHaveValue("Try again after timeout") }) @@ -102,10 +88,6 @@ test.describe("Error Handling", () => { const textId = `text_${Date.now()}` const messageId = `msg_${Date.now()}` - // Truncated XML (missing closing tags) - const truncatedXml = ` - { type: "tool-input-available", toolCallId, toolName: "display_diagram", - input: { xml: truncatedXml }, + input: { xml: TRUNCATED_XML }, }, { type: "tool-output-error", @@ -142,15 +124,9 @@ test.describe("Error Handling", () => { }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) - - await chatInput.fill("Draw something") - await chatInput.press("ControlOrMeta+Enter") + await sendMessage(page, "Draw something") // Should show truncated badge await expect(page.locator('text="Truncated"')).toBeVisible({ diff --git a/tests/e2e/file-upload.spec.ts b/tests/e2e/file-upload.spec.ts index 213e355..6348a75 100644 --- a/tests/e2e/file-upload.spec.ts +++ b/tests/e2e/file-upload.spec.ts @@ -1,46 +1,40 @@ -import { expect, test } from "@playwright/test" +import { SINGLE_BOX_XML } from "./fixtures/diagrams" +import { + expect, + getChatInput, + getIframe, + sendMessage, + test, +} from "./lib/fixtures" import { createMockSSEResponse } from "./lib/helpers" test.describe("File Upload", () => { test("upload button opens file picker", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Find the upload button (image icon) const uploadButton = page.locator( 'button[aria-label="Upload file"], button:has(svg.lucide-image)', ) await expect(uploadButton.first()).toBeVisible({ timeout: 10000 }) - - // Click should trigger hidden file input - // Just verify the button is clickable await expect(uploadButton.first()).toBeEnabled() }) test("shows file preview after selecting image", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Find the hidden file input const fileInput = page.locator('input[type="file"]') - // Create a test image file await fileInput.setInputFiles({ name: "test-image.png", mimeType: "image/png", buffer: Buffer.from( - // Minimal valid PNG (1x1 transparent pixel) "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64", ), }) - // File input should have processed the file - // Check that no error toast appeared await expect( page.locator('[role="alert"][data-type="error"]'), ).not.toBeVisible({ timeout: 2000 }) @@ -48,13 +42,10 @@ test.describe("File Upload", () => { test("can remove uploaded file", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) const fileInput = page.locator('input[type="file"]') - // Upload a file await fileInput.setInputFiles({ name: "test-image.png", mimeType: "image/png", @@ -64,17 +55,14 @@ test.describe("File Upload", () => { ), }) - // Wait for file preview or no error await expect( page.locator('[role="alert"][data-type="error"]'), ).not.toBeVisible({ timeout: 2000 }) - // Find and click remove button if it exists (X icon) const removeButton = page.locator( '[data-testid="remove-file-button"], button[aria-label*="Remove"], button:has(svg.lucide-x)', ) - // Remove button feature may not exist in all versions - skip if not available const removeButtonCount = await removeButton.count() if (removeButtonCount === 0) { test.skip() @@ -82,7 +70,6 @@ test.describe("File Upload", () => { } await removeButton.first().click() - // Verify button is gone or file preview is removed await expect(removeButton.first()).not.toBeVisible({ timeout: 2000 }) }) @@ -95,21 +82,17 @@ test.describe("File Upload", () => { status: 200, contentType: "text/event-stream", body: createMockSSEResponse( - ``, + SINGLE_BOX_XML, "Based on your image, here is a diagram:", ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) const fileInput = page.locator('input[type="file"]') - const chatInput = page.locator('textarea[aria-label="Chat input"]') - // Upload a file await fileInput.setInputFiles({ name: "architecture.png", mimeType: "image/png", @@ -119,29 +102,21 @@ test.describe("File Upload", () => { ), }) - // Type message and send - await chatInput.fill("Convert this to a diagram") - await chatInput.press("ControlOrMeta+Enter") + await sendMessage(page, "Convert this to a diagram") - // Wait for response await expect( page.locator('text="Based on your image, here is a diagram:"'), ).toBeVisible({ timeout: 15000 }) - // Verify request was made (file should be in request body as base64) expect(capturedRequest).not.toBeNull() }) test("shows error for oversized file", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) const fileInput = page.locator('input[type="file"]') - - // Create a large file (> 2MB limit) - const largeBuffer = Buffer.alloc(3 * 1024 * 1024, "x") // 3MB + const largeBuffer = Buffer.alloc(3 * 1024 * 1024, "x") await fileInput.setInputFiles({ name: "large-image.png", @@ -149,8 +124,6 @@ test.describe("File Upload", () => { buffer: largeBuffer, }) - // Should show error toast/message about file size - // The exact error message depends on the app implementation await expect( page.locator('[role="alert"], [data-sonner-toast]').first(), ).toBeVisible({ timeout: 5000 }) @@ -158,13 +131,10 @@ test.describe("File Upload", () => { test("drag and drop file upload works", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) const chatForm = page.locator("form").first() - // Create a DataTransfer with a file const dataTransfer = await page.evaluateHandle(() => { const dt = new DataTransfer() const file = new File(["test content"], "dropped-image.png", { @@ -174,13 +144,9 @@ test.describe("File Upload", () => { return dt }) - // Dispatch drag and drop events await chatForm.dispatchEvent("dragover", { dataTransfer }) await chatForm.dispatchEvent("drop", { dataTransfer }) - // Should not crash - verify page is still functional - await expect( - page.locator('textarea[aria-label="Chat input"]'), - ).toBeVisible({ timeout: 3000 }) + await expect(getChatInput(page)).toBeVisible({ timeout: 3000 }) }) }) diff --git a/tests/e2e/fixtures/diagrams.ts b/tests/e2e/fixtures/diagrams.ts new file mode 100644 index 0000000..7009253 --- /dev/null +++ b/tests/e2e/fixtures/diagrams.ts @@ -0,0 +1,50 @@ +/** + * Shared XML diagram fixtures for E2E tests + */ + +// Simple cat diagram +export const CAT_DIAGRAM_XML = ` + + + + +` + +// Simple flowchart +export const FLOWCHART_XML = ` + + + + + + + +` + +// Simple single box +export const SINGLE_BOX_XML = ` + +` + +// Test node for iframe interaction tests +export const TEST_NODE_XML = ` + +` + +// Architecture box +export const ARCHITECTURE_XML = ` + +` + +// New node for append tests +export const NEW_NODE_XML = ` + +` + +// Truncated XML for error tests +export const TRUNCATED_XML = ` + + `` diff --git a/tests/e2e/history-restore.spec.ts b/tests/e2e/history-restore.spec.ts index 05b3646..cc440d0 100644 --- a/tests/e2e/history-restore.spec.ts +++ b/tests/e2e/history-restore.spec.ts @@ -1,4 +1,16 @@ -import { expect, test } from "@playwright/test" +import { SINGLE_BOX_XML } from "./fixtures/diagrams" +import { + expect, + expectBeforeAndAfterReload, + getChatInput, + getIframe, + getIframeContent, + openSettings, + sendMessage, + test, + waitForComplete, + waitForText, +} from "./lib/fixtures" import { createMockSSEResponse } from "./lib/helpers" test.describe("History and Session Restore", () => { @@ -8,55 +20,43 @@ test.describe("History and Session Restore", () => { status: 200, contentType: "text/event-stream", body: createMockSSEResponse( - ``, + SINGLE_BOX_XML, "Created your test diagram.", ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) - - // Send a message - await chatInput.fill("Create a test diagram") - await chatInput.press("ControlOrMeta+Enter") - - // Wait for response - await expect( - page.locator('text="Created your test diagram."'), - ).toBeVisible({ - timeout: 15000, + await test.step("create a conversation", async () => { + await sendMessage(page, "Create a test diagram") + await waitForText(page, "Created your test diagram.") }) - // Find and click new chat button - const newChatButton = page.locator('[data-testid="new-chat-button"]') - await expect(newChatButton).toBeVisible({ timeout: 5000 }) + await test.step("click new chat button", async () => { + const newChatButton = page.locator( + '[data-testid="new-chat-button"]', + ) + await expect(newChatButton).toBeVisible({ timeout: 5000 }) + await newChatButton.click() + }) - await newChatButton.click() - - // Conversation should be cleared - await expect( - page.locator('text="Created your test diagram."'), - ).not.toBeVisible({ timeout: 5000 }) + await test.step("verify conversation is cleared", async () => { + await expect( + page.locator('text="Created your test diagram."'), + ).not.toBeVisible({ timeout: 5000 }) + }) }) test("chat history sidebar shows past conversations", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Look for history/sidebar button that is enabled const historyButton = page.locator( 'button[aria-label*="History"]:not([disabled]), button:has(svg.lucide-history):not([disabled]), button:has(svg.lucide-menu):not([disabled]), button:has(svg.lucide-sidebar):not([disabled]), button:has(svg.lucide-panel-left):not([disabled])', ) - // History feature may not exist in all versions - skip if not available const buttonCount = await historyButton.count() if (buttonCount === 0) { test.skip() @@ -64,10 +64,7 @@ test.describe("History and Session Restore", () => { } await historyButton.first().click() - // Wait for sidebar/panel to appear or verify page still works - await expect( - page.locator('textarea[aria-label="Chat input"]'), - ).toBeVisible({ timeout: 3000 }) + await expect(getChatInput(page)).toBeVisible({ timeout: 3000 }) }) test("conversation persists after page reload", async ({ page }) => { @@ -76,44 +73,30 @@ test.describe("History and Session Restore", () => { status: 200, contentType: "text/event-stream", body: createMockSSEResponse( - ``, + SINGLE_BOX_XML, "This message should persist.", ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await test.step("create conversation", async () => { + await sendMessage(page, "Create persistent diagram") + await waitForText(page, "This message should persist.") + }) - // Send a message - await chatInput.fill("Create persistent diagram") - await chatInput.press("ControlOrMeta+Enter") - - // Wait for response - await expect( - page.locator('text="This message should persist."'), - ).toBeVisible({ timeout: 15000 }) - - // Reload page - await page.reload({ waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) - - // Verify page is functional after reload - await expect( - page.locator('textarea[aria-label="Chat input"]'), - ).toBeVisible({ timeout: 10000 }) - - // Verify the message persisted after reload - await expect( - page.locator('text="This message should persist."'), - ).toBeVisible({ timeout: 10000 }) + await expectBeforeAndAfterReload( + page, + "conversation message persists", + async () => { + await expect(getChatInput(page)).toBeVisible({ timeout: 10000 }) + await expect( + page.locator('text="This message should persist."'), + ).toBeVisible({ timeout: 10000 }) + }, + ) }) test("diagram state persists after reload", async ({ page }) => { @@ -122,36 +105,22 @@ test.describe("History and Session Restore", () => { status: 200, contentType: "text/event-stream", body: createMockSSEResponse( - ``, + SINGLE_BOX_XML, "Created a diagram that should be saved.", ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await sendMessage(page, "Create saveable diagram") + await waitForComplete(page) - // Generate a diagram - await chatInput.fill("Create saveable diagram") - await chatInput.press("ControlOrMeta+Enter") - - await expect(page.locator('text="Complete"')).toBeVisible({ - timeout: 15000, - }) - - // Reload await page.reload({ waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Diagram state is typically stored - check iframe is still functional - const frame = page.frameLocator("iframe") + const frame = getIframeContent(page) await expect( frame .locator(".geMenubarContainer, .geDiagramContainer, canvas") @@ -165,88 +134,46 @@ test.describe("History and Session Restore", () => { status: 200, contentType: "text/event-stream", body: createMockSSEResponse( - ``, + SINGLE_BOX_XML, "Testing browser navigation.", ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await sendMessage(page, "Test navigation") + await waitForText(page, "Testing browser navigation.") - // Send a message - await chatInput.fill("Test navigation") - await chatInput.press("ControlOrMeta+Enter") - - await expect( - page.locator('text="Testing browser navigation."'), - ).toBeVisible({ - timeout: 15000, - }) - - // Navigate to about page await page.goto("/about", { waitUntil: "networkidle" }) - - // Go back await page.goBack({ waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Page should be functional - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await expect(getChatInput(page)).toBeVisible({ timeout: 10000 }) }) test("settings are restored after reload", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings - const settingsButton = page.locator('[data-testid="settings-button"]') - await settingsButton.click() - - // Settings dialog should open - await expect( - page.locator('[role="dialog"], [role="menu"], form').first(), - ).toBeVisible({ timeout: 5000 }) - - // Close settings + await openSettings(page) await page.keyboard.press("Escape") - // Reload await page.reload({ waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings again - await settingsButton.click() - - // Settings should still be accessible - await expect( - page.locator('[role="dialog"], [role="menu"], form').first(), - ).toBeVisible({ timeout: 5000 }) + await openSettings(page) }) test("model selection persists", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Find model selector const modelSelector = page.locator( 'button[aria-label*="Model"], [data-testid="model-selector"], button:has-text("Claude")', ) - // Model selector feature may not exist in all versions - skip if not available const selectorCount = await modelSelector.count() if (selectorCount === 0) { test.skip() @@ -255,39 +182,28 @@ test.describe("History and Session Restore", () => { const initialModel = await modelSelector.first().textContent() - // Reload page await page.reload({ waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Check model is still selected const modelAfterReload = await modelSelector.first().textContent() expect(modelAfterReload).toBe(initialModel) }) test("handles localStorage quota exceeded gracefully", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Fill up localStorage (simulate quota exceeded scenario) await page.evaluate(() => { try { - // This might throw if quota is exceeded - const largeData = "x".repeat(5 * 1024 * 1024) // 5MB + const largeData = "x".repeat(5 * 1024 * 1024) localStorage.setItem("test-large-data", largeData) } catch { // Expected to fail on some browsers } }) - // App should still function - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await expect(getChatInput(page)).toBeVisible({ timeout: 10000 }) - // Clean up await page.evaluate(() => { localStorage.removeItem("test-large-data") }) diff --git a/tests/e2e/history.spec.ts b/tests/e2e/history.spec.ts index db3403c..bbd55e6 100644 --- a/tests/e2e/history.spec.ts +++ b/tests/e2e/history.spec.ts @@ -1,11 +1,9 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, test } from "./lib/fixtures" test.describe("History Dialog", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("history button exists in UI", async ({ page }) => { diff --git a/tests/e2e/iframe-interaction.spec.ts b/tests/e2e/iframe-interaction.spec.ts index fc5c26d..134dabd 100644 --- a/tests/e2e/iframe-interaction.spec.ts +++ b/tests/e2e/iframe-interaction.spec.ts @@ -1,15 +1,24 @@ -import { expect, test } from "@playwright/test" +import { TEST_NODE_XML } from "./fixtures/diagrams" +import { + expect, + getChatInput, + getIframe, + getIframeContent, + sendMessage, + test, + waitForComplete, +} from "./lib/fixtures" import { createMockSSEResponse } from "./lib/helpers" test.describe("Iframe Interaction", () => { test("draw.io iframe loads successfully", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - const iframe = page.locator("iframe") + const iframe = getIframe(page) await expect(iframe).toBeVisible({ timeout: 30000 }) // iframe should have loaded draw.io content - const frame = page.frameLocator("iframe") + const frame = getIframeContent(page) await expect( frame .locator(".geMenubarContainer, .geDiagramContainer, canvas") @@ -19,11 +28,9 @@ test.describe("Iframe Interaction", () => { test("can interact with draw.io toolbar", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const frame = page.frameLocator("iframe") + const frame = getIframeContent(page) // Draw.io menu items should be accessible await expect( @@ -36,62 +43,42 @@ test.describe("Iframe Interaction", () => { test("diagram XML is rendered in iframe after generation", async ({ page, }) => { - const testXml = ` - -` - await page.route("**/api/chat", async (route) => { await route.fulfill({ status: 200, contentType: "text/event-stream", - body: createMockSSEResponse(testXml, "Here is your diagram:"), + body: createMockSSEResponse( + TEST_NODE_XML, + "Here is your diagram:", + ), }) }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) + await sendMessage(page, "Create a test node") + await waitForComplete(page) - await chatInput.fill("Create a test node") - await chatInput.press("ControlOrMeta+Enter") - - // Wait for completion - await expect(page.locator('text="Complete"')).toBeVisible({ - timeout: 15000, - }) - - // The diagram should now be in the iframe - // We can check the iframe's internal state via postMessage or visible elements - // At minimum, verify no error state - await page.waitForTimeout(1000) // Give draw.io time to render + // Give draw.io time to render + await page.waitForTimeout(1000) }) test("zoom controls work in draw.io", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const frame = page.frameLocator("iframe") + const frame = getIframeContent(page) // draw.io should be loaded and functional - check for diagram container await expect( frame.locator(".geDiagramContainer, canvas").first(), ).toBeVisible({ timeout: 10000 }) - - // Zoom controls may or may not be visible depending on UI mode - // Just verify iframe is interactive }) test("can resize the panel divider", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) // Find the resizer/divider between panels const resizer = page.locator( @@ -99,10 +86,8 @@ test.describe("Iframe Interaction", () => { ) if ((await resizer.count()) > 0) { - // Resizer should be draggable await expect(resizer.first()).toBeVisible() - // Try to drag it const box = await resizer.first().boundingBox() if (box) { await page.mouse.move( @@ -118,11 +103,9 @@ test.describe("Iframe Interaction", () => { test("iframe responds to window resize", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - const iframe = page.locator("iframe") + const iframe = getIframe(page) const initialBox = await iframe.boundingBox() // Resize window @@ -131,9 +114,7 @@ test.describe("Iframe Interaction", () => { const newBox = await iframe.boundingBox() - // iframe should have adjusted expect(newBox).toBeDefined() - // Size should be different or at least still valid if (initialBox && newBox) { expect(newBox.width).toBeLessThanOrEqual(800) } diff --git a/tests/e2e/keyboard.spec.ts b/tests/e2e/keyboard.spec.ts index db811c6..236b298 100644 --- a/tests/e2e/keyboard.spec.ts +++ b/tests/e2e/keyboard.spec.ts @@ -1,32 +1,22 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, openSettings, test } from "./lib/fixtures" test.describe("Keyboard Interactions", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("Escape closes settings dialog", async ({ page }) => { - // Find settings button using aria-label or icon - const settingsButton = page.locator( - 'button[aria-label*="settings"], button:has(svg[class*="settings"])', - ) - await expect(settingsButton).toBeVisible() - await settingsButton.click() + await openSettings(page) - // Wait for dialog to appear const dialog = page.locator('[role="dialog"]') await expect(dialog).toBeVisible({ timeout: 5000 }) - // Press Escape and verify dialog closes await page.keyboard.press("Escape") await expect(dialog).not.toBeVisible({ timeout: 2000 }) }) test("page is keyboard accessible", async ({ page }) => { - // Verify page has focusable elements const focusableElements = page.locator( 'button, [tabindex="0"], input, textarea, a[href]', ) diff --git a/tests/e2e/language.spec.ts b/tests/e2e/language.spec.ts index 847ac0f..4ed10e7 100644 --- a/tests/e2e/language.spec.ts +++ b/tests/e2e/language.spec.ts @@ -1,113 +1,90 @@ -import { expect, test } from "@playwright/test" +import { + expect, + expectBeforeAndAfterReload, + getChatInput, + getIframe, + openSettings, + sleep, + test, +} from "./lib/fixtures" test.describe("Language Switching", () => { test("loads English by default", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Check for English UI text - const chatInput = page.locator('textarea[aria-label="Chat input"]') + const chatInput = getChatInput(page) await expect(chatInput).toBeVisible({ timeout: 10000 }) - // Send button should say "Send" await expect(page.locator('button:has-text("Send")')).toBeVisible() }) test("can switch to Japanese", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings - const settingsButton = page.locator( - 'button[aria-label*="Settings"], button:has(svg.lucide-settings)', - ) - await settingsButton.first().click() + await test.step("open settings and select Japanese", async () => { + await openSettings(page) + const languageSelector = page.locator('button:has-text("English")') + await languageSelector.first().click() + await page.locator('text="日本語"').click() + }) - // Find language selector - const languageSelector = page.locator('button:has-text("English")') - await languageSelector.first().click() - - // Select Japanese - await page.locator('text="日本語"').click() - - // UI should update to Japanese - await expect(page.locator('button:has-text("送信")')).toBeVisible({ - timeout: 5000, + await test.step("verify UI is in Japanese", async () => { + await expect(page.locator('button:has-text("送信")')).toBeVisible({ + timeout: 5000, + }) }) }) test("can switch to Chinese", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings - const settingsButton = page.locator( - 'button[aria-label*="Settings"], button:has(svg.lucide-settings)', - ) - await settingsButton.first().click() + await test.step("open settings and select Chinese", async () => { + await openSettings(page) + const languageSelector = page.locator('button:has-text("English")') + await languageSelector.first().click() + await page.locator('text="中文"').click() + }) - // Find language selector and switch to Chinese - const languageSelector = page.locator('button:has-text("English")') - await languageSelector.first().click() - - await page.locator('text="中文"').click() - - // UI should update to Chinese - await expect(page.locator('button:has-text("发送")')).toBeVisible({ - timeout: 5000, + await test.step("verify UI is in Chinese", async () => { + await expect(page.locator('button:has-text("发送")')).toBeVisible({ + timeout: 5000, + }) }) }) test("language persists after reload", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings and switch to Japanese - const settingsButton = page.locator( - 'button[aria-label*="Settings"], button:has(svg.lucide-settings)', + await test.step("switch to Japanese", async () => { + await openSettings(page) + const languageSelector = page.locator('button:has-text("English")') + await languageSelector.first().click() + await page.locator('text="日本語"').click() + await page.keyboard.press("Escape") + await sleep(500) + }) + + await expectBeforeAndAfterReload( + page, + "Japanese language setting", + async () => { + await expect( + page.locator('button:has-text("送信")'), + ).toBeVisible({ + timeout: 10000, + }) + }, ) - await settingsButton.first().click() - - const languageSelector = page.locator('button:has-text("English")') - await languageSelector.first().click() - await page.locator('text="日本語"').click() - - // Verify Japanese UI - await expect(page.locator('button:has-text("送信")')).toBeVisible({ - timeout: 5000, - }) - - // Close dialog and reload - await page.keyboard.press("Escape") - await page.waitForTimeout(500) - - // Use domcontentloaded to avoid networkidle timeout - await page.reload({ waitUntil: "domcontentloaded" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) - - // Japanese should persist - await expect(page.locator('button:has-text("送信")')).toBeVisible({ - timeout: 10000, - }) }) test("Japanese locale URL works", async ({ page }) => { await page.goto("/ja", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Should show Japanese UI await expect(page.locator('button:has-text("送信")')).toBeVisible({ timeout: 10000, }) @@ -115,11 +92,8 @@ test.describe("Language Switching", () => { test("Chinese locale URL works", async ({ page }) => { await page.goto("/zh", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Should show Chinese UI await expect(page.locator('button:has-text("发送")')).toBeVisible({ timeout: 10000, }) diff --git a/tests/e2e/lib/fixtures.ts b/tests/e2e/lib/fixtures.ts new file mode 100644 index 0000000..f63f3ac --- /dev/null +++ b/tests/e2e/lib/fixtures.ts @@ -0,0 +1,208 @@ +/** + * Playwright test fixtures for E2E tests + * Uses test.extend to provide common setup and helpers + */ + +import { test as base, expect, type Page, type Route } from "@playwright/test" +import { createMockSSEResponse, createTextOnlyResponse } from "./helpers" + +/** + * Extended test with common fixtures + */ +export const test = base.extend<{ + /** Page with iframe already loaded */ + appPage: Page +}>({ + appPage: async ({ page }, use) => { + await page.goto("/", { waitUntil: "networkidle" }) + await page + .locator("iframe") + .waitFor({ state: "visible", timeout: 30000 }) + await use(page) + }, +}) + +export { expect } + +// ============================================ +// Locator helpers +// ============================================ + +/** Get the chat input textarea */ +export function getChatInput(page: Page) { + return page.locator('textarea[aria-label="Chat input"]') +} + +/** Get the draw.io iframe */ +export function getIframe(page: Page) { + return page.locator("iframe") +} + +/** Get the iframe's frame locator for internal queries */ +export function getIframeContent(page: Page) { + return page.frameLocator("iframe") +} + +/** Get the settings button */ +export function getSettingsButton(page: Page) { + return page.locator('[data-testid="settings-button"]') +} + +// ============================================ +// Action helpers +// ============================================ + +/** Send a message in the chat input */ +export async function sendMessage(page: Page, message: string) { + const chatInput = getChatInput(page) + await expect(chatInput).toBeVisible({ timeout: 10000 }) + await chatInput.fill(message) + await chatInput.press("ControlOrMeta+Enter") +} + +/** Wait for diagram generation to complete */ +export async function waitForComplete(page: Page, timeout = 15000) { + await expect(page.locator('text="Complete"')).toBeVisible({ timeout }) +} + +/** Wait for N "Complete" badges */ +export async function waitForCompleteCount( + page: Page, + count: number, + timeout = 15000, +) { + await expect(page.locator('text="Complete"')).toHaveCount(count, { + timeout, + }) +} + +/** Wait for a specific text to appear */ +export async function waitForText(page: Page, text: string, timeout = 15000) { + await expect(page.locator(`text="${text}"`)).toBeVisible({ timeout }) +} + +/** Open settings dialog */ +export async function openSettings(page: Page) { + await getSettingsButton(page).click() + await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }) +} + +// ============================================ +// Mock helpers +// ============================================ + +interface MockResponse { + xml: string + text: string + toolName?: string +} + +/** + * Create a multi-turn mock handler + * Each request gets the next response in the array + */ +export function createMultiTurnMock(responses: MockResponse[]) { + let requestCount = 0 + return async (route: Route) => { + const response = + responses[requestCount] || responses[responses.length - 1] + requestCount++ + await route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: createMockSSEResponse( + response.xml, + response.text, + response.toolName, + ), + }) + } +} + +/** + * Create a mock that returns text-only responses + */ +export function createTextOnlyMock(responses: string[]) { + let requestCount = 0 + return async (route: Route) => { + const text = responses[requestCount] || responses[responses.length - 1] + requestCount++ + await route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: createTextOnlyResponse(text), + }) + } +} + +/** + * Create a mock that alternates between text and diagram responses + */ +export function createMixedMock( + responses: Array< + | { type: "text"; text: string } + | { type: "diagram"; xml: string; text: string } + >, +) { + let requestCount = 0 + return async (route: Route) => { + const response = + responses[requestCount] || responses[responses.length - 1] + requestCount++ + if (response.type === "text") { + await route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: createTextOnlyResponse(response.text), + }) + } else { + await route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: createMockSSEResponse(response.xml, response.text), + }) + } + } +} + +/** + * Create a mock that returns an error + */ +export function createErrorMock(status: number, error: string) { + return async (route: Route) => { + await route.fulfill({ + status, + contentType: "application/json", + body: JSON.stringify({ error }), + }) + } +} + +// ============================================ +// Persistence helpers +// ============================================ + +/** + * Test that state persists across page reload. + * Runs assertions before reload, reloads page, then runs assertions again. + * Keep assertions narrow and explicit - test one specific thing. + * + * @param page - Playwright page + * @param description - What persistence is being tested (for debugging) + * @param assertion - Async function with expect() calls + */ +export async function expectBeforeAndAfterReload( + page: Page, + description: string, + assertion: () => Promise, +) { + await test.step(`verify ${description} before reload`, assertion) + await page.reload({ waitUntil: "networkidle" }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) + await test.step(`verify ${description} after reload`, assertion) +} + +/** Simple sleep helper */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/tests/e2e/model-config.spec.ts b/tests/e2e/model-config.spec.ts index e324482..9b974ec 100644 --- a/tests/e2e/model-config.spec.ts +++ b/tests/e2e/model-config.spec.ts @@ -1,27 +1,17 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, openSettings, test } from "./lib/fixtures" test.describe("Model Configuration", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("settings dialog opens and shows configuration options", async ({ page, }) => { - // Open settings - const settingsButton = page.locator( - 'button[aria-label*="settings"], button:has(svg[class*="settings"])', - ) - await expect(settingsButton).toBeVisible() - await settingsButton.click() + await openSettings(page) const dialog = page.locator('[role="dialog"]') - await expect(dialog).toBeVisible({ timeout: 5000 }) - - // Settings dialog should have some configuration UI const buttons = dialog.locator("button") const buttonCount = await buttons.count() expect(buttonCount).toBeGreaterThan(0) diff --git a/tests/e2e/multi-turn.spec.ts b/tests/e2e/multi-turn.spec.ts index e722eb5..a58ba22 100644 --- a/tests/e2e/multi-turn.spec.ts +++ b/tests/e2e/multi-turn.spec.ts @@ -1,46 +1,44 @@ -import { expect, test } from "@playwright/test" -import { createMockSSEResponse, createTextOnlyResponse } from "./lib/helpers" +import { ARCHITECTURE_XML, createBoxXml } from "./fixtures/diagrams" +import { + createMixedMock, + createMultiTurnMock, + expect, + getChatInput, + sendMessage, + test, + waitForComplete, + waitForText, +} from "./lib/fixtures" +import { createTextOnlyResponse } from "./lib/helpers" test.describe("Multi-turn Conversation", () => { test("handles multiple diagram requests in sequence", async ({ page }) => { - let requestCount = 0 - await page.route("**/api/chat", async (route) => { - requestCount++ - const xml = - requestCount === 1 - ? `` - : `` - await route.fulfill({ - status: 200, - contentType: "text/event-stream", - body: createMockSSEResponse( - xml, - `Creating diagram ${requestCount}...`, - ), - }) - }) + await page.route( + "**/api/chat", + createMultiTurnMock([ + { + xml: createBoxXml("box1", "First"), + text: "Creating diagram 1...", + }, + { + xml: createBoxXml("box2", "Second", 200), + text: "Creating diagram 2...", + }, + ]), + ) await page.goto("/", { waitUntil: "networkidle" }) await page .locator("iframe") .waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) - // First request - await chatInput.fill("Draw first box") - await chatInput.press("ControlOrMeta+Enter") - await expect(page.locator('text="Creating diagram 1..."')).toBeVisible({ - timeout: 15000, - }) + await sendMessage(page, "Draw first box") + await waitForText(page, "Creating diagram 1...") // Second request - await chatInput.fill("Draw second box") - await chatInput.press("ControlOrMeta+Enter") - await expect(page.locator('text="Creating diagram 2..."')).toBeVisible({ - timeout: 15000, - }) + await sendMessage(page, "Draw second box") + await waitForText(page, "Creating diagram 2...") // Both messages should be visible await expect(page.locator('text="Draw first box"')).toBeVisible() @@ -56,7 +54,6 @@ test.describe("Multi-turn Conversation", () => { // Verify messages array grows with each request if (requestCount === 2) { - // Second request should have previous messages expect(body.messages?.length).toBeGreaterThan(1) } @@ -72,70 +69,45 @@ test.describe("Multi-turn Conversation", () => { .locator("iframe") .waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) - // First message - await chatInput.fill("Hello") - await chatInput.press("ControlOrMeta+Enter") - await expect(page.locator('text="Response 1"')).toBeVisible({ - timeout: 15000, - }) + await sendMessage(page, "Hello") + await waitForText(page, "Response 1") // Second message (should include history) - await chatInput.fill("Follow up question") - await chatInput.press("ControlOrMeta+Enter") - await expect(page.locator('text="Response 2"')).toBeVisible({ - timeout: 15000, - }) + await sendMessage(page, "Follow up question") + await waitForText(page, "Response 2") }) test("can continue after a text-only response", async ({ page }) => { - let requestCount = 0 - await page.route("**/api/chat", async (route) => { - requestCount++ - if (requestCount === 1) { - // First: text-only explanation - await route.fulfill({ - status: 200, - contentType: "text/event-stream", - body: createTextOnlyResponse( - "I understand. Let me explain the architecture first.", - ), - }) - } else { - // Second: diagram generation - const xml = `` - await route.fulfill({ - status: 200, - contentType: "text/event-stream", - body: createMockSSEResponse(xml, "Here is the diagram:"), - }) - } - }) + await page.route( + "**/api/chat", + createMixedMock([ + { + type: "text", + text: "I understand. Let me explain the architecture first.", + }, + { + type: "diagram", + xml: ARCHITECTURE_XML, + text: "Here is the diagram:", + }, + ]), + ) await page.goto("/", { waitUntil: "networkidle" }) await page .locator("iframe") .waitFor({ state: "visible", timeout: 30000 }) - const chatInput = page.locator('textarea[aria-label="Chat input"]') - await expect(chatInput).toBeVisible({ timeout: 10000 }) - // Ask for explanation first - await chatInput.fill("Explain the architecture") - await chatInput.press("ControlOrMeta+Enter") - await expect( - page.locator( - 'text="I understand. Let me explain the architecture first."', - ), - ).toBeVisible({ timeout: 15000 }) + await sendMessage(page, "Explain the architecture") + await waitForText( + page, + "I understand. Let me explain the architecture first.", + ) // Then ask for diagram - await chatInput.fill("Now show it as a diagram") - await chatInput.press("ControlOrMeta+Enter") - await expect(page.locator('text="Complete"')).toBeVisible({ - timeout: 15000, - }) + await sendMessage(page, "Now show it as a diagram") + await waitForComplete(page) }) }) diff --git a/tests/e2e/save.spec.ts b/tests/e2e/save.spec.ts index d7fe957..995f338 100644 --- a/tests/e2e/save.spec.ts +++ b/tests/e2e/save.spec.ts @@ -1,15 +1,12 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, test } from "./lib/fixtures" test.describe("Save Dialog", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("save/download buttons exist", async ({ page }) => { - // Check that buttons with icons exist (save/download functionality) const buttons = page .locator("button") .filter({ has: page.locator("svg") }) diff --git a/tests/e2e/settings.spec.ts b/tests/e2e/settings.spec.ts index 198f6ad..41ea092 100644 --- a/tests/e2e/settings.spec.ts +++ b/tests/e2e/settings.spec.ts @@ -1,41 +1,33 @@ -import { expect, test } from "@playwright/test" +import { + expect, + getIframe, + getSettingsButton, + openSettings, + test, +} from "./lib/fixtures" test.describe("Settings", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("settings dialog opens", async ({ page }) => { - const settingsButton = page.locator('[data-testid="settings-button"]') - await expect(settingsButton).toBeVisible() - await settingsButton.click() - - const dialog = page.locator('[role="dialog"]') - await expect(dialog).toBeVisible({ timeout: 5000 }) + await openSettings(page) + // openSettings already verifies dialog is visible }) test("language selection is available", async ({ page }) => { - const settingsButton = page.locator('[data-testid="settings-button"]') - await settingsButton.click() + await openSettings(page) const dialog = page.locator('[role="dialog"]') - await expect(dialog).toBeVisible({ timeout: 5000 }) - - // Should have language selector showing English await expect(dialog.locator('text="English"')).toBeVisible() }) test("draw.io theme toggle exists", async ({ page }) => { - const settingsButton = page.locator('[data-testid="settings-button"]') - await settingsButton.click() + await openSettings(page) const dialog = page.locator('[role="dialog"]') - await expect(dialog).toBeVisible({ timeout: 5000 }) - - // Should have draw.io theme option (sketch or minimal) const themeText = dialog.locator("text=/sketch|minimal/i") await expect(themeText.first()).toBeVisible() }) diff --git a/tests/e2e/smoke.spec.ts b/tests/e2e/smoke.spec.ts index 3ba9160..ffc4795 100644 --- a/tests/e2e/smoke.spec.ts +++ b/tests/e2e/smoke.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, openSettings, test } from "./lib/fixtures" test.describe("Smoke Tests", () => { test("homepage loads without errors", async ({ page }) => { @@ -8,8 +8,7 @@ test.describe("Smoke Tests", () => { await page.goto("/", { waitUntil: "networkidle" }) await expect(page).toHaveTitle(/Draw\.io/i, { timeout: 10000 }) - // Wait for draw.io iframe to be present - const iframe = page.locator("iframe") + const iframe = getIframe(page) await expect(iframe).toBeVisible({ timeout: 30000 }) expect(errors).toEqual([]) @@ -22,7 +21,7 @@ test.describe("Smoke Tests", () => { await page.goto("/ja", { waitUntil: "networkidle" }) await expect(page).toHaveTitle(/Draw\.io/i, { timeout: 10000 }) - const iframe = page.locator("iframe") + const iframe = getIframe(page) await expect(iframe).toBeVisible({ timeout: 30000 }) expect(errors).toEqual([]) @@ -30,20 +29,8 @@ test.describe("Smoke Tests", () => { test("settings dialog opens", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Wait for page to load - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) - - // Click settings button (gear icon) - const settingsButton = page.locator('[data-testid="settings-button"]') - await expect(settingsButton).toBeVisible() - await settingsButton.click() - - // Check if settings dialog appears - await expect(page.locator('[role="dialog"]')).toBeVisible({ - timeout: 5000, - }) + await openSettings(page) }) }) diff --git a/tests/e2e/theme.spec.ts b/tests/e2e/theme.spec.ts index 66024fc..2c7a8a4 100644 --- a/tests/e2e/theme.spec.ts +++ b/tests/e2e/theme.spec.ts @@ -1,112 +1,88 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, openSettings, sleep, test } from "./lib/fixtures" test.describe("Theme Switching", () => { test("can toggle app dark mode", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings - const settingsButton = page.locator( - 'button[aria-label*="Settings"], button:has(svg.lucide-settings)', - ) - await settingsButton.first().click() + await openSettings(page) - // Wait for settings dialog to open - await page.waitForTimeout(500) - - // Check initial state (should have html class) const html = page.locator("html") const initialClass = await html.getAttribute("class") - // Find and click the theme toggle button (sun/moon icon) const themeButton = page.locator( "button:has(svg.lucide-sun), button:has(svg.lucide-moon)", ) if ((await themeButton.count()) > 0) { - await themeButton.first().click() + await test.step("toggle theme", async () => { + await themeButton.first().click() + await sleep(500) + }) - // Class should change - await page.waitForTimeout(500) // Wait for theme transition - const newClass = await html.getAttribute("class") - expect(newClass).not.toBe(initialClass) + await test.step("verify theme changed", async () => { + const newClass = await html.getAttribute("class") + expect(newClass).not.toBe(initialClass) + }) } }) test("theme persists after page reload", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings and toggle theme - const settingsButton = page.locator( - 'button[aria-label*="Settings"], button:has(svg.lucide-settings)', - ) - await settingsButton.first().click() + await openSettings(page) - // Click theme button if available const themeButton = page.locator( "button:has(svg.lucide-sun), button:has(svg.lucide-moon)", ) if ((await themeButton.count()) > 0) { - await themeButton.first().click() - await page.waitForTimeout(300) + let themeClass: string | null - // Get current theme class - const html = page.locator("html") - const themeClass = await html.getAttribute("class") + await test.step("change theme", async () => { + await themeButton.first().click() + await sleep(300) + themeClass = await page.locator("html").getAttribute("class") + await page.keyboard.press("Escape") + }) - // Close dialog - await page.keyboard.press("Escape") + await test.step("reload page", async () => { + await page.reload({ waitUntil: "networkidle" }) + await getIframe(page).waitFor({ + state: "visible", + timeout: 30000, + }) + }) - // Reload page - await page.reload({ waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) - - // Theme should persist - const reloadedClass = await html.getAttribute("class") - expect(reloadedClass).toBe(themeClass) + await test.step("verify theme persisted", async () => { + const reloadedClass = await page + .locator("html") + .getAttribute("class") + expect(reloadedClass).toBe(themeClass) + }) } }) test("draw.io theme toggle exists", async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Open settings - const settingsButton = page.locator( - 'button[aria-label*="Settings"], button:has(svg.lucide-settings)', - ) - await settingsButton.first().click() + await openSettings(page) - // At least some settings content should be visible await expect( page.locator('[role="dialog"], [role="menu"], form').first(), ).toBeVisible({ timeout: 5000 }) }) test("system theme preference is respected", async ({ page }) => { - // Emulate dark mode preference await page.emulateMedia({ colorScheme: "dark" }) await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) - // Check if dark mode is applied (depends on implementation) const html = page.locator("html") const classes = await html.getAttribute("class") - // Should have dark class or similar - // This depends on the app's theme implementation expect(classes).toBeDefined() }) }) diff --git a/tests/e2e/upload.spec.ts b/tests/e2e/upload.spec.ts index 10f4dfe..ec2a339 100644 --- a/tests/e2e/upload.spec.ts +++ b/tests/e2e/upload.spec.ts @@ -1,21 +1,17 @@ -import { expect, test } from "@playwright/test" +import { expect, getIframe, test } from "./lib/fixtures" test.describe("File Upload Area", () => { test.beforeEach(async ({ page }) => { await page.goto("/", { waitUntil: "networkidle" }) - await page - .locator("iframe") - .waitFor({ state: "visible", timeout: 30000 }) + await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) }) test("page loads without console errors", async ({ page }) => { const errors: string[] = [] page.on("pageerror", (err) => errors.push(err.message)) - // Give page time to settle await page.waitForTimeout(1000) - // Filter out non-critical errors const criticalErrors = errors.filter( (e) => !e.includes("ResizeObserver") && !e.includes("Script error"), )