/** * E2E tests for ObsiGate Split View (#75). * * Prerequisites: * cd tests/e2e && npm install && npx playwright install chromium * * Run: * OBSIGATE_URL=http://192.168.30.101:2020 npx playwright test * OBSIGATE_URL=http://localhost:2021 npx playwright test --headed */ import { test, expect } from '@playwright/test'; const CREDS = { username: process.env.OBSIGATE_USER || 'admin', password: process.env.OBSIGATE_PASS || 'test123', }; async function login(page) { await page.goto('/'); // Wait for login screen const loginForm = page.locator('#login-screen'); await expect(loginForm).toBeVisible({ timeout: 5000 }).catch(() => { // Already logged in (session cookie) }); if (await loginForm.isVisible()) { await page.fill('#login-username', CREDS.username); await page.fill('#login-password', CREDS.password); await page.click('#login-btn'); await page.waitForSelector('#app:not(.hidden)', { timeout: 10000 }); } } async function openFile(page, vault, filePath) { // Ouvre via l'arbre sidebar : le clic passe par TabManager (système d'onglets). // Le deep-link #file= ouvre l'ancien viewer sans onglets — inutilisable ici. // Pas de reload si déjà sur la page : un goto('/') perdrait les onglets // simple-pane (non restaurés depuis localStorage au chargement). if (!(await page.locator('#sidebar').count())) { await page.goto('/'); await page.waitForTimeout(300); } const treeItem = page.locator(`.tree-item[data-vault="${vault}"][data-path="${filePath}"]`); if (!(await treeItem.isVisible().catch(() => false))) { const vaultItem = page.locator(`.vault-item[data-vault="${vault}"]`); if (await vaultItem.count() > 0) { await vaultItem.click(); await page.waitForTimeout(700); } } // dblclick → openPersistent : onglet persistant (le clic simple ouvre un // preview qui remplace l'onglet précédent — inutilisable pour les tests multi-onglets) await treeItem.dblclick({ timeout: 5000 }); await page.waitForTimeout(500); } async function openFileViaSidebar(page, vault, filePath) { // Click the file in the tree sidebar const treeItem = page.locator(`.tree-item[data-vault="${vault}"][data-path="${filePath}"]`); if (await treeItem.isVisible()) { await treeItem.click(); await page.waitForTimeout(500); } } // ============================================================================= // Basic split view tests // ============================================================================= test.describe('Split View — Basic Operations', () => { test.beforeEach(async ({ page }) => { await login(page); }); test('opens single file in tab', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); // Tab should be visible const tabBar = page.locator('#tab-bar'); await expect(tabBar).toBeVisible(); const tabs = page.locator('.tab-item'); await expect(tabs).toHaveCount(2); // navigation (racine vault) + fichier (#158) // Content area should show the file const content = page.locator('#content-area'); await expect(content).not.toBeEmpty(); }); test('opens two files and shows two tabs', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await page.locator('#header-logo').click(); // go home await page.waitForTimeout(300); await openFile(page, 'TestVault', 'note2.md'); const tabs = page.locator('.tab-item'); await expect(tabs).toHaveCount(4); // accueil + navigation + 2 fichiers (#158) }); test('split right via keyboard shortcut', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); // Trigger split right await page.keyboard.press('Control+Alt+\\'); // Wait for pane grid to appear await page.waitForSelector('.pane-grid', { timeout: 3000 }); const panes = page.locator('.pane-container'); await expect(panes).toHaveCount(2); // Both panes should have content areas const contentAreas = page.locator('.content-area'); await expect(contentAreas).toHaveCount(2); }); test('split down via keyboard shortcut', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); await page.keyboard.press('Control+Alt+Shift+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); const panes = page.locator('.pane-container'); await expect(panes).toHaveCount(2); }); test('split right via context menu', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); // Right-click on tab to open context menu const tab = page.locator('.tab-item').first(); await tab.click({ button: 'right' }); // Click "Diviser à droite" const menuItem = page.locator('#tab-context-menu .context-menu-item', { hasText: 'Diviser à droite' }); await expect(menuItem).toBeVisible(); await menuItem.click(); await page.waitForSelector('.pane-grid', { timeout: 3000 }); const panes = page.locator('.pane-container'); await expect(panes).toHaveCount(2); }); test('closes active pane via keyboard', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Close active pane await page.keyboard.press('Control+Alt+w'); await page.waitForTimeout(500); // Should be back to single pane const panes = page.locator('.pane-container'); await expect(panes).toHaveCount(0); // collapsed to single }); }); // ============================================================================= // Tab drag and drop tests // ============================================================================= test.describe('Split View — Tab Drag & Drop', () => { test.beforeEach(async ({ page }) => { await login(page); }); test('drag tab from pane 0 to pane 1', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Get first tab const sourceTab = page.locator('.tab-item').first(); // Get target pane's tab bar const targetPane = page.locator('.pane-container').nth(1); const targetTabBar = targetPane.locator('.tab-bar'); // Drag tab to target const sourceBox = await sourceTab.boundingBox(); const targetBox = await targetTabBar.boundingBox(); if (sourceBox && targetBox) { await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2); await page.mouse.down(); await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { steps: 10 }); await page.mouse.up(); } await page.waitForTimeout(500); // Source pane should have 1 tab, target should have 1 tab const pane0Tabs = page.locator('.pane-container').nth(0).locator('.tab-item'); const pane1Tabs = page.locator('.pane-container').nth(1).locator('.tab-item'); await expect(pane0Tabs).toHaveCount(2); // navigation + fichier (#158) await expect(pane1Tabs).toHaveCount(1); }); test('drag tab to right edge creates split', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); const sourceTab = page.locator('.tab-item').first(); const contentArea = page.locator('#content-area'); const tabBox = await sourceTab.boundingBox(); const areaBox = await contentArea.boundingBox(); if (tabBox && areaBox) { // Start drag on tab await page.mouse.move(tabBox.x + tabBox.width / 2, tabBox.y + tabBox.height / 2); await page.mouse.down(); // Move to right 80% of content area await page.mouse.move( areaBox.x + areaBox.width * 0.85, areaBox.y + areaBox.height / 2, { steps: 15 } ); // Wait for drop zone indicator await page.waitForTimeout(300); await page.mouse.up(); } await page.waitForSelector('.pane-grid', { timeout: 3000 }); const panes = page.locator('.pane-container'); await expect(panes).toHaveCount(2); }); test('drag from tree to pane opens file there', async ({ page }) => { // First create split await openFile(page, 'TestVault', 'note1.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Find a tree item const treeItem = page.locator('.tree-item[data-path="note2.md"]').first(); if (await treeItem.isVisible()) { // Drag from tree to pane 1's content area (dragTo = drag natif HTML5) const targetPane = page.locator('.pane-container').nth(1); const targetContent = targetPane.locator('.content-area'); try { await treeItem.dragTo(targetContent, { timeout: 5000 }); await page.waitForTimeout(500); } catch (e) { /* drag not delivered — assertion below will fail */ } } // Pane 1 should have a tab now const pane1Tabs = page.locator('.pane-container').nth(1).locator('.tab-item'); const count = await pane1Tabs.count(); expect(count).toBeGreaterThanOrEqual(1); }); }); // ============================================================================= // Persistence tests // ============================================================================= test.describe('Split View — Persistence', () => { test.beforeEach(async ({ page }) => { await login(page); // Clear any saved pane state await page.evaluate(() => localStorage.removeItem('obsigate-panes')); }); test('split state persists across page reload', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Verify localStorage has pane data const saved = await page.evaluate(() => localStorage.getItem('obsigate-panes')); expect(saved).toBeTruthy(); const data = JSON.parse(saved); expect(data.panes).toHaveLength(2); // Reload await page.reload(); await login(page); await page.waitForTimeout(1000); // Should restore split const restored = await page.evaluate(() => localStorage.getItem('obsigate-panes')); if (restored) { const restoredData = JSON.parse(restored); expect(restoredData.panes.length).toBeGreaterThanOrEqual(2); } }); test('reset panes command removes localStorage', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Open command palette and reset. // NB : l'input de la palette porte la CLASSE .cp-input (pas d'id) — // c'est ce sélecteur qui doit être utilisé. await page.keyboard.press('Control+Shift+Space'); const input = page.locator('.cp-input'); // Strict : la palette DOIT s'ouvrir avec Ctrl+Shift+Space await expect(input).toBeVisible({ timeout: 5000 }); // Type ">réinitialiser" (le préfixe '>' cherche une COMMANDE, // sinon le palette cherche des fichiers et ne trouve pas la commande) await input.fill('>réinitialiser'); // Attendre le rendu de la liste filtrée (debounce 150ms du palette) await page.waitForSelector('.cp-item', { timeout: 3000 }); await page.keyboard.press('Enter'); // localStorage should be cleared const cleared = await page.evaluate(() => localStorage.getItem('obsigate-panes')); expect(cleared).toBeNull(); }); }); // ============================================================================= // Regression tests // ============================================================================= test.describe('Split View — Regression', () => { test.beforeEach(async ({ page }) => { await login(page); await page.evaluate(() => localStorage.removeItem('obsigate-panes')); }); test('single pane mode works identically to before', async ({ page }) => { // In single-pane mode, everything should use #content-area await openFile(page, 'TestVault', 'note1.md'); const contentArea = page.locator('#content-area'); await expect(contentArea).toBeVisible(); await expect(contentArea).not.toBeEmpty(); // No pane grid should exist const paneGrid = page.locator('.pane-grid'); await expect(paneGrid).toHaveCount(0); // Tab bar should show #tab-list const tabList = page.locator('#tab-list'); await expect(tabList).toBeVisible(); }); test('closing last tab shows dashboard', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); // ouvre aussi l'onglet navigation (#158) // Close tabs via middle-click until none remains (dernier onglet -> dashboard) while (await page.locator('.tab-item').count()) { await page.locator('.tab-item').first().click({ button: 'middle' }); await page.waitForTimeout(300); } // Dashboard should be visible const dashboard = page.locator('#dashboard-home'); await expect(dashboard).toBeVisible(); // Tab bar should be hidden const tabBar = page.locator('#tab-bar'); await expect(tabBar).toBeHidden(); }); test('bookmark click opens file in tab system', async ({ page }) => { // First bookmark a file for the test await openFile(page, 'TestVault', 'note1.md'); // Go home await page.locator('#header-logo').click(); await page.waitForTimeout(500); // If bookmark card exists, click it const bookmarkGrid = page.locator('#dashboard-bookmarks-grid'); if (await bookmarkGrid.isVisible()) { const cards = bookmarkGrid.locator('.dashboard-card'); const count = await cards.count(); if (count > 0) { await cards.first().click(); await page.waitForTimeout(500); // Should have opened in a tab const tabs = page.locator('.tab-item'); await expect(tabs).toHaveCount(1); } } }); }); // ============================================================================= // Keyboard navigation tests // ============================================================================= test.describe('Split View — Keyboard Navigation', () => { test.beforeEach(async ({ page }) => { await login(page); await page.evaluate(() => localStorage.removeItem('obsigate-panes')); }); test('navigate between panes with Ctrl+Alt+Arrow', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Laisser la copie asynchrone de l'onglet actif se terminer (setTimeout 150ms // dans _copyActiveTabToNewPane) — sinon elle réactive le pane 0 après la navigation. await page.waitForTimeout(600); // Pane 0 should be active initially const pane0 = page.locator('.pane-container').nth(0); await expect(pane0).toHaveClass(/pane-active/); // Navigate right await page.keyboard.press('Control+Alt+ArrowRight'); await page.waitForTimeout(300); // Pane 1 should now be active const pane1 = page.locator('.pane-container').nth(1); await expect(pane1).toHaveClass(/pane-active/); // Navigate left back await page.keyboard.press('Control+Alt+ArrowLeft'); await page.waitForTimeout(300); await expect(pane0).toHaveClass(/pane-active/); }); test('close other panes with Ctrl+Shift+Alt+W', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); await page.keyboard.press('Control+Alt+\\'); await page.waitForSelector('.pane-grid', { timeout: 3000 }); // Create a third pane await page.keyboard.press('Control+Alt+\\'); await page.waitForTimeout(500); let panes = page.locator('.pane-container'); const initialCount = await panes.count(); expect(initialCount).toBeGreaterThanOrEqual(2); // Close other panes await page.keyboard.press('Control+Shift+Alt+w'); await page.waitForTimeout(500); // Should be back to single pane panes = page.locator('.pane-container'); await expect(panes).toHaveCount(0); }); });