Prechádzať zdrojové kódy

fix(04-01): fix E2E tests and add WebSocket mocking

- Add WebSocket mocking for /ws/status, /ws/logs, /ws/cache-progress
- Fix Playwright config to use port 5174 to avoid conflicts
- Add static file and additional API endpoint mocks
- Fix pattern-flow test to use exact button name
- Fix playlist-flow test to use title selector for Run button
tuanchris 1 týždeň pred
rodič
commit
fa94d6a0a8

+ 76 - 6
frontend/e2e/mocks/api.ts

@@ -1,4 +1,4 @@
-import { Page } from '@playwright/test'
+import { Page, WebSocketRoute } from '@playwright/test'
 
 // Mock data
 export const mockPatterns = [
@@ -89,12 +89,14 @@ export async function setupApiMocks(page: Page) {
   })
 
   await page.route('**/run_playlist', async route => {
-    const body = route.request().postDataJSON() as { name: string }
-    const playlist = mockPlaylists[body?.name as keyof typeof mockPlaylists]
+    const body = route.request().postDataJSON() as { playlist_name?: string; name?: string }
+    // Support both playlist_name (actual API) and name (legacy)
+    const playlistName = body?.playlist_name || body?.name
+    const playlist = mockPlaylists[playlistName as keyof typeof mockPlaylists]
     if (playlist && playlist.length > 0) {
       currentStatus.is_running = true
       currentStatus.playlist_mode = true
-      currentStatus.playlist_name = body.name
+      currentStatus.playlist_name = playlistName || null
       currentStatus.current_file = playlist[0]
       currentStatus.queue = playlist.slice(1)
     }
@@ -148,8 +150,76 @@ export async function setupApiMocks(page: Page) {
     await route.fulfill({ json: {} })
   })
 
-  // WebSocket - return empty to prevent connection attempts
-  // Playwright doesn't intercept WebSocket by default, but we can handle the fallback
+  // Logs endpoint
+  await page.route('**/api/logs**', async route => {
+    await route.fulfill({ json: { logs: [], total: 0, has_more: false } })
+  })
+
+  // Pattern history (individual)
+  await page.route('**/api/pattern_history/**', async route => {
+    await route.fulfill({ json: { actual_time_formatted: null, speed: null } })
+  })
+
+  // Serial ports
+  await page.route('**/list_serial_ports', async route => {
+    await route.fulfill({ json: [] })
+  })
+
+  // Static files - return 200 with placeholder
+  await page.route('**/static/**', async route => {
+    // Return a 1x1 transparent PNG for images
+    const base64Png = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
+    await route.fulfill({
+      status: 200,
+      contentType: 'image/png',
+      body: Buffer.from(base64Png, 'base64'),
+    })
+  })
+
+  // WebSocket mocking - critical for bypassing the "Connecting to Backend" overlay
+  // The Layout component shows a blocking overlay until WebSocket connects
+  await page.routeWebSocket('**/ws/status', (ws: WebSocketRoute) => {
+    // Don't connect to server - we're mocking everything
+    // Send status updates to simulate backend status messages
+    const statusMessage = JSON.stringify({
+      type: 'status_update',
+      data: {
+        ...currentStatus,
+        connection_status: true,
+        is_homing: false,
+      }
+    })
+
+    // Send initial status immediately after connection
+    // The client's onopen handler will fire, setting isBackendConnected = true
+    setTimeout(() => {
+      ws.send(statusMessage)
+    }, 100)
+
+    // Send periodic updates
+    const interval = setInterval(() => {
+      ws.send(statusMessage)
+    }, 1000)
+
+    ws.onClose(() => {
+      clearInterval(interval)
+    })
+  })
+
+  // Mock other WebSocket endpoints
+  await page.routeWebSocket('**/ws/logs', (_ws: WebSocketRoute) => {
+    // Just accept the connection - don't need to send anything
+  })
+
+  await page.routeWebSocket('**/ws/cache-progress', (ws: WebSocketRoute) => {
+    // Send "not running" status
+    setTimeout(() => {
+      ws.send(JSON.stringify({
+        type: 'cache_progress',
+        data: { is_running: false, stage: 'idle' }
+      }))
+    }, 100)
+  })
 }
 
 export function getMockStatus() {

+ 2 - 4
frontend/e2e/pattern-flow.spec.ts

@@ -26,10 +26,8 @@ test.describe('Pattern Flow E2E', () => {
     await page.getByText('star.thr').click()
 
     // Detail panel should open (Sheet component)
-    // Look for common detail panel indicators
-    await expect(
-      page.getByRole('dialog').or(page.locator('[data-state="open"]'))
-    ).toBeVisible({ timeout: 5000 })
+    // The sheet contains a "Play" button with exact text (not "Play Next")
+    await expect(page.getByRole('button', { name: 'play_arrow Play' })).toBeVisible({ timeout: 5000 })
   })
 
   test('can run pattern and UI shows running state', async ({ page }) => {

+ 6 - 5
frontend/e2e/playlist-flow.spec.ts

@@ -24,12 +24,13 @@ test.describe('Playlist Flow E2E', () => {
     // Click playlist to select
     await page.getByText('default').click()
 
-    // Wait for selection
-    await page.waitForTimeout(500)
+    // Wait for the playlist patterns to load
+    // The Play button should become enabled once patterns are loaded
+    await page.waitForTimeout(1000)
 
-    // Find and click run button
-    const runButton = page.getByRole('button', { name: /run|play|start/i }).first()
-    await expect(runButton).toBeVisible()
+    // Find and click run button by its title attribute
+    const runButton = page.locator('button[title="Run Playlist"]')
+    await expect(runButton).toBeVisible({ timeout: 5000 })
     await runButton.click()
 
     // Verify playlist is running

+ 6 - 3
frontend/playwright.config.ts

@@ -1,5 +1,8 @@
 import { defineConfig, devices } from '@playwright/test'
 
+// Use a dedicated port for E2E tests to avoid conflicts with other dev servers
+const E2E_PORT = 5174
+
 export default defineConfig({
   testDir: './e2e',
   fullyParallel: true,
@@ -8,7 +11,7 @@ export default defineConfig({
   workers: process.env.CI ? 1 : undefined,
   reporter: 'html',
   use: {
-    baseURL: 'http://localhost:5173',
+    baseURL: `http://localhost:${E2E_PORT}`,
     trace: 'on-first-retry',
   },
   projects: [
@@ -18,8 +21,8 @@ export default defineConfig({
     },
   ],
   webServer: {
-    command: 'npm run dev',
-    url: 'http://localhost:5173',
+    command: `npm run dev -- --port ${E2E_PORT}`,
+    url: `http://localhost:${E2E_PORT}`,
     reuseExistingServer: !process.env.CI,
   },
 })