CI / lint (push) Successful in 28s
CI / security (push) Successful in 21s
CI / test (push) Successful in 33s
CI / build (push) Successful in 13s
CI / e2e (push) Failing after 6m12s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
- search.js : suppression par flag (jusqu a la prochaine frappe) au lieu d un timeout 500ms qui expirait avant le clic sur le bouton Forge - split-view.spec.js : le test reset panes attend l apparition de la liste de commandes (debounce 150ms du palette) avant Entree
446 lines
16 KiB
JavaScript
446 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(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 (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
|
|
await page.keyboard.press('Control+Shift+ '); // Ctrl+Shift+Space
|
|
await page.waitForTimeout(300);
|
|
|
|
// Type ">réinitialiser" (le préfixe '>' cherche une COMMANDE,
|
|
// sinon le palette cherche des fichiers et ne trouve pas la commande)
|
|
const input = page.locator('#cp-input');
|
|
if (await input.isVisible()) {
|
|
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');
|
|
|
|
// 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 });
|
|
// 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);
|
|
});
|
|
});
|