Files
next-ai-draw-io/tests/e2e/copy-paste.spec.ts
Dayuan Jiang c7a85d398f test: add Vitest and Playwright testing infrastructure (#512)
* test: add Vitest and Playwright testing infrastructure

- Add Vitest for unit tests (39 tests)
  - cached-responses.test.ts
  - ai-providers.test.ts
  - chat-helpers.test.ts
  - utils.test.ts
- Add Playwright for E2E tests (3 smoke tests)
  - Homepage load
  - Japanese locale
  - Settings dialog
- Add CI workflow (.github/workflows/test.yml)
- Add vitest.config.mts and playwright.config.ts
- Update .gitignore for test artifacts

* test: add more E2E tests for UI components

- Chat panel tests (interactive elements, iframe)
- Settings tests (dark mode, language, draw.io theme)
- Save dialog tests (buttons exist)
- History dialog tests
- Model config tests
- Keyboard interaction tests
- Upload area tests

Total: 15 E2E tests, all passing

* test: fix E2E test issues from review

Fixes based on Gemini and Codex review:
- Remove brittle nth(1) selector in keyboard tests
- Remove waitForTimeout(500) race condition
- Remove if(isVisible) silent skip patterns
- Add proper assertions instead of no-op checks
- Remove expect(count >= 0) that always passes
- Remove unused hasProviderUI variable

All 14 E2E tests and 39 unit tests pass.

* style: auto-format with Biome

* fix: resolve lint errors for CI

* test(e2e): add diagram generation tests with mocked AI responses

- Add tests for generate, edit, and append diagram operations
- Use SSE mocked responses matching AI SDK UI message stream format
- Generate mxCell XML directly in tests for deterministic assertions
- Tests verify tool card rendering and 'Complete' badge state

* test: add comprehensive E2E tests for all major features

- Error handling tests (API errors, rate limits, network timeout, truncated XML)
- Multi-turn conversation tests (sequential requests, history preservation)
- File upload tests (upload button, file preview, sending with message)
- Theme switching tests (dark mode toggle, persistence, system preference)
- Language switching tests (EN/JA/ZH, persistence, locale URLs)
- Iframe interaction tests (draw.io loading, toolbar, diagram rendering)
- Copy/paste tests (chat input, XML input, special characters)
- History restore tests (new chat, persistence, browser navigation)

* refactor: extract shared test helpers and improve error assertions

- Create tests/e2e/lib/helpers.ts with shared SSE mock functions
- Add proper error UI assertions to error-handling.spec.ts
- Remove waitForTimeout calls in favor of real assertions
- Update 6 test files to use shared helpers

* docs: add testing section to CONTRIBUTING.md

* fix: improve test infrastructure based on PR review

- Fix double build in CI: remove redundant build from playwright webServer
- Export chat helpers from shared module for proper unit testing
- Replace waitForTimeout with explicit waits in E2E tests
- Add data-testid attributes to settings and new chat buttons
- Add list reporter for CI to show failures in logs
- Add Playwright browser caching to speed up CI
- Add vitest coverage configuration
- Fix conditional test assertions to use test.skip() instead of silent pass
- Remove unused variables flagged by linter

* fix: improve E2E test assertions and remove silent skips

- Replace silent test.skip() with explicit conditional skips
- Add actual persistence assertion after page reload
- Use data-testid selector for new chat button test

* 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

* fix: make persistence tests more reliable

- Remove expectBeforeAndAfterReload from mocked API tests
- Add explicit test.step() for before/after reload checks
- Add retry config for flaky clipboard tests
- Add sleep after reload for language persistence test

* test: remove flaky XML paste test

* docs: run both unit and e2e tests before PR

* chore: add type check and unit test git hooks

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-05 01:37:32 +09:00

138 lines
4.8 KiB
TypeScript

import { SINGLE_BOX_XML } from "./fixtures/diagrams"
import {
expect,
getChatInput,
getIframe,
sendMessage,
test,
} from "./lib/fixtures"
import { createMockSSEResponse } from "./lib/helpers"
test.describe("Copy/Paste Functionality", () => {
test("can paste text into chat input", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
const chatInput = getChatInput(page)
await expect(chatInput).toBeVisible({ timeout: 10000 })
await chatInput.focus()
await page.keyboard.insertText("Create a flowchart diagram")
await expect(chatInput).toHaveValue("Create a flowchart diagram")
})
test("can paste multiline text into chat input", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
const chatInput = getChatInput(page)
await expect(chatInput).toBeVisible({ timeout: 10000 })
await chatInput.focus()
const multilineText = "Line 1\nLine 2\nLine 3"
await page.keyboard.insertText(multilineText)
await expect(chatInput).toHaveValue(multilineText)
})
test("copy button copies response text", async ({ page }) => {
await page.route("**/api/chat", async (route) => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
body: createMockSSEResponse(
SINGLE_BOX_XML,
"Here is your diagram with a test box.",
),
})
})
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
await sendMessage(page, "Create a test box")
// Wait for response
await expect(
page.locator('text="Here is your diagram with a test box."'),
).toBeVisible({ timeout: 15000 })
// Find copy button in message
const copyButton = page.locator(
'[data-testid="copy-button"], button[aria-label*="Copy"], button:has(svg.lucide-copy), button:has(svg.lucide-clipboard)',
)
// Copy button feature may not exist - skip if not available
const buttonCount = await copyButton.count()
if (buttonCount === 0) {
test.skip()
return
}
await copyButton.first().click()
await expect(
page.locator('text="Copied"').or(page.locator("svg.lucide-check")),
).toBeVisible({ timeout: 3000 })
})
test("keyboard shortcuts work in chat input", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
const chatInput = getChatInput(page)
await expect(chatInput).toBeVisible({ timeout: 10000 })
await chatInput.fill("Hello world")
await chatInput.press("ControlOrMeta+a")
await chatInput.fill("New text")
await expect(chatInput).toHaveValue("New text")
})
test("can undo/redo in chat input", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
const chatInput = getChatInput(page)
await expect(chatInput).toBeVisible({ timeout: 10000 })
await chatInput.fill("First text")
await chatInput.press("Tab")
await chatInput.focus()
await chatInput.fill("Second text")
await chatInput.press("ControlOrMeta+z")
// Verify page is still functional after undo
await expect(chatInput).toBeVisible()
})
test("chat input handles special characters", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
const chatInput = getChatInput(page)
await expect(chatInput).toBeVisible({ timeout: 10000 })
const specialText = "Test <>&\"' special chars 日本語 中文 🎉"
await chatInput.fill(specialText)
await expect(chatInput).toHaveValue(specialText)
})
test("long text in chat input scrolls", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
const chatInput = getChatInput(page)
await expect(chatInput).toBeVisible({ timeout: 10000 })
const longText = "This is a very long text. ".repeat(50)
await chatInput.fill(longText)
const value = await chatInput.inputValue()
expect(value.length).toBeGreaterThan(500)
})
})