import { test, expect } from '@playwright/test'; test.describe('OpenCode Chat Workflow', () => { // Skip: OpenCode (Go binary) has stdout buffering issues when run as subprocess from Java // Go binaries ignore stdbuf and require a pseudo-terminal for proper streaming test.skip('create new chat and send message to OpenCode', async ({ page }) => { // Enable console logging to debug WebSocket issues page.on('console', (msg) => { console.log(`[Browser ${msg.type()}]`, msg.text()); }); // Log WebSocket frames page.on('websocket', (ws) => { console.log(`[WebSocket] Connected to ${ws.url()}`); ws.on('framesent', (frame) => console.log(`[WS Sent]`, frame.payload)); ws.on('framereceived', (frame) => console.log(`[WS Received]`, frame.payload)); ws.on('close', () => console.log('[WebSocket] Closed')); }); // Log network requests to /api page.on('request', (request) => { if (request.url().includes('/api')) { console.log(`[Request] ${request.method()} ${request.url()}`); } }); page.on('response', (response) => { if (response.url().includes('/api')) { console.log(`[Response] ${response.status()} ${response.url()}`); } }); // 1. Navigate to homepage await page.goto('/'); await expect(page).toHaveTitle(/Spiceflow/i); // 2. Click the + button to open new session menu const createButton = page.locator('button[title="New Session"]'); await expect(createButton).toBeVisible(); await createButton.click(); // 3. Select OpenCode from the dropdown const opencodeOption = page.locator('button:has-text("OpenCode")'); await expect(opencodeOption).toBeVisible(); await opencodeOption.click(); // 4. Wait for navigation to session page await page.waitForURL(/\/session\/.+/); console.log('[Test] Navigated to session page:', page.url()); // 5. Wait for the page to load (no loading spinner) await expect(page.locator('text=Loading')).not.toBeVisible({ timeout: 5000 }); // 6. Verify we see the empty message state await expect(page.locator('text=No messages yet')).toBeVisible(); // 7. Type a message in the textarea const textarea = page.locator('textarea'); await expect(textarea).toBeVisible(); await textarea.fill('say hi. respond in a single concise sentence'); // 8. Click the send button const sendButton = page.locator('button[type="submit"]'); await expect(sendButton).toBeEnabled(); await sendButton.click(); // 9. Verify user message appears immediately (optimistic update) await expect(page.locator('text=say hi. respond in a single concise sentence')).toBeVisible(); console.log('[Test] User message displayed'); // 10. Verify thinking indicator appears immediately after sending // The assistant bubble with bouncing dots should show right away (isThinking state) const assistantMessage = page.locator('.rounded-lg.border').filter({ has: page.locator('text=Assistant') }).last(); // Thinking indicator should appear almost immediately (within 2 seconds) await expect(assistantMessage).toBeVisible({ timeout: 2000 }); console.log('[Test] Thinking indicator appeared immediately'); // Verify bouncing dots are present (thinking state) const bouncingDotsInAssistant = assistantMessage.locator('.animate-bounce'); await expect(bouncingDotsInAssistant.first()).toBeVisible({ timeout: 2000 }); console.log('[Test] Bouncing dots visible in thinking state'); // 11. Wait for streaming to complete - progress indicator should disappear // The streaming indicator has animate-bounce dots and animate-pulse cursor // Note: With fast responses, the indicator may appear and disappear quickly, // so we just verify it's gone after the response is visible const bouncingDots = page.locator('.animate-bounce'); // Only look for pulsing cursor inside markdown-content (not the header status indicator) const pulsingCursor = page.locator('.markdown-content .animate-pulse'); // Wait for streaming indicators to disappear (they should be gone after message-stop) await expect(bouncingDots).toHaveCount(0, { timeout: 30000 }); await expect(pulsingCursor).toHaveCount(0, { timeout: 30000 }); console.log('[Test] Streaming complete - progress indicator disappeared'); // 12. Verify the response contains some text content const responseText = await assistantMessage.locator('.font-mono').textContent(); console.log('[Test] Assistant response text:', responseText); expect(responseText).toBeTruthy(); expect(responseText!.length).toBeGreaterThan(0); }); });