test: I2 + I3 — 46 tests d'intégration + suite E2E Playwright
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)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "obsigate-e2e",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npx playwright test",
|
||||
"test:headed": "npx playwright test --headed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
timeout: 30000,
|
||||
expect: { timeout: 10000 },
|
||||
fullyParallel: false,
|
||||
retries: 1,
|
||||
workers: 1,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: process.env.OBSIGATE_URL || 'http://192.168.30.101:2020',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
+414
-116
@@ -1,196 +1,494 @@
|
||||
"""
|
||||
Tests for the Multi-Pane Split View (#75).
|
||||
Tests for the Multi-Pane Split View (#75) — I1 + I2 + I4 combined.
|
||||
|
||||
Since PaneManager is a frontend JS module, these tests verify:
|
||||
1. Backend integration: all file/sidebar/vault APIs still work (no regression)
|
||||
2. Static file serving: pane-manager.js is served correctly
|
||||
3. CSS: style.css includes pane grid classes
|
||||
4. HTML: index.html includes the content-wrapper structure needed for panes
|
||||
Covers:
|
||||
I1: Unit tests for PaneManager JS module structure
|
||||
I2: Integration tests (JS code patterns, DOM flow, data formats, CSS classes)
|
||||
I4: Backend regression tests
|
||||
"""
|
||||
import pytest
|
||||
import json
|
||||
import re
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestPaneManagerStaticAssets:
|
||||
"""Verify pane-manager.js, style.css, and index.html are properly structured."""
|
||||
# ============================================================================
|
||||
# I1 — Unit tests: JS module structure, exports, methods
|
||||
# ============================================================================
|
||||
|
||||
def test_pane_manager_js_exists(self):
|
||||
"""PaneManager module file is present."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
assert path.exists(), f"pane-manager.js not found at {path}"
|
||||
class TestPaneManagerModuleStructure:
|
||||
"""Validate pane-manager.js module exports, methods, and internal structure."""
|
||||
|
||||
def test_pane_manager_js_exports(self):
|
||||
"""PaneManager exports the expected API."""
|
||||
@pytest.fixture(autouse=True)
|
||||
def _load(self):
|
||||
self.path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
self.content = self.path.read_text()
|
||||
|
||||
def test_file_exists(self):
|
||||
assert self.path.exists()
|
||||
|
||||
def test_exports_default(self):
|
||||
assert "export default PaneManager" in self.content
|
||||
|
||||
def test_exposes_globals(self):
|
||||
assert "window.PaneManager = PaneManager" in self.content
|
||||
assert "window.getActiveTabManager = getActiveTabManager" in self.content
|
||||
assert "window.getActiveContentArea = getActiveContentArea" in self.content
|
||||
|
||||
def test_factory_function_exists(self):
|
||||
assert "function createPaneTabManager(" in self.content
|
||||
|
||||
def test_all_split_methods_present(self):
|
||||
for method in ["splitRight", "splitDown", "closePane", "closeActivePane",
|
||||
"closeOtherPanes", "isSplit", "getActiveContentArea",
|
||||
"getActiveTabManager", "transferTab", "setActivePane",
|
||||
"_buildGrid", "_createPane", "_addPane", "_collapseToSingle",
|
||||
"_setupPaneDropZone", "_persist"]:
|
||||
assert method + "(" in self.content or method + " (" in self.content, \
|
||||
f"Missing method: {method}"
|
||||
|
||||
def test_pane_tab_manager_methods(self):
|
||||
"""Factory creates objects with all required methods."""
|
||||
for method in ["openPreview", "openPersistent", "open", "activate",
|
||||
"close", "closeAll", "closeRight", "closeOthers",
|
||||
"moveTab", "transferTabTo", "_renderTabs",
|
||||
"_showTabContextMenu", "init", "getContentArea"]:
|
||||
assert method in self.content, f"Missing PaneTabManager method: {method}"
|
||||
|
||||
def test_max_panes_constant(self):
|
||||
assert "PANE_MAX = 4" in self.content or "PANE_MAX=4" in self.content
|
||||
|
||||
def test_brace_balance(self):
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert self.content.count(char) == self.content.count(close_map[char]), \
|
||||
f"Unbalanced {name}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# I2 — Integration tests: JS code patterns, data formats, DOM flow
|
||||
# ============================================================================
|
||||
|
||||
class TestPaneManagerDataFlow:
|
||||
"""Validate data format conventions, migration logic, and persistence."""
|
||||
|
||||
def test_tab_drag_payload_format(self):
|
||||
"""dragstart sets correct JSON payload on text/plain."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "export default PaneManager" in content
|
||||
assert "window.PaneManager = PaneManager" in content
|
||||
# Must use JSON.stringify for paneId/tabId/index
|
||||
assert 'JSON.stringify' in content
|
||||
assert 'paneId' in content
|
||||
assert 'tabId' in content
|
||||
|
||||
def test_pane_manager_has_split_methods(self):
|
||||
"""PaneManager has splitRight and splitDown methods."""
|
||||
def test_tree_drag_format(self):
|
||||
"""Tree drags use 'tree:' prefix to distinguish from tab drags."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "dragdrop.js"
|
||||
content = path.read_text()
|
||||
assert "tree:" in content
|
||||
|
||||
def test_singleton_dragstart_uses_json(self):
|
||||
"""Singleton TabManager dragstart uses JSON format, not plain index."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
# Should have JSON.stringify in dragstart
|
||||
dragstart_section = content.split('"dragstart"')[1].split('"dragend"')[0] \
|
||||
if '"dragstart"' in content else ""
|
||||
assert 'JSON.stringify' in dragstart_section or \
|
||||
'paneId' in dragstart_section, \
|
||||
"Singleton TabManager dragstart must use JSON format"
|
||||
|
||||
def test_singleton_drop_handles_json(self):
|
||||
"""Singleton TabManager drop handler parses JSON with fallback."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
assert 'JSON.parse' in content
|
||||
|
||||
def test_drop_handler_handles_tree_drags(self):
|
||||
"""Container drop handler checks for 'tree:' prefix."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "startsWith('tree:')" in content or '.startsWith("tree:")' in content
|
||||
|
||||
def test_drop_handler_handles_tab_drags(self):
|
||||
"""Container drop handler handles tab JSON payloads."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "'text/plain'" in content or '"text/plain"' in content
|
||||
|
||||
def test_migration_logic_exists(self):
|
||||
"""Tab migration singleton→PaneTabManager exists in _buildGrid."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "_singletonTabManager" in content
|
||||
assert "migrating" in content.lower()
|
||||
|
||||
def test_reverse_migration_exists(self):
|
||||
"""Tab migration PaneTabManager→singleton exists in _collapseToSingle."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
# _collapseToSingle should reference _singletonTabManager
|
||||
collapse_section = content.split("_collapseToSingle()")[1].split(" },")[0] \
|
||||
if "_collapseToSingle()" in content else ""
|
||||
assert "_singletonTabManager" in collapse_section or \
|
||||
"stm._tabs" in content, \
|
||||
"Reverse migration missing in _collapseToSingle"
|
||||
|
||||
def test_localstorage_key(self):
|
||||
"""Persistence uses 'obsigate-panes' key."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "obsigate-panes" in content
|
||||
|
||||
def test_localstorage_payload_structure(self):
|
||||
"""_persist saves layout, panes array with id/width/activeTab/tabs."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "layout" in content
|
||||
assert "panes" in content
|
||||
assert "obsigate-panes" in content
|
||||
|
||||
|
||||
class TestPaneManagerDOMFlow:
|
||||
"""Validate DOM creation, manipulation, and destruction patterns."""
|
||||
|
||||
def test_build_grid_creates_pane_containers(self):
|
||||
"""_buildGrid creates .pane-grid and .pane-container elements."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "pane-grid" in content
|
||||
assert "pane-container" in content
|
||||
|
||||
def test_create_pane_suffixes_ids(self):
|
||||
"""Pane IDs are suffixed: tab-bar-N, tab-list-N, content-area-N."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "tab-bar-' +" in content or 'tab-bar-" +' in content or \
|
||||
"`tab-bar-" in content
|
||||
assert "content-area-' +" in content or 'content-area-" +' in content or \
|
||||
"`content-area-" in content
|
||||
|
||||
def test_pane_zero_reuses_original_ids(self):
|
||||
"""Pane 0 reuses #tab-bar and #content-area (no suffix)."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "id === 0" in content or "i === 0" in content
|
||||
|
||||
def test_lazy_rendering_hides_inactive(self):
|
||||
"""setActivePane hides old pane content with display:none."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
active_section = content.split("setActivePane(paneId)")[1].split(" },")[0] \
|
||||
if "setActivePane(paneId)" in content else ""
|
||||
assert "display" in active_section.lower(), \
|
||||
"setActivePane must toggle display for lazy rendering"
|
||||
|
||||
def test_collapse_restores_original_dom(self):
|
||||
"""_collapseToSingle restores #tab-bar and #content-area IDs."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "_collapseToSingle" in content
|
||||
# Should reference tab-bar and content-area restoration
|
||||
assert "tab-bar" in content
|
||||
assert "content-area" in content
|
||||
|
||||
def test_empty_pane_closes_itself(self):
|
||||
"""When all tabs are closed in a non-last pane, the pane closes."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "closePane(this.paneId)" in content or \
|
||||
"closePane(" in content
|
||||
|
||||
def test_transfer_tab_empties_source(self):
|
||||
"""transferTabTo removes tab from source and adds to target."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
transfer_section = content.split("transferTabTo(")[1].split(" },")[0] \
|
||||
if "transferTabTo(" in content else ""
|
||||
assert "_tabs.splice" in content # removal
|
||||
assert "targetTm._tabs.push" in content # addition
|
||||
|
||||
|
||||
class TestPaneManagerKeyboardShortcuts:
|
||||
"""Validate all keyboard shortcuts are registered correctly."""
|
||||
|
||||
def test_split_right_shortcut(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
# Ctrl+Alt+\ → splitRight
|
||||
assert "splitRight()" in content
|
||||
assert "splitDown()" in content
|
||||
assert "closePane(" in content
|
||||
assert "closeActivePane()" in content
|
||||
assert "isSplit()" in content
|
||||
assert "getActiveContentArea()" in content
|
||||
|
||||
def test_pane_manager_has_keyboard_shortcuts(self):
|
||||
"""Keyboard shortcuts are registered."""
|
||||
def test_split_down_shortcut(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "ctrlKey" in content
|
||||
assert '"\\\\"' in content or "'\\\\'" in content # backslash key
|
||||
assert "splitDown()" in content
|
||||
|
||||
def test_style_css_has_pane_grid_classes(self):
|
||||
"""style.css includes pane grid CSS classes."""
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
def test_close_pane_shortcut(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert ".pane-grid" in content
|
||||
assert ".pane-container" in content
|
||||
assert ".pane-resize-handle" in content
|
||||
assert ".pane-active" in content
|
||||
assert "closeActivePane()" in content
|
||||
|
||||
def test_style_css_has_responsive_split(self):
|
||||
"""Responsive rules disable split below 768px."""
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
def test_close_other_panes_shortcut(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "@media (max-width: 768px)" in content
|
||||
assert ".pane-grid" in content
|
||||
assert "closeOtherPanes()" in content
|
||||
|
||||
def test_viewer_uses_pane_aware_helper(self):
|
||||
"""viewer.js has getContentArea() helper and uses it."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "viewer.js"
|
||||
def test_navigate_panes_shortcut(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "function getContentArea()" in content
|
||||
assert "window.PaneManager" in content
|
||||
# Should NOT have direct document.getElementById("content-area") except in fallback
|
||||
direct_refs = [l for l in content.split("\n")
|
||||
if 'document.getElementById("content-area")' in l
|
||||
and "fallback" not in l.lower()]
|
||||
# Only the fallback inside getContentArea should remain
|
||||
assert len(direct_refs) <= 2, f"Too many direct content-area refs: {direct_refs}"
|
||||
assert "ArrowRight" in content
|
||||
assert "ArrowLeft" in content
|
||||
|
||||
def test_ui_tab_manager_has_delegation(self):
|
||||
"""TabManager in ui.js delegates to getActiveTabManager() when split."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
def test_all_shortcuts_have_prevent_default(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "getActiveTabManager" in content
|
||||
assert "PaneManager.isSplit()" in content
|
||||
# Must have delegation in entry-point methods
|
||||
assert "window.getActiveTabManager().openPreview" in content
|
||||
assert "window.getActiveTabManager().openPersistent" in content
|
||||
assert "window.getActiveTabManager().close(" in content
|
||||
shortcut_section = content.split("// Keyboard shortcuts")[1].split("// Init")[0] \
|
||||
if "// Keyboard shortcuts" in content else ""
|
||||
prevent_count = shortcut_section.count("preventDefault()")
|
||||
assert prevent_count >= 4, f"Expected >=4 preventDefault calls, got {prevent_count}"
|
||||
|
||||
def test_ui_context_menu_has_split_actions(self):
|
||||
"""Tab context menu includes 'Diviser à droite' and 'Diviser en bas'."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
|
||||
class TestPaneManagerContextMenu:
|
||||
"""Validate context menu structure and actions."""
|
||||
|
||||
def test_menu_has_split_actions(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "Diviser à droite" in content
|
||||
assert "Diviser en bas" in content
|
||||
assert "Fermer le panneau" in content
|
||||
assert 'data-action="splitRight"' in content
|
||||
assert 'data-action="splitDown"' in content
|
||||
|
||||
def test_html_has_content_wrapper(self):
|
||||
"""index.html has the content-wrapper structure needed for panes."""
|
||||
def test_menu_has_move_to_submenu(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "Déplacer vers" in content
|
||||
|
||||
def test_menu_has_close_actions(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "Fermer</span>" in content or "Fermer" in content
|
||||
assert "Fermer les autres" in content
|
||||
assert "Fermer à droite" in content
|
||||
assert "Fermer tout" in content
|
||||
|
||||
def test_ui_singleton_menu_has_split_actions(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
assert "Diviser à droite" in content
|
||||
assert "Diviser en bas" in content
|
||||
|
||||
|
||||
class TestPaneManagerCSS:
|
||||
"""Validate CSS classes used by the pane system."""
|
||||
|
||||
def test_pane_grid_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert ".pane-grid" in content
|
||||
assert "display: grid" in content
|
||||
|
||||
def test_pane_container_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert ".pane-container" in content
|
||||
assert "position: relative" in content # needed for drop indicators
|
||||
|
||||
def test_resize_handle_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert ".pane-resize-handle" in content
|
||||
# Cursor (col-resize/row-resize) is set in JS, not CSS
|
||||
js = (PROJECT_ROOT / "frontend" / "js" / "pane-manager.js").read_text()
|
||||
assert "col-resize" in js or "row-resize" in js
|
||||
|
||||
def test_drop_zone_indicator_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert "pane-drop-zone-indicator" in content
|
||||
|
||||
def test_tab_split_buttons_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert ".tab-split-buttons" in content
|
||||
assert ".tab-split-btn" in content
|
||||
|
||||
def test_drop_target_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert ".tab-bar.drop-target" in content
|
||||
|
||||
def test_responsive_css(self):
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert "@media (max-width: 768px)" in content
|
||||
# Should hide non-first panes on mobile
|
||||
assert "not(:first-child)" in content
|
||||
|
||||
|
||||
class TestPaneManagerHTML:
|
||||
"""Validate HTML structure for pane support."""
|
||||
|
||||
def test_content_wrapper_exists(self):
|
||||
path = PROJECT_ROOT / "frontend" / "index.html"
|
||||
content = path.read_text()
|
||||
assert 'class="content-wrapper"' in content
|
||||
assert 'id="content-area"' in content
|
||||
|
||||
def test_tab_bar_exists(self):
|
||||
path = PROJECT_ROOT / "frontend" / "index.html"
|
||||
content = path.read_text()
|
||||
assert 'id="tab-bar"' in content
|
||||
assert 'id="tab-list"' in content
|
||||
|
||||
def test_html_uses_es_modules(self):
|
||||
"""index.html uses ES module scripts for the frontend modules."""
|
||||
def test_content_area_exists(self):
|
||||
path = PROJECT_ROOT / "frontend" / "index.html"
|
||||
content = path.read_text()
|
||||
assert 'id="content-area"' in content
|
||||
|
||||
def test_es_modules_loaded(self):
|
||||
path = PROJECT_ROOT / "frontend" / "index.html"
|
||||
content = path.read_text()
|
||||
# Should load the ES module app.js orchestrator
|
||||
assert 'type="module"' in content
|
||||
|
||||
def test_brace_balance_pane_manager(self):
|
||||
"""PaneManager JS has balanced braces, parens, brackets."""
|
||||
def test_pane_manager_imported_in_app(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "app.js"
|
||||
content = path.read_text()
|
||||
assert "pane-manager.js" in content or "PaneManager" in content
|
||||
|
||||
|
||||
class TestPaneManagerPalette:
|
||||
"""Validate command palette integration."""
|
||||
|
||||
def test_palette_has_split_commands(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "palette.js"
|
||||
content = path.read_text()
|
||||
assert "Diviser à droite" in content
|
||||
assert "Diviser en bas" in content
|
||||
assert "Fermer le panneau actif" in content
|
||||
assert "Panneau suivant" in content
|
||||
assert "Panneau précédent" in content
|
||||
|
||||
def test_palette_has_reset_command(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "palette.js"
|
||||
content = path.read_text()
|
||||
assert "Réinitialiser les panneaux" in content
|
||||
|
||||
def test_palette_category_panneaux(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "palette.js"
|
||||
content = path.read_text()
|
||||
assert "cat:'Panneaux'" in content or 'cat:"Panneaux"' in content
|
||||
|
||||
|
||||
class TestEditorLock:
|
||||
"""Validate editor lock prevents duplicate file in multiple panes."""
|
||||
|
||||
def test_editor_lock_check_exists(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert content.count(char) == content.count(close_map[char]), \
|
||||
f"Unbalanced {name}: {content.count(char)} opens vs {content.count(close_map[char])} closes"
|
||||
# The 'open' method should check other panes for same file
|
||||
open_section = content.split("async open(vault, path)")[1].split(" async")[0] \
|
||||
if "async open(vault, path)" in content else content
|
||||
assert "tabManagers" in open_section or \
|
||||
"isSplit()" in content, \
|
||||
"Editor lock check missing in PaneTabManager.open()"
|
||||
|
||||
def test_brace_balance_viewer(self):
|
||||
"""viewer.js has balanced braces, parens, brackets."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "viewer.js"
|
||||
def test_editor_lock_focuses_existing(self):
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert content.count(char) == content.count(close_map[char]), \
|
||||
f"Unbalanced {name}: {content.count(char)} opens vs {content.count(close_map[char])} closes"
|
||||
assert "setActivePane" in content
|
||||
assert "focus" in content.lower() or "activate" in content.lower()
|
||||
|
||||
def test_brace_balance_ui(self):
|
||||
"""ui.js has balanced braces, parens, brackets."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert content.count(char) == content.count(close_map[char]), \
|
||||
f"Unbalanced {name}: {content.count(char)} opens vs {content.count(close_map[char])} closes"
|
||||
|
||||
# ============================================================================
|
||||
# I4 — Backend regression tests
|
||||
# ============================================================================
|
||||
|
||||
class TestBackendRegression:
|
||||
"""Verify backend APIs still work with frontend changes."""
|
||||
"""Verify all backend APIs still work after split view changes."""
|
||||
|
||||
def test_health_endpoint(self, client):
|
||||
resp = client.get("/api/health")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "ok"
|
||||
|
||||
def test_file_view_endpoint(self, client):
|
||||
"""GET /api/file/{vault} still returns expected fields."""
|
||||
resp = client.get("/api/file/TestVault", params={"path": "note1.md"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "title" in data or "path" in data
|
||||
|
||||
def test_vault_list_endpoint(self, client):
|
||||
"""GET /api/vaults still works."""
|
||||
def test_vault_list(self, client):
|
||||
resp = client.get("/api/vaults")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert isinstance(resp.json(), list)
|
||||
|
||||
def test_search_endpoint(self, client):
|
||||
"""GET /api/search still works."""
|
||||
def test_search_works(self, client):
|
||||
resp = client.get("/api/search?q=test")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "results" in data
|
||||
assert "results" in resp.json()
|
||||
|
||||
def test_sidebar_tree_endpoint(self, client):
|
||||
"""GET /api/sidebar/tree/{vault} still works."""
|
||||
resp = client.get("/api/sidebar/tree/vault1")
|
||||
def test_sidebar_tree(self, client):
|
||||
resp = client.get("/api/sidebar/tree/TestVault")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_static_css_served(self, client):
|
||||
"""style.css is served correctly."""
|
||||
def test_bookmarks_endpoint(self, client):
|
||||
resp = client.get("/api/bookmarks")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_recent_files_endpoint(self, client):
|
||||
resp = client.get("/api/recent")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_static_css(self, client):
|
||||
resp = client.get("/static/style.css")
|
||||
assert resp.status_code == 200
|
||||
assert "text/css" in resp.headers.get("content-type", "")
|
||||
|
||||
def test_static_js_served(self, client):
|
||||
"""app.js (ES module) is served correctly."""
|
||||
def test_static_js_app(self, client):
|
||||
resp = client.get("/static/js/app.js")
|
||||
assert resp.status_code == 200
|
||||
assert "javascript" in resp.headers.get("content-type", "").lower()
|
||||
|
||||
def test_pane_manager_js_served(self, client):
|
||||
"""pane-manager.js is served as static asset."""
|
||||
def test_static_js_pane_manager(self, client):
|
||||
resp = client.get("/static/js/pane-manager.js")
|
||||
assert resp.status_code == 200
|
||||
assert "javascript" in resp.headers.get("content-type", "").lower()
|
||||
assert "PaneManager" in resp.text
|
||||
assert len(resp.text) > 30000 # should be substantial
|
||||
|
||||
def test_index_html_served(self, client):
|
||||
"""index.html is served and contains content-wrapper."""
|
||||
def test_static_js_viewer(self, client):
|
||||
resp = client.get("/static/js/viewer.js")
|
||||
assert resp.status_code == 200
|
||||
assert "getContentArea" in resp.text
|
||||
|
||||
def test_static_js_ui(self, client):
|
||||
resp = client.get("/static/js/ui.js")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_static_js_palette(self, client):
|
||||
resp = client.get("/static/js/palette.js")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_static_js_dragdrop(self, client):
|
||||
resp = client.get("/static/js/dragdrop.js")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_index_html(self, client):
|
||||
resp = client.get("/")
|
||||
# May redirect or return 200 depending on auth
|
||||
assert resp.status_code in (200, 302, 401)
|
||||
|
||||
|
||||
class TestDashboardRegression:
|
||||
"""Verify dashboard widgets (bookmarks, recent) use TabManager."""
|
||||
|
||||
def test_dashboard_uses_tabmanager_not_openfile(self, client):
|
||||
"""Dashboard must use TabManager.openPreview, not openFile directly."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "dashboard.js"
|
||||
content = path.read_text()
|
||||
# openFile should NOT be called directly from dashboard click handlers
|
||||
card_click = content.split("card.addEventListener")[1].split("});")[0] \
|
||||
if "card.addEventListener" in content else ""
|
||||
assert "openFile" not in card_click, \
|
||||
"Dashboard must NOT call openFile() directly — use TabManager.openPreview() instead"
|
||||
assert "TabManager.openPreview" in card_click, \
|
||||
"Dashboard must use TabManager.openPreview() for file clicks"
|
||||
|
||||
Reference in New Issue
Block a user