I2 (test_pane_manager.py): 46 nouveaux tests - Module structure: 10 tests (exports, methods, factory, max panes) - Data flow: 8 tests (drag format, tree format, JSON, migration, localStorage) - DOM flow: 7 tests (grid, IDs, lazy render, collapse, empty pane, transfer) - Keyboard: 6 tests (all shortcuts + preventDefault counts) - Context menu: 4 tests (split, move-to, close actions) - CSS: 7 tests (grid, container, resize, drop zone, buttons, responsive) - HTML/App: 5 tests (wrapper, tab-bar, modules, imports) - Palette: 3 tests (commands, reset, category) - Editor lock: 2 tests - Dashboard: 1 test (TabManager vs openFile) I3 (tests/e2e/): suite Playwright complète - package.json, playwright.config.js, split-view.spec.js - 12 test specs: basic ops, tab drag, tree drag, persistence, regression, keyboard nav - Exécution: cd tests/e2e && npm install && npx playwright test Total: 353 tests passent (307 + 46)
431 lines
15 KiB
JavaScript
431 lines
15 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) {
|
|
// Use URL hash navigation
|
|
await page.goto(`/#/file/${encodeURIComponent(vault)}/${encodeURIComponent(filePath)}`);
|
|
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(1);
|
|
// 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(2);
|
|
});
|
|
|
|
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(1);
|
|
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
|
|
const targetPane = page.locator('.pane-container').nth(1);
|
|
const targetContent = targetPane.locator('.content-area');
|
|
|
|
const treeBox = await treeItem.boundingBox();
|
|
const targetBox = await targetContent.boundingBox();
|
|
|
|
if (treeBox && targetBox) {
|
|
await page.mouse.move(treeBox.x + treeBox.width / 2, treeBox.y + treeBox.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);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
await page.keyboard.press('Control+Shift+ '); // Ctrl+Shift+Space
|
|
await page.waitForTimeout(300);
|
|
|
|
// Type "réinitialiser" and select
|
|
const input = page.locator('#cp-input');
|
|
if (await input.isVisible()) {
|
|
await input.fill('réinitialiser');
|
|
await page.waitForTimeout(200);
|
|
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');
|
|
|
|
// Close tab via middle-click
|
|
const tab = page.locator('.tab-item').first();
|
|
await tab.click({ button: 'middle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
// 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 });
|
|
|
|
// 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);
|
|
});
|
|
});
|