Add Tauri mock layer for browser testing and visual verification workflow
This commit is contained in:
19
CLAUDE.md
19
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
|
||||
|
||||
8
e2e/screenshot.spec.ts
Normal file
8
e2e/screenshot.spec.ts
Normal 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 })
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
46
src/App.tsx
46
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<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
196
src/mock-tauri.ts
Normal 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
14
src/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user