Skip to content

Commit 65c409e

Browse files
committed
Merge branch 'worktree-2025-12-19T06-15-44'
2 parents a1e10ba + 5774e6a commit 65c409e

File tree

7 files changed

+812
-147
lines changed

7 files changed

+812
-147
lines changed

packages/ui-vite/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"build": "tsc -b && vite build",
1010
"lint": "eslint .",
1111
"preview": "vite preview --port 3000",
12-
"typecheck": "tsc --noEmit"
12+
"typecheck": "tsc --noEmit",
13+
"test": "vitest run",
14+
"test:watch": "vitest"
1315
},
1416
"dependencies": {
1517
"@leanspec/ui-components": "workspace:*",
@@ -27,6 +29,9 @@
2729
"@eslint/js": "^9.39.1",
2830
"@tailwindcss/typography": "^0.5.15",
2931
"@tauri-apps/api": "^2.9.1",
32+
"@testing-library/jest-dom": "^6.9.1",
33+
"@testing-library/react": "^16.3.1",
34+
"@testing-library/user-event": "^14.6.1",
3035
"@types/node": "^24.10.1",
3136
"@types/react": "^19.2.5",
3237
"@types/react-dom": "^19.2.3",
@@ -36,11 +41,13 @@
3641
"eslint-plugin-react-hooks": "^7.0.1",
3742
"eslint-plugin-react-refresh": "^0.4.24",
3843
"globals": "^16.5.0",
44+
"jsdom": "^27.3.0",
3945
"postcss": "^8.4.49",
4046
"tailwindcss": "^3.4.17",
4147
"typescript": "~5.9.3",
4248
"typescript-eslint": "^8.46.4",
43-
"vite": "^7.2.4"
49+
"vite": "^7.2.4",
50+
"vitest": "^4.0.6"
4451
},
4552
"keywords": [
4653
"leanspec",
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { api } from './api';
3+
4+
// Mock fetch
5+
const mockFetch = vi.fn();
6+
global.fetch = mockFetch as any;
7+
8+
describe('API Client', () => {
9+
beforeEach(() => {
10+
vi.clearAllMocks();
11+
});
12+
13+
describe('getProjects', () => {
14+
it('should fetch projects successfully', async () => {
15+
const mockResponse = {
16+
current: { id: 'proj1', name: 'Project 1', path: '/path/1' },
17+
available: [
18+
{ id: 'proj1', name: 'Project 1', path: '/path/1' },
19+
{ id: 'proj2', name: 'Project 2', path: '/path/2' },
20+
],
21+
};
22+
23+
mockFetch.mockResolvedValueOnce({
24+
ok: true,
25+
json: async () => mockResponse,
26+
});
27+
28+
const result = await api.getProjects();
29+
expect(result).toEqual(mockResponse);
30+
expect(mockFetch).toHaveBeenCalledWith(
31+
expect.stringContaining('/api/projects'),
32+
expect.any(Object)
33+
);
34+
});
35+
36+
it('should throw error on failure', async () => {
37+
mockFetch.mockResolvedValueOnce({
38+
ok: false,
39+
status: 500,
40+
statusText: 'Internal Server Error',
41+
text: async () => 'Server error',
42+
});
43+
44+
await expect(api.getProjects()).rejects.toThrow();
45+
});
46+
});
47+
48+
describe('getSpecs', () => {
49+
it('should fetch specs successfully', async () => {
50+
const mockSpecs = [
51+
{ name: 'spec-001', title: 'Test Spec', status: 'planned' as const },
52+
];
53+
54+
mockFetch.mockResolvedValueOnce({
55+
ok: true,
56+
json: async () => ({ specs: mockSpecs }),
57+
});
58+
59+
const result = await api.getSpecs();
60+
expect(result).toEqual(mockSpecs);
61+
});
62+
});
63+
64+
describe('getSpec', () => {
65+
it('should fetch spec details successfully', async () => {
66+
const mockSpec = {
67+
name: 'spec-001',
68+
title: 'Test Spec',
69+
status: 'planned' as const,
70+
content: '# Test',
71+
metadata: {},
72+
};
73+
74+
mockFetch.mockResolvedValueOnce({
75+
ok: true,
76+
json: async () => ({ spec: mockSpec }),
77+
});
78+
79+
const result = await api.getSpec('spec-001');
80+
expect(result).toEqual(mockSpec);
81+
expect(mockFetch).toHaveBeenCalledWith(
82+
expect.stringContaining('/api/specs/spec-001'),
83+
expect.any(Object)
84+
);
85+
});
86+
});
87+
88+
describe('updateSpec', () => {
89+
it('should update spec metadata successfully', async () => {
90+
const updates = { status: 'in-progress' as const, priority: 'high' as const };
91+
92+
mockFetch.mockResolvedValueOnce({
93+
ok: true,
94+
json: async () => ({}),
95+
});
96+
97+
await api.updateSpec('spec-001', updates);
98+
99+
expect(mockFetch).toHaveBeenCalledWith(
100+
expect.stringContaining('/api/specs/spec-001'),
101+
expect.objectContaining({
102+
method: 'PATCH',
103+
body: JSON.stringify(updates),
104+
})
105+
);
106+
});
107+
});
108+
109+
describe('getStats', () => {
110+
it('should fetch stats successfully', async () => {
111+
const mockStats = {
112+
total: 10,
113+
by_status: { planned: 5, 'in-progress': 3, complete: 2 },
114+
by_priority: { high: 3, medium: 4, low: 3 },
115+
by_tag: { frontend: 5, backend: 5 },
116+
};
117+
118+
mockFetch.mockResolvedValueOnce({
119+
ok: true,
120+
json: async () => ({ stats: mockStats }),
121+
});
122+
123+
const result = await api.getStats();
124+
expect(result).toEqual(mockStats);
125+
});
126+
});
127+
128+
describe('getDependencies', () => {
129+
it('should fetch dependency graph successfully', async () => {
130+
const mockGraph = {
131+
nodes: [
132+
{ id: 'spec-001', name: 'Spec 001', status: 'planned' },
133+
{ id: 'spec-002', name: 'Spec 002', status: 'in-progress' },
134+
],
135+
edges: [
136+
{ source: 'spec-001', target: 'spec-002', type: 'depends_on' as const },
137+
],
138+
};
139+
140+
mockFetch.mockResolvedValueOnce({
141+
ok: true,
142+
json: async () => ({ graph: mockGraph }),
143+
});
144+
145+
const result = await api.getDependencies();
146+
expect(result).toEqual(mockGraph);
147+
});
148+
149+
it('should fetch dependencies for specific spec', async () => {
150+
const mockGraph = {
151+
nodes: [],
152+
edges: [],
153+
};
154+
155+
mockFetch.mockResolvedValueOnce({
156+
ok: true,
157+
json: async () => ({ graph: mockGraph }),
158+
});
159+
160+
await api.getDependencies('spec-001');
161+
expect(mockFetch).toHaveBeenCalledWith(
162+
expect.stringContaining('/api/specs/spec-001/dependencies'),
163+
expect.any(Object)
164+
);
165+
});
166+
});
167+
});

packages/ui-vite/src/test/setup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { afterEach } from 'vitest';
2+
import { cleanup } from '@testing-library/react';
3+
import '@testing-library/jest-dom/vitest';
4+
5+
afterEach(() => {
6+
cleanup();
7+
});

packages/ui-vite/tsconfig.app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
"noFallthroughCasesInSwitch": true,
2525
"noUncheckedSideEffectImports": true
2626
},
27-
"include": ["src"]
27+
"include": ["src"],
28+
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test"]
2829
}

packages/ui-vite/vitest.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineConfig } from 'vitest/config';
2+
import react from '@vitejs/plugin-react';
3+
import { fileURLToPath } from 'node:url';
4+
5+
export default defineConfig({
6+
plugins: [react()],
7+
test: {
8+
globals: true,
9+
environment: 'jsdom',
10+
include: ['src/**/*.test.{ts,tsx}'],
11+
setupFiles: './src/test/setup.ts',
12+
},
13+
resolve: {
14+
alias: {
15+
'@': fileURLToPath(new URL('./src', import.meta.url)),
16+
},
17+
},
18+
});

0 commit comments

Comments
 (0)