Skip to content

Commit 16d701c

Browse files
committed
improved api-key-manager, git-repo-selector, added search bar, refined zoom feature, established display boundaries
1 parent 535a11d commit 16d701c

35 files changed

+1522
-79
lines changed

TESTING_STRATEGY.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Relentless Testing Strategy for Vibe Commit Visualizer
2+
3+
To ensure `vibecommit-visualizer` is production-ready, we need a multi-layered testing approach that goes beyond simple unit tests. We must test the UI, the Electron integration, performance with large repositories, and edge cases.
4+
5+
## 1. Foundation: Unit & Integration Tests (Backend Logic)
6+
**Current Status:** Partially implemented (`tests/unit`, `tests/integration`).
7+
**Goal:** 100% coverage of business logic and IPC handlers.
8+
9+
- **Action Items:**
10+
- **IPC Handlers**: Ensure every `ipcMain.handle` is tested with various inputs (valid, invalid, missing params).
11+
- **Git Parsing**: Test the regex/parsing logic for `git log` output against weird commit messages (emojis, multi-line, special characters).
12+
- **Security**: Verify `child_process` arguments are always sanitized (already seemingly covered, but double-check).
13+
14+
## 2. Component Testing (Frontend UI)
15+
**Current Status:** Missing.
16+
**Goal:** Verify individual React components render and behave correctly in isolation.
17+
**Tools:** `React Testing Library` + `Jest`.
18+
19+
- **Action Items:**
20+
- **GitVisualizer**:
21+
- Test rendering with 0 commits (empty state).
22+
- Test rendering with 10 commits.
23+
- Test clicking a node triggers `onCommitSelect`.
24+
- Test loading state (spinner).
25+
- **ApiKeyManager**:
26+
- Test input validation (empty key, invalid format).
27+
- Test saving/loading from `sessionStorage`.
28+
29+
## 3. End-to-End (E2E) Testing with Playwright
30+
**Current Status:** Missing.
31+
**Goal:** Test the actual packaged Electron app running on the OS.
32+
**Tools:** `Playwright` (supports Electron).
33+
34+
- **Action Items:**
35+
- **Setup**: Configure Playwright to launch the Electron executable.
36+
- **Smoke Test**: Launch app -> Verify window opens -> Verify "Loading" -> Verify Graph appears.
37+
- **Interaction**:
38+
- Click a commit node -> Verify details panel opens.
39+
- Click "Generate" -> Verify Vibe CLI is called (mocked or real).
40+
- **Visual Regression**: Take screenshots of the graph and compare against baselines to catch layout breakages.
41+
42+
## 4. "Relentless" Stress & Performance Testing
43+
**Current Status:** Missing.
44+
**Goal:** Ensure the app doesn't freeze or crash under heavy load (User reported freezing).
45+
46+
- **Action Items:**
47+
- **Large Repo Test**:
48+
- Create a script to generate a dummy git repo with 1,000, 5,000, and 10,000 commits.
49+
- Point the visualizer to this repo.
50+
- Measure time to render.
51+
- **Pass Criteria**: UI remains responsive (60fps), graph renders within X seconds.
52+
- **Complex Graph Test**:
53+
- Generate a repo with many merges/branches (complex topology) to stress the `dagre` layout engine.
54+
- **Memory Leak Test**:
55+
- Open/close the app 10 times.
56+
- Switch branches 50 times rapidly.
57+
- Monitor RAM usage.
58+
59+
## 5. Chaos & Edge Case Testing
60+
**Goal:** Break the app intentionally.
61+
62+
- **Action Items:**
63+
- **Missing Dependencies**: Run app where `git` is uninstalled or `vibe` CLI is missing. Verify error messages.
64+
- **Corrupt Repo**: Point app to a `.git` folder with missing HEAD or corrupt objects.
65+
- **Network Failures**: Disconnect internet while using AI features.
66+
- **Concurrent Operations**: Spam the "Refresh" button while a command is running.
67+
68+
## 6. Packaging & Distribution Testing
69+
**Goal:** Ensure the built artifact works on fresh machines.
70+
71+
- **Action Items:**
72+
- **Clean VM Test**: Run the `.exe` (Windows) or `.dmg` (Mac) on a fresh VM with no Node/Python installed.
73+
- **Path Spaces**: Test with project paths containing spaces and special characters (e.g., `C:\Users\Name\My Projects\Vibe Commit`).
74+
75+
---
76+
77+
# Implementation Plan
78+
79+
## Phase 1: Setup E2E Framework
80+
1. Install Playwright: `pnpm add -D @playwright/test playwright`.
81+
2. Create `playwright.config.ts` configured for Electron.
82+
3. Write a basic "App Launches" test.
83+
84+
## Phase 2: Stress Test Script
85+
1. Create `scripts/generate-large-repo.js`.
86+
2. Run the visualizer against it and profile performance.
87+
88+
## Phase 3: Component Tests
89+
1. Add `testing-library` dependencies.
90+
2. Write `GitVisualizer.test.tsx`.

