Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';import { tmpdir } from 'node:os';import { join } from 'node:path';import { _electron as electron, type ElectronApplication, type Page,} from '@playwright/test';
const repoRoot = join(__dirname, '..');
const VAULT_ID = 'e2e';
const filler = (label: string, lines = 40) => Array.from({ length: lines }, (_, i) => `${label} body line ${i + 1}.`).join( '\n', );
export const NOTES: Record<string, string> = { 'Target.md': [ '# Target', filler('intro'), '', '## Middle Section', filler('middle'), '', '## Later Section', filler('later'), ].join('\n'), 'Source.md': [ '# Source', '', 'A plain link to [[Target]].', '', 'An anchored link to [[Target#Middle Section]].', '', filler('source'), ].join('\n'), 'Other.md': ['# Other', '', filler('other')].join('\n'),};
/** Not `firstWindow()`: DevTools is a window too, and driving it fails silently. */async function appWindow(app: ElectronApplication): Promise<Page> { const deadline = Date.now() + 30_000; while (Date.now() < deadline) { const candidate = app .windows() .find((w) => !w.url().startsWith('devtools://')); if (candidate) return candidate; await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error('Timed out waiting for a non-DevTools application window');}
export type LaunchedApp = { app: ElectronApplication; window: Page; vaultDir: string; close: () => Promise<void>;};
/** * Launch against a throwaway vault. The registry reads `localStorage` once at * load, so seed-then-reload beats driving the vault dialog. */export async function launchApp(): Promise<LaunchedApp> { const vaultDir = await mkdtemp(join(tmpdir(), 'textile-e2e-vault-')); const userDataDir = await mkdtemp(join(tmpdir(), 'textile-e2e-userdata-')); await mkdir(vaultDir, { recursive: true }); for (const [name, body] of Object.entries(NOTES)) { await writeFile(join(vaultDir, name), body, 'utf-8'); }
// Inherited, this runs the binary as plain Node: no `electron.app`. const { ELECTRON_RUN_AS_NODE: _ignored, ...parentEnv } = process.env;
const app = await electron.launch({ args: ['.'], cwd: repoRoot, env: { ...parentEnv, TEXTILE_USER_DATA: userDataDir }, });
const window = await appWindow(app); await window.waitForLoadState('domcontentloaded');
// CM renders only its viewport, so a sliver of a window hides the document. await app.evaluate(({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; if (!win) return; win.webContents.closeDevTools(); win.setBounds({ x: 0, y: 0, width: 1400, height: 900 }); });
await window.evaluate( ({ dir, id }) => { localStorage.setItem( 'textile.openVaults', JSON.stringify([{ state: 'local', id, name: 'E2E Vault', path: dir }]), ); localStorage.setItem('textile.selectedVault', `vault-${id}`); }, { dir: vaultDir, id: VAULT_ID }, ); await window.reload(); await window.waitForLoadState('domcontentloaded');
await window.getByText('Target.md', { exact: true }).first().waitFor();
return { app, window, vaultDir, close: async () => { // The app outlives its last window on macOS, so `close()` alone can hang. const proc = app.process(); await Promise.race([ app.close().catch(() => {}), new Promise((resolve) => setTimeout(resolve, 5_000)), ]); try { if (proc.exitCode === null && proc.signalCode === null) { proc.kill('SIGKILL'); } } catch { // Already gone. } await rm(vaultDir, { recursive: true, force: true }); await rm(userDataDir, { recursive: true, force: true }); }, };}
/** * Reset between tests. Specs share one app because quitting Electron often * hangs mid-exit on macOS. Restoring the notes matters: the editor persists * edits, so a test that types corrupts the vault for later ones. */export async function resetWorkspace(app: LaunchedApp): Promise<void> { for (const [name, body] of Object.entries(NOTES)) { await writeFile(join(app.vaultDir, name), body, 'utf-8'); } await app.window.evaluate(() => { for (const key of Object.keys(localStorage)) { if (key.startsWith('textile.tiling.')) localStorage.removeItem(key); } }); await app.window.reload(); await app.window.waitForLoadState('domcontentloaded'); await app.window.getByText('Target.md', { exact: true }).first().waitFor();}
export async function openNote(window: Page, fileName: string): Promise<void> { await window.getByText(fileName, { exact: true }).first().click(); await window.locator('.cm-content').first().waitFor();}
export function scroller(window: Page) { return window.locator('.cm-scroller').first();}
export const scrollTop = (window: Page) => scroller(window).evaluate((el) => el.scrollTop);