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"),
)