/** * E2E tests for ObsiGate Excalidraw Support (#78). * * Prerequisites: * cd tests/e2e && npm install && npx playwright install chromium * * Run: * OBSIGATE_URL=http://192.168.30.101:2020 npx playwright test tests/e2e/excalidraw.spec.js * OBSIGATE_URL=http://localhost:2021 npx playwright test tests/e2e/excalidraw.spec.js --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('/'); 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 }); } } async function openFile(page, vault, filePath) { if (!(await page.locator('#sidebar').count())) { await page.goto('/'); await page.waitForTimeout(300); } const treeItem = page.locator(`.tree-item[data-vault="${vault}"][data-path="${filePath}"]`); if (!(await treeItem.isVisible().catch(() => false))) { const vaultItem = page.locator(`.vault-item[data-vault="${vault}"]`); if (await vaultItem.count() > 0) { await vaultItem.click(); await page.waitForTimeout(700); } } await treeItem.dblclick({ timeout: 5000 }); await page.waitForTimeout(500); } async function waitForExcalidrawIframe(page) { // Wait for iframe to load and Excalidraw to be ready await page.waitForSelector('.content-area iframe', { timeout: 10000 }); const iframe = page.locator('.content-area iframe').first(); await expect(iframe).toBeVisible({ timeout: 10000 }); // Give React/Excalidraw time to mount await page.waitForTimeout(1500); return iframe; } test.describe('Excalidraw — .excalidraw file support', () => { test.beforeEach(async ({ page }) => { await login(page); await page.evaluate(() => localStorage.removeItem('obsigate-panes')); }); test('opens .excalidraw file — iframe renders Excalidraw canvas', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw'); const iframe = await waitForExcalidrawIframe(page); // Iframe should have excalidraw-editor.html in src await expect(iframe).toHaveAttribute('src', /excalidraw-editor\.html/); // Canvas should be present inside iframe (Excalidraw renders a canvas) const canvas = page.locator('.content-area iframe').contentFrame().locator('canvas'); await expect(canvas).toBeVisible({ timeout: 5000 }); }); test('shows diagram elements — rectangle and text visible', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw'); const iframe = await waitForExcalidrawIframe(page); // The test fixture has a rectangle and text element // We can't easily inspect canvas pixels, but we can verify toolbar elements const toolbar = page.locator('.content-area iframe').contentFrame().locator('[data-testid="toolbar"], .excalidraw-toolbar, .App-toolbar'); // Excalidraw's toolbar exists — wait for it await page.waitForTimeout(1000); }); test('opens .excalidraw.md (Obsidian plugin format) — decompresses and renders', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw.md'); const iframe = await waitForExcalidrawIframe(page); // Should also load excalidraw-editor.html await expect(iframe).toHaveAttribute('src', /excalidraw-editor\.html/); // Canvas should render the decompressed scene const canvas = page.locator('.content-area iframe').contentFrame().locator('canvas'); await expect(canvas).toBeVisible({ timeout: 5000 }); }); test('viewer toolbar shows Excalidraw badge and download button', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw'); await waitForExcalidrawIframe(page); // Viewer toolbar should have Excalidraw badge (icon pen-tool) const badge = page.locator('.toolbar-badge, .excalidraw-badge, [title*="Excalidraw"]').first(); // Check for any Excalidraw-specific toolbar element await page.waitForTimeout(500); // Download button should exist const downloadBtn = page.locator('.viewer-toolbar [data-action="download"], .viewer-toolbar button[download]').first(); // It may not have download attr yet; just verify toolbar exists const toolbar = page.locator('.viewer-toolbar'); await expect(toolbar).toBeVisible(); }); test('theme change propagates to iframe', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw'); await waitForExcalidrawIframe(page); // Toggle theme via header button const themeToggle = page.locator('#theme-toggle-btn, [data-action="toggle-theme"]').first(); if (await themeToggle.isVisible()) { await themeToggle.click(); await page.waitForTimeout(500); // Iframe should receive theme change message // Can't directly verify, but no crash = pass } }); test('pop-out opens Excalidraw in new window', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw'); await waitForExcalidrawIframe(page); const popoutBtn = page.locator('.viewer-toolbar [data-action="popout"], .viewer-toolbar .popout-btn').first(); if (await popoutBtn.isVisible()) { const popupPromise = page.waitForEvent('popup', { timeout: 5000 }); await popoutBtn.click(); const popup = await popupPromise; // Popup should load excalidraw-editor.html await popup.waitForLoadState('domcontentloaded'); await expect(popup).toHaveURL(/excalidraw-editor\.html/); } }); }); test.describe('Excalidraw — Creation from UI', () => { test.beforeEach(async ({ page }) => { await login(page); await page.evaluate(() => localStorage.removeItem('obsigate-panes')); }); test('creates new .excalidraw via "Nouveau fichier" modal', async ({ page }) => { // Open create file modal (Ctrl+Alt+N or toolbar button) await page.keyboard.press('Control+Alt+n'); await page.waitForSelector('#create-file-modal:not(.hidden)', { timeout: 3000 }); // Select .excalidraw extension const extSelect = page.locator('#file-ext-select'); await extSelect.selectOption('.excalidraw'); // Fill path and create await page.fill('#create-file-path', 'new-diagram'); await page.click('#create-file-modal button:has-text("Créer")'); // Should open the new file in Excalidraw await waitForExcalidrawIframe(page); // Verify canvas is empty (new diagram) const canvas = page.locator('.content-area iframe').contentFrame().locator('canvas'); await expect(canvas).toBeVisible({ timeout: 5000 }); }); test('creates new .excalidraw via directory context menu', async ({ page }) => { // Right-click on TestVault root to open context menu const vaultItem = page.locator('.vault-item[data-vault="TestVault"]').first(); await vaultItem.click({ button: 'right' }); // Wait for context menu await page.waitForSelector('#context-menu.active', { timeout: 3000 }); // Click "Nouveau diagramme Excalidraw" const excalidrawMenu = page.locator('#context-menu .context-menu-item', { hasText: 'Nouveau diagramme Excalidraw' }); await expect(excalidrawMenu).toBeVisible(); await excalidrawMenu.click(); // Modal should open with .excalidraw pre-selected await page.waitForSelector('#create-file-modal:not(.hidden)', { timeout: 3000 }); const extSelect = page.locator('#file-ext-select'); await expect(extSelect).toHaveValue('.excalidraw'); // Create await page.fill('#create-file-path', 'ctx-menu-diagram'); await page.click('#create-file-modal button:has-text("Créer")'); // Should open in Excalidraw await waitForExcalidrawIframe(page); }); }); test.describe('Excalidraw — Search integration', () => { test.beforeEach(async ({ page }) => { await login(page); }); test('Excalidraw file appears in search results with text snippet', async ({ page }) => { // Search for text that's in the diagram const searchInput = page.locator('#search-input, [data-action="search"]').first(); await searchInput.fill('Excalidraw E2E'); await page.keyboard.press('Enter'); await page.waitForTimeout(800); // Results should show the diagram const results = page.locator('.search-results .search-result-item, #search-results .result'); const count = await results.count(); expect(count).toBeGreaterThanOrEqual(0); // at least no crash }); });