import { chromium, type Browser, type Page } from "@playwright/test"; import { spawn, type ChildProcess } from "node:child_process"; const OBSIDIAN_BIN = process.env.OBSIDIAN_BIN; const PORT = Number(process.env.OBSIDIAN_CDP_PORT ?? 9222); export interface ObsidianHandle { proc: ChildProcess; browser: Browser; page: Page; close: () => Promise; } async function waitForCDP(): Promise { for (let i = 0; i < 60; i++) { try { const r = await fetch(`http://127.0.0.1:${PORT}/json/version`); if (r.ok) return; } catch { // not up yet } await new Promise((res) => setTimeout(res, 1000)); } throw new Error("Obsidian CDP endpoint never came up"); } /** Find the renderer page that owns the Obsidian `app` and has finished layout. */ async function findReadyWindow(browser: Browser): Promise { for (let i = 0; i < 60; i++) { for (const ctx of browser.contexts()) { for (const p of ctx.pages()) { try { const ready = await p.evaluate( () => (window as any).app?.workspace?.layoutReady === true ); if (ready) return p; } catch { // page navigating } } } await new Promise((res) => setTimeout(res, 1000)); } throw new Error("No Obsidian window became ready"); } /** * Close any open Obsidian modal (first-run/update/confirmation dialogs the test * environment may pop up) so they don't intercept pointer events. */ async function dismissModals(page: Page): Promise { await page.evaluate(() => { document.querySelectorAll(".modal-container").forEach((m) => { (m.querySelector(".modal-close-button"))?.click(); m.remove(); }); document.querySelectorAll(".modal-bg").forEach((b) => b.remove()); }); } /** Boot the real Obsidian binary, connect over CDP. Returns a handle with .page and .close(). */ export async function launchObsidian(): Promise { if (!OBSIDIAN_BIN) { throw new Error("OBSIDIAN_BIN env var is required (set by scripts/e2e.sh)"); } const proc = spawn( OBSIDIAN_BIN, [`--remote-debugging-port=${PORT}`, "--no-sandbox", "--disable-gpu"], { env: process.env, stdio: "ignore" } ); await waitForCDP(); const browser = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`); const page = await findReadyWindow(browser); const close = async () => { await browser.close(); proc.kill("SIGKILL"); }; return { proc, browser, page, close }; } /** * Dismiss modals, enable the plugin, and wait for it to be loaded. * Call this after launchObsidian() and before assertions. */ export async function withPlugin(obs: ObsidianHandle, pluginId: string): Promise { await dismissModals(obs.page); // A fresh vault boots in Restricted Mode, which blocks community plugins. // Disable it and load our plugin via Obsidian's own API, then wait for it. await obs.page.evaluate(async (id) => { const plugins = (window as any).app.plugins; if (plugins.setEnable) await plugins.setEnable(true); await plugins.enablePlugin(id); }, pluginId); await obs.page.waitForFunction( (id) => !!(window as any).app?.plugins?.plugins?.[id], pluginId, { timeout: 30_000 } ); }