Add Tauri mock layer for browser testing and visual verification workflow

This commit is contained in:
lucaronin
2026-02-14 18:54:27 +01:00
parent bc75647c82
commit e72d66ae64
6 changed files with 259 additions and 28 deletions

View File

@@ -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

8
e2e/screenshot.spec.ts Normal file
View File

@@ -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 })
})

View File

@@ -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"

View File

@@ -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<VaultEntry[]>([])
@@ -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<VaultEntry[]>('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<VaultEntry[]>('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<VaultEntry[]>('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) => {

196
src/mock-tauri.ts Normal file
View File

@@ -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<string, (args: any) => any> = {
list_vault: () => MOCK_ENTRIES,
}
export function isTauri(): boolean {
return typeof window !== 'undefined' && '__TAURI__' in window
}
export async function mockInvoke<T>(cmd: string, args?: any): Promise<T> {
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}`)
}

14
src/types.ts Normal file
View File

@@ -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
}