test(e2e): #75 I3 — matrice ouverture/fermeture split view (21 tests) + fix chemins Windows run-e2e-local.sh
This commit is contained in:
+10
-1
@@ -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
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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=$!
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user