/** * E2E tests for ObsiGate Excalidraw Support (#78). * * Run (local): * BASE_URL=http://localhost:2029 npx playwright test tests/e2e/excalidraw.spec.js * BASE_URL=http://localhost:2021 npx playwright test tests/e2e/excalidraw.spec.js --headed */ import { test, expect } from '@playwright/test'; const BASE = process.env.BASE_URL || 'http://localhost:2029'; const CREDS = { username: process.env.OBSIGATE_USER || 'admin', password: process.env.OBSIGATE_PASS || 'test123', }; async function login(page) { await page.goto(BASE); 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'); } // Wait until app init() resolved (all handlers bound). await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 }); } async function openFile(page, vault, filePath) { 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}"]`).first(); if (await vaultItem.count() > 0) { await vaultItem.click(); await page.waitForTimeout(800); } } await treeItem.dblclick({ timeout: 5000 }); await page.waitForTimeout(500); } async function waitForExcalidrawIframe(page) { await page.waitForSelector('.content-area iframe', { timeout: 15000 }); const iframe = page.locator('.content-area iframe').first(); await expect(iframe).toBeVisible({ timeout: 10000 }); // Give React/Excalidraw time to mount (loads React + Excalidraw from esm.sh) await page.waitForTimeout(2500); 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); await expect(iframe).toHaveAttribute('src', /excalidraw-editor\.html/); // Excalidraw mounts two layers (static + interactive) — target the first. const canvas = page.locator('.content-area iframe').contentFrame().locator('canvas').first(); await expect(canvas).toBeVisible({ timeout: 20000 }); }); test('opens an Excalidraw-app export (collaborators as JSON object) — canvas renders', async ({ page }) => { // Regression: files exported by the Excalidraw app / Obsidian plugin store // `appState.collaborators` as a plain `{}` (a Map serialized to JSON). // Passing it back to `initialData` made Excalidraw 0.18 crash with // "e.appState.collaborators.forEach is not a function" and left the canvas // blank (0 in the iframe). await openFile(page, 'TestVault', 'diagram-app-export.excalidraw'); await waitForExcalidrawIframe(page); const frame = page.locator('.content-area iframe').contentFrame(); const canvas = frame.locator('canvas').first(); await expect(canvas).toBeVisible({ timeout: 20000 }); }); test('loads Excalidraw stylesheet — canvas is sized to the viewport (not the 2^25 cap)', async ({ page }) => { // Regression: without `@excalidraw/excalidraw`'s stylesheet, `.excalidraw` // has no fixed height; Excalidraw's ResizeObserver feedback loop then grows // the canvas to its 2^25 hard cap (33554432px), which the browser cannot // draw → the scene stays blank even though the data was loaded. await openFile(page, 'TestVault', 'diagram.excalidraw'); await waitForExcalidrawIframe(page); const frame = page.locator('.content-area iframe').contentFrame(); const canvas = frame.locator('canvas').first(); await expect(canvas).toBeVisible({ timeout: 20000 }); const dims = await canvas.evaluate((el) => ({ w: el.width, h: el.height })); expect(dims.h).toBeGreaterThan(100); expect(dims.h).toBeLessThan(32768); const wrapH = await frame.locator('.excalidraw').first() .evaluate((el) => parseFloat(getComputedStyle(el).height)); expect(wrapH).toBeLessThan(32768); }); test('opens .excalidraw.md (Obsidian plugin format) — decompresses and renders', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw.md'); const iframe = await waitForExcalidrawIframe(page); await expect(iframe).toHaveAttribute('src', /excalidraw-editor\.html/); const canvas = page.locator('.content-area iframe').contentFrame().locator('canvas').first(); await expect(canvas).toBeVisible({ timeout: 20000 }); }); test('theme toggle propagates to iframe without crash', async ({ page }) => { await openFile(page, 'TestVault', 'diagram.excalidraw'); await waitForExcalidrawIframe(page); const themeToggle = page.locator('#theme-toggle').first(); if (await themeToggle.isVisible()) { await themeToggle.click(); await page.waitForTimeout(500); await themeToggle.click(); } // No crash = pass; iframe still present await expect(page.locator('.content-area iframe').first()).toBeVisible(); }); }); 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 directory context menu (C8)', async ({ page }) => { // Expand the vault so a directory node is visible. const vaultItem = page.locator('.vault-item[data-vault="TestVault"]').first(); await vaultItem.click(); await page.waitForTimeout(800); // Right-click a directory (context menu offers "Nouveau diagramme Excalidraw"). const dirItem = page.locator('.tree-item[data-vault="TestVault"][data-path="IT"]').first(); await dirItem.click({ button: 'right' }); await page.waitForSelector('#context-menu.active', { timeout: 5000 }); const excalidrawMenu = page.locator('#context-menu .context-menu-item', { hasText: 'Nouveau diagramme Excalidraw' }); await expect(excalidrawMenu).toBeVisible(); await excalidrawMenu.click(); // Modal opens with .excalidraw pre-selected. await page.waitForSelector('.obsigate-modal-overlay.active', { timeout: 5000 }); const extSelect = page.locator('#file-ext-select'); await expect(extSelect).toHaveValue('.excalidraw'); const unique = `e2e-diagram-${Date.now()}`; await page.fill('#file-name-input', unique); await page.click('#file-create-btn'); // New file opens in the Excalidraw editor. 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 }) => { const searchInput = page.locator('#search-input').first(); await searchInput.fill('diagram'); await searchInput.press('Enter'); await page.waitForSelector('.search-result-item, .search-results-header', { timeout: 10000 }); const results = page.locator('.search-result-item'); expect(await results.count()).toBeGreaterThan(0); }); });