mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-10 02:02:31 +08:00
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
This commit is contained in:
@@ -1,25 +1,21 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Chat Panel", () => {
|
test.describe("Chat Panel", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("page has interactive elements", async ({ page }) => {
|
test("page has interactive elements", async ({ page }) => {
|
||||||
// Verify buttons exist (settings, etc.)
|
|
||||||
const buttons = page.locator("button")
|
const buttons = page.locator("button")
|
||||||
const count = await buttons.count()
|
const count = await buttons.count()
|
||||||
expect(count).toBeGreaterThan(0)
|
expect(count).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("draw.io iframe is interactive", async ({ page }) => {
|
test("draw.io iframe is interactive", async ({ page }) => {
|
||||||
const iframe = page.locator("iframe")
|
const iframe = getIframe(page)
|
||||||
await expect(iframe).toBeVisible()
|
await expect(iframe).toBeVisible()
|
||||||
|
|
||||||
// Iframe should have loaded draw.io
|
|
||||||
const src = await iframe.getAttribute("src")
|
const src = await iframe.getAttribute("src")
|
||||||
expect(src).toBeTruthy()
|
expect(src).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"
|
import { createMockSSEResponse } from "./lib/helpers"
|
||||||
|
|
||||||
test.describe("Copy/Paste Functionality", () => {
|
test.describe("Copy/Paste Functionality", () => {
|
||||||
test("can paste text into chat input", async ({ page }) => {
|
test("can paste text into chat input", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// Focus and paste text
|
|
||||||
await chatInput.focus()
|
await chatInput.focus()
|
||||||
await page.keyboard.insertText("Create a flowchart diagram")
|
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 }) => {
|
test("can paste multiline text into chat input", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
await chatInput.focus()
|
await chatInput.focus()
|
||||||
@@ -40,23 +42,16 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
contentType: "text/event-stream",
|
||||||
body: createMockSSEResponse(
|
body: createMockSSEResponse(
|
||||||
`<mxCell id="box" value="Test" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="100" height="50" as="geometry"/></mxCell>`,
|
SINGLE_BOX_XML,
|
||||||
"Here is your diagram with a test box.",
|
"Here is your diagram with a test box.",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Create a test box")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
// Send a message
|
|
||||||
await chatInput.fill("Create a test box")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Wait for response
|
// Wait for response
|
||||||
await expect(
|
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)',
|
'[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()
|
const buttonCount = await copyButton.count()
|
||||||
if (buttonCount === 0) {
|
if (buttonCount === 0) {
|
||||||
test.skip()
|
test.skip()
|
||||||
@@ -76,7 +71,6 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await copyButton.first().click()
|
await copyButton.first().click()
|
||||||
// Should show copied confirmation (toast or button state change)
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('text="Copied"').or(page.locator("svg.lucide-check")),
|
page.locator('text="Copied"').or(page.locator("svg.lucide-check")),
|
||||||
).toBeVisible({ timeout: 3000 })
|
).toBeVisible({ timeout: 3000 })
|
||||||
@@ -84,16 +78,14 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
|
|
||||||
test("paste XML into XML input works", async ({ page }) => {
|
test("paste XML into XML input works", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Find XML input textarea (different from chat input)
|
// Find XML input textarea
|
||||||
const xmlInput = page.locator(
|
const xmlInput = page.locator(
|
||||||
'textarea[placeholder*="XML"], textarea[placeholder*="mxCell"]',
|
'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(
|
const xmlToggle = page.locator(
|
||||||
'button:has-text("XML"), [data-testid*="xml"], details summary',
|
'button:has-text("XML"), [data-testid*="xml"], details summary',
|
||||||
)
|
)
|
||||||
@@ -101,7 +93,7 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
await xmlToggle.first().click()
|
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 xmlInputCount = await xmlInput.count()
|
||||||
const isXmlVisible =
|
const isXmlVisible =
|
||||||
xmlInputCount > 0 && (await xmlInput.first().isVisible())
|
xmlInputCount > 0 && (await xmlInput.first().isVisible())
|
||||||
@@ -120,20 +112,13 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
|
|
||||||
test("keyboard shortcuts work in chat input", async ({ page }) => {
|
test("keyboard shortcuts work in chat input", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// Type some text
|
|
||||||
await chatInput.fill("Hello world")
|
await chatInput.fill("Hello world")
|
||||||
|
|
||||||
// Select all with Ctrl+A
|
|
||||||
await chatInput.press("ControlOrMeta+a")
|
await chatInput.press("ControlOrMeta+a")
|
||||||
|
|
||||||
// Type replacement text
|
|
||||||
await chatInput.fill("New text")
|
await chatInput.fill("New text")
|
||||||
|
|
||||||
await expect(chatInput).toHaveValue("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 }) => {
|
test("can undo/redo in chat input", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// Type text
|
|
||||||
await chatInput.fill("First text")
|
await chatInput.fill("First text")
|
||||||
await chatInput.press("Tab") // Blur to register change
|
await chatInput.press("Tab")
|
||||||
|
|
||||||
await chatInput.focus()
|
await chatInput.focus()
|
||||||
await chatInput.fill("Second text")
|
await chatInput.fill("Second text")
|
||||||
|
|
||||||
// Undo with Ctrl+Z
|
|
||||||
await chatInput.press("ControlOrMeta+z")
|
await chatInput.press("ControlOrMeta+z")
|
||||||
|
|
||||||
// Verify page is still functional after undo
|
// Verify page is still functional after undo
|
||||||
@@ -164,11 +144,9 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
|
|
||||||
test("chat input handles special characters", async ({ page }) => {
|
test("chat input handles special characters", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
const specialText = "Test <>&\"' special chars 日本語 中文 🎉"
|
const specialText = "Test <>&\"' special chars 日本語 中文 🎉"
|
||||||
@@ -179,18 +157,14 @@ test.describe("Copy/Paste Functionality", () => {
|
|||||||
|
|
||||||
test("long text in chat input scrolls", async ({ page }) => {
|
test("long text in chat input scrolls", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// Very long text
|
|
||||||
const longText = "This is a very long text. ".repeat(50)
|
const longText = "This is a very long text. ".repeat(50)
|
||||||
await chatInput.fill(longText)
|
await chatInput.fill(longText)
|
||||||
|
|
||||||
// Input should handle it without error
|
|
||||||
const value = await chatInput.inputValue()
|
const value = await chatInput.inputValue()
|
||||||
expect(value.length).toBeGreaterThan(500)
|
expect(value.length).toBeGreaterThan(500)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"
|
import { createMockSSEResponse } from "./lib/helpers"
|
||||||
|
|
||||||
// Simple cat diagram XML for testing (mxCell elements only, no wrapper)
|
|
||||||
const CAT_DIAGRAM_XML = `<mxCell id="cat-head" value="Cat Head" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE4B5;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="200" y="100" width="100" height="80" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="cat-body" value="Cat Body" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE4B5;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="180" y="180" width="140" height="100" as="geometry"/>
|
|
||||||
</mxCell>`
|
|
||||||
|
|
||||||
// Simple flowchart XML for testing edits
|
|
||||||
const FLOWCHART_XML = `<mxCell id="start" value="Start" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="200" y="50" width="100" height="40" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="process" value="Process" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="200" y="130" width="100" height="40" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="end" value="End" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="200" y="210" width="100" height="40" as="geometry"/>
|
|
||||||
</mxCell>`
|
|
||||||
|
|
||||||
test.describe("Diagram Generation", () => {
|
test.describe("Diagram Generation", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// Mock the chat API to return our test XML
|
|
||||||
await page.route("**/api/chat", async (route) => {
|
await page.route("**/api/chat", async (route) => {
|
||||||
const response = createMockSSEResponse(
|
|
||||||
CAT_DIAGRAM_XML,
|
|
||||||
"I'll create a diagram for you.",
|
|
||||||
)
|
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
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 }) => {
|
test("generates and displays a diagram", async ({ page }) => {
|
||||||
// Find the chat input by aria-label
|
await sendMessage(page, "Draw a cat")
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
// Type a prompt
|
|
||||||
await chatInput.fill("Draw a cat")
|
|
||||||
|
|
||||||
// Submit using Cmd/Ctrl+Enter
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Wait for the tool card with "Generate Diagram" header and "Complete" badge
|
|
||||||
await expect(page.locator('text="Generate Diagram"')).toBeVisible({
|
await expect(page.locator('text="Generate Diagram"')).toBeVisible({
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
})
|
})
|
||||||
await expect(page.locator('text="Complete"')).toBeVisible({
|
await waitForComplete(page)
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("chat input clears after sending", async ({ 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 expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
await chatInput.fill("Draw a cat")
|
await chatInput.fill("Draw a cat")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await chatInput.press("ControlOrMeta+Enter")
|
||||||
|
|
||||||
// Input should clear after sending
|
|
||||||
await expect(chatInput).toHaveValue("", { timeout: 5000 })
|
await expect(chatInput).toHaveValue("", { timeout: 5000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("user message appears in chat", async ({ page }) => {
|
test("user message appears in chat", async ({ page }) => {
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Draw a cute cat")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await chatInput.fill("Draw a cute cat")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// User message should appear
|
|
||||||
await expect(page.locator('text="Draw a cute cat"')).toBeVisible({
|
await expect(page.locator('text="Draw a cute cat"')).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("assistant text message appears in chat", async ({ page }) => {
|
test("assistant text message appears in chat", async ({ page }) => {
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Draw a cat")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await chatInput.fill("Draw a cat")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Assistant message should appear
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('text="I\'ll create a diagram for you."'),
|
page.locator('text="I\'ll create a diagram for you."'),
|
||||||
).toBeVisible({ timeout: 10000 })
|
).toBeVisible({ timeout: 10000 })
|
||||||
@@ -101,36 +68,16 @@ test.describe("Diagram Generation", () => {
|
|||||||
|
|
||||||
test.describe("Diagram Edit", () => {
|
test.describe("Diagram Edit", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// First request: display initial diagram
|
await page.route(
|
||||||
// Second request: edit diagram
|
"**/api/chat",
|
||||||
let requestCount = 0
|
createMultiTurnMock([
|
||||||
await page.route("**/api/chat", async (route) => {
|
{ xml: FLOWCHART_XML, text: "I'll create a diagram for you." },
|
||||||
requestCount++
|
{
|
||||||
if (requestCount === 1) {
|
xml: FLOWCHART_XML.replace("Process", "Updated Process"),
|
||||||
await route.fulfill({
|
text: "I'll create a diagram for you.",
|
||||||
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.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await page
|
||||||
@@ -139,57 +86,29 @@ test.describe("Diagram Edit", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("can edit an existing diagram", async ({ page }) => {
|
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
|
// First: create initial diagram
|
||||||
await chatInput.fill("Create a flowchart")
|
await sendMessage(page, "Create a flowchart")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForComplete(page)
|
||||||
await expect(page.locator('text="Complete"').first()).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Second: edit the diagram
|
// Second: edit the diagram
|
||||||
await chatInput.fill("Change Process to Updated Process")
|
await sendMessage(page, "Change Process to Updated Process")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForCompleteCount(page, 2)
|
||||||
|
|
||||||
// Should see second "Complete" badge
|
|
||||||
await expect(page.locator('text="Complete"')).toHaveCount(2, {
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe("Diagram Append", () => {
|
test.describe("Diagram Append", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
let requestCount = 0
|
await page.route(
|
||||||
await page.route("**/api/chat", async (route) => {
|
"**/api/chat",
|
||||||
requestCount++
|
createMultiTurnMock([
|
||||||
if (requestCount === 1) {
|
{ xml: FLOWCHART_XML, text: "I'll create a diagram for you." },
|
||||||
await route.fulfill({
|
{
|
||||||
status: 200,
|
xml: NEW_NODE_XML,
|
||||||
contentType: "text/event-stream",
|
text: "I'll create a diagram for you.",
|
||||||
body: createMockSSEResponse(
|
toolName: "append_diagram",
|
||||||
FLOWCHART_XML,
|
},
|
||||||
"I'll create a diagram for you.",
|
]),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Append response - adds new element
|
|
||||||
const appendXml = `<mxCell id="new-node" value="New Node" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="350" y="130" width="100" height="40" as="geometry"/>
|
|
||||||
</mxCell>`
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: "text/event-stream",
|
|
||||||
body: createMockSSEResponse(
|
|
||||||
appendXml,
|
|
||||||
"I'll create a diagram for you.",
|
|
||||||
"append_diagram",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await page
|
||||||
@@ -198,23 +117,12 @@ test.describe("Diagram Append", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("can append to an existing diagram", async ({ page }) => {
|
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
|
// First: create initial diagram
|
||||||
await chatInput.fill("Create a flowchart")
|
await sendMessage(page, "Create a flowchart")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForComplete(page)
|
||||||
await expect(page.locator('text="Complete"').first()).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Second: append to diagram
|
// Second: append to diagram
|
||||||
await chatInput.fill("Add a new node to the right")
|
await sendMessage(page, "Add a new node to the right")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForCompleteCount(page, 2)
|
||||||
|
|
||||||
// Should see second "Complete" badge
|
|
||||||
await expect(page.locator('text="Complete"')).toHaveCount(2, {
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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.describe("Error Handling", () => {
|
||||||
test("displays error message when API returns 500", async ({ page }) => {
|
test("displays error message when API returns 500", async ({ page }) => {
|
||||||
await page.route("**/api/chat", async (route) => {
|
await page.route(
|
||||||
await route.fulfill({
|
"**/api/chat",
|
||||||
status: 500,
|
createErrorMock(500, "Internal server error"),
|
||||||
contentType: "application/json",
|
)
|
||||||
body: JSON.stringify({ error: "Internal server error" }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Draw a cat")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await chatInput.fill("Draw a cat")
|
// Should show error indication
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Should show error indication (toast, alert, or error text)
|
|
||||||
const errorIndicator = page
|
const errorIndicator = page
|
||||||
.locator('[role="alert"]')
|
.locator('[role="alert"]')
|
||||||
.or(page.locator("[data-sonner-toast]"))
|
.or(page.locator("[data-sonner-toast]"))
|
||||||
.or(page.locator("text=/error|failed|something went wrong/i"))
|
.or(page.locator("text=/error|failed|something went wrong/i"))
|
||||||
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
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 chatInput.fill("Retry message")
|
||||||
await expect(chatInput).toHaveValue("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 ({
|
test("displays error message when API returns 429 rate limit", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.route("**/api/chat", async (route) => {
|
await page.route(
|
||||||
await route.fulfill({
|
"**/api/chat",
|
||||||
status: 429,
|
createErrorMock(429, "Rate limit exceeded"),
|
||||||
contentType: "application/json",
|
)
|
||||||
body: JSON.stringify({ error: "Rate limit exceeded" }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Draw a cat")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await chatInput.fill("Draw a cat")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Should show error indication for rate limit
|
// Should show error indication for rate limit
|
||||||
const errorIndicator = page
|
const errorIndicator = page
|
||||||
@@ -63,27 +54,21 @@ test.describe("Error Handling", () => {
|
|||||||
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// User should be able to type again
|
// User should be able to type again
|
||||||
|
const chatInput = getChatInput(page)
|
||||||
await chatInput.fill("Retry after rate limit")
|
await chatInput.fill("Retry after rate limit")
|
||||||
await expect(chatInput).toHaveValue("Retry after rate limit")
|
await expect(chatInput).toHaveValue("Retry after rate limit")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("handles network timeout gracefully", async ({ page }) => {
|
test("handles network timeout gracefully", async ({ page }) => {
|
||||||
await page.route("**/api/chat", async (route) => {
|
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 new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
await route.abort("timedout")
|
await route.abort("timedout")
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Draw a cat")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await chatInput.fill("Draw a cat")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Should show error indication for network failure
|
// Should show error indication for network failure
|
||||||
const errorIndicator = page
|
const errorIndicator = page
|
||||||
@@ -93,6 +78,7 @@ test.describe("Error Handling", () => {
|
|||||||
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// After timeout, user should be able to type again
|
// After timeout, user should be able to type again
|
||||||
|
const chatInput = getChatInput(page)
|
||||||
await chatInput.fill("Try again after timeout")
|
await chatInput.fill("Try again after timeout")
|
||||||
await expect(chatInput).toHaveValue("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 textId = `text_${Date.now()}`
|
||||||
const messageId = `msg_${Date.now()}`
|
const messageId = `msg_${Date.now()}`
|
||||||
|
|
||||||
// Truncated XML (missing closing tags)
|
|
||||||
const truncatedXml = `<mxCell id="node1" value="Start" style="rounded=1;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="100" y="100" width="100" height="40"`
|
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
{ type: "start", messageId },
|
{ type: "start", messageId },
|
||||||
{ type: "text-start", id: textId },
|
{ type: "text-start", id: textId },
|
||||||
@@ -120,7 +102,7 @@ test.describe("Error Handling", () => {
|
|||||||
type: "tool-input-available",
|
type: "tool-input-available",
|
||||||
toolCallId,
|
toolCallId,
|
||||||
toolName: "display_diagram",
|
toolName: "display_diagram",
|
||||||
input: { xml: truncatedXml },
|
input: { xml: TRUNCATED_XML },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "tool-output-error",
|
type: "tool-output-error",
|
||||||
@@ -142,15 +124,9 @@ test.describe("Error Handling", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Draw something")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await chatInput.fill("Draw something")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Should show truncated badge
|
// Should show truncated badge
|
||||||
await expect(page.locator('text="Truncated"')).toBeVisible({
|
await expect(page.locator('text="Truncated"')).toBeVisible({
|
||||||
|
|||||||
@@ -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"
|
import { createMockSSEResponse } from "./lib/helpers"
|
||||||
|
|
||||||
test.describe("File Upload", () => {
|
test.describe("File Upload", () => {
|
||||||
test("upload button opens file picker", async ({ page }) => {
|
test("upload button opens file picker", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Find the upload button (image icon)
|
|
||||||
const uploadButton = page.locator(
|
const uploadButton = page.locator(
|
||||||
'button[aria-label="Upload file"], button:has(svg.lucide-image)',
|
'button[aria-label="Upload file"], button:has(svg.lucide-image)',
|
||||||
)
|
)
|
||||||
await expect(uploadButton.first()).toBeVisible({ timeout: 10000 })
|
await expect(uploadButton.first()).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// Click should trigger hidden file input
|
|
||||||
// Just verify the button is clickable
|
|
||||||
await expect(uploadButton.first()).toBeEnabled()
|
await expect(uploadButton.first()).toBeEnabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("shows file preview after selecting image", async ({ page }) => {
|
test("shows file preview after selecting image", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Find the hidden file input
|
|
||||||
const fileInput = page.locator('input[type="file"]')
|
const fileInput = page.locator('input[type="file"]')
|
||||||
|
|
||||||
// Create a test image file
|
|
||||||
await fileInput.setInputFiles({
|
await fileInput.setInputFiles({
|
||||||
name: "test-image.png",
|
name: "test-image.png",
|
||||||
mimeType: "image/png",
|
mimeType: "image/png",
|
||||||
buffer: Buffer.from(
|
buffer: Buffer.from(
|
||||||
// Minimal valid PNG (1x1 transparent pixel)
|
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||||
"base64",
|
"base64",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
// File input should have processed the file
|
|
||||||
// Check that no error toast appeared
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('[role="alert"][data-type="error"]'),
|
page.locator('[role="alert"][data-type="error"]'),
|
||||||
).not.toBeVisible({ timeout: 2000 })
|
).not.toBeVisible({ timeout: 2000 })
|
||||||
@@ -48,13 +42,10 @@ test.describe("File Upload", () => {
|
|||||||
|
|
||||||
test("can remove uploaded file", async ({ page }) => {
|
test("can remove uploaded file", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const fileInput = page.locator('input[type="file"]')
|
const fileInput = page.locator('input[type="file"]')
|
||||||
|
|
||||||
// Upload a file
|
|
||||||
await fileInput.setInputFiles({
|
await fileInput.setInputFiles({
|
||||||
name: "test-image.png",
|
name: "test-image.png",
|
||||||
mimeType: "image/png",
|
mimeType: "image/png",
|
||||||
@@ -64,17 +55,14 @@ test.describe("File Upload", () => {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wait for file preview or no error
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('[role="alert"][data-type="error"]'),
|
page.locator('[role="alert"][data-type="error"]'),
|
||||||
).not.toBeVisible({ timeout: 2000 })
|
).not.toBeVisible({ timeout: 2000 })
|
||||||
|
|
||||||
// Find and click remove button if it exists (X icon)
|
|
||||||
const removeButton = page.locator(
|
const removeButton = page.locator(
|
||||||
'[data-testid="remove-file-button"], button[aria-label*="Remove"], button:has(svg.lucide-x)',
|
'[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()
|
const removeButtonCount = await removeButton.count()
|
||||||
if (removeButtonCount === 0) {
|
if (removeButtonCount === 0) {
|
||||||
test.skip()
|
test.skip()
|
||||||
@@ -82,7 +70,6 @@ test.describe("File Upload", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await removeButton.first().click()
|
await removeButton.first().click()
|
||||||
// Verify button is gone or file preview is removed
|
|
||||||
await expect(removeButton.first()).not.toBeVisible({ timeout: 2000 })
|
await expect(removeButton.first()).not.toBeVisible({ timeout: 2000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -95,21 +82,17 @@ test.describe("File Upload", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
contentType: "text/event-stream",
|
||||||
body: createMockSSEResponse(
|
body: createMockSSEResponse(
|
||||||
`<mxCell id="img" value="Diagram" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="100" height="100" as="geometry"/></mxCell>`,
|
SINGLE_BOX_XML,
|
||||||
"Based on your image, here is a diagram:",
|
"Based on your image, here is a diagram:",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const fileInput = page.locator('input[type="file"]')
|
const fileInput = page.locator('input[type="file"]')
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
|
|
||||||
// Upload a file
|
|
||||||
await fileInput.setInputFiles({
|
await fileInput.setInputFiles({
|
||||||
name: "architecture.png",
|
name: "architecture.png",
|
||||||
mimeType: "image/png",
|
mimeType: "image/png",
|
||||||
@@ -119,29 +102,21 @@ test.describe("File Upload", () => {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Type message and send
|
await sendMessage(page, "Convert this to a diagram")
|
||||||
await chatInput.fill("Convert this to a diagram")
|
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
|
||||||
|
|
||||||
// Wait for response
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('text="Based on your image, here is a diagram:"'),
|
page.locator('text="Based on your image, here is a diagram:"'),
|
||||||
).toBeVisible({ timeout: 15000 })
|
).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
// Verify request was made (file should be in request body as base64)
|
|
||||||
expect(capturedRequest).not.toBeNull()
|
expect(capturedRequest).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("shows error for oversized file", async ({ page }) => {
|
test("shows error for oversized file", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const fileInput = page.locator('input[type="file"]')
|
const fileInput = page.locator('input[type="file"]')
|
||||||
|
const largeBuffer = Buffer.alloc(3 * 1024 * 1024, "x")
|
||||||
// Create a large file (> 2MB limit)
|
|
||||||
const largeBuffer = Buffer.alloc(3 * 1024 * 1024, "x") // 3MB
|
|
||||||
|
|
||||||
await fileInput.setInputFiles({
|
await fileInput.setInputFiles({
|
||||||
name: "large-image.png",
|
name: "large-image.png",
|
||||||
@@ -149,8 +124,6 @@ test.describe("File Upload", () => {
|
|||||||
buffer: largeBuffer,
|
buffer: largeBuffer,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Should show error toast/message about file size
|
|
||||||
// The exact error message depends on the app implementation
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('[role="alert"], [data-sonner-toast]').first(),
|
page.locator('[role="alert"], [data-sonner-toast]').first(),
|
||||||
).toBeVisible({ timeout: 5000 })
|
).toBeVisible({ timeout: 5000 })
|
||||||
@@ -158,13 +131,10 @@ test.describe("File Upload", () => {
|
|||||||
|
|
||||||
test("drag and drop file upload works", async ({ page }) => {
|
test("drag and drop file upload works", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatForm = page.locator("form").first()
|
const chatForm = page.locator("form").first()
|
||||||
|
|
||||||
// Create a DataTransfer with a file
|
|
||||||
const dataTransfer = await page.evaluateHandle(() => {
|
const dataTransfer = await page.evaluateHandle(() => {
|
||||||
const dt = new DataTransfer()
|
const dt = new DataTransfer()
|
||||||
const file = new File(["test content"], "dropped-image.png", {
|
const file = new File(["test content"], "dropped-image.png", {
|
||||||
@@ -174,13 +144,9 @@ test.describe("File Upload", () => {
|
|||||||
return dt
|
return dt
|
||||||
})
|
})
|
||||||
|
|
||||||
// Dispatch drag and drop events
|
|
||||||
await chatForm.dispatchEvent("dragover", { dataTransfer })
|
await chatForm.dispatchEvent("dragover", { dataTransfer })
|
||||||
await chatForm.dispatchEvent("drop", { dataTransfer })
|
await chatForm.dispatchEvent("drop", { dataTransfer })
|
||||||
|
|
||||||
// Should not crash - verify page is still functional
|
await expect(getChatInput(page)).toBeVisible({ timeout: 3000 })
|
||||||
await expect(
|
|
||||||
page.locator('textarea[aria-label="Chat input"]'),
|
|
||||||
).toBeVisible({ timeout: 3000 })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
50
tests/e2e/fixtures/diagrams.ts
Normal file
50
tests/e2e/fixtures/diagrams.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Shared XML diagram fixtures for E2E tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Simple cat diagram
|
||||||
|
export const CAT_DIAGRAM_XML = `<mxCell id="cat-head" value="Cat Head" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE4B5;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="100" width="100" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="cat-body" value="Cat Body" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE4B5;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="180" y="180" width="140" height="100" as="geometry"/>
|
||||||
|
</mxCell>`
|
||||||
|
|
||||||
|
// Simple flowchart
|
||||||
|
export const FLOWCHART_XML = `<mxCell id="start" value="Start" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="50" width="100" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="process" value="Process" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="130" width="100" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="end" value="End" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="210" width="100" height="40" as="geometry"/>
|
||||||
|
</mxCell>`
|
||||||
|
|
||||||
|
// Simple single box
|
||||||
|
export const SINGLE_BOX_XML = `<mxCell id="box" value="Test Box" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>`
|
||||||
|
|
||||||
|
// Test node for iframe interaction tests
|
||||||
|
export const TEST_NODE_XML = `<mxCell id="test-node-123" value="Test Node" style="rounded=1;fillColor=#d5e8d4;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>`
|
||||||
|
|
||||||
|
// Architecture box
|
||||||
|
export const ARCHITECTURE_XML = `<mxCell id="arch" value="Architecture" style="rounded=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="120" height="50" as="geometry"/>
|
||||||
|
</mxCell>`
|
||||||
|
|
||||||
|
// New node for append tests
|
||||||
|
export const NEW_NODE_XML = `<mxCell id="new-node" value="New Node" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="350" y="130" width="100" height="40" as="geometry"/>
|
||||||
|
</mxCell>`
|
||||||
|
|
||||||
|
// Truncated XML for error tests
|
||||||
|
export const TRUNCATED_XML = `<mxCell id="node1" value="Start" style="rounded=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="100" height="40"`
|
||||||
|
|
||||||
|
// Simple boxes for multi-turn tests
|
||||||
|
export const createBoxXml = (id: string, label: string, y = 100) =>
|
||||||
|
`<mxCell id="${id}" value="${label}" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="${y}" width="100" height="40" as="geometry"/></mxCell>`
|
||||||
@@ -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"
|
import { createMockSSEResponse } from "./lib/helpers"
|
||||||
|
|
||||||
test.describe("History and Session Restore", () => {
|
test.describe("History and Session Restore", () => {
|
||||||
@@ -8,55 +20,43 @@ test.describe("History and Session Restore", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
contentType: "text/event-stream",
|
||||||
body: createMockSSEResponse(
|
body: createMockSSEResponse(
|
||||||
`<mxCell id="node" value="Test" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="100" height="50" as="geometry"/></mxCell>`,
|
SINGLE_BOX_XML,
|
||||||
"Created your test diagram.",
|
"Created your test diagram.",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await test.step("create a conversation", async () => {
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
await sendMessage(page, "Create a test diagram")
|
||||||
|
await waitForText(page, "Created your test diagram.")
|
||||||
// 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,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Find and click new chat button
|
await test.step("click new chat button", async () => {
|
||||||
const newChatButton = page.locator('[data-testid="new-chat-button"]')
|
const newChatButton = page.locator(
|
||||||
await expect(newChatButton).toBeVisible({ timeout: 5000 })
|
'[data-testid="new-chat-button"]',
|
||||||
|
)
|
||||||
|
await expect(newChatButton).toBeVisible({ timeout: 5000 })
|
||||||
|
await newChatButton.click()
|
||||||
|
})
|
||||||
|
|
||||||
await newChatButton.click()
|
await test.step("verify conversation is cleared", async () => {
|
||||||
|
await expect(
|
||||||
// Conversation should be cleared
|
page.locator('text="Created your test diagram."'),
|
||||||
await expect(
|
).not.toBeVisible({ timeout: 5000 })
|
||||||
page.locator('text="Created your test diagram."'),
|
})
|
||||||
).not.toBeVisible({ timeout: 5000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("chat history sidebar shows past conversations", async ({ page }) => {
|
test("chat history sidebar shows past conversations", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Look for history/sidebar button that is enabled
|
|
||||||
const historyButton = page.locator(
|
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])',
|
'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()
|
const buttonCount = await historyButton.count()
|
||||||
if (buttonCount === 0) {
|
if (buttonCount === 0) {
|
||||||
test.skip()
|
test.skip()
|
||||||
@@ -64,10 +64,7 @@ test.describe("History and Session Restore", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await historyButton.first().click()
|
await historyButton.first().click()
|
||||||
// Wait for sidebar/panel to appear or verify page still works
|
await expect(getChatInput(page)).toBeVisible({ timeout: 3000 })
|
||||||
await expect(
|
|
||||||
page.locator('textarea[aria-label="Chat input"]'),
|
|
||||||
).toBeVisible({ timeout: 3000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("conversation persists after page reload", async ({ page }) => {
|
test("conversation persists after page reload", async ({ page }) => {
|
||||||
@@ -76,44 +73,30 @@ test.describe("History and Session Restore", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
contentType: "text/event-stream",
|
||||||
body: createMockSSEResponse(
|
body: createMockSSEResponse(
|
||||||
`<mxCell id="persist" value="Persistent" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="100" height="50" as="geometry"/></mxCell>`,
|
SINGLE_BOX_XML,
|
||||||
"This message should persist.",
|
"This message should persist.",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await test.step("create conversation", async () => {
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
await sendMessage(page, "Create persistent diagram")
|
||||||
|
await waitForText(page, "This message should persist.")
|
||||||
|
})
|
||||||
|
|
||||||
// Send a message
|
await expectBeforeAndAfterReload(
|
||||||
await chatInput.fill("Create persistent diagram")
|
page,
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
"conversation message persists",
|
||||||
|
async () => {
|
||||||
// Wait for response
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('text="This message should persist."'),
|
page.locator('text="This message should persist."'),
|
||||||
).toBeVisible({ timeout: 15000 })
|
).toBeVisible({ timeout: 10000 })
|
||||||
|
},
|
||||||
// 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 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("diagram state persists after reload", async ({ page }) => {
|
test("diagram state persists after reload", async ({ page }) => {
|
||||||
@@ -122,36 +105,22 @@ test.describe("History and Session Restore", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
contentType: "text/event-stream",
|
||||||
body: createMockSSEResponse(
|
body: createMockSSEResponse(
|
||||||
`<mxCell id="saved" value="Saved Diagram" style="rounded=1;fillColor=#d5e8d4;" vertex="1" parent="1"><mxGeometry x="150" y="150" width="140" height="70" as="geometry"/></mxCell>`,
|
SINGLE_BOX_XML,
|
||||||
"Created a diagram that should be saved.",
|
"Created a diagram that should be saved.",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Create saveable diagram")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
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.reload({ waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Diagram state is typically stored - check iframe is still functional
|
const frame = getIframeContent(page)
|
||||||
const frame = page.frameLocator("iframe")
|
|
||||||
await expect(
|
await expect(
|
||||||
frame
|
frame
|
||||||
.locator(".geMenubarContainer, .geDiagramContainer, canvas")
|
.locator(".geMenubarContainer, .geDiagramContainer, canvas")
|
||||||
@@ -165,88 +134,46 @@ test.describe("History and Session Restore", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
contentType: "text/event-stream",
|
||||||
body: createMockSSEResponse(
|
body: createMockSSEResponse(
|
||||||
`<mxCell id="nav" value="Navigation Test" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="50" as="geometry"/></mxCell>`,
|
SINGLE_BOX_XML,
|
||||||
"Testing browser navigation.",
|
"Testing browser navigation.",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Test navigation")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
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" })
|
await page.goto("/about", { waitUntil: "networkidle" })
|
||||||
|
|
||||||
// Go back
|
|
||||||
await page.goBack({ waitUntil: "networkidle" })
|
await page.goBack({ waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Page should be functional
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("settings are restored after reload", async ({ page }) => {
|
test("settings are restored after reload", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings
|
await openSettings(page)
|
||||||
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 page.keyboard.press("Escape")
|
await page.keyboard.press("Escape")
|
||||||
|
|
||||||
// Reload
|
|
||||||
await page.reload({ waitUntil: "networkidle" })
|
await page.reload({ waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings again
|
await openSettings(page)
|
||||||
await settingsButton.click()
|
|
||||||
|
|
||||||
// Settings should still be accessible
|
|
||||||
await expect(
|
|
||||||
page.locator('[role="dialog"], [role="menu"], form').first(),
|
|
||||||
).toBeVisible({ timeout: 5000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("model selection persists", async ({ page }) => {
|
test("model selection persists", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Find model selector
|
|
||||||
const modelSelector = page.locator(
|
const modelSelector = page.locator(
|
||||||
'button[aria-label*="Model"], [data-testid="model-selector"], button:has-text("Claude")',
|
'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()
|
const selectorCount = await modelSelector.count()
|
||||||
if (selectorCount === 0) {
|
if (selectorCount === 0) {
|
||||||
test.skip()
|
test.skip()
|
||||||
@@ -255,39 +182,28 @@ test.describe("History and Session Restore", () => {
|
|||||||
|
|
||||||
const initialModel = await modelSelector.first().textContent()
|
const initialModel = await modelSelector.first().textContent()
|
||||||
|
|
||||||
// Reload page
|
|
||||||
await page.reload({ waitUntil: "networkidle" })
|
await page.reload({ waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Check model is still selected
|
|
||||||
const modelAfterReload = await modelSelector.first().textContent()
|
const modelAfterReload = await modelSelector.first().textContent()
|
||||||
expect(modelAfterReload).toBe(initialModel)
|
expect(modelAfterReload).toBe(initialModel)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("handles localStorage quota exceeded gracefully", async ({ page }) => {
|
test("handles localStorage quota exceeded gracefully", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Fill up localStorage (simulate quota exceeded scenario)
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
try {
|
try {
|
||||||
// This might throw if quota is exceeded
|
const largeData = "x".repeat(5 * 1024 * 1024)
|
||||||
const largeData = "x".repeat(5 * 1024 * 1024) // 5MB
|
|
||||||
localStorage.setItem("test-large-data", largeData)
|
localStorage.setItem("test-large-data", largeData)
|
||||||
} catch {
|
} catch {
|
||||||
// Expected to fail on some browsers
|
// Expected to fail on some browsers
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// App should still function
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
localStorage.removeItem("test-large-data")
|
localStorage.removeItem("test-large-data")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("History Dialog", () => {
|
test.describe("History Dialog", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("history button exists in UI", async ({ page }) => {
|
test("history button exists in UI", async ({ page }) => {
|
||||||
|
|||||||
@@ -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"
|
import { createMockSSEResponse } from "./lib/helpers"
|
||||||
|
|
||||||
test.describe("Iframe Interaction", () => {
|
test.describe("Iframe Interaction", () => {
|
||||||
test("draw.io iframe loads successfully", async ({ page }) => {
|
test("draw.io iframe loads successfully", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
|
|
||||||
const iframe = page.locator("iframe")
|
const iframe = getIframe(page)
|
||||||
await expect(iframe).toBeVisible({ timeout: 30000 })
|
await expect(iframe).toBeVisible({ timeout: 30000 })
|
||||||
|
|
||||||
// iframe should have loaded draw.io content
|
// iframe should have loaded draw.io content
|
||||||
const frame = page.frameLocator("iframe")
|
const frame = getIframeContent(page)
|
||||||
await expect(
|
await expect(
|
||||||
frame
|
frame
|
||||||
.locator(".geMenubarContainer, .geDiagramContainer, canvas")
|
.locator(".geMenubarContainer, .geDiagramContainer, canvas")
|
||||||
@@ -19,11 +28,9 @@ test.describe("Iframe Interaction", () => {
|
|||||||
|
|
||||||
test("can interact with draw.io toolbar", async ({ page }) => {
|
test("can interact with draw.io toolbar", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const frame = page.frameLocator("iframe")
|
const frame = getIframeContent(page)
|
||||||
|
|
||||||
// Draw.io menu items should be accessible
|
// Draw.io menu items should be accessible
|
||||||
await expect(
|
await expect(
|
||||||
@@ -36,62 +43,42 @@ test.describe("Iframe Interaction", () => {
|
|||||||
test("diagram XML is rendered in iframe after generation", async ({
|
test("diagram XML is rendered in iframe after generation", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const testXml = `<mxCell id="test-node-123" value="Test Node" style="rounded=1;fillColor=#d5e8d4;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
|
||||||
</mxCell>`
|
|
||||||
|
|
||||||
await page.route("**/api/chat", async (route) => {
|
await page.route("**/api/chat", async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/event-stream",
|
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.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
await sendMessage(page, "Create a test node")
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
await waitForComplete(page)
|
||||||
|
|
||||||
await chatInput.fill("Create a test node")
|
// Give draw.io time to render
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
// 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
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("zoom controls work in draw.io", async ({ page }) => {
|
test("zoom controls work in draw.io", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.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
|
// draw.io should be loaded and functional - check for diagram container
|
||||||
await expect(
|
await expect(
|
||||||
frame.locator(".geDiagramContainer, canvas").first(),
|
frame.locator(".geDiagramContainer, canvas").first(),
|
||||||
).toBeVisible({ timeout: 10000 })
|
).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 }) => {
|
test("can resize the panel divider", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Find the resizer/divider between panels
|
// Find the resizer/divider between panels
|
||||||
const resizer = page.locator(
|
const resizer = page.locator(
|
||||||
@@ -99,10 +86,8 @@ test.describe("Iframe Interaction", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ((await resizer.count()) > 0) {
|
if ((await resizer.count()) > 0) {
|
||||||
// Resizer should be draggable
|
|
||||||
await expect(resizer.first()).toBeVisible()
|
await expect(resizer.first()).toBeVisible()
|
||||||
|
|
||||||
// Try to drag it
|
|
||||||
const box = await resizer.first().boundingBox()
|
const box = await resizer.first().boundingBox()
|
||||||
if (box) {
|
if (box) {
|
||||||
await page.mouse.move(
|
await page.mouse.move(
|
||||||
@@ -118,11 +103,9 @@ test.describe("Iframe Interaction", () => {
|
|||||||
|
|
||||||
test("iframe responds to window resize", async ({ page }) => {
|
test("iframe responds to window resize", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
const iframe = page.locator("iframe")
|
const iframe = getIframe(page)
|
||||||
const initialBox = await iframe.boundingBox()
|
const initialBox = await iframe.boundingBox()
|
||||||
|
|
||||||
// Resize window
|
// Resize window
|
||||||
@@ -131,9 +114,7 @@ test.describe("Iframe Interaction", () => {
|
|||||||
|
|
||||||
const newBox = await iframe.boundingBox()
|
const newBox = await iframe.boundingBox()
|
||||||
|
|
||||||
// iframe should have adjusted
|
|
||||||
expect(newBox).toBeDefined()
|
expect(newBox).toBeDefined()
|
||||||
// Size should be different or at least still valid
|
|
||||||
if (initialBox && newBox) {
|
if (initialBox && newBox) {
|
||||||
expect(newBox.width).toBeLessThanOrEqual(800)
|
expect(newBox.width).toBeLessThanOrEqual(800)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,22 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, openSettings, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Keyboard Interactions", () => {
|
test.describe("Keyboard Interactions", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Escape closes settings dialog", async ({ page }) => {
|
test("Escape closes settings dialog", async ({ page }) => {
|
||||||
// Find settings button using aria-label or icon
|
await openSettings(page)
|
||||||
const settingsButton = page.locator(
|
|
||||||
'button[aria-label*="settings"], button:has(svg[class*="settings"])',
|
|
||||||
)
|
|
||||||
await expect(settingsButton).toBeVisible()
|
|
||||||
await settingsButton.click()
|
|
||||||
|
|
||||||
// Wait for dialog to appear
|
|
||||||
const dialog = page.locator('[role="dialog"]')
|
const dialog = page.locator('[role="dialog"]')
|
||||||
await expect(dialog).toBeVisible({ timeout: 5000 })
|
await expect(dialog).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
// Press Escape and verify dialog closes
|
|
||||||
await page.keyboard.press("Escape")
|
await page.keyboard.press("Escape")
|
||||||
await expect(dialog).not.toBeVisible({ timeout: 2000 })
|
await expect(dialog).not.toBeVisible({ timeout: 2000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("page is keyboard accessible", async ({ page }) => {
|
test("page is keyboard accessible", async ({ page }) => {
|
||||||
// Verify page has focusable elements
|
|
||||||
const focusableElements = page.locator(
|
const focusableElements = page.locator(
|
||||||
'button, [tabindex="0"], input, textarea, a[href]',
|
'button, [tabindex="0"], input, textarea, a[href]',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.describe("Language Switching", () => {
|
||||||
test("loads English by default", async ({ page }) => {
|
test("loads English by default", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Check for English UI text
|
const chatInput = getChatInput(page)
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
// Send button should say "Send"
|
|
||||||
await expect(page.locator('button:has-text("Send")')).toBeVisible()
|
await expect(page.locator('button:has-text("Send")')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can switch to Japanese", async ({ page }) => {
|
test("can switch to Japanese", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings
|
await test.step("open settings and select Japanese", async () => {
|
||||||
const settingsButton = page.locator(
|
await openSettings(page)
|
||||||
'button[aria-label*="Settings"], button:has(svg.lucide-settings)',
|
const languageSelector = page.locator('button:has-text("English")')
|
||||||
)
|
await languageSelector.first().click()
|
||||||
await settingsButton.first().click()
|
await page.locator('text="日本語"').click()
|
||||||
|
})
|
||||||
|
|
||||||
// Find language selector
|
await test.step("verify UI is in Japanese", async () => {
|
||||||
const languageSelector = page.locator('button:has-text("English")')
|
await expect(page.locator('button:has-text("送信")')).toBeVisible({
|
||||||
await languageSelector.first().click()
|
timeout: 5000,
|
||||||
|
})
|
||||||
// Select Japanese
|
|
||||||
await page.locator('text="日本語"').click()
|
|
||||||
|
|
||||||
// UI should update to Japanese
|
|
||||||
await expect(page.locator('button:has-text("送信")')).toBeVisible({
|
|
||||||
timeout: 5000,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can switch to Chinese", async ({ page }) => {
|
test("can switch to Chinese", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings
|
await test.step("open settings and select Chinese", async () => {
|
||||||
const settingsButton = page.locator(
|
await openSettings(page)
|
||||||
'button[aria-label*="Settings"], button:has(svg.lucide-settings)',
|
const languageSelector = page.locator('button:has-text("English")')
|
||||||
)
|
await languageSelector.first().click()
|
||||||
await settingsButton.first().click()
|
await page.locator('text="中文"').click()
|
||||||
|
})
|
||||||
|
|
||||||
// Find language selector and switch to Chinese
|
await test.step("verify UI is in Chinese", async () => {
|
||||||
const languageSelector = page.locator('button:has-text("English")')
|
await expect(page.locator('button:has-text("发送")')).toBeVisible({
|
||||||
await languageSelector.first().click()
|
timeout: 5000,
|
||||||
|
})
|
||||||
await page.locator('text="中文"').click()
|
|
||||||
|
|
||||||
// UI should update to Chinese
|
|
||||||
await expect(page.locator('button:has-text("发送")')).toBeVisible({
|
|
||||||
timeout: 5000,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("language persists after reload", async ({ page }) => {
|
test("language persists after reload", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings and switch to Japanese
|
await test.step("switch to Japanese", async () => {
|
||||||
const settingsButton = page.locator(
|
await openSettings(page)
|
||||||
'button[aria-label*="Settings"], button:has(svg.lucide-settings)',
|
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 }) => {
|
test("Japanese locale URL works", async ({ page }) => {
|
||||||
await page.goto("/ja", { waitUntil: "networkidle" })
|
await page.goto("/ja", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Should show Japanese UI
|
|
||||||
await expect(page.locator('button:has-text("送信")')).toBeVisible({
|
await expect(page.locator('button:has-text("送信")')).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
})
|
})
|
||||||
@@ -115,11 +92,8 @@ test.describe("Language Switching", () => {
|
|||||||
|
|
||||||
test("Chinese locale URL works", async ({ page }) => {
|
test("Chinese locale URL works", async ({ page }) => {
|
||||||
await page.goto("/zh", { waitUntil: "networkidle" })
|
await page.goto("/zh", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Should show Chinese UI
|
|
||||||
await expect(page.locator('button:has-text("发送")')).toBeVisible({
|
await expect(page.locator('button:has-text("发送")')).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
})
|
})
|
||||||
|
|||||||
208
tests/e2e/lib/fixtures.ts
Normal file
208
tests/e2e/lib/fixtures.ts
Normal file
@@ -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<void>,
|
||||||
|
) {
|
||||||
|
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<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
@@ -1,27 +1,17 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, openSettings, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Model Configuration", () => {
|
test.describe("Model Configuration", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("settings dialog opens and shows configuration options", async ({
|
test("settings dialog opens and shows configuration options", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
// Open settings
|
await openSettings(page)
|
||||||
const settingsButton = page.locator(
|
|
||||||
'button[aria-label*="settings"], button:has(svg[class*="settings"])',
|
|
||||||
)
|
|
||||||
await expect(settingsButton).toBeVisible()
|
|
||||||
await settingsButton.click()
|
|
||||||
|
|
||||||
const dialog = page.locator('[role="dialog"]')
|
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 buttons = dialog.locator("button")
|
||||||
const buttonCount = await buttons.count()
|
const buttonCount = await buttons.count()
|
||||||
expect(buttonCount).toBeGreaterThan(0)
|
expect(buttonCount).toBeGreaterThan(0)
|
||||||
|
|||||||
@@ -1,46 +1,44 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { ARCHITECTURE_XML, createBoxXml } from "./fixtures/diagrams"
|
||||||
import { createMockSSEResponse, createTextOnlyResponse } from "./lib/helpers"
|
import {
|
||||||
|
createMixedMock,
|
||||||
|
createMultiTurnMock,
|
||||||
|
expect,
|
||||||
|
getChatInput,
|
||||||
|
sendMessage,
|
||||||
|
test,
|
||||||
|
waitForComplete,
|
||||||
|
waitForText,
|
||||||
|
} from "./lib/fixtures"
|
||||||
|
import { createTextOnlyResponse } from "./lib/helpers"
|
||||||
|
|
||||||
test.describe("Multi-turn Conversation", () => {
|
test.describe("Multi-turn Conversation", () => {
|
||||||
test("handles multiple diagram requests in sequence", async ({ page }) => {
|
test("handles multiple diagram requests in sequence", async ({ page }) => {
|
||||||
let requestCount = 0
|
await page.route(
|
||||||
await page.route("**/api/chat", async (route) => {
|
"**/api/chat",
|
||||||
requestCount++
|
createMultiTurnMock([
|
||||||
const xml =
|
{
|
||||||
requestCount === 1
|
xml: createBoxXml("box1", "First"),
|
||||||
? `<mxCell id="box1" value="First" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="100" height="40" as="geometry"/></mxCell>`
|
text: "Creating diagram 1...",
|
||||||
: `<mxCell id="box2" value="Second" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="200" width="100" height="40" as="geometry"/></mxCell>`
|
},
|
||||||
await route.fulfill({
|
{
|
||||||
status: 200,
|
xml: createBoxXml("box2", "Second", 200),
|
||||||
contentType: "text/event-stream",
|
text: "Creating diagram 2...",
|
||||||
body: createMockSSEResponse(
|
},
|
||||||
xml,
|
]),
|
||||||
`Creating diagram ${requestCount}...`,
|
)
|
||||||
),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await page
|
||||||
.locator("iframe")
|
.locator("iframe")
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
.waitFor({ state: "visible", timeout: 30000 })
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
// First request
|
// First request
|
||||||
await chatInput.fill("Draw first box")
|
await sendMessage(page, "Draw first box")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForText(page, "Creating diagram 1...")
|
||||||
await expect(page.locator('text="Creating diagram 1..."')).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Second request
|
// Second request
|
||||||
await chatInput.fill("Draw second box")
|
await sendMessage(page, "Draw second box")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForText(page, "Creating diagram 2...")
|
||||||
await expect(page.locator('text="Creating diagram 2..."')).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Both messages should be visible
|
// Both messages should be visible
|
||||||
await expect(page.locator('text="Draw first box"')).toBeVisible()
|
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
|
// Verify messages array grows with each request
|
||||||
if (requestCount === 2) {
|
if (requestCount === 2) {
|
||||||
// Second request should have previous messages
|
|
||||||
expect(body.messages?.length).toBeGreaterThan(1)
|
expect(body.messages?.length).toBeGreaterThan(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,70 +69,45 @@ test.describe("Multi-turn Conversation", () => {
|
|||||||
.locator("iframe")
|
.locator("iframe")
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
.waitFor({ state: "visible", timeout: 30000 })
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
// First message
|
// First message
|
||||||
await chatInput.fill("Hello")
|
await sendMessage(page, "Hello")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForText(page, "Response 1")
|
||||||
await expect(page.locator('text="Response 1"')).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Second message (should include history)
|
// Second message (should include history)
|
||||||
await chatInput.fill("Follow up question")
|
await sendMessage(page, "Follow up question")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForText(page, "Response 2")
|
||||||
await expect(page.locator('text="Response 2"')).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can continue after a text-only response", async ({ page }) => {
|
test("can continue after a text-only response", async ({ page }) => {
|
||||||
let requestCount = 0
|
await page.route(
|
||||||
await page.route("**/api/chat", async (route) => {
|
"**/api/chat",
|
||||||
requestCount++
|
createMixedMock([
|
||||||
if (requestCount === 1) {
|
{
|
||||||
// First: text-only explanation
|
type: "text",
|
||||||
await route.fulfill({
|
text: "I understand. Let me explain the architecture first.",
|
||||||
status: 200,
|
},
|
||||||
contentType: "text/event-stream",
|
{
|
||||||
body: createTextOnlyResponse(
|
type: "diagram",
|
||||||
"I understand. Let me explain the architecture first.",
|
xml: ARCHITECTURE_XML,
|
||||||
),
|
text: "Here is the diagram:",
|
||||||
})
|
},
|
||||||
} else {
|
]),
|
||||||
// Second: diagram generation
|
)
|
||||||
const xml = `<mxCell id="arch" value="Architecture" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="50" as="geometry"/></mxCell>`
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: "text/event-stream",
|
|
||||||
body: createMockSSEResponse(xml, "Here is the diagram:"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await page
|
||||||
.locator("iframe")
|
.locator("iframe")
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
.waitFor({ state: "visible", timeout: 30000 })
|
||||||
|
|
||||||
const chatInput = page.locator('textarea[aria-label="Chat input"]')
|
|
||||||
await expect(chatInput).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
// Ask for explanation first
|
// Ask for explanation first
|
||||||
await chatInput.fill("Explain the architecture")
|
await sendMessage(page, "Explain the architecture")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForText(
|
||||||
await expect(
|
page,
|
||||||
page.locator(
|
"I understand. Let me explain the architecture first.",
|
||||||
'text="I understand. Let me explain the architecture first."',
|
)
|
||||||
),
|
|
||||||
).toBeVisible({ timeout: 15000 })
|
|
||||||
|
|
||||||
// Then ask for diagram
|
// Then ask for diagram
|
||||||
await chatInput.fill("Now show it as a diagram")
|
await sendMessage(page, "Now show it as a diagram")
|
||||||
await chatInput.press("ControlOrMeta+Enter")
|
await waitForComplete(page)
|
||||||
await expect(page.locator('text="Complete"')).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Save Dialog", () => {
|
test.describe("Save Dialog", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("save/download buttons exist", async ({ page }) => {
|
test("save/download buttons exist", async ({ page }) => {
|
||||||
// Check that buttons with icons exist (save/download functionality)
|
|
||||||
const buttons = page
|
const buttons = page
|
||||||
.locator("button")
|
.locator("button")
|
||||||
.filter({ has: page.locator("svg") })
|
.filter({ has: page.locator("svg") })
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import {
|
||||||
|
expect,
|
||||||
|
getIframe,
|
||||||
|
getSettingsButton,
|
||||||
|
openSettings,
|
||||||
|
test,
|
||||||
|
} from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Settings", () => {
|
test.describe("Settings", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("settings dialog opens", async ({ page }) => {
|
test("settings dialog opens", async ({ page }) => {
|
||||||
const settingsButton = page.locator('[data-testid="settings-button"]')
|
await openSettings(page)
|
||||||
await expect(settingsButton).toBeVisible()
|
// openSettings already verifies dialog is visible
|
||||||
await settingsButton.click()
|
|
||||||
|
|
||||||
const dialog = page.locator('[role="dialog"]')
|
|
||||||
await expect(dialog).toBeVisible({ timeout: 5000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("language selection is available", async ({ page }) => {
|
test("language selection is available", async ({ page }) => {
|
||||||
const settingsButton = page.locator('[data-testid="settings-button"]')
|
await openSettings(page)
|
||||||
await settingsButton.click()
|
|
||||||
|
|
||||||
const dialog = page.locator('[role="dialog"]')
|
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()
|
await expect(dialog.locator('text="English"')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("draw.io theme toggle exists", async ({ page }) => {
|
test("draw.io theme toggle exists", async ({ page }) => {
|
||||||
const settingsButton = page.locator('[data-testid="settings-button"]')
|
await openSettings(page)
|
||||||
await settingsButton.click()
|
|
||||||
|
|
||||||
const dialog = page.locator('[role="dialog"]')
|
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")
|
const themeText = dialog.locator("text=/sketch|minimal/i")
|
||||||
await expect(themeText.first()).toBeVisible()
|
await expect(themeText.first()).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, openSettings, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Smoke Tests", () => {
|
test.describe("Smoke Tests", () => {
|
||||||
test("homepage loads without errors", async ({ page }) => {
|
test("homepage loads without errors", async ({ page }) => {
|
||||||
@@ -8,8 +8,7 @@ test.describe("Smoke Tests", () => {
|
|||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await expect(page).toHaveTitle(/Draw\.io/i, { timeout: 10000 })
|
await expect(page).toHaveTitle(/Draw\.io/i, { timeout: 10000 })
|
||||||
|
|
||||||
// Wait for draw.io iframe to be present
|
const iframe = getIframe(page)
|
||||||
const iframe = page.locator("iframe")
|
|
||||||
await expect(iframe).toBeVisible({ timeout: 30000 })
|
await expect(iframe).toBeVisible({ timeout: 30000 })
|
||||||
|
|
||||||
expect(errors).toEqual([])
|
expect(errors).toEqual([])
|
||||||
@@ -22,7 +21,7 @@ test.describe("Smoke Tests", () => {
|
|||||||
await page.goto("/ja", { waitUntil: "networkidle" })
|
await page.goto("/ja", { waitUntil: "networkidle" })
|
||||||
await expect(page).toHaveTitle(/Draw\.io/i, { timeout: 10000 })
|
await expect(page).toHaveTitle(/Draw\.io/i, { timeout: 10000 })
|
||||||
|
|
||||||
const iframe = page.locator("iframe")
|
const iframe = getIframe(page)
|
||||||
await expect(iframe).toBeVisible({ timeout: 30000 })
|
await expect(iframe).toBeVisible({ timeout: 30000 })
|
||||||
|
|
||||||
expect(errors).toEqual([])
|
expect(errors).toEqual([])
|
||||||
@@ -30,20 +29,8 @@ test.describe("Smoke Tests", () => {
|
|||||||
|
|
||||||
test("settings dialog opens", async ({ page }) => {
|
test("settings dialog opens", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
|
|
||||||
// Wait for page to load
|
await openSettings(page)
|
||||||
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,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,112 +1,88 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, openSettings, sleep, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("Theme Switching", () => {
|
test.describe("Theme Switching", () => {
|
||||||
test("can toggle app dark mode", async ({ page }) => {
|
test("can toggle app dark mode", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings
|
await openSettings(page)
|
||||||
const settingsButton = page.locator(
|
|
||||||
'button[aria-label*="Settings"], button:has(svg.lucide-settings)',
|
|
||||||
)
|
|
||||||
await settingsButton.first().click()
|
|
||||||
|
|
||||||
// Wait for settings dialog to open
|
|
||||||
await page.waitForTimeout(500)
|
|
||||||
|
|
||||||
// Check initial state (should have html class)
|
|
||||||
const html = page.locator("html")
|
const html = page.locator("html")
|
||||||
const initialClass = await html.getAttribute("class")
|
const initialClass = await html.getAttribute("class")
|
||||||
|
|
||||||
// Find and click the theme toggle button (sun/moon icon)
|
|
||||||
const themeButton = page.locator(
|
const themeButton = page.locator(
|
||||||
"button:has(svg.lucide-sun), button:has(svg.lucide-moon)",
|
"button:has(svg.lucide-sun), button:has(svg.lucide-moon)",
|
||||||
)
|
)
|
||||||
|
|
||||||
if ((await themeButton.count()) > 0) {
|
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 test.step("verify theme changed", async () => {
|
||||||
await page.waitForTimeout(500) // Wait for theme transition
|
const newClass = await html.getAttribute("class")
|
||||||
const newClass = await html.getAttribute("class")
|
expect(newClass).not.toBe(initialClass)
|
||||||
expect(newClass).not.toBe(initialClass)
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("theme persists after page reload", async ({ page }) => {
|
test("theme persists after page reload", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings and toggle theme
|
await openSettings(page)
|
||||||
const settingsButton = page.locator(
|
|
||||||
'button[aria-label*="Settings"], button:has(svg.lucide-settings)',
|
|
||||||
)
|
|
||||||
await settingsButton.first().click()
|
|
||||||
|
|
||||||
// Click theme button if available
|
|
||||||
const themeButton = page.locator(
|
const themeButton = page.locator(
|
||||||
"button:has(svg.lucide-sun), button:has(svg.lucide-moon)",
|
"button:has(svg.lucide-sun), button:has(svg.lucide-moon)",
|
||||||
)
|
)
|
||||||
|
|
||||||
if ((await themeButton.count()) > 0) {
|
if ((await themeButton.count()) > 0) {
|
||||||
await themeButton.first().click()
|
let themeClass: string | null
|
||||||
await page.waitForTimeout(300)
|
|
||||||
|
|
||||||
// Get current theme class
|
await test.step("change theme", async () => {
|
||||||
const html = page.locator("html")
|
await themeButton.first().click()
|
||||||
const themeClass = await html.getAttribute("class")
|
await sleep(300)
|
||||||
|
themeClass = await page.locator("html").getAttribute("class")
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
})
|
||||||
|
|
||||||
// Close dialog
|
await test.step("reload page", async () => {
|
||||||
await page.keyboard.press("Escape")
|
await page.reload({ waitUntil: "networkidle" })
|
||||||
|
await getIframe(page).waitFor({
|
||||||
|
state: "visible",
|
||||||
|
timeout: 30000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Reload page
|
await test.step("verify theme persisted", async () => {
|
||||||
await page.reload({ waitUntil: "networkidle" })
|
const reloadedClass = await page
|
||||||
await page
|
.locator("html")
|
||||||
.locator("iframe")
|
.getAttribute("class")
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
expect(reloadedClass).toBe(themeClass)
|
||||||
|
})
|
||||||
// Theme should persist
|
|
||||||
const reloadedClass = await html.getAttribute("class")
|
|
||||||
expect(reloadedClass).toBe(themeClass)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("draw.io theme toggle exists", async ({ page }) => {
|
test("draw.io theme toggle exists", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Open settings
|
await openSettings(page)
|
||||||
const settingsButton = page.locator(
|
|
||||||
'button[aria-label*="Settings"], button:has(svg.lucide-settings)',
|
|
||||||
)
|
|
||||||
await settingsButton.first().click()
|
|
||||||
|
|
||||||
// At least some settings content should be visible
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('[role="dialog"], [role="menu"], form').first(),
|
page.locator('[role="dialog"], [role="menu"], form').first(),
|
||||||
).toBeVisible({ timeout: 5000 })
|
).toBeVisible({ timeout: 5000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("system theme preference is respected", async ({ page }) => {
|
test("system theme preference is respected", async ({ page }) => {
|
||||||
// Emulate dark mode preference
|
|
||||||
await page.emulateMedia({ colorScheme: "dark" })
|
await page.emulateMedia({ colorScheme: "dark" })
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
|
|
||||||
// Check if dark mode is applied (depends on implementation)
|
|
||||||
const html = page.locator("html")
|
const html = page.locator("html")
|
||||||
const classes = await html.getAttribute("class")
|
const classes = await html.getAttribute("class")
|
||||||
// Should have dark class or similar
|
|
||||||
// This depends on the app's theme implementation
|
|
||||||
expect(classes).toBeDefined()
|
expect(classes).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, getIframe, test } from "./lib/fixtures"
|
||||||
|
|
||||||
test.describe("File Upload Area", () => {
|
test.describe("File Upload Area", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "networkidle" })
|
await page.goto("/", { waitUntil: "networkidle" })
|
||||||
await page
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
||||||
.locator("iframe")
|
|
||||||
.waitFor({ state: "visible", timeout: 30000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("page loads without console errors", async ({ page }) => {
|
test("page loads without console errors", async ({ page }) => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
page.on("pageerror", (err) => errors.push(err.message))
|
page.on("pageerror", (err) => errors.push(err.message))
|
||||||
|
|
||||||
// Give page time to settle
|
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
// Filter out non-critical errors
|
|
||||||
const criticalErrors = errors.filter(
|
const criticalErrors = errors.filter(
|
||||||
(e) => !e.includes("ResizeObserver") && !e.includes("Script error"),
|
(e) => !e.includes("ResizeObserver") && !e.includes("Script error"),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user