// ObsiGate E2E Tests — Playwright // Tests flows critiques : login, navigation, recherche, edition, Mermaid, PDF, theme, responsive import { test, expect } from '@playwright/test'; const BASE = process.env.BASE_URL || 'http://localhost:2029'; // ── Helpers ────────────────────────────────────────────────────────── async function goHome(page) { await page.goto(BASE); // Wait until app init() resolved (all handlers bound, incl. search). await page.waitForFunction(() => (window as any).__OBSIGATE_BOOTED === true, { timeout: 20000 }); await page.waitForSelector('#dashboard-home, #search-input', { timeout: 15000 }); } async function searchFor(page, query) { const input = page.locator('#search-input'); await input.fill(query); // La recherche se déclenche sur Entrée (l'autocomplete s'affiche pendant la frappe) await input.press('Enter'); // Wait for results to appear (debounce 300ms + network) await page.waitForSelector('.search-result-item, .search-results-header', { timeout: 8000 }); } async function openFirstResult(page) { const first = page.locator('.search-result-item').first(); await first.click(); await page.waitForSelector('[class*="markdown"]', { timeout: 5000 }).catch(() => {}); } // ── 1. Dashboard & Navigation ──────────────────────────────────────── test.describe('Dashboard & Navigation', () => { test('should load the dashboard with stats', async ({ page }) => { await goHome(page); // Dashboard stats grid should be visible await expect(page.locator('#dashboard-home')).toBeVisible(); await expect(page.locator('.dashboard-tab[data-tab="stats"]')).toBeVisible(); }); test('should show dashboard tabs (Stats, Bookmarks, Recents, Partages)', async ({ page }) => { await goHome(page); const tabs = ['stats', 'bookmarks', 'recent', 'shared']; for (const tab of tabs) { await expect(page.locator(`.dashboard-tab[data-tab="${tab}"]`)).toBeVisible(); } }); test('should show Quick Help section', async ({ page }) => { await goHome(page); await expect(page.locator('#quick-help')).toBeVisible(); await expect(page.locator('#quick-help .qh-header')).toContainText('Raccourcis'); }); test('should toggle Quick Help collapsed state', async ({ page }) => { await goHome(page); // Wait for JS to initialize await page.waitForTimeout(500); const toggle = page.locator('#qh-toggle'); const body = page.locator('#qh-body'); await expect(body).toBeVisible(); await toggle.click(); await page.waitForTimeout(200); await expect(body).toHaveClass(/collapsed/); await toggle.click(); await page.waitForTimeout(200); await expect(body).not.toHaveClass(/collapsed/); }); test('should show sidebar with vaults', async ({ page }) => { await goHome(page); await expect(page.locator('#sidebar')).toBeVisible(); // Should have vault items await expect(page.locator('.vault-item, [data-vault]').first()).toBeVisible({ timeout: 5000 }); }); test('should show search bar in header', async ({ page }) => { await goHome(page); await expect(page.locator('#search-input')).toBeVisible(); await expect(page.locator('#search-input')).toHaveAttribute('placeholder', 'Rechercher dans toutes les vaults...'); }); test('should show floating results bar when searching', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await expect(page.locator('#results-bar')).toBeVisible(); await expect(page.locator('#rb-count')).toBeVisible(); }); test('should hide floating results bar when clearing search', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await expect(page.locator('#results-bar')).toBeVisible(); await page.locator('#search-clear-btn').click(); await expect(page.locator('#results-bar')).not.toBeVisible(); }); }); // ── 2. Full-Text Search ────────────────────────────────────────────── test.describe('Full-Text Search', () => { test('should find results for a query', async ({ page }) => { await goHome(page); await searchFor(page, 'python'); const count = await page.locator('.search-result-item').count(); expect(count).toBeGreaterThan(0); }); test('should show result count in header', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await expect(page.locator('.search-results-summary-text')).toContainText('résultat'); }); test('should show result count in floating bar', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const countText = await page.locator('#rb-count').textContent(); expect(countText).toMatch(/\d+\/\d+/); }); test('should highlight search terms in results', async ({ page }) => { await goHome(page); await searchFor(page, 'python'); // Should have highlighted spans const highlights = page.locator('mark, .search-highlight, [class*="highlight"]'); const count = await highlights.count(); expect(count).toBeGreaterThan(0); }); test('should show relevance/date sort buttons', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await expect(page.locator('.search-sort__btn:has-text("Pertinence")')).toBeVisible(); await expect(page.locator('.search-sort__btn:has-text("Date")')).toBeVisible(); }); test('should show tags section in results', async ({ page }) => { await goHome(page); await searchFor(page, 'python'); // Tags section may appear if results have tags const tagsSection = page.locator('text=TAGS'); // Not always present — depends on vault content }); test('should show the extensions facet and collapse the facets panel (#157)', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const extFacet = page.locator('.search-facets__group[data-facet="extensions"]'); await expect(extFacet).toBeVisible(); await expect(extFacet.locator('.search-facets__item').first()).toBeVisible(); // discreet collapse/expand of the whole vaults+tags+extensions section await page.locator('.search-facets__toggle').click(); await expect(page.locator('.search-facets__body')).toBeHidden(); await page.locator('.search-facets__toggle').click(); await expect(page.locator('.search-facets__body')).toBeVisible(); // mobile: the toggle stays compact (no full-width stretch) and only the // icon rotates — the button box itself never spins await page.setViewportSize({ width: 390, height: 844 }); const toggle = page.locator('.search-facets__toggle'); const box = await toggle.boundingBox(); expect(box).not.toBeNull(); expect(box!.width).toBeLessThan(150); await toggle.click(); expect(await toggle.evaluate((el) => getComputedStyle(el).transform)).toBe('none'); const icon = toggle.locator('svg'); expect(await icon.evaluate((el) => getComputedStyle(el).transform)).not.toBe('none'); await toggle.click(); await page.setViewportSize({ width: 1280, height: 720 }); }); test('should navigate results with floating bar arrows', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const results = page.locator('.search-result-item'); const count = await results.count(); if (count > 1) { const initialCount = await page.locator('#rb-count').textContent(); await page.locator('#rb-next').click(); const newCount = await page.locator('#rb-count').textContent(); expect(newCount).not.toBe(initialCount); } }); test('should toggle Aa case-sensitive via floating bar', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const caseBtn = page.locator('#rb-case'); await caseBtn.click(); await expect(caseBtn).toHaveClass(/active/); await caseBtn.click(); await expect(caseBtn).not.toHaveClass(/active/); }); test('should toggle wd whole-word via floating bar', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const wordBtn = page.locator('#rb-word'); await wordBtn.click(); await expect(wordBtn).toHaveClass(/active/); await wordBtn.click(); await expect(wordBtn).not.toHaveClass(/active/); }); test('should show save search button', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await expect(page.locator('.search-save-btn')).toBeVisible(); }); }); // ── 3. File Navigation & Viewer ────────────────────────────────────── test.describe('File Navigation & Viewer', () => { test('should open a file from search results in preview', async ({ page }) => { await goHome(page); await searchFor(page, 'python'); const firstResult = page.locator('.search-result-item').first(); await firstResult.click(); // Should open a preview tab or content await expect(page.locator('#content-area')).toBeVisible(); }); test('should open a file from sidebar tree', async ({ page }) => { await goHome(page); // Expand a vault in sidebar const vaultItem = page.locator('.vault-item, [data-vault]').first(); if (await vaultItem.count() > 0) { await vaultItem.click(); await page.waitForTimeout(500); // Click first file const fileItem = page.locator('.tree-item[data-path]').first(); if (await fileItem.count() > 0) { await fileItem.click(); await page.waitForTimeout(500); await expect(page.locator('#content-area')).toBeVisible(); } } }); test('should render markdown content in viewer', async ({ page }) => { await goHome(page); await searchFor(page, 'python'); const results = page.locator('.search-result-item'); const count = await results.count(); if (count > 0) { await results.first().click(); await page.waitForTimeout(1000); // Content area should have some rendered HTML const contentHTML = await page.locator('#content-area').innerHTML(); expect(contentHTML.length).toBeGreaterThan(50); } }); test('should show file metadata (frontmatter) if present', async ({ page }) => { await goHome(page); await searchFor(page, 'python'); await openFirstResult(page); // Frontmatter may be rendered as badges or metadata section // Just verify the viewer loaded await page.waitForSelector('#content-area', { timeout: 5000 }); }); test('should show viewer toolbar actions', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await openFirstResult(page); await page.waitForTimeout(500); // Toolbar buttons should be present const toolbar = page.locator('.btn-action, [class*="toolbar"]'); // May or may not be visible depending on file type }); }); // ── 4. Editor (CodeMirror) ─────────────────────────────────────────── test.describe('Editor', () => { test('should open editor via context menu or button', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const firstResult = page.locator('.search-result-item').first(); await firstResult.click({ button: 'right' }); await page.waitForTimeout(300); // Context menu should appear const contextMenu = page.locator('#context-menu, .context-menu'); if (await contextMenu.isVisible()) { // Look for edit-related option const editOption = contextMenu.locator('text=Éditer, text=Edit, text=Modifier'); // Right-click to close await page.keyboard.press('Escape'); } }); test('should open Forge editor', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); await openFirstResult(page); await page.waitForTimeout(500); // Look for the Forge button in the viewer toolbar const forgeBtn = page.locator('.btn-forge').first(); if (await forgeBtn.isVisible()) { await forgeBtn.click(); await page.waitForTimeout(500); // Forge iframe should be present const forgeFrame = page.locator('#forge-iframe'); if (await forgeFrame.count() > 0) { await expect(forgeFrame).toBeVisible(); } } }); test('should open basic editor modal', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); // Right-click on first result const firstResult = page.locator('.search-result-item').first(); await firstResult.click({ button: 'right' }); await page.waitForTimeout(300); // Context menu may have edit option await page.keyboard.press('Escape'); // Try clicking Edit button in toolbar const editBtn = page.locator('text=Éditer, text=Edit').first(); // Not all files may show this }); test('Ctrl+S saves and creates backup', async ({ page }) => { // This test requires the editor to be open with content // For now, verify the save mechanism exists in the UI await goHome(page); await searchFor(page, 'test'); await openFirstResult(page); await page.waitForTimeout(500); // Try opening Forge const forgeBtn = page.locator('.btn-forge').first(); if (await forgeBtn.isVisible()) { await forgeBtn.click(); await page.waitForSelector('#forge-iframe', { timeout: 5000 }); // Verify the editor loaded const frame = page.frameLocator('#forge-iframe'); const textarea = frame.locator('#ta'); if (await textarea.isVisible({ timeout: 3000 }).catch(() => false)) { const originalText = await textarea.inputValue(); await textarea.fill(originalText + '\n\nTest E2E modification.'); // Trigger save via parent modal button const saveBtn = page.locator('#editor-save'); if (await saveBtn.isVisible()) { await saveBtn.click(); await page.waitForTimeout(1500); } } } }); }); // ── 5. Mermaid Diagrams ────────────────────────────────────────────── test.describe('Mermaid Diagrams', () => { test('should render Mermaid blocks in viewer', async ({ page }) => { await goHome(page); await searchFor(page, 'mermaid'); const results = page.locator('.search-result-item'); if (await results.count() > 0) { await results.first().click(); await page.waitForTimeout(1000); // Look for rendered Mermaid SVG const svgs = page.locator('.mermaid-rendered svg, [class*="mermaid"] svg'); const count = await svgs.count(); // May have diagrams if the file contains mermaid code blocks } }); test('should show Mermaid in Forge preview', async ({ page }) => { await goHome(page); await searchFor(page, 'mermaid'); const results = page.locator('.search-result-item'); if (await results.count() > 0) { await results.first().click(); await page.waitForTimeout(500); const forgeBtn = page.locator('.btn-forge').first(); if (await forgeBtn.isVisible()) { await forgeBtn.click(); await page.waitForSelector('#forge-iframe', { timeout: 5000 }); // Preview toggle const frame = page.frameLocator('#forge-iframe'); const previewBtn = frame.locator('#btn-preview'); if (await previewBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await previewBtn.click(); await page.waitForTimeout(1000); } } } }); }); // ── 6. PDF Export ──────────────────────────────────────────────────── test.describe('PDF Export', () => { test('should have PDF export button in share view', async ({ page }) => { await goHome(page); await searchFor(page, 'test'); const results = page.locator('.search-result-item'); if (await results.count() > 0) { await results.first().click(); await page.waitForTimeout(500); // Look for PDF button in toolbar (only on shared docs) const shareBtn = page.locator('text=Partager'); // This test verifies the UI element exists } }); }); // ── 7. Dark Mode ───────────────────────────────────────────────────── test.describe('Dark Mode', () => { test('should default to dark theme', async ({ page }) => { await goHome(page); const html = page.locator('html'); const theme = await html.getAttribute('data-theme'); // Le thème par défaut est sombre : 'dark' (ui.js legacy) ou // 'defaut-obsigate' (moteur de thèmes themes.js, mode dark par défaut) expect(['dark', 'defaut-obsigate']).toContain(theme); }); test('should toggle theme and persist', async ({ page }) => { await goHome(page); // Find theme toggle const themeToggle = page.locator('#theme-toggle, [data-theme-toggle], button:has([data-lucide="moon"]), button:has([data-lucide="sun"])').first(); if (await themeToggle.isVisible()) { // Get current theme const html = page.locator('html'); const beforeTheme = await html.getAttribute('data-theme'); await themeToggle.click(); await page.waitForTimeout(300); const afterTheme = await html.getAttribute('data-theme'); expect(afterTheme).not.toBe(beforeTheme); // Verify persistence via localStorage const stored = await page.evaluate(() => localStorage.getItem('theme')); expect(stored).toBeTruthy(); } }); test('should toggle and persist', async ({ page }) => { await goHome(page); // Check localStorage const stored = await page.evaluate(() => localStorage.getItem('theme')); console.log('Stored theme:', stored); }); test('should apply dark background color', async ({ page }) => { await goHome(page); const bgColor = await page.locator('body').evaluate(el => window.getComputedStyle(el).backgroundColor ); // Dark mode should have a dark background (not white) expect(bgColor).not.toBe('rgb(255, 255, 255)'); }); }); // ── 8. Responsive / Mobile ─────────────────────────────────────────── test.describe('Responsive / Mobile', () => { test('should show mobile-friendly layout on small screen', async ({ page }) => { // Use desktop but set small viewport await page.setViewportSize({ width: 375, height: 812 }); await goHome(page); // Sidebar may be hidden or collapsed on mobile const sidebar = page.locator('#sidebar'); // Content should still be visible await expect(page.locator('#search-input')).toBeVisible(); }); test('should keep search functional on mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await goHome(page); await searchFor(page, 'test'); const results = page.locator('.search-result-item'); expect(await results.count()).toBeGreaterThan(0); }); test('should show floating results bar on mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await goHome(page); await searchFor(page, 'test'); await expect(page.locator('#results-bar')).toBeVisible(); }); }); // ── 9. Saved Searches ──────────────────────────────────────────────── test.describe('Saved Searches', () => { test('should open saved searches sidebar tab', async ({ page }) => { await goHome(page); const savedTab = page.locator('#sidebar-tab-saved'); if (await savedTab.isVisible()) { await savedTab.click(); await page.waitForTimeout(300); await expect(page.locator('#sidebar-panel-saved')).toBeVisible(); } }); test('should show saved filter bar with 3 pills', async ({ page }) => { await goHome(page); const savedTab = page.locator('#sidebar-tab-saved'); if (await savedTab.isVisible()) { await savedTab.click(); await page.waitForTimeout(300); await expect(page.locator('#saved-filter-bar')).toBeVisible(); const pills = page.locator('.saved-filter-pill'); expect(await pills.count()).toBe(3); } }); test('should filter saved searches by type', async ({ page }) => { await goHome(page); const savedTab = page.locator('#sidebar-tab-saved'); if (await savedTab.isVisible()) { await savedTab.click(); await page.waitForTimeout(300); // Click "Recherches" filter await page.locator('.saved-filter-pill[data-filter="search"]').click(); await expect(page.locator('.saved-filter-pill[data-filter="search"]')).toHaveClass(/active/); // Click "Repertoires" filter await page.locator('.saved-filter-pill[data-filter="directory"]').click(); await expect(page.locator('.saved-filter-pill[data-filter="directory"]')).toHaveClass(/active/); } }); }); // ── 10. Keyboard Shortcuts ─────────────────────────────────────────── test.describe('Keyboard Shortcuts', () => { test('Ctrl+K focuses search input', async ({ page }) => { await goHome(page); await page.keyboard.press('Control+k'); const focused = await page.locator('#search-input'); await expect(focused).toBeFocused(); }); test('/ key focuses search when not in input', async ({ page }) => { await goHome(page); // Click somewhere outside any input await page.locator('#dashboard-home').click(); await page.keyboard.press('/'); await expect(page.locator('#search-input')).toBeFocused(); }); test('Escape closes editor modal if open', async ({ page }) => { await goHome(page); const modal = page.locator('#editor-modal'); if (await modal.evaluate(el => el.classList.contains('active'))) { await page.keyboard.press('Escape'); await expect(modal).not.toHaveClass(/active/); } }); }); // ── 11. Context Menu — Directory Save ──────────────────────────────── test.describe('Context Menu — Directory', () => { test('should show context menu on directory right-click', async ({ page }) => { await goHome(page); // Expand a vault in sidebar const vaultItem = page.locator('.vault-item, [data-vault]').first(); if (await vaultItem.count() > 0) { await vaultItem.click(); await page.waitForTimeout(500); // Look for directory items (folder icon) const dirItems = page.locator('.tree-item'); const count = await dirItems.count(); if (count > 0) { await dirItems.first().click({ button: 'right' }); await page.waitForTimeout(300); await expect(page.locator('#context-menu, .context-menu')).toBeVisible(); await page.keyboard.press('Escape'); } } }); });