Files
vessel/frontend/e2e/app.spec.ts
vikingowl d81430e1aa test: extend test coverage for backend and frontend
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
2026-01-22 11:05:49 +01:00

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 });
});
});