__mocks__/keytar.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
getPassword: jest.fn(),
3+
setPassword: jest.fn(),
4+
deletePassword: jest.fn(),
5+
};

app/globals.css

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
* {
120120
@apply border-border outline-ring/50;
121121
}
122+
122123
body {
123124
@apply bg-background text-foreground;
124125
}
@@ -137,6 +138,7 @@
137138

138139
/* Dark mode overrides for diff2html */
139140
.dark .diff2html-wrapper {
141+
140142
/* File header styling */
141143
.d2h-file-header {
142144
background-color: oklch(0.2 0 0) !important;
@@ -217,12 +219,12 @@
217219

218220
/* Inline changes (word-level diffs) */
219221
.d2h-ins .d2h-change {
220-
background-color: oklch(0.3 0.15 150) !important;
222+
background-color: oklch(0.22 0.06 150) !important;
221223
color: oklch(0.95 0 0) !important;
222224
}
223225

224226
.d2h-del .d2h-change {
225-
background-color: oklch(0.3 0.15 25) !important;
227+
background-color: oklch(0.22 0.06 25) !important;
226228
color: oklch(0.95 0 0) !important;
227229
}
228230

@@ -277,4 +279,4 @@
277279
.d2h-diff-tbody tr td {
278280
border-color: oklch(0.25 0 0) !important;
279281
}
280-
}
282+
}

app/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const metadata: Metadata = {
3131
},
3232
};
3333

34+
import { Toaster } from "@/components/ui/toaster";
35+
3436
export default function RootLayout({
3537
children,
3638
}: Readonly<{
@@ -40,6 +42,7 @@ export default function RootLayout({
4042
<html lang="en" suppressHydrationWarning>
4143
<body className={`font-sans antialiased`}>
4244
<ThemeProvider>{children}</ThemeProvider>
45+
<Toaster />
4346
</body>
4447
</html>
4548
);

app/page.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { GitRepoSelector } from "@/components/git-repo-selector";
66
import { BranchSelector } from "@/components/branch-selector";
77
import { DiffDialog } from "@/components/diff-dialog";
88
import { apiService } from "@/lib/ipc-api";
9+
import { useToast } from "@/components/ui/use-toast";
10+
import { ChevronUp } from "lucide-react";
911

1012
const LAST_REPO_KEY = "git-visualizer-last-repo";
1113

@@ -21,6 +23,7 @@ export default function Page() {
2123
model: string;
2224
apiKey: string;
2325
} | null>(null);
26+
const { toast } = useToast();
2427

2528
// Load saved repository path on mount
2629
useEffect(() => {
@@ -56,8 +59,16 @@ export default function Page() {
5659
} else {
5760
setSelectedBranch("");
5861
}
59-
} catch (error) {
62+
} catch (error: any) {
6063
console.error("Failed to fetch branches:", error);
64+
if (error.message && error.message.includes("Invalid repository path")) {
65+
toast({
66+
variant: "destructive",
67+
title: "Invalid repository path",
68+
description: "The specified path is not a valid git repository.",
69+
});
70+
setSelectedRepo(""); // Clear the invalid path
71+
}
6172
setBranches([]);
6273
setSelectedBranch("");
6374
} finally {
@@ -114,9 +125,19 @@ export default function Page() {
114125
</div>
115126
)}
116127

117-
{/* Repo selector at bottom-center */}
118-
<div className="pointer-events-auto absolute inset-x-0 bottom-3 z-40 flex w-full justify-center">
119-
<div className="w-full max-w-xl px-3">
128+
{/* Repo selector */}
129+
<div
130+
className={`pointer-events-auto absolute inset-x-0 z-40 flex flex-col items-center justify-center transition-all duration-500 ease-in-out ${selectedRepo
131+
? "bottom-0 translate-y-[calc(100%-24px)] hover:translate-y-0"
132+
: "top-1/2 -translate-y-1/2"
133+
}`}
134+
>
135+
{selectedRepo && (
136+
<div className="flex h-6 w-16 items-center justify-center rounded-t-lg bg-card border-t border-x border-border shadow-[0_-2px_10px_rgba(0,0,0,0.1)] cursor-pointer">
137+
<ChevronUp className="h-4 w-4 text-muted-foreground" />
138+
</div>
139+
)}
140+
<div className="w-full max-w-xl px-3 pb-3">
120141
<GitRepoSelector
121142
onRepoSelect={setSelectedRepo}
122143
initialValue={selectedRepo}

babel.jest.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ module.exports = {
88
},
99
},
1010
],
11+
"@babel/preset-react",
12+
"@babel/preset-typescript",
1113
],
1214
};

