/**
* Oracle v2 UI Automated Tests
*
* Run with: npx playwright test tests/ui.test.ts
* Or: npm run test:ui
*/
import { test, expect, Page } from '@playwright/test';
const BASE_URL = 'http://localhost:37778';
const UI_FILE = 'file://' + process.cwd() + '/src/ui.html';
// Helper to wait for results
async function waitForResults(page: Page) {
await page.waitForSelector('.result-card, .loading:not(:has-text("Loading"))', { timeout: 10000 });
}
test.describe('Oracle v2 UI Tests', () => {
test.beforeEach(async ({ page }) => {
// Navigate to UI
await page.goto(UI_FILE);
// Wait for stats to load (confirms API is running)
await page.waitForSelector('.stat-value', { timeout: 5000 });
});
test.describe('Stats Loading', () => {
test('should display stats on page load', async ({ page }) => {
const stats = await page.locator('.stats').textContent();
expect(stats).toContain('total');
expect(stats).toContain('principles');
expect(stats).toContain('learnings');
expect(stats).toContain('retros');
});
test('should show numeric values in stats', async ({ page }) => {
const statValues = await page.locator('.stat-value').allTextContents();
// At least one stat should have a number
expect(statValues.some(v => /\d+/.test(v))).toBe(true);
});
});
test.describe('Browse Functionality', () => {
test('should load documents when clicking Browse', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
// Should show result cards
const cards = await page.locator('.result-card').count();
expect(cards).toBeGreaterThan(0);
});
test('should show document type badges', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
// Should have type badges
const typeBadges = await page.locator('.result-type').count();
expect(typeBadges).toBeGreaterThan(0);
});
test('should show pagination for many documents', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
// Check if pagination exists (may not if < 20 docs)
const pageInfo = await page.locator('.page-info').textContent();
expect(pageInfo).toContain('documents') || expect(pageInfo).toContain('Page');
});
});
test.describe('Tab Filtering', () => {
test('should filter by Principles tab in browse mode', async ({ page }) => {
// Start in browse mode
await page.click('button:has-text("Browse")');
await waitForResults(page);
// Click Principles tab
await page.click('.tab[data-type="principle"]');
await waitForResults(page);
// All visible types should be principle
const types = await page.locator('.result-type').allTextContents();
if (types.length > 0) {
types.forEach(t => expect(t.toLowerCase()).toBe('principle'));
}
});
test('should filter by Learnings tab in browse mode', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
await page.click('.tab[data-type="learning"]');
await waitForResults(page);
const types = await page.locator('.result-type').allTextContents();
if (types.length > 0) {
types.forEach(t => expect(t.toLowerCase()).toBe('learning'));
}
});
test('should filter by Retros tab in browse mode', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
await page.click('.tab[data-type="retro"]');
await waitForResults(page);
const types = await page.locator('.result-type').allTextContents();
if (types.length > 0) {
types.forEach(t => expect(t.toLowerCase()).toBe('retro'));
}
});
test('should show all types when All tab clicked', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
// First filter by principle
await page.click('.tab[data-type="principle"]');
await waitForResults(page);
// Then click All
await page.click('.tab[data-type="all"]');
await waitForResults(page);
// Should have results (may include multiple types)
const cards = await page.locator('.result-card').count();
expect(cards).toBeGreaterThan(0);
});
});
test.describe('Pagination', () => {
test('should navigate to next page in browse mode', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
const nextBtn = page.locator('button:has-text("Next")');
if (await nextBtn.isVisible() && await nextBtn.isEnabled()) {
// Get first card content before pagination
const firstCardBefore = await page.locator('.result-card').first().textContent();
await nextBtn.click();
await waitForResults(page);
// Content should change
const firstCardAfter = await page.locator('.result-card').first().textContent();
expect(firstCardAfter).not.toBe(firstCardBefore);
}
});
test('should navigate to previous page', async ({ page }) => {
await page.click('button:has-text("Browse")');
await waitForResults(page);
const nextBtn = page.locator('button:has-text("Next")');
if (await nextBtn.isVisible() && await nextBtn.isEnabled()) {
await nextBtn.click();
await waitForResults(page);
// Now go back
const prevBtn = page.locator('button:has-text("Prev")');
await prevBtn.click();
await waitForResults(page);
// Page info should show page 1
const pageInfo = await page.locator('.page-info').textContent();
expect(pageInfo).toContain('Page 1');
}
});
});
test.describe('Search Functionality', () => {
test('should search when entering query and clicking Search', async ({ page }) => {
await page.fill('#query', 'nothing deleted');
await page.click('button:has-text("Search")');
// Wait for loading to finish
await page.waitForFunction(() => {
const results = document.getElementById('results');
return results && !results.textContent?.includes('Searching...');
}, { timeout: 10000 });
// Should show results or "No results found"
const content = await page.locator('#results').textContent();
expect(content).toBeTruthy();
expect(content).not.toContain('Searching...');
});
test('should search when pressing Enter', async ({ page }) => {
await page.fill('#query', 'safety');
await page.press('#query', 'Enter');
// Wait for loading to finish
await page.waitForFunction(() => {
const results = document.getElementById('results');
return results && !results.textContent?.includes('Searching...');
}, { timeout: 10000 });
const content = await page.locator('#results').textContent();
expect(content).toBeTruthy();
expect(content).not.toContain('Searching...');
});
test('should maintain search mode when switching tabs', async ({ page }) => {
await page.fill('#query', 'git');
await page.click('button:has-text("Search")');
await waitForResults(page);
// Now click a tab - should stay in search mode
await page.click('.tab[data-type="learning"]');
await waitForResults(page);
// If results, should be filtered to learnings
const types = await page.locator('.result-type').allTextContents();
if (types.length > 0) {
types.forEach(t => expect(t.toLowerCase()).toBe('learning'));
}
});
});
test.describe('Consult Functionality', () => {
test('should show guidance when consulting', async ({ page }) => {
await page.fill('#query', 'force push');
await page.click('button:has-text("Consult")');
// Wait for guidance box
await page.waitForSelector('.guidance-box', { timeout: 10000 });
const guidance = await page.locator('.guidance-box').textContent();
expect(guidance).toContain('Guidance');
});
test('should show relevant principles in consult', async ({ page }) => {
await page.fill('#query', 'safety');
await page.click('button:has-text("Consult")');
await page.waitForSelector('.guidance-box', { timeout: 10000 });
// May or may not have principles section
const results = await page.locator('#results').textContent();
expect(results).toBeTruthy();
});
});
test.describe('Reflect Functionality', () => {
test('should show random wisdom when clicking Reflect', async ({ page }) => {
await page.click('button:has-text("Reflect")');
// Wait for loading to finish
await page.waitForFunction(() => {
const results = document.getElementById('results');
return results && !results.textContent?.includes('Reflecting...');
}, { timeout: 10000 });
// Should show a single result card with special styling
const card = await page.locator('.result-card').first();
expect(await card.isVisible()).toBe(true);
// Should have type badge
const type = await page.locator('.result-type').first().textContent();
expect(['principle', 'learning', 'retro']).toContain(type?.toLowerCase());
});
});
test.describe('Graph View', () => {
test('should show graph when clicking Graph button', async ({ page }) => {
await page.click('button:has-text("Graph")');
// Wait for canvas
await page.waitForSelector('#graph', { timeout: 10000 });
const canvas = await page.locator('#graph');
expect(await canvas.isVisible()).toBe(true);
});
test('should show graph legend', async ({ page }) => {
await page.click('button:has-text("Graph")');
await page.waitForSelector('.graph-legend', { timeout: 10000 });
const legend = await page.locator('.graph-legend').textContent();
expect(legend).toContain('Principle');
expect(legend).toContain('Learning');
expect(legend).toContain('Retro');
});
test('should show graph controls', async ({ page }) => {
await page.click('button:has-text("Graph")');
await page.waitForSelector('.graph-controls', { timeout: 10000 });
const resetBtn = await page.locator('.graph-controls button:has-text("Reset")');
expect(await resetBtn.isVisible()).toBe(true);
});
});
test.describe('Learn Modal', () => {
test('should open Learn modal when clicking Learn button', async ({ page }) => {
await page.click('button:has-text("Learn")');
const modal = await page.locator('#learnModal');
expect(await modal.isVisible()).toBe(true);
});
test('should close modal when clicking X', async ({ page }) => {
await page.click('button:has-text("Learn")');
await page.waitForSelector('#learnModal.active');
await page.click('.modal-close');
const modal = await page.locator('#learnModal.active');
expect(await modal.isVisible()).toBe(false);
});
test('should close modal when pressing ESC', async ({ page }) => {
await page.click('button:has-text("Learn")');
await page.waitForSelector('#learnModal.active');
await page.keyboard.press('Escape');
const modal = await page.locator('#learnModal.active');
expect(await modal.isVisible()).toBe(false);
});
test('should close modal when clicking outside', async ({ page }) => {
await page.click('button:has-text("Learn")');
await page.waitForSelector('#learnModal.active');
// Click on modal backdrop
await page.click('#learnModal', { position: { x: 10, y: 10 } });
const modal = await page.locator('#learnModal.active');
expect(await modal.isVisible()).toBe(false);
});
test('should have required pattern field', async ({ page }) => {
await page.click('button:has-text("Learn")');
await page.waitForSelector('#learnModal.active');
const patternField = await page.locator('#pattern');
expect(await patternField.getAttribute('required')).toBe('');
});
test('should submit new learning successfully', async ({ page }) => {
await page.click('button:has-text("Learn")');
await page.waitForSelector('#learnModal.active');
// Fill form
const testPattern = `Test pattern from UI test - ${Date.now()}`;
await page.fill('#pattern', testPattern);
await page.fill('#source', 'UI Test');
await page.fill('#concepts', 'test, automation');
// Submit
await page.click('button[type="submit"]');
// Wait for success message
await page.waitForSelector('.success-message.active', { timeout: 5000 });
const successMsg = await page.locator('.success-message').textContent();
expect(successMsg).toContain('successfully');
});
});
test.describe('Mode Persistence', () => {
test('should stay in browse mode when switching tabs after browse', async ({ page }) => {
// Click Browse first
await page.click('button:has-text("Browse")');
await waitForResults(page);
// Switch tabs - should stay in browse mode
await page.click('.tab[data-type="principle"]');
await waitForResults(page);
// Should NOT show "No results" if there are principles (no query needed)
const resultCount = await page.locator('.result-card').count();
// In browse mode, we get results without a query
// Just verify the UI updated
const pageInfo = await page.locator('.page-info').textContent();
expect(pageInfo).toContain('documents');
});
test('should stay in search mode when switching tabs after search', async ({ page }) => {
// Search first
await page.fill('#query', 'git');
await page.click('button:has-text("Search")');
await waitForResults(page);
// Switch tabs - should stay in search mode
await page.click('.tab[data-type="learning"]');
await waitForResults(page);
// If we have results, they should be filtered to learnings
const resultCount = await page.locator('.result-card').count();
if (resultCount > 0) {
const pageInfo = await page.locator('.page-info').textContent();
expect(pageInfo).toContain('results'); // search mode says "results"
}
});
});
test.describe('Error Handling', () => {
test('should handle empty search gracefully', async ({ page }) => {
// Clear query and click search
await page.fill('#query', '');
await page.click('button:has-text("Search")');
// Should show initial message (no crash)
const results = await page.locator('#results').textContent();
expect(results).toBeTruthy();
});
});
test.describe('URL Hash Navigation', () => {
test('should load graph view when URL has #graph', async ({ page }) => {
// Navigate directly with hash
await page.goto(UI_FILE);
// First wait for stats (API connection)
await page.waitForSelector('.stat-value', { timeout: 5000 });
// Now navigate with hash
await page.evaluate(() => {
window.location.hash = 'graph';
// Trigger the check manually since we're already loaded
if (typeof (window as any).showGraph === 'function') {
(window as any).showGraph();
}
});
// Wait for graph canvas
await page.waitForSelector('#graph', { timeout: 10000 });
const canvas = await page.locator('#graph');
expect(await canvas.isVisible()).toBe(true);
});
});
});