Files
ObsiGate/tests/e2e/split-view.spec.js
T
bruno 2add24a9c1
CI / lint (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / security (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
feat: onglet Accueil, menus contextuels de navigation et racine vault #158
- clic sur le titre: la page d'accueil s'ouvre dans un onglet Accueil (icone Maison) epingle en tete de barre, reactivable d'un clic (onglet propre au panneau actif en split)
- page de navigation: menus contextuels des repertoires et des fichiers identiques a ceux de la sidebar, clic sur la racine d'un vault qui ouvre la vue, icone dossier a la couleur de l'arbre de navigation
- BUG-101: bloc « Keyboard shortcuts for tabs » duplique dans ui.js (un seul Ctrl+W fermait deux onglets)
- BUG-102: le menu contextuel se refermait 10 ms apres son ouverture (scroll de reflow des icones lucide)
2026-10-02 14:05:01 -04:00

448 lines
16 KiB
JavaScript

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