components/api-key-manager.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface ApiKeyManagerProps {
3737
}
3838

3939
// Available models with provider mappings (stored as provider:modelname)
40-
const AVAILABLE_MODELS: Model[] = [
40+
export const AVAILABLE_MODELS: Model[] = [
4141
// Google
4242
{
4343
id: "google:gemini-2.5-flash-lite",

components/git-repo-selector.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import { useState, useEffect, useRef } from "react";
44
import { Button } from "@/components/ui/button";
55
import { Input } from "@/components/ui/input";
66
import {
@@ -20,33 +20,66 @@ export function GitRepoSelector({
2020
initialValue?: string;
2121
}) {
2222
const [repoPath, setRepoPath] = useState(initialValue);
23+
const inputRef = useRef<HTMLInputElement>(null);
2324

2425
// Update local state when initialValue changes
2526
useEffect(() => {
2627
setRepoPath(initialValue);
2728
}, [initialValue]);
2829

30+
// Scroll input to end when repoPath changes
31+
useEffect(() => {
32+
if (inputRef.current) {
33+
inputRef.current.scrollLeft = inputRef.current.scrollWidth;
34+
}
35+
}, [repoPath]);
36+
2937
const handleSelect = () => {
3038
if (repoPath.trim()) {
3139
onRepoSelect(repoPath.trim());
3240
}
3341
};
3442

43+
const handleBrowse = async () => {
44+
try {
45+
// @ts-ignore - electronAPI is exposed via preload
46+
const path = await window.electronAPI.openDirectory();
47+
if (path) {
48+
setRepoPath(path);
49+
onRepoSelect(path);
50+
}
51+
} catch (error) {
52+
console.error("Failed to open directory selector:", error);
53+
}
54+
};
55+
3556
return (
3657
<Card className="border border-border bg-card">
37-
<CardHeader>
38-
<CardTitle className="text-lg flex items-center gap-2">
58+
<CardHeader className="flex flex-row items-start justify-between space-y-0">
59+
<div className="flex flex-col gap-1.5">
60+
<CardTitle className="text-lg flex items-center gap-2">
61+
<FolderOpen className="h-4 w-4" />
62+
Repository
63+
</CardTitle>
64+
<CardDescription>Select a local git repository</CardDescription>
65+
</div>
66+
<Button
67+
variant="outline"
68+
size="icon"
69+
onClick={handleBrowse}
70+
title="Browse for repository"
71+
className="hover:bg-primary hover:text-primary-foreground hover:border-primary hover:shadow-[0_0_15px_var(--primary)] transition-all duration-300"
72+
>
3973
<FolderOpen className="h-4 w-4" />
40-
Repository
41-
</CardTitle>
42-
<CardDescription>Select a local git repository</CardDescription>
74+
</Button>
4375
</CardHeader>
4476
<CardContent className="space-y-3">
4577
<Input
78+
ref={inputRef}
4679
placeholder="/path/to/repo"
4780
value={repoPath}
4881
onChange={(e) => setRepoPath(e.target.value)}
49-
onKeyPress={(e) => e.key === "Enter" && handleSelect()}
82+
onKeyDown={(e) => e.key === "Enter" && handleSelect()}
5083
className="bg-input text-foreground"
5184
/>
5285
<Button onClick={handleSelect} className="w-full">

0 commit comments

Comments
 (0)