Backend: - Add fetcher_test.go (HTML stripping, URL fetching utilities) - Add model_registry_test.go (parsing, size ranges, model matching) - Add database_test.go (CRUD operations, migrations) - Add tests for geolocation, search, tools, version handlers Frontend unit tests (469 total): - OllamaClient: 22 tests for API methods with mocked fetch - Memory/RAG: tokenizer, chunker, summarizer, embeddings, vector-store - Services: prompt-resolution, conversation-summary - Components: Skeleton, BranchNavigator, ConfirmDialog, ThinkingBlock - Utils: export, import, file-processor, keyboard - Tools: builtin math parser (44 tests) E2E tests (28 total): - Set up Playwright with Chromium - App loading, sidebar navigation, settings page - Chat interface, responsive design, accessibility - Import dialog, project modal interactions Config changes: - Add browser conditions to vitest.config.ts for Svelte 5 components - Add playwright.config.ts for E2E testing - Add test:e2e scripts to package.json - Update .gitignore to exclude test artifacts Closes #8
308 lines
8.6 KiB
TypeScript
308 lines
8.6 KiB
TypeScript
/**
|
|
* E2E tests for core application functionality
|
|
*
|
|
* Tests the main app UI, navigation, and user interactions
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('App Loading', () => {
|
|
test('loads the application', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Should have the main app container
|
|
await expect(page.locator('body')).toBeVisible();
|
|
|
|
// Should have the sidebar (aside element with aria-label)
|
|
await expect(page.locator('aside[aria-label="Sidebar navigation"]')).toBeVisible();
|
|
});
|
|
|
|
test('shows the Vessel branding', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Look for Vessel text in sidebar
|
|
await expect(page.getByText('Vessel')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('has proper page title', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
await expect(page).toHaveTitle(/vessel/i);
|
|
});
|
|
});
|
|
|
|
test.describe('Sidebar Navigation', () => {
|
|
test('sidebar is visible', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Sidebar is an aside element
|
|
const sidebar = page.locator('aside[aria-label="Sidebar navigation"]');
|
|
await expect(sidebar).toBeVisible();
|
|
});
|
|
|
|
test('has new chat link', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// New Chat is an anchor tag with "New Chat" text
|
|
const newChatLink = page.getByRole('link', { name: /new chat/i });
|
|
await expect(newChatLink).toBeVisible();
|
|
});
|
|
|
|
test('clicking new chat navigates to home', async ({ page }) => {
|
|
await page.goto('/settings');
|
|
|
|
// Click new chat link
|
|
const newChatLink = page.getByRole('link', { name: /new chat/i });
|
|
await newChatLink.click();
|
|
|
|
// Should navigate to home
|
|
await expect(page).toHaveURL('/');
|
|
});
|
|
|
|
test('has settings link', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Settings is an anchor tag
|
|
const settingsLink = page.getByRole('link', { name: /settings/i });
|
|
await expect(settingsLink).toBeVisible();
|
|
});
|
|
|
|
test('can navigate to settings', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Click settings link
|
|
const settingsLink = page.getByRole('link', { name: /settings/i });
|
|
await settingsLink.click();
|
|
|
|
// Should navigate to settings
|
|
await expect(page).toHaveURL('/settings');
|
|
});
|
|
|
|
test('has new project button', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// New Project button
|
|
const newProjectButton = page.getByRole('button', { name: /new project/i });
|
|
await expect(newProjectButton).toBeVisible();
|
|
});
|
|
|
|
test('has import button', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Import button has aria-label
|
|
const importButton = page.getByRole('button', { name: /import/i });
|
|
await expect(importButton).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Page', () => {
|
|
test('settings page loads', async ({ page }) => {
|
|
await page.goto('/settings');
|
|
|
|
// Should show settings content
|
|
await expect(page.getByText(/general|models|prompts|tools/i).first()).toBeVisible({
|
|
timeout: 10000
|
|
});
|
|
});
|
|
|
|
test('has settings tabs', async ({ page }) => {
|
|
await page.goto('/settings');
|
|
|
|
// Wait for page to load
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Should have multiple tabs/sections
|
|
const content = await page.content();
|
|
expect(content.toLowerCase()).toMatch(/general|models|prompts|tools|memory/);
|
|
});
|
|
});
|
|
|
|
test.describe('Chat Interface', () => {
|
|
test('home page shows chat area', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Look for chat-related elements (message input area)
|
|
const chatArea = page.locator('main, [class*="chat"]').first();
|
|
await expect(chatArea).toBeVisible();
|
|
});
|
|
|
|
test('has textarea for message input', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Chat input textarea
|
|
const textarea = page.locator('textarea').first();
|
|
await expect(textarea).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('can type in chat input', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Find and type in textarea
|
|
const textarea = page.locator('textarea').first();
|
|
await textarea.fill('Hello, this is a test message');
|
|
|
|
await expect(textarea).toHaveValue('Hello, this is a test message');
|
|
});
|
|
|
|
test('has send button', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Send button (usually has submit type or send icon)
|
|
const sendButton = page
|
|
.locator('button[type="submit"]')
|
|
.or(page.getByRole('button', { name: /send/i }));
|
|
await expect(sendButton.first()).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
test.describe('Model Selection', () => {
|
|
test('chat page renders model-related UI', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// The app should render without crashing
|
|
// Model selection depends on Ollama availability
|
|
await expect(page.locator('body')).toBeVisible();
|
|
|
|
// Check that there's either a model selector or a message about models
|
|
const hasModelUI = await page
|
|
.locator('[class*="model"], [class*="Model"]')
|
|
.or(page.getByText(/model|ollama/i))
|
|
.count();
|
|
|
|
// Just verify app renders - model UI depends on backend state
|
|
expect(hasModelUI).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
test.describe('Responsive Design', () => {
|
|
test('works on mobile viewport', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
await page.goto('/');
|
|
|
|
// App should still render
|
|
await expect(page.locator('body')).toBeVisible();
|
|
await expect(page.getByText('Vessel')).toBeVisible();
|
|
});
|
|
|
|
test('sidebar collapses on mobile', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
await page.goto('/');
|
|
|
|
// Sidebar should be collapsed (width: 0) on mobile
|
|
const sidebar = page.locator('aside[aria-label="Sidebar navigation"]');
|
|
|
|
// Check if sidebar has collapsed class or is hidden
|
|
await expect(sidebar).toHaveClass(/w-0|hidden/);
|
|
});
|
|
|
|
test('works on tablet viewport', async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
await page.goto('/');
|
|
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('works on desktop viewport', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
await page.goto('/');
|
|
|
|
await expect(page.locator('body')).toBeVisible();
|
|
|
|
// Sidebar should be visible on desktop
|
|
const sidebar = page.locator('aside[aria-label="Sidebar navigation"]');
|
|
await expect(sidebar).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Accessibility', () => {
|
|
test('has main content area', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Should have main element
|
|
const main = page.locator('main');
|
|
await expect(main).toBeVisible();
|
|
});
|
|
|
|
test('sidebar has proper aria-label', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
const sidebar = page.locator('aside[aria-label="Sidebar navigation"]');
|
|
await expect(sidebar).toBeVisible();
|
|
});
|
|
|
|
test('interactive elements are focusable', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// New Chat link should be focusable
|
|
const newChatLink = page.getByRole('link', { name: /new chat/i });
|
|
await newChatLink.focus();
|
|
await expect(newChatLink).toBeFocused();
|
|
});
|
|
|
|
test('can tab through interface', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Focus on the first interactive element in the page
|
|
const firstLink = page.getByRole('link').first();
|
|
await firstLink.focus();
|
|
|
|
// Tab should move focus to another element
|
|
await page.keyboard.press('Tab');
|
|
|
|
// Wait a bit for focus to shift
|
|
await page.waitForTimeout(100);
|
|
|
|
// Verify we can interact with the page via keyboard
|
|
// Just check that pressing Tab doesn't cause errors
|
|
await page.keyboard.press('Tab');
|
|
await page.keyboard.press('Tab');
|
|
|
|
// Page should still be responsive
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Import Dialog', () => {
|
|
test('import button opens dialog', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Click import button
|
|
const importButton = page.getByRole('button', { name: /import/i });
|
|
await importButton.click();
|
|
|
|
// Dialog should appear
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('import dialog can be closed', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Open import dialog
|
|
const importButton = page.getByRole('button', { name: /import/i });
|
|
await importButton.click();
|
|
|
|
// Wait for dialog
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible();
|
|
|
|
// Press escape to close
|
|
await page.keyboard.press('Escape');
|
|
|
|
// Dialog should be closed
|
|
await expect(dialog).not.toBeVisible({ timeout: 2000 });
|
|
});
|
|
});
|
|
|
|
test.describe('Project Modal', () => {
|
|
test('new project button opens modal', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Click new project button
|
|
const newProjectButton = page.getByRole('button', { name: /new project/i });
|
|
await newProjectButton.click();
|
|
|
|
// Modal should appear
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|