Skip to content

Commit 8a487c7

Browse files
Test Userclaude
andcommitted
test(qa): add usePresetManagement hook tests for PR #77
- Generated by Night Watch QA agent - 18 new tests covering: - Constants validation (built-in presets) - Preset CRUD operations (add, edit, delete, reset) - Modal state management - Delete protection for built-in presets - Reference tracking for in-use presets - API integration and error handling Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ec3aa10 commit 8a487c7

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { renderHook, act } from '@testing-library/react';
3+
import { usePresetManagement, BUILT_IN_PRESET_IDS, BUILT_IN_PRESETS } from '../../../hooks/usePresetManagement.js';
4+
import * as api from '../../../api.js';
5+
import type { IProviderPreset, IJobProviders } from '@shared/types';
6+
7+
// Mock the api module
8+
vi.mock('../../../api.js', () => ({
9+
updateConfig: vi.fn(),
10+
}));
11+
12+
// Mock the store
13+
vi.mock('../../../store/useStore.js', () => ({
14+
useStore: () => ({
15+
addToast: vi.fn(),
16+
}),
17+
}));
18+
19+
function makeForm(provider: string = 'claude', providerPresets: Record<string, IProviderPreset> = {}, jobProviders: IJobProviders = {}) {
20+
return {
21+
provider,
22+
providerPresets,
23+
jobProviders,
24+
};
25+
}
26+
27+
describe('usePresetManagement', () => {
28+
let mockOnFormUpdate: ReturnType<typeof vi.fn>;
29+
30+
beforeEach(() => {
31+
vi.clearAllMocks();
32+
mockOnFormUpdate = vi.fn();
33+
});
34+
35+
describe('constants', () => {
36+
it('should have correct built-in preset IDs', () => {
37+
expect(BUILT_IN_PRESET_IDS).toContain('claude');
38+
expect(BUILT_IN_PRESET_IDS).toContain('claude-sonnet-4-6');
39+
expect(BUILT_IN_PRESET_IDS).toContain('claude-opus-4-6');
40+
expect(BUILT_IN_PRESET_IDS).toContain('codex');
41+
expect(BUILT_IN_PRESET_IDS).toContain('glm-47');
42+
expect(BUILT_IN_PRESET_IDS).toContain('glm-5');
43+
});
44+
45+
it('should have built-in presets with required fields', () => {
46+
for (const [, preset] of Object.entries(BUILT_IN_PRESETS)) {
47+
expect(preset.name).toBeDefined();
48+
expect(preset.command).toBeDefined();
49+
}
50+
});
51+
});
52+
53+
describe('allPresets and presetOptions', () => {
54+
it('should combine built-in and custom presets', () => {
55+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
56+
const { result } = renderHook(() =>
57+
usePresetManagement({
58+
form: makeForm('claude', { custom: customPreset }),
59+
onFormUpdate: mockOnFormUpdate,
60+
}),
61+
);
62+
63+
expect(result.current.allPresets['claude']).toBeDefined();
64+
expect(result.current.allPresets['custom']).toEqual(customPreset);
65+
});
66+
67+
it('should generate correct preset options', () => {
68+
const { result } = renderHook(() =>
69+
usePresetManagement({
70+
form: makeForm(),
71+
onFormUpdate: mockOnFormUpdate,
72+
}),
73+
);
74+
75+
const claudeOption = result.current.presetOptions.find((o) => o.value === 'claude');
76+
expect(claudeOption?.label).toBe('Claude');
77+
});
78+
});
79+
80+
describe('isBuiltIn', () => {
81+
it('should return true for built-in presets', () => {
82+
const { result } = renderHook(() =>
83+
usePresetManagement({
84+
form: makeForm(),
85+
onFormUpdate: mockOnFormUpdate,
86+
}),
87+
);
88+
89+
expect(result.current.isBuiltIn('claude')).toBe(true);
90+
expect(result.current.isBuiltIn('codex')).toBe(true);
91+
});
92+
93+
it('should return false for custom presets', () => {
94+
const { result } = renderHook(() =>
95+
usePresetManagement({
96+
form: makeForm(),
97+
onFormUpdate: mockOnFormUpdate,
98+
}),
99+
);
100+
101+
expect(result.current.isBuiltIn('my-custom-preset')).toBe(false);
102+
});
103+
});
104+
105+
describe('handleAddPreset', () => {
106+
it('should open modal with null editing state', () => {
107+
const { result } = renderHook(() =>
108+
usePresetManagement({
109+
form: makeForm(),
110+
onFormUpdate: mockOnFormUpdate,
111+
}),
112+
);
113+
114+
expect(result.current.presetModalOpen).toBe(false);
115+
116+
act(() => {
117+
result.current.handleAddPreset();
118+
});
119+
120+
expect(result.current.presetModalOpen).toBe(true);
121+
expect(result.current.editingPresetId).toBe(null);
122+
expect(result.current.editingPreset).toBe(null);
123+
});
124+
});
125+
126+
describe('handleEditPreset', () => {
127+
it('should open modal with preset data', () => {
128+
const { result } = renderHook(() =>
129+
usePresetManagement({
130+
form: makeForm(),
131+
onFormUpdate: mockOnFormUpdate,
132+
}),
133+
);
134+
135+
act(() => {
136+
result.current.handleEditPreset('claude');
137+
});
138+
139+
expect(result.current.presetModalOpen).toBe(true);
140+
expect(result.current.editingPresetId).toBe('claude');
141+
expect(result.current.editingPreset?.name).toBe('Claude');
142+
});
143+
144+
it('should not open modal for non-existent preset', () => {
145+
const { result } = renderHook(() =>
146+
usePresetManagement({
147+
form: makeForm(),
148+
onFormUpdate: mockOnFormUpdate,
149+
}),
150+
);
151+
152+
act(() => {
153+
result.current.handleEditPreset('non-existent');
154+
});
155+
156+
expect(result.current.presetModalOpen).toBe(false);
157+
});
158+
});
159+
160+
describe('closePresetModal', () => {
161+
it('should close modal and clear editing state', () => {
162+
const { result } = renderHook(() =>
163+
usePresetManagement({
164+
form: makeForm(),
165+
onFormUpdate: mockOnFormUpdate,
166+
}),
167+
);
168+
169+
act(() => {
170+
result.current.handleEditPreset('claude');
171+
});
172+
expect(result.current.presetModalOpen).toBe(true);
173+
174+
act(() => {
175+
result.current.closePresetModal();
176+
});
177+
178+
expect(result.current.presetModalOpen).toBe(false);
179+
expect(result.current.editingPresetId).toBe(null);
180+
expect(result.current.editingPreset).toBe(null);
181+
});
182+
});
183+
184+
describe('handleSavePreset', () => {
185+
it('should add new preset and call API', async () => {
186+
vi.mocked(api.updateConfig).mockResolvedValueOnce({});
187+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
188+
189+
const { result } = renderHook(() =>
190+
usePresetManagement({
191+
form: makeForm(),
192+
onFormUpdate: mockOnFormUpdate,
193+
}),
194+
);
195+
196+
let success: boolean;
197+
await act(async () => {
198+
success = await result.current.handleSavePreset('my-custom', customPreset);
199+
});
200+
201+
expect(success!).toBe(true);
202+
expect(mockOnFormUpdate).toHaveBeenCalledWith('providerPresets', { 'my-custom': customPreset });
203+
expect(api.updateConfig).toHaveBeenCalledWith({ providerPresets: { 'my-custom': customPreset } });
204+
});
205+
206+
it('should revert on API failure', async () => {
207+
vi.mocked(api.updateConfig).mockRejectedValueOnce(new Error('API error'));
208+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
209+
210+
const { result } = renderHook(() =>
211+
usePresetManagement({
212+
form: makeForm('claude', { existing: { name: 'Existing', command: 'existing' } }),
213+
onFormUpdate: mockOnFormUpdate,
214+
}),
215+
);
216+
217+
let success: boolean;
218+
await act(async () => {
219+
success = await result.current.handleSavePreset('my-custom', customPreset);
220+
});
221+
222+
expect(success!).toBe(false);
223+
// Should have been called twice - once for optimistic update, once for revert
224+
expect(mockOnFormUpdate).toHaveBeenCalledTimes(2);
225+
});
226+
});
227+
228+
describe('handleDeletePreset', () => {
229+
it('should prevent deletion of built-in presets', () => {
230+
const { result } = renderHook(() =>
231+
usePresetManagement({
232+
form: makeForm(),
233+
onFormUpdate: mockOnFormUpdate,
234+
}),
235+
);
236+
237+
act(() => {
238+
result.current.handleDeletePreset('claude');
239+
});
240+
241+
expect(mockOnFormUpdate).not.toHaveBeenCalled();
242+
});
243+
244+
it('should show warning when preset is in use', () => {
245+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
246+
247+
const { result } = renderHook(() =>
248+
usePresetManagement({
249+
form: makeForm('custom', { custom: customPreset }, { executor: 'custom' }),
250+
onFormUpdate: mockOnFormUpdate,
251+
}),
252+
);
253+
254+
act(() => {
255+
result.current.handleDeletePreset('custom');
256+
});
257+
258+
expect(result.current.deleteWarning).not.toBeNull();
259+
expect(result.current.deleteWarning?.presetId).toBe('custom');
260+
expect(result.current.deleteWarning?.references).toContain('Global Provider');
261+
expect(result.current.deleteWarning?.references).toContain('Executor');
262+
expect(mockOnFormUpdate).not.toHaveBeenCalled();
263+
});
264+
265+
it('should delete unused custom preset', () => {
266+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
267+
268+
const { result } = renderHook(() =>
269+
usePresetManagement({
270+
form: makeForm('claude', { custom: customPreset }),
271+
onFormUpdate: mockOnFormUpdate,
272+
}),
273+
);
274+
275+
act(() => {
276+
result.current.handleDeletePreset('custom');
277+
});
278+
279+
expect(mockOnFormUpdate).toHaveBeenCalledWith('providerPresets', {});
280+
expect(result.current.deleteWarning).toBeNull();
281+
});
282+
});
283+
284+
describe('handleConfirmDelete', () => {
285+
it('should delete preset and clear references', () => {
286+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
287+
288+
const { result } = renderHook(() =>
289+
usePresetManagement({
290+
form: makeForm('custom', { custom: customPreset }, { executor: 'custom' }),
291+
onFormUpdate: mockOnFormUpdate,
292+
}),
293+
);
294+
295+
// First trigger the warning
296+
act(() => {
297+
result.current.handleDeletePreset('custom');
298+
});
299+
expect(result.current.deleteWarning).not.toBeNull();
300+
301+
// Then confirm delete
302+
act(() => {
303+
result.current.handleConfirmDelete();
304+
});
305+
306+
expect(mockOnFormUpdate).toHaveBeenCalledWith('providerPresets', {});
307+
expect(mockOnFormUpdate).toHaveBeenCalledWith('jobProviders', {});
308+
expect(mockOnFormUpdate).toHaveBeenCalledWith('provider', 'claude');
309+
expect(result.current.deleteWarning).toBeNull();
310+
});
311+
});
312+
313+
describe('handleResetPreset', () => {
314+
it('should remove custom override for built-in preset', () => {
315+
const customOverride: IProviderPreset = { name: 'Custom Claude', command: 'claude-custom' };
316+
317+
const { result } = renderHook(() =>
318+
usePresetManagement({
319+
form: makeForm('claude', { claude: customOverride }),
320+
onFormUpdate: mockOnFormUpdate,
321+
}),
322+
);
323+
324+
act(() => {
325+
result.current.handleResetPreset('claude');
326+
});
327+
328+
expect(mockOnFormUpdate).toHaveBeenCalledWith('providerPresets', {});
329+
});
330+
});
331+
332+
describe('closeDeleteWarning', () => {
333+
it('should clear delete warning', () => {
334+
const customPreset: IProviderPreset = { name: 'Custom', command: 'custom' };
335+
336+
const { result } = renderHook(() =>
337+
usePresetManagement({
338+
form: makeForm('custom', { custom: customPreset }, { executor: 'custom' }),
339+
onFormUpdate: mockOnFormUpdate,
340+
}),
341+
);
342+
343+
act(() => {
344+
result.current.handleDeletePreset('custom');
345+
});
346+
expect(result.current.deleteWarning).not.toBeNull();
347+
348+
act(() => {
349+
result.current.closeDeleteWarning();
350+
});
351+
expect(result.current.deleteWarning).toBeNull();
352+
});
353+
});
354+
});

0 commit comments

Comments
 (0)