mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
- Fix duplicate onConversationCreated callbacks by tracking activeConversationId - Fix history loss when switching conversations with Map-based deduplication - Disable input while conversation is loading to prevent message routing issues - Gate WebSocket debug logs behind DEV flag (import.meta.env.DEV) - Downgrade server logging from info to debug level for reduced noise - Fix .gitignore prefixes for playwright paths (ui/playwright-report/, ui/test-results/) - Remove debug console.log from ConversationHistory.tsx - Add staleTime (30s) to single conversation query for better caching - Increase history message cap from 20 to 35 for better context - Replace fixed timeouts with condition-based waits in e2e tests
566 lines
21 KiB
TypeScript
566 lines
21 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
|
|
/**
|
|
* E2E tests for the Conversation History feature in the Assistant panel.
|
|
*
|
|
* Two test groups:
|
|
* 1. UI Tests - Only test UI elements, no API needed
|
|
* 2. Integration Tests - Test full flow with API (skipped if API unavailable)
|
|
*
|
|
* Run tests:
|
|
* cd ui && npm run test:e2e
|
|
* cd ui && npm run test:e2e:ui (interactive mode)
|
|
*/
|
|
|
|
// =============================================================================
|
|
// UI TESTS - No API required, just test UI elements
|
|
// =============================================================================
|
|
test.describe('Assistant Panel UI', () => {
|
|
test.setTimeout(30000)
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/')
|
|
await page.waitForSelector('button:has-text("Select Project")', { timeout: 10000 })
|
|
})
|
|
|
|
async function selectProject(page: import('@playwright/test').Page) {
|
|
const projectSelector = page.locator('button:has-text("Select Project")')
|
|
if (await projectSelector.isVisible()) {
|
|
await projectSelector.click()
|
|
const projectItem = page.locator('.neo-dropdown-item').first()
|
|
const hasProject = await projectItem.isVisible().catch(() => false)
|
|
if (!hasProject) {
|
|
return false
|
|
}
|
|
await projectItem.click()
|
|
// Wait for dropdown to close (project selected)
|
|
await expect(projectSelector).not.toBeVisible({ timeout: 5000 }).catch(() => {})
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
async function waitForPanelOpen(page: import('@playwright/test').Page) {
|
|
await page.waitForFunction(() => {
|
|
const panel = document.querySelector('[aria-label="Project Assistant"]')
|
|
return panel && panel.getAttribute('aria-hidden') !== 'true'
|
|
}, { timeout: 5000 })
|
|
}
|
|
|
|
async function waitForPanelClosed(page: import('@playwright/test').Page) {
|
|
await page.waitForFunction(() => {
|
|
const panel = document.querySelector('[aria-label="Project Assistant"]')
|
|
return !panel || panel.getAttribute('aria-hidden') === 'true'
|
|
}, { timeout: 5000 })
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Panel open/close tests
|
|
// --------------------------------------------------------------------------
|
|
test('Panel opens and closes with A key', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
const panel = page.locator('[aria-label="Project Assistant"]')
|
|
|
|
// Panel should be closed initially
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'true')
|
|
|
|
// Press A to open
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'false')
|
|
|
|
// Press A again to close
|
|
await page.keyboard.press('a')
|
|
await waitForPanelClosed(page)
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'true')
|
|
})
|
|
|
|
test('Panel closes when clicking backdrop', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
const panel = page.locator('[aria-label="Project Assistant"]')
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'false')
|
|
|
|
// Click on the backdrop
|
|
const backdrop = page.locator('.fixed.inset-0.bg-black\\/20')
|
|
await backdrop.click()
|
|
|
|
// Panel should close
|
|
await waitForPanelClosed(page)
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'true')
|
|
})
|
|
|
|
test('Panel closes with X button', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
const panel = page.locator('[aria-label="Project Assistant"]')
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'false')
|
|
|
|
// Click X button (inside the panel dialog, not the floating button)
|
|
const closeButton = page.locator('[aria-label="Project Assistant"] button[title="Close Assistant (Press A)"]')
|
|
await closeButton.click()
|
|
|
|
// Panel should close
|
|
await waitForPanelClosed(page)
|
|
await expect(panel).toHaveAttribute('aria-hidden', 'true')
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Header buttons tests
|
|
// --------------------------------------------------------------------------
|
|
test('New chat and history buttons are visible and clickable', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Verify New Chat button
|
|
const newChatButton = page.locator('button[title="New conversation"]')
|
|
await expect(newChatButton).toBeVisible()
|
|
await expect(newChatButton).toBeEnabled()
|
|
|
|
// Verify History button
|
|
const historyButton = page.locator('button[title="Conversation history"]')
|
|
await expect(historyButton).toBeVisible()
|
|
await expect(historyButton).toBeEnabled()
|
|
})
|
|
|
|
test('History dropdown opens and closes', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Click history button
|
|
const historyButton = page.locator('button[title="Conversation history"]')
|
|
await historyButton.click()
|
|
|
|
// Dropdown should be visible
|
|
const historyDropdown = page.locator('h3:has-text("Conversation History")')
|
|
await expect(historyDropdown).toBeVisible({ timeout: 5000 })
|
|
|
|
// Dropdown should be inside the panel (not hidden by edge)
|
|
const dropdownBox = await page.locator('.neo-dropdown:has-text("Conversation History")').boundingBox()
|
|
const panelBox = await page.locator('[aria-label="Project Assistant"]').boundingBox()
|
|
|
|
if (dropdownBox && panelBox) {
|
|
// Dropdown left edge should be >= panel left edge (not cut off)
|
|
expect(dropdownBox.x).toBeGreaterThanOrEqual(panelBox.x - 10) // small tolerance
|
|
}
|
|
|
|
// Close dropdown by pressing Escape (more reliable than clicking backdrop)
|
|
await page.keyboard.press('Escape')
|
|
await expect(historyDropdown).not.toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
test('History dropdown shows empty state or conversations', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Click history button
|
|
const historyButton = page.locator('button[title="Conversation history"]')
|
|
await historyButton.click()
|
|
|
|
// Should show either "No conversations yet" or a list of conversations
|
|
const dropdown = page.locator('.neo-dropdown:has-text("Conversation History")')
|
|
await expect(dropdown).toBeVisible({ timeout: 5000 })
|
|
|
|
// Check content - either empty state or conversation items
|
|
const emptyState = dropdown.locator('text=No conversations yet')
|
|
const conversationItems = dropdown.locator('.neo-dropdown-item')
|
|
|
|
const hasEmpty = await emptyState.isVisible().catch(() => false)
|
|
const itemCount = await conversationItems.count()
|
|
|
|
// Should have either empty state or some items
|
|
expect(hasEmpty || itemCount > 0).toBe(true)
|
|
console.log(`History shows: ${hasEmpty ? 'empty state' : `${itemCount} conversations`}`)
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Input area tests
|
|
// --------------------------------------------------------------------------
|
|
test('Input textarea exists and is focusable', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Input should exist
|
|
const inputArea = page.locator('textarea[placeholder="Ask about the codebase..."]')
|
|
await expect(inputArea).toBeVisible()
|
|
|
|
// Should be able to type in it (even if disabled, we can check it exists)
|
|
const placeholder = await inputArea.getAttribute('placeholder')
|
|
expect(placeholder).toBe('Ask about the codebase...')
|
|
})
|
|
|
|
test('Send button exists', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Send button should exist
|
|
const sendButton = page.locator('button[title="Send message"]')
|
|
await expect(sendButton).toBeVisible()
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Connection status tests
|
|
// --------------------------------------------------------------------------
|
|
test('Connection status indicator exists', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Wait for any status to appear
|
|
await page.waitForFunction(() => {
|
|
const text = document.body.innerText
|
|
return text.includes('Connecting...') || text.includes('Connected') || text.includes('Disconnected')
|
|
}, { timeout: 10000 })
|
|
|
|
// One of the status indicators should be visible
|
|
const connecting = await page.locator('text=Connecting...').isVisible().catch(() => false)
|
|
const connected = await page.locator('text=Connected').isVisible().catch(() => false)
|
|
const disconnected = await page.locator('text=Disconnected').isVisible().catch(() => false)
|
|
|
|
expect(connecting || connected || disconnected).toBe(true)
|
|
console.log(`Connection status: ${connected ? 'Connected' : disconnected ? 'Disconnected' : 'Connecting'}`)
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Panel header tests
|
|
// --------------------------------------------------------------------------
|
|
test('Panel header shows project name', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
// Open panel
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Header should show "Project Assistant"
|
|
const header = page.locator('h2:has-text("Project Assistant")')
|
|
await expect(header).toBeVisible()
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// INTEGRATION TESTS - Require API connection
|
|
// =============================================================================
|
|
test.describe('Conversation History Integration', () => {
|
|
test.setTimeout(120000)
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/')
|
|
await page.waitForSelector('button:has-text("Select Project")', { timeout: 10000 })
|
|
})
|
|
|
|
async function selectProject(page: import('@playwright/test').Page) {
|
|
const projectSelector = page.locator('button:has-text("Select Project")')
|
|
if (await projectSelector.isVisible()) {
|
|
await projectSelector.click()
|
|
const projectItem = page.locator('.neo-dropdown-item').first()
|
|
const hasProject = await projectItem.isVisible().catch(() => false)
|
|
if (!hasProject) return false
|
|
await projectItem.click()
|
|
// Wait for dropdown to close (project selected)
|
|
await expect(projectSelector).not.toBeVisible({ timeout: 5000 }).catch(() => {})
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
async function waitForPanelOpen(page: import('@playwright/test').Page) {
|
|
await page.waitForFunction(() => {
|
|
const panel = document.querySelector('[aria-label="Project Assistant"]')
|
|
return panel && panel.getAttribute('aria-hidden') !== 'true'
|
|
}, { timeout: 5000 })
|
|
}
|
|
|
|
async function waitForPanelClosed(page: import('@playwright/test').Page) {
|
|
await page.waitForFunction(() => {
|
|
const panel = document.querySelector('[aria-label="Project Assistant"]')
|
|
return !panel || panel.getAttribute('aria-hidden') === 'true'
|
|
}, { timeout: 5000 })
|
|
}
|
|
|
|
async function waitForAssistantReady(page: import('@playwright/test').Page): Promise<boolean> {
|
|
try {
|
|
await page.waitForSelector('text=Connected', { timeout: 15000 })
|
|
const inputArea = page.locator('textarea[placeholder="Ask about the codebase..."]')
|
|
await expect(inputArea).toBeEnabled({ timeout: 30000 })
|
|
return true
|
|
} catch {
|
|
console.log('Assistant not available - API may not be configured')
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function sendMessageAndWaitForResponse(page: import('@playwright/test').Page, message: string) {
|
|
const inputArea = page.locator('textarea[placeholder="Ask about the codebase..."]')
|
|
await inputArea.fill(message)
|
|
await inputArea.press('Enter')
|
|
await expect(page.locator(`text=${message}`).first()).toBeVisible({ timeout: 5000 })
|
|
await page.waitForSelector('text=Thinking...', { timeout: 10000 }).catch(() => {})
|
|
await expect(inputArea).toBeEnabled({ timeout: 60000 })
|
|
// Wait for any streaming to complete (input enabled means response done)
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Full flow test
|
|
// --------------------------------------------------------------------------
|
|
test('Full conversation flow: create, persist, switch conversations', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
if (!await waitForAssistantReady(page)) {
|
|
test.skip(true, 'Assistant API not available')
|
|
return
|
|
}
|
|
|
|
// STEP 1: Send first message
|
|
console.log('STEP 1: Ask 1+1')
|
|
await sendMessageAndWaitForResponse(page, 'how much is 1+1')
|
|
await expect(page.locator('.flex-1.overflow-y-auto')).toContainText('2', { timeout: 5000 })
|
|
|
|
// Count greeting messages before closing
|
|
const greetingSelector = 'text=Hello! I\'m your project assistant'
|
|
const greetingCountBefore = await page.locator(greetingSelector).count()
|
|
console.log(`Greeting count before close: ${greetingCountBefore}`)
|
|
|
|
// STEP 2: Close and reopen - should see same conversation WITHOUT new greeting
|
|
console.log('STEP 2: Close and reopen')
|
|
const closeButton = page.locator('[aria-label="Project Assistant"] button[title="Close Assistant (Press A)"]')
|
|
await closeButton.click()
|
|
await waitForPanelClosed(page)
|
|
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
// Verify our question is still visible (conversation resumed)
|
|
await expect(page.locator('text=how much is 1+1').first()).toBeVisible({ timeout: 10000 })
|
|
|
|
// CRITICAL: Verify NO new greeting was added (bug fix verification)
|
|
const greetingCountAfter = await page.locator(greetingSelector).count()
|
|
console.log(`Greeting count after reopen: ${greetingCountAfter}`)
|
|
expect(greetingCountAfter).toBe(greetingCountBefore)
|
|
|
|
// STEP 3: Start new chat
|
|
console.log('STEP 3: New chat')
|
|
const newChatButton = page.locator('button[title="New conversation"]')
|
|
await newChatButton.click()
|
|
|
|
if (!await waitForAssistantReady(page)) {
|
|
test.skip(true, 'Assistant API not available')
|
|
return
|
|
}
|
|
|
|
await expect(page.locator('text=how much is 1+1')).not.toBeVisible({ timeout: 5000 })
|
|
|
|
// STEP 4: Send second message in new chat
|
|
console.log('STEP 4: Ask 2+2')
|
|
await sendMessageAndWaitForResponse(page, 'how much is 2+2')
|
|
await expect(page.locator('.flex-1.overflow-y-auto')).toContainText('4', { timeout: 5000 })
|
|
|
|
// STEP 5: Check history has both conversations
|
|
console.log('STEP 5: Check history')
|
|
const historyButton = page.locator('button[title="Conversation history"]')
|
|
await historyButton.click()
|
|
await expect(page.locator('h3:has-text("Conversation History")')).toBeVisible()
|
|
|
|
const conversationItems = page.locator('.neo-dropdown:has-text("Conversation History") .neo-dropdown-item')
|
|
const count = await conversationItems.count()
|
|
console.log(`Found ${count} conversations`)
|
|
expect(count).toBeGreaterThanOrEqual(2)
|
|
|
|
// STEP 6: Switch to first conversation
|
|
console.log('STEP 6: Switch conversation')
|
|
await conversationItems.nth(1).click()
|
|
// Wait for conversation to load by checking for the expected message
|
|
await expect(page.locator('text=how much is 1+1').first()).toBeVisible({ timeout: 10000 })
|
|
await expect(page.locator('text=how much is 2+2')).not.toBeVisible()
|
|
|
|
console.log('All steps completed!')
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Delete conversation test
|
|
// --------------------------------------------------------------------------
|
|
test('Delete conversation from history', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
if (!await waitForAssistantReady(page)) {
|
|
test.skip(true, 'Assistant API not available')
|
|
return
|
|
}
|
|
|
|
// Create a conversation
|
|
await sendMessageAndWaitForResponse(page, `test delete ${Date.now()}`)
|
|
|
|
// Open history and get count
|
|
const historyButton = page.locator('button[title="Conversation history"]')
|
|
await historyButton.click()
|
|
await expect(page.locator('h3:has-text("Conversation History")')).toBeVisible()
|
|
|
|
const conversationItems = page.locator('.neo-dropdown:has-text("Conversation History") .neo-dropdown-item')
|
|
const countBefore = await conversationItems.count()
|
|
|
|
// Delete first conversation
|
|
const deleteButton = page.locator('.neo-dropdown:has-text("Conversation History") button[title="Delete conversation"]').first()
|
|
await deleteButton.click()
|
|
|
|
// Confirm
|
|
const confirmButton = page.locator('button:has-text("Delete")').last()
|
|
await expect(confirmButton).toBeVisible()
|
|
await confirmButton.click()
|
|
|
|
// Wait for confirmation dialog to close
|
|
await expect(confirmButton).not.toBeVisible({ timeout: 5000 })
|
|
|
|
// Verify count decreased
|
|
await historyButton.click()
|
|
const countAfter = await conversationItems.count()
|
|
expect(countAfter).toBeLessThan(countBefore)
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Send button state test
|
|
// --------------------------------------------------------------------------
|
|
test('Send button disabled when empty, enabled with text', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
if (!await waitForAssistantReady(page)) {
|
|
test.skip(true, 'Assistant API not available')
|
|
return
|
|
}
|
|
|
|
const inputArea = page.locator('textarea[placeholder="Ask about the codebase..."]')
|
|
const sendButton = page.locator('button[title="Send message"]')
|
|
|
|
// Empty = disabled
|
|
await inputArea.fill('')
|
|
await expect(sendButton).toBeDisabled()
|
|
|
|
// With text = enabled
|
|
await inputArea.fill('test')
|
|
await expect(sendButton).toBeEnabled()
|
|
|
|
// Empty again = disabled
|
|
await inputArea.fill('')
|
|
await expect(sendButton).toBeDisabled()
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Shift+Enter test
|
|
// --------------------------------------------------------------------------
|
|
test('Shift+Enter adds newline, Enter sends', async ({ page }) => {
|
|
const hasProject = await selectProject(page)
|
|
if (!hasProject) {
|
|
test.skip(true, 'No projects available')
|
|
return
|
|
}
|
|
|
|
await page.keyboard.press('a')
|
|
await waitForPanelOpen(page)
|
|
|
|
if (!await waitForAssistantReady(page)) {
|
|
test.skip(true, 'Assistant API not available')
|
|
return
|
|
}
|
|
|
|
const inputArea = page.locator('textarea[placeholder="Ask about the codebase..."]')
|
|
|
|
// Type and add newline
|
|
await inputArea.fill('Line 1')
|
|
await inputArea.press('Shift+Enter')
|
|
await inputArea.pressSequentially('Line 2')
|
|
|
|
const value = await inputArea.inputValue()
|
|
expect(value).toContain('Line 1')
|
|
expect(value).toContain('Line 2')
|
|
|
|
// Enter sends
|
|
await inputArea.press('Enter')
|
|
await expect(page.locator('text=Line 1').first()).toBeVisible({ timeout: 5000 })
|
|
})
|
|
})
|