test(e2e): #75 I3 — matrice ouverture/fermeture split view (21 tests) + fix chemins Windows run-e2e-local.sh
CI / lint (push) Successful in 42s
CI / security (push) Successful in 27s
CI / test (push) Successful in 55s
CI / build (push) Successful in 23s
CI / e2e (push) Successful in 9m33s

This commit is contained in:
2026-09-08 09:38:10 -04:00
parent ccfcb58ed1
commit 212753f311
4 changed files with 446 additions and 9 deletions
+10 -1
View File
@@ -10,7 +10,7 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.1.0] — 2026-09-07
## [2.1.0] — 2026-09-08
### Ajouté
@@ -27,6 +27,15 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **#77 Desktop — port auto-increment** : `pick_free_port()` scanne 17890..17899 si le port
est occupé (2 instances côte à côte possibles) + 3 tests Rust (19 au total côté desktop).
- Docs : sections PDF README FR/EN (Range, /pdf/info), variables env, guide WebAuthn.
- **#75 Split View — matrice E2E complète (37 tests)** : 16 tests `split-view.spec.js` (raccourcis,
drag & drop, persistance, navigation clavier) + 21 tests `split-view-matrix.spec.js` couvrant
100% des cas d'ouverture/fermeture : boutons ⊞→/⊞↓, menu contextuel (diviser bas, fermer, fermer
les autres/à droite/tout, fermer le panneau, flyout « Déplacer vers… »), palette (diviser, focus
suivant/précédent, reset), fermeture par X/double-clic/molette/Ctrl+W, auto-collapse d'un panneau
vidé, plafond 4 panneaux, drag-to-split bas, pas de doublon à la réouverture.
- Fix `scripts/run-e2e-local.sh` : chemins Windows via `cygpath` (les env vars MSYS n'étaient pas
converties → vaults vides, les 16 tests split-view échouaient en local alors qu'ils passaient
en CI).
### Corrigé
+22 -6
View File
@@ -345,9 +345,9 @@
---
### 75. Éditeur multi-panneaux (Split View) — ✅ TERMINÉ (reste I3 : tests E2E split view)
### 75. Éditeur multi-panneaux (Split View) — ✅ TERMINÉ (E2E matriciels inclus)
- **Effort :** 5-7 jours (réalisé) | **Impact :** 🟡
- **Statut :** Fonctionnel — toutes les sous-tâches implémentées (reste tests I2/I3)
- **Statut :** Fonctionnel — 100% implémenté + 37 tests E2E (16 split-view.spec.js + 21 split-view-matrix.spec.js, 2026-09-08)
- **Fichiers clés :** `frontend/js/pane-manager.js` (1082 loc), `frontend/js/viewer.js` (modifié), `frontend/js/ui.js` (modifié), `frontend/js/dashboard.js` (modifié), `frontend/js/palette.js` (modifié), `frontend/style.css` (modifié), `tests/test_pane_manager.py` (22 tests)
- **Description :** Système de panneaux divisés permettant d'afficher plusieurs documents côte à côte ou superposés, à la manière d'un éditeur de code (VS Code). Drag & drop des onglets entre les panneaux, drag-to-split, menu contextuel, raccourcis clavier, palette de commandes.
- **Architecture :**
@@ -407,7 +407,7 @@
- [x] **I1.** 22 tests statiques (structure, CSS, brace balance, régression backend)
- [x] **I4.** 307 tests de régression passent
- [x] **I2.** Tests d'intégration frontend (JSDOM) — `tests/frontend/pane-manager.test.mjs` (9 tests)
- [ ] **I3.** Tests E2E Playwright (#58) — le split view n'est pas couvert par les 44 tests e2e
- [x] **I3.** Tests E2E Playwright (#58) — 2026-09 : `split-view.spec.js` (16) + `split-view-matrix.spec.js` (21) = 37 tests verts en local + CI
##### Reste à faire
- [x] **C1b.** Sous-menu « Déplacer vers... » dans le menu contextuel des onglets
@@ -416,7 +416,23 @@
- [x] **E4.** Verrouillage éditeur multi-panneau — même fichier ouvert dans 2 panneaux → focus le panneau existant
- [x] **G3.** Bouton reset dans la palette de commandes (🔄 Réinitialiser les panneaux)
- [x] **I2.** Tests d'intégration frontend (JSDOM) — `tests/frontend/pane-manager.test.mjs` (9 tests, 100% verts)
- [ ] **I3.** Tests E2E Playwright (split view non couvert)
- [x] **I3.** Tests E2E Playwright — matrice complète ouverture/fermeture couverte (voir split-view-matrix.spec.js)
##### Matrice de couverture ouverture/fermeture (validée 2026-09-08, 37/37 verts)
| Cas | Surface testée | Spec |
|---|---|---|
| Ouverture fichier → onglet | clic arbre, double-clic preview vs persistant | split-view |
| Split droit | raccourci Ctrl+Alt+\ · bouton ⊞→ · menu contextuel · palette · drag-to-split bord droit | les 5 |
| Split bas | raccourci Ctrl+Alt+Shift+\ · bouton ⊞↓ · menu contextuel · drag-to-split bord bas | les 4 |
| Déplacer onglet | drag cross-pane · flyout « Déplacer vers... » · drag depuis arbre vers panneau | les 3 |
| Navigation | Ctrl+Alt+←/→ (horizontal) · Ctrl+Alt+↑/↓ (vertical) · palette suivant/précédent | les 3 |
| Fermeture onglet | bouton X · double-clic · clic molette · Ctrl+W (single) | les 4 |
| Fermeture menu ctx | Fermer · Fermer les autres · Fermer à droite · Fermer tout → dashboard | les 4 |
| Fermeture panneau | Ctrl+Alt+W (clavier) · menu « Fermer le panneau » · Ctrl+Shift+Alt+W (autres) · auto-collapse panneau vidé (C4) · fermeture pane 0 → voisin survit · onglets du panneau fermé perdus (documenté) | les 5 |
| Persistance | layout restauré au reload · reset palette vide localStorage | les 2 |
| Edge | plafond 4 panneaux (PANE_MAX) · pas de doublon à la réouverture (E4) | les 2 |
Non couvert par E2E (hors périmètre ouverture/fermeture) : poignée de redimensionnement drag/dblclick-reset (A3) — logique vérifiée par les 22 tests statiques + JSDOM.
---
@@ -875,13 +891,13 @@
| Priorité | Items | Effort total estimé |
|---|---|---|
| ✅ Complété | #1 → #59, #63-66, #71, #74, #75*, #76 (58 E2E, 59 offline, 63 i18n, 64 MFA TOTP+WebAuthn, 65 thèmes, 66 export, 71 admin, 74 PDF, 76 BooksLM) | ~75 jours réalisés |
| ✅ Complété | #1 → #59, #63-66, #71, #74, #75, #76 (58 E2E, 59 offline, 63 i18n, 64 MFA TOTP+WebAuthn, 65 thèmes, 66 export, 71 admin, 74 PDF, 76 BooksLM) | ~75 jours réalisés |
| 🔵 P2 restant | #77 Desktop : signature code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E **manuels** | ~1-2 jours |
| ⚪ P3 restant | #61 Plugins système (4-5j) · #62 Collaboration Yjs (5-7j) · #78 Excalidraw finitions (B5 recherche, C8, F3 E2E, BUG-002) (~1-1.5j) | ~10-13.5 jours |
| ⚪ P4 restant | #67 Push (2j) · #68 Health enrichi (1j) · #69 Mobile éditeur (2-3j) · #70 Sémantique (4-5j) · #72 OpenAPI (1-2j) · #73 Sync (6-8j) | 16-21 jours |
| **Total restant** | **9 items + finitions** | **~28-37 jours** |
\* #75 : 100% fonctionnel, il ne reste que les tests E2E Playwright de la sous-tâche I3.
#75 : E2E matriciels 37/37 verts (split-view.spec.js + split-view-matrix.spec.js) — 2026-09-08.
---
+11 -2
View File
@@ -57,11 +57,20 @@ fi
# ----- Démarrage du serveur (mêmes conditions que le CI e2e) -----
echo "[INFO] Démarrage d'ObsiGate sur $BASE_URL (auth désactivée)..."
mkdir -p data
# Sous MSYS/git-bash, les env vars ne sont PAS converties : un chemin
# '/c/dev/...' est illisible pour le python natif Windows → vaults vides.
if command -v cygpath &>/dev/null; then
VP="$(cygpath -w "$PWD/test_vault")"
DP="$(cygpath -w "$PWD/test_dir")"
else
VP="$PWD/test_vault"
DP="$PWD/test_dir"
fi
OBSIGATE_AUTH_ENABLED=false \
VAULT_1_NAME=TestVault \
VAULT_1_PATH="$(pwd)/test_vault" \
VAULT_1_PATH="$VP" \
DIR_1_NAME=TestDir \
DIR_1_PATH="$(pwd)/test_dir" \
DIR_1_PATH="$DP" \
"$PYTHON" -m uvicorn backend.main:app --host 127.0.0.1 --port "$PORT" \
>"$SERVER_LOG" 2>&1 &
SERVER_PID=$!
+403
View File
@@ -0,0 +1,403 @@
/**
* E2E tests for ObsiGate Split View (#75) — COMPLETION MATRIX.
*
* split-view.spec.js covers the core flows; this file closes the remaining
* gaps of the open/close usage matrix:
* Ouverture : boutons ⊞→/⊞↓ (C3), menu contextuel (splitDown, moveTo),
* drag-to-split BAS (D3b), palette (C→, ↓, focus next/prev, reset)
* Fermeture : .tab-close (X), dblclick, Ctrl+W single-pane, menu contextuel
* Fermer / Fermer les autres / Fermer à droite / Fermer tout /
* Fermer le panneau, Ctrl+Alt+W via menu
* Edge : plafond 4 panneaux, auto-collapse d'un panneau vidé,
* focus voisin à la fermeture, dashboard si dernier onglet fermé
*
* Run: ./scripts/run-e2e-local.sh -g "Split View Matrix"
*/
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('/');
const loginForm = page.locator('#login-screen');
await expect(loginForm).toBeVisible({ timeout: 5000 }).catch(() => {});
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 });
}
await page.evaluate(() => localStorage.removeItem('obsigate-panes'));
}
async function openFile(page, vault, filePath) {
const treeItem = page.locator(`.tree-item[data-vault="${vault}"][data-path="${filePath}"]`);
// Expand the vault root if not expanded
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);
}
}
// Expand parent folders one level at a time (lazy tree)
const parts = filePath.split('/');
for (let i = 1; i < parts.length; i++) {
if (await treeItem.isVisible().catch(() => false)) break;
const dirPath = parts.slice(0, i).join('/');
const dir = page.locator(`.tree-item[data-vault="${vault}"][data-path="${dirPath}"]`);
if (await dir.count() > 0) {
await dir.click();
await page.waitForTimeout(600);
}
}
// Re-expand vault if clicking a folder collapsed the tree
if (!(await treeItem.isVisible().catch(() => false))) {
const vaultItem = page.locator(`.vault-item[data-vault="${vault}"]`);
await vaultItem.click().catch(() => {});
await page.waitForTimeout(500);
for (let i = 1; i < parts.length; i++) {
const dirPath = parts.slice(0, i).join('/');
const dir = page.locator(`.tree-item[data-vault="${vault}"][data-path="${dirPath}"]`);
if (await dir.count() > 0) { await dir.click(); await page.waitForTimeout(500); }
}
}
await treeItem.dblclick({ timeout: 5000 });
await page.waitForTimeout(500);
}
// Open a file programmatically in a given pane (robust setup for pane>0)
async function openInPane(page, paneId, vault, path) {
await page.evaluate(([pid, v, f]) => {
const tm = window.PaneManager.tabManagers[pid];
if (!tm) throw new Error('no pane ' + pid);
tm.open(v, f);
}, [paneId, vault, path]);
await page.waitForTimeout(800);
}
async function paneCount(page) {
return page.locator('.pane-container').count();
}
async function splitVia(page) {
await page.keyboard.press('Control+Alt+\\');
await page.waitForSelector('.pane-grid', { timeout: 3000 });
}
test.describe('Split View Matrix — Boutons & menu contextuel', () => {
test.beforeEach(async ({ page }) => { await login(page); });
test('⊞→ button in tab bar splits right (C3)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
const splitBtn = page.locator('#tab-bar .tab-split-btn[data-action="split-right"]');
await expect(splitBtn).toBeAttached();
await splitBtn.click({ force: true });
await page.waitForSelector('.pane-grid', { timeout: 3000 });
await expect(page.locator('.pane-container')).toHaveCount(2);
});
test('⊞↓ button in tab bar splits down (C3)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
const splitBtn = page.locator('#tab-bar .tab-split-btn[data-action="split-down"]');
await expect(splitBtn).toBeAttached();
await splitBtn.click({ force: true });
await page.waitForSelector('.pane-grid', { timeout: 3000 });
await expect(page.locator('.pane-container')).toHaveCount(2);
});
test('context menu → Diviser en bas (C1)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await page.locator('.tab-item').first().click({ button: 'right' });
const menuItem = page.locator('#tab-context-menu .context-menu-item', { hasText: 'Diviser en bas' });
await expect(menuItem).toBeVisible();
await menuItem.click();
await page.waitForSelector('.pane-grid', { timeout: 3000 });
await expect(page.locator('.pane-container')).toHaveCount(2);
});
test('context menu → Fermer le panneau returns to single pane', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await splitVia(page);
await page.waitForTimeout(600);
// put a tab into pane 1 so the pane actually holds content
await openInPane(page, 1, 'TestVault', 'Accueil.md');
// right-click pane 1's tab — active pane becomes 1 — use the contextual close
const pane1Tab = page.locator('.pane-container').nth(1).locator('.tab-item').first();
await pane1Tab.click({ button: 'right' });
const closePaneItem = page.locator('#tab-context-menu [data-action="closePane"]');
await expect(closePaneItem).toBeVisible();
await closePaneItem.click();
await page.waitForTimeout(600);
await expect(page.locator('.pane-container')).toHaveCount(0);
// pane 0 tabs survived (single mode)
await expect(page.locator('#tab-list .tab-item').first()).toBeVisible();
});
test('context menu → Déplacer vers... flyout transfers tab (C1b)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await splitVia(page);
await page.waitForTimeout(600);
// pane 1 starts empty after split — put a file there via API
await openInPane(page, 1, 'TestVault', 'note2.md');
const pane1Tabs = page.locator('.pane-container').nth(1).locator('.tab-item');
await expect(pane1Tabs.first()).toBeVisible({ timeout: 5000 });
await pane1Tabs.first().click({ button: 'right' });
const moveTo = page.locator('#tab-context-menu [data-action="moveTo"]');
await expect(moveTo).toBeVisible();
await moveTo.hover();
const flyout = page.locator('.context-submenu .context-menu-item', { hasText: 'Panneau 1' });
await expect(flyout).toBeVisible({ timeout: 3000 });
await flyout.click();
await page.waitForTimeout(600);
// Pane 0 (single) holds both files now; empty source pane auto-collapsed
await expect(page.locator('.pane-container')).toHaveCount(0);
await expect(page.locator('#tab-list .tab-item')).toHaveCount(2);
});
});
test.describe('Split View Matrix — Palette de commandes', () => {
test.beforeEach(async ({ page }) => { await login(page); });
test('palette command ⊞→ splits (G3/H1)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await page.keyboard.press('Control+Shift+Space');
const input = page.locator('.cp-input');
await expect(input).toBeVisible({ timeout: 5000 });
await input.fill('>Diviser à droite');
await page.waitForSelector('.cp-item', { timeout: 3000 });
await page.keyboard.press('Enter');
await page.waitForSelector('.pane-grid', { timeout: 3000 });
await expect(page.locator('.pane-container')).toHaveCount(2);
});
test('palette Panneau suivant / Panneau précédent navigate', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await splitVia(page);
await page.waitForTimeout(600);
await page.keyboard.press('Control+Shift+Space');
const input = page.locator('.cp-input');
await expect(input).toBeVisible({ timeout: 5000 });
await input.fill('>Panneau suivant');
await page.waitForSelector('.cp-item', { timeout: 3000 });
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await expect(page.locator('.pane-container').nth(1)).toHaveClass(/pane-active/);
});
});
test.describe('Split View Matrix — Fermeture', () => {
test.beforeEach(async ({ page }) => { await login(page); });
test('X button on tab closes it', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await expect(page.locator('.tab-item')).toHaveCount(2);
await page.locator('.tab-item').first().locator('.tab-close').click();
await page.waitForTimeout(300);
await expect(page.locator('.tab-item')).toHaveCount(1);
});
test('double-click on tab closes it', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await page.locator('.tab-item').first().dblclick();
await page.waitForTimeout(300);
await expect(page.locator('.tab-item')).toHaveCount(1);
});
test('Ctrl+W closes active tab (single pane)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await page.keyboard.press('Control+w');
await page.waitForTimeout(400);
await expect(page.locator('.tab-item')).toHaveCount(0);
await expect(page.locator('#dashboard-home')).toBeVisible();
});
test('context menu Fermer (close single)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await page.locator('.tab-item').first().click({ button: 'right' });
await page.locator('#tab-context-menu [data-action="close"]').click();
await page.waitForTimeout(300);
await expect(page.locator('.tab-item')).toHaveCount(1);
});
test('context menu Fermer les autres keeps one', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await page.locator('#header-logo').click(); await page.waitForTimeout(300);
await openFile(page, 'TestVault', 'Accueil.md');
await page.waitForTimeout(300);
const tabs = page.locator('.tab-item');
await expect(tabs).toHaveCount(3);
await tabs.nth(1).click({ button: 'right' });
await page.locator('#tab-context-menu [data-action="closeOthers"]').click();
await page.waitForTimeout(300);
await expect(page.locator('.tab-item')).toHaveCount(1);
});
test('context menu Fermer à droite', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await page.locator('#header-logo').click(); await page.waitForTimeout(300);
await openFile(page, 'TestVault', 'Accueil.md');
await page.waitForTimeout(300);
await expect(page.locator('.tab-item')).toHaveCount(3);
await page.locator('.tab-item').first().click({ button: 'right' });
await page.locator('#tab-context-menu [data-action="closeRight"]').click();
await page.waitForTimeout(300);
await expect(page.locator('.tab-item')).toHaveCount(1);
});
test('context menu Fermer tout → dashboard', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await page.locator('.tab-item').first().click({ button: 'right' });
await page.locator('#tab-context-menu [data-action="closeAll"]').click();
await page.waitForTimeout(400);
// closeAll hides the tab bar and shows dashboard (tabs list not re-rendered)
await expect(page.locator('#tab-bar')).toBeHidden();
await expect(page.locator('#dashboard-home')).toBeVisible();
});
test('closing last tab of a non-last pane auto-collapses pane (C4)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await splitVia(page);
await page.waitForTimeout(600);
await openInPane(page, 1, 'TestVault', 'note2.md');
const pane1 = page.locator('.pane-container').nth(1);
await expect(pane1.locator('.tab-item')).toHaveCount(1);
// close pane 1's only tab → pane itself auto-collapses
await pane1.locator('.tab-item').first().locator('.tab-close').click({ force: true });
await page.waitForTimeout(700);
await expect(page.locator('.pane-container')).toHaveCount(0);
// pane 0 file is still open in single mode
await expect(page.locator('#tab-list .tab-item')).toHaveCount(1);
});
test('closing pane 0 of 2 focuses neighbour then collapses correctly', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await splitVia(page);
await page.waitForTimeout(600);
// give pane 1 its own tab so the neighbour survives pane-0 closure
await openInPane(page, 1, 'TestVault', 'Accueil.md');
// activate pane 0 (both migrated tabs live there) then close it —
// documented C4 behaviour: tabs of the closed pane are LOST, neighbour survives
await page.locator('.pane-container').nth(0).click({ position: { x: 50, y: 200 } });
await page.waitForTimeout(200);
await page.keyboard.press('Control+Alt+w');
await page.waitForTimeout(700);
// collapsed back to single-pane grid with only the surviving Accueil tab
await expect(page.locator('.pane-container')).toHaveCount(0);
const tabs = page.locator('#tab-list .tab-item');
await expect(tabs).toHaveCount(1);
await expect(tabs.first()).toContainText('Accueil');
});
});
test.describe('Split View Matrix — Edge cases', () => {
test.beforeEach(async ({ page }) => { await login(page); });
test('PANE_MAX: split is capped at 4 panes', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
// grow to 4 through the API (fast, deterministic — the cap lives in splitRight/splitDown)
await page.evaluate(() => {
window.PaneManager.splitRight();
window.PaneManager.splitDown();
window.PaneManager.splitRight();
});
await page.waitForTimeout(800);
await expect(page.locator('.pane-container')).toHaveCount(4);
// 5th attempt must be a no-op
await page.evaluate(() => window.PaneManager.splitRight());
await page.waitForTimeout(400);
await expect(page.locator('.pane-container')).toHaveCount(4);
});
test('split down then Ctrl+Alt+Down navigates vertically', 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 });
await page.waitForTimeout(600);
await page.keyboard.press('Control+Alt+ArrowDown');
await page.waitForTimeout(300);
await expect(page.locator('.pane-container').nth(1)).toHaveClass(/pane-active/);
await page.keyboard.press('Control+Alt+ArrowUp');
await page.waitForTimeout(300);
await expect(page.locator('.pane-container').nth(0)).toHaveClass(/pane-active/);
});
test('drag-to-split BOTTOM zone creates vertical split (D3)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
const tabBox = await page.locator('.tab-item').first().boundingBox();
const areaBox = await page.locator('#content-area').boundingBox();
expect(tabBox && areaBox).toBeTruthy();
await page.mouse.move(tabBox.x + tabBox.width / 2, tabBox.y + tabBox.height / 2);
await page.mouse.down();
await page.mouse.move(
areaBox.x + areaBox.width / 2,
areaBox.y + areaBox.height * 0.9,
{ steps: 15 }
);
await page.waitForTimeout(300);
await page.mouse.up();
await page.waitForSelector('.pane-grid', { timeout: 3000 });
await expect(page.locator('.pane-container')).toHaveCount(2);
// vertical layout → gridTemplateRows defines 3 rows
const rows = await page.evaluate(() =>
document.querySelector('.pane-grid')?.style.gridTemplateRows || '');
expect(rows).toContain('1fr');
});
test('reopening an open file focuses existing tab — no duplicate (E4)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
// dblclick note1 again
await page.locator('.tree-item[data-path="note1.md"]').dblclick();
await page.waitForTimeout(400);
await expect(page.locator('.tab-item')).toHaveCount(2);
// it should be the active one
const active = page.locator('.tab-item.active');
await expect(active).toHaveCount(1);
await expect(active).toContainText('note1');
});
test('reset panes from palette fully collapses split (G3)', async ({ page }) => {
await openFile(page, 'TestVault', 'note1.md');
await openFile(page, 'TestVault', 'note2.md');
await splitVia(page);
await page.waitForTimeout(600);
await page.keyboard.press('Control+Shift+Space');
const input = page.locator('.cp-input');
await expect(input).toBeVisible({ timeout: 5000 });
await input.fill('>Réinitialiser les panneaux');
await page.waitForSelector('.cp-item', { timeout: 3000 });
await page.keyboard.press('Enter');
await page.waitForTimeout(600);
await expect(page.locator('.pane-container')).toHaveCount(0);
const saved = await page.evaluate(() => localStorage.getItem('obsigate-panes'));
expect(saved).toBeNull();
});
});