From e72d66ae641f50173fbc224f8cd1af1dfe0aff31 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 14 Feb 2026 18:54:27 +0100 Subject: [PATCH] Add Tauri mock layer for browser testing and visual verification workflow --- CLAUDE.md | 19 ++++ e2e/screenshot.spec.ts | 8 ++ src-tauri/Cargo.toml | 4 +- src/App.tsx | 46 +++++----- src/mock-tauri.ts | 196 +++++++++++++++++++++++++++++++++++++++++ src/types.ts | 14 +++ 6 files changed, 259 insertions(+), 28 deletions(-) create mode 100644 e2e/screenshot.spec.ts create mode 100644 src/mock-tauri.ts create mode 100644 src/types.ts diff --git a/CLAUDE.md b/CLAUDE.md index bfaaa359..e13f0e41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,25 @@ Laputa App is a personal knowledge and life management desktop app, built with T - If something is hacky or temporary, leave a `// TODO:` comment explaining why and what the real solution would be - Error handling: don't silently swallow errors. Log them, surface them, or return Result types (Rust) +### Visual Verification (MANDATORY) +Before declaring any milestone or feature complete, you MUST visually verify it works: + +1. **Start the dev server**: `pnpm dev` (Vite only, no Tauri needed) +2. **Run Playwright screenshot**: `npx playwright test e2e/screenshot.spec.ts` +3. **Check the screenshot** at `test-results/app-screenshot.png` — does it look right? Are notes showing? Is the layout correct? +4. **Interact and verify**: Write a Playwright test that clicks, navigates, and screenshots the result + +The app has a **Tauri mock layer** (`src/mock-tauri.ts`): when running in a browser (not Tauri), it returns realistic test data. This means Playwright and Chrome can test the full UI without the Rust backend. + +**Key rule**: passing unit tests ≠ working app. If you can't see it working in a screenshot, it's not done. + +### Playwright for Testing & Verification +- `npx playwright test` — runs all E2E tests +- `npx playwright test e2e/screenshot.spec.ts` — captures a screenshot for review +- You can write ad-hoc Playwright scripts to click elements, type, scroll, and screenshot +- Use `page.screenshot({ path: 'test-results/something.png' })` to capture state +- Always screenshot before AND after interactions to verify changes + ### When Stuck - Use Context7 MCP to look up current API docs (Tauri v2, CodeMirror 6, etc.) - If a dependency doesn't work as expected, check its version and docs before trying workarounds diff --git a/e2e/screenshot.spec.ts b/e2e/screenshot.spec.ts new file mode 100644 index 00000000..98d49cf4 --- /dev/null +++ b/e2e/screenshot.spec.ts @@ -0,0 +1,8 @@ +import { test } from '@playwright/test' + +test('capture app screenshot for review', async ({ page }) => { + await page.goto('/') + // Wait for mock data to load + await page.waitForTimeout(500) + await page.screenshot({ path: 'test-results/app-screenshot.png', fullPage: true }) +}) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dc967785..80b38d47 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,13 +13,13 @@ name = "laputa_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { version = "2.5.4" } +tauri-build = { version = "2.5.4", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.10.0" } +tauri = { version = "2.10.0", features = [] } tauri-plugin-log = "2" gray_matter = "0.2" walkdir = "2" diff --git a/src/App.tsx b/src/App.tsx index 3cb600fc..d8f3f901 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,25 +5,12 @@ import { NoteList } from './components/NoteList' import { Editor } from './components/Editor' import { Inspector } from './components/Inspector' import { ResizeHandle } from './components/ResizeHandle' +import { isTauri, mockInvoke } from './mock-tauri' +import type { VaultEntry } from './types' import './App.css' -interface VaultEntry { - path: string - filename: string - title: string - isA: string | null - aliases: string[] - belongsTo: string[] - relatedTo: string[] - status: string | null - owner: string | null - cadence: string | null - modifiedAt: number | null - fileSize: number -} - // TODO: Make vault path configurable via settings -const TEST_VAULT_PATH = '~/vault' +const TEST_VAULT_PATH = '~/Laputa' function App() { const [entries, setEntries] = useState([]) @@ -33,17 +20,24 @@ function App() { const [inspectorCollapsed, setInspectorCollapsed] = useState(false) useEffect(() => { - // Expand ~ to home directory for the test path - const path = TEST_VAULT_PATH.replace('~', '/Users/' + (import.meta.env.VITE_USER || 'user')) - - invoke('list_vault', { path }) - .then((result) => { - console.log(`Vault scan complete: ${result.length} entries found`, result) + const loadVault = async () => { + try { + let result: VaultEntry[] + if (isTauri()) { + const path = TEST_VAULT_PATH.replace('~', '/Users/luca') + result = await invoke('list_vault', { path }) + } else { + // Running in browser (not Tauri) — use mock data for visual testing + console.info('[mock] Using mock Tauri data for browser testing') + result = await mockInvoke('list_vault', {}) + } + console.log(`Vault scan complete: ${result.length} entries found`) setEntries(result) - }) - .catch((err) => { - console.warn('Vault scan failed (expected if no vault at path):', err) - }) + } catch (err) { + console.warn('Vault scan failed:', err) + } + } + loadVault() }, []) const handleSidebarResize = useCallback((delta: number) => { diff --git a/src/mock-tauri.ts b/src/mock-tauri.ts new file mode 100644 index 00000000..3ba72fd5 --- /dev/null +++ b/src/mock-tauri.ts @@ -0,0 +1,196 @@ +/** + * Mock Tauri invoke for browser testing. + * When running outside Tauri (e.g. in Chrome via localhost:5173), + * this provides realistic test data so the UI can be verified visually. + */ + +import type { VaultEntry } from './types' + +const MOCK_ENTRIES: VaultEntry[] = [ + { + path: '/Users/luca/Laputa/project/26q1-laputa-app.md', + filename: '26q1-laputa-app.md', + title: 'Build Laputa App', + isA: 'Project', + aliases: ['Laputa App'], + belongsTo: ['[[quarter/q1-2026]]'], + relatedTo: ['[[topic/software-development]]'], + status: 'Active', + owner: 'Luca Rossi', + cadence: null, + modifiedAt: Date.now() / 1000, + fileSize: 2048, + }, + { + path: '/Users/luca/Laputa/responsibility/grow-newsletter.md', + filename: 'grow-newsletter.md', + title: 'Grow Newsletter', + isA: 'Responsibility', + aliases: [], + belongsTo: [], + relatedTo: ['[[topic/growth]]'], + status: 'Active', + owner: 'Luca Rossi', + cadence: null, + modifiedAt: Date.now() / 1000 - 3600, + fileSize: 1024, + }, + { + path: '/Users/luca/Laputa/responsibility/manage-sponsorships.md', + filename: 'manage-sponsorships.md', + title: 'Manage Sponsorships', + isA: 'Responsibility', + aliases: [], + belongsTo: [], + relatedTo: [], + status: 'Active', + owner: 'Matteo Cellini', + cadence: null, + modifiedAt: Date.now() / 1000 - 7200, + fileSize: 890, + }, + { + path: '/Users/luca/Laputa/procedure/write-weekly-essays.md', + filename: 'write-weekly-essays.md', + title: 'Write Weekly Essays', + isA: 'Procedure', + aliases: [], + belongsTo: ['[[responsibility/grow-newsletter]]'], + relatedTo: [], + status: 'Active', + owner: 'Luca Rossi', + cadence: 'Weekly', + modifiedAt: Date.now() / 1000 - 86400, + fileSize: 512, + }, + { + path: '/Users/luca/Laputa/procedure/run-sponsorships.md', + filename: 'run-sponsorships.md', + title: 'Run Sponsorships', + isA: 'Procedure', + aliases: [], + belongsTo: ['[[responsibility/manage-sponsorships]]'], + relatedTo: [], + status: 'Active', + owner: 'Matteo Cellini', + cadence: 'Weekly', + modifiedAt: Date.now() / 1000 - 86400 * 2, + fileSize: 640, + }, + { + path: '/Users/luca/Laputa/experiment/stock-screener.md', + filename: 'stock-screener.md', + title: 'Stock Screener — EMA200 Wick Bounce', + isA: 'Experiment', + aliases: ['Trading Screener'], + belongsTo: [], + relatedTo: ['[[topic/trading]]', '[[topic/algorithmic-trading]]'], + status: 'Active', + owner: 'Luca Rossi', + cadence: null, + modifiedAt: Date.now() / 1000 - 86400, + fileSize: 3200, + }, + { + path: '/Users/luca/Laputa/note/facebook-ads-strategy.md', + filename: 'facebook-ads-strategy.md', + title: 'Facebook Ads Strategy', + isA: 'Note', + aliases: [], + belongsTo: ['[[project/26q1-laputa-app]]'], + relatedTo: ['[[topic/growth]]', '[[topic/ads]]'], + status: null, + owner: null, + cadence: null, + modifiedAt: Date.now() / 1000 - 3600 * 5, + fileSize: 847, + }, + { + path: '/Users/luca/Laputa/note/budget-allocation.md', + filename: 'budget-allocation.md', + title: 'Budget Allocation', + isA: 'Note', + aliases: [], + belongsTo: ['[[project/26q1-laputa-app]]'], + relatedTo: [], + status: null, + owner: null, + cadence: null, + modifiedAt: Date.now() / 1000 - 86400, + fileSize: 560, + }, + { + path: '/Users/luca/Laputa/person/matteo-cellini.md', + filename: 'matteo-cellini.md', + title: 'Matteo Cellini', + isA: 'Person', + aliases: ['Matteo'], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + modifiedAt: Date.now() / 1000 - 86400 * 7, + fileSize: 320, + }, + { + path: '/Users/luca/Laputa/event/2026-02-14-laputa-app-kickoff.md', + filename: '2026-02-14-laputa-app-kickoff.md', + title: 'Laputa App Design Session', + isA: 'Event', + aliases: [], + belongsTo: [], + relatedTo: ['[[project/26q1-laputa-app]]', '[[person/matteo-cellini]]'], + status: null, + owner: null, + cadence: null, + modifiedAt: Date.now() / 1000 - 3600 * 2, + fileSize: 1200, + }, + { + path: '/Users/luca/Laputa/topic/software-development.md', + filename: 'software-development.md', + title: 'Software Development', + isA: 'Topic', + aliases: ['Dev', 'Coding'], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + modifiedAt: Date.now() / 1000 - 86400 * 30, + fileSize: 256, + }, + { + path: '/Users/luca/Laputa/topic/trading.md', + filename: 'trading.md', + title: 'Trading', + isA: 'Topic', + aliases: ['Algorithmic Trading'], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + modifiedAt: Date.now() / 1000 - 86400 * 14, + fileSize: 180, + }, +] + +const mockHandlers: Record any> = { + list_vault: () => MOCK_ENTRIES, +} + +export function isTauri(): boolean { + return typeof window !== 'undefined' && '__TAURI__' in window +} + +export async function mockInvoke(cmd: string, args?: any): Promise { + const handler = mockHandlers[cmd] + if (handler) { + // Simulate async delay + await new Promise((r) => setTimeout(r, 100)) + return handler(args) as T + } + throw new Error(`No mock handler for command: ${cmd}`) +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..b82469c1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,14 @@ +export interface VaultEntry { + path: string + filename: string + title: string + isA: string | null + aliases: string[] + belongsTo: string[] + relatedTo: string[] + status: string | null + owner: string | null + cadence: string | null + modifiedAt: number | null + fileSize: number +}