From e2c324074e3b15302df158c8cc9247f806e3d8d4 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 17 Jul 2026 15:12:43 -0500 Subject: [PATCH] build/test: establish a v1 baseline --- TODO.md | 8 +- apps/desktop/src-tauri/src/lib.rs | 29 +- apps/web/package.json | 98 +- .../web/src/lib/canvas/canvas-store.svelte.ts | 7 +- .../controllers/tool-controller.svelte.ts | 4 +- .../src/lib/components/StencilPalette.svelte | 9 +- apps/web/src/lib/tests/Canvas.svelte.test.ts | 2 +- .../web/src/lib/tests/TitleBar.svelte.test.ts | 6 +- apps/web/src/lib/tests/Toolbar.colors.test.ts | 7 +- apps/web/src/lib/tests/input.test.ts | 49 +- .../src/lib/tests/persistence.desktop.test.ts | 18 + apps/web/src/lib/theme.svelte.ts | 12 +- apps/web/tsconfig.json | 36 +- apps/web/vite.config.ts | 8 +- docs/v1-baselines.md | 77 + .../v1/desktop/all-features.inkfinite.json | 396 + fixtures/v1/history/history-edits.json | 75 + .../dangling-references.inkfinite.json | 47 + .../v1/invalid/duplicate-order.inkfinite.json | 49 + .../v1/invalid/malformed-json.inkfinite.json | 2 + .../v1/invalid/missing-envelope-fields.json | 8 + fixtures/v1/manifest.json | 36 + fixtures/v1/performance/baseline.json | 53 + .../v1/performance/board-10000.inkfinite.json | 170035 +++++++++++++++ fixtures/v1/rendering/all-shapes.json | 402 + fixtures/v1/web/all-features.web.json | 396 + package.json | 5 +- packages/core/tests/geom.test.ts | 2 +- packages/core/tests/v1-compatibility.test.ts | 158 + .../tests/v1-rendering-baseline.test.ts | 107 + packages/renderer/vitest.config.ts | 6 +- pnpm-workspace.yaml | 4 + scripts/capture-v1-performance.mjs | 200 + scripts/generate-v1-fixtures.mjs | 270 + 34 files changed, 172508 insertions(+), 113 deletions(-) create mode 100644 docs/v1-baselines.md create mode 100644 fixtures/v1/desktop/all-features.inkfinite.json create mode 100644 fixtures/v1/history/history-edits.json create mode 100644 fixtures/v1/invalid/dangling-references.inkfinite.json create mode 100644 fixtures/v1/invalid/duplicate-order.inkfinite.json create mode 100644 fixtures/v1/invalid/malformed-json.inkfinite.json create mode 100644 fixtures/v1/invalid/missing-envelope-fields.json create mode 100644 fixtures/v1/manifest.json create mode 100644 fixtures/v1/performance/baseline.json create mode 100644 fixtures/v1/performance/board-10000.inkfinite.json create mode 100644 fixtures/v1/rendering/all-shapes.json create mode 100644 fixtures/v1/web/all-features.web.json create mode 100644 packages/core/tests/v1-compatibility.test.ts create mode 100644 packages/renderer/tests/v1-rendering-baseline.test.ts create mode 100644 scripts/capture-v1-performance.mjs create mode 100644 scripts/generate-v1-fixtures.mjs diff --git a/TODO.md b/TODO.md index 56848af..b8bb3c4 100644 --- a/TODO.md +++ b/TODO.md @@ -18,13 +18,13 @@ Blocked by: None - can start immediately Acceptance criteria: -- [ ] Fixtures cover every current shape, bindings, groups, pages, Markdown, +- [x] Fixtures cover every current shape, bindings, groups, pages, Markdown, stencils, history-relevant edits, and persisted ordering. -- [ ] Import, render, hit-test, open/save, and cursor-after-resize behavior have +- [x] Import, render, hit-test, open/save, and cursor-after-resize behavior have baseline tests; known failures are marked as such rather than normalized. -- [ ] The performance harness records hardware, runtime versions, fixture seed, +- [x] The performance harness records hardware, runtime versions, fixture seed, visible-shape count, frame time, hit-test time, memory, open, and save time. -- [ ] Current package tests, type checks, web checks, lint, and desktop Rust tests +- [x] Current package tests, type checks, web checks, lint, and desktop Rust tests run from documented commands. Verification: diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 1f7b5e5..6ad64f9 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use tauri::AppHandle; #[derive(serde::Serialize, serde::Deserialize)] @@ -14,23 +14,23 @@ pub struct FileEntry { fn read_directory(directory: String, pattern: Option) -> Result, String> { let path = Path::new(&directory); if !path.exists() { - return Err(format!("Directory does not exist: {}", directory)); + return Err(format!("Directory does not exist: {directory}")); } if !path.is_dir() { - return Err(format!("Path is not a directory: {}", directory)); + return Err(format!("Path is not a directory: {directory}")); } - let entries = fs::read_dir(path).map_err(|e| format!("Failed to read directory: {}", e))?; + let entries = fs::read_dir(path).map_err(|e| format!("Failed to read directory: {e}"))?; let mut results = Vec::new(); let pattern = pattern.unwrap_or_else(|| "*.inkfinite.json".to_string()); for entry in entries { - let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?; let entry_path = entry.path(); let metadata = entry .metadata() - .map_err(|e| format!("Failed to read metadata: {}", e))?; + .map_err(|e| format!("Failed to read metadata: {e}"))?; let name = entry.file_name().to_string_lossy().to_string(); @@ -73,10 +73,10 @@ fn rename_file(old_path: String, new_path: String) -> Result<(), String> { let new = Path::new(&new_path); if !old.exists() { - return Err(format!("Source file does not exist: {}", old_path)); + return Err(format!("Source file does not exist: {old_path}")); } - fs::rename(old, new).map_err(|e| format!("Failed to rename file: {}", e))?; + fs::rename(old, new).map_err(|e| format!("Failed to rename file: {e}"))?; Ok(()) } @@ -87,14 +87,14 @@ fn delete_file(file_path: String) -> Result<(), String> { let path = Path::new(&file_path); if !path.exists() { - return Err(format!("File does not exist: {}", file_path)); + return Err(format!("File does not exist: {file_path}")); } if path.is_dir() { - return Err(format!("Path is a directory, not a file: {}", file_path)); + return Err(format!("Path is a directory, not a file: {file_path}")); } - fs::remove_file(path).map_err(|e| format!("Failed to delete file: {}", e))?; + fs::remove_file(path).map_err(|e| format!("Failed to delete file: {e}"))?; Ok(()) } @@ -102,12 +102,15 @@ fn delete_file(file_path: String) -> Result<(), String> { /// Pick a workspace directory using the system folder picker #[tauri::command] async fn pick_workspace_directory(app: AppHandle) -> Result, String> { - use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; + use tauri_plugin_dialog::DialogExt; let result = app.dialog().file().blocking_pick_folder(); match result { - Some(path) => Ok(Some(path.to_string_lossy().to_string())), + Some(path) => path + .into_path() + .map(|path| Some(path.to_string_lossy().into_owned())) + .map_err(|error| format!("Failed to resolve selected directory: {error}")), None => Ok(None), } } diff --git a/apps/web/package.json b/apps/web/package.json index 179eb3a..63f8998 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,51 +1,51 @@ { - "name": "inkfinite-web", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint . && prettier --check .", - "test:unit": "vitest", - "test": "npm run test:unit -- --run", - "format": "prettier --write ." - }, - "dependencies": { - "@tauri-apps/api": "^2.9.1", - "@tauri-apps/plugin-dialog": "^2.4.2", - "@tauri-apps/plugin-fs": "^2.4.4", - "@tauri-apps/plugin-store": "^2.4.1", - "dexie": "^4.2.1", - "inkfinite-core": "workspace:*", - "inkfinite-renderer": "workspace:*" - }, - "devDependencies": { - "@eslint/compat": "^1.4.0", - "@eslint/js": "^9.39.1", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.49.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@types/node": "^24", - "@vitest/browser-playwright": "^4.0.15", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.13.1", - "globals": "^16.5.0", - "playwright": "^1.57.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.0", - "svelte": "^5.45.6", - "svelte-check": "^4.3.4", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.2.6", - "vite-plugin-devtools-json": "^1.0.0", - "vitest": "^4.0.15", - "vitest-browser-svelte": "^2.0.1" - } + "name": "inkfinite-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint . && prettier --check .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run", + "format": "prettier --write ." + }, + "dependencies": { + "@tauri-apps/api": "^2.9.1", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-fs": "^2.4.4", + "@tauri-apps/plugin-store": "^2.4.1", + "dexie": "^4.2.1", + "inkfinite-core": "workspace:*", + "inkfinite-renderer": "workspace:*" + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.39.1", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^24", + "@vitest/browser-playwright": "^4.0.15", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.13.1", + "globals": "^16.5.0", + "playwright": "^1.57.0", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^4.0.15", + "vitest-browser-svelte": "^2.0.1" + } } diff --git a/apps/web/src/lib/canvas/canvas-store.svelte.ts b/apps/web/src/lib/canvas/canvas-store.svelte.ts index 4047b1c..ec3efa5 100644 --- a/apps/web/src/lib/canvas/canvas-store.svelte.ts +++ b/apps/web/src/lib/canvas/canvas-store.svelte.ts @@ -312,7 +312,7 @@ export function createCanvasController(bindings: CanvasControllerBindings) { return null; } - const groupsToDissolve = new Set(); + const groupsToDissolve = new SvelteSet(); const shapes = state.doc.shapes; for (const id of selectionIds) { @@ -580,8 +580,9 @@ export function createCanvasController(bindings: CanvasControllerBindings) { const unsubSnap = snapStore.subscribe(() => renderer?.markDirty()); $effect(() => { - themeStore.current; - renderer?.markDirty(); + if (themeStore.current) { + renderer?.markDirty(); + } }); inputAdapter = createInputAdapter({ diff --git a/apps/web/src/lib/canvas/controllers/tool-controller.svelte.ts b/apps/web/src/lib/canvas/controllers/tool-controller.svelte.ts index 444eb7f..544a6ec 100644 --- a/apps/web/src/lib/canvas/controllers/tool-controller.svelte.ts +++ b/apps/web/src/lib/canvas/controllers/tool-controller.svelte.ts @@ -1,11 +1,11 @@ -import { switchTool, type Store, type ToolId } from "inkfinite-core"; +import { switchTool, type Store, type Tool, type ToolId } from "inkfinite-core"; export class ToolController { currentToolId = $state("select"); constructor( private store: Store, - private tools: Map, + private tools: Map, ) { store.subscribe((state) => { this.currentToolId = state.ui.toolId; diff --git a/apps/web/src/lib/components/StencilPalette.svelte b/apps/web/src/lib/components/StencilPalette.svelte index f1e8c0d..3ef91e2 100644 --- a/apps/web/src/lib/components/StencilPalette.svelte +++ b/apps/web/src/lib/components/StencilPalette.svelte @@ -1,5 +1,6 @@