Skip to content

Commit 436ad8f

Browse files
Merge branch 'main' into fix/2361-blank-preview-panel-vscode-1.116
2 parents a253d0d + e7eb331 commit 436ad8f

35 files changed

Lines changed: 3651 additions & 466 deletions

.github/workflows/build-calm-studio-desktop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ jobs:
116116
workspaces: calm-suite/calm-studio/apps/studio/src-tauri
117117

118118
- name: Download sidecar binary
119-
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
119+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
120120
with:
121121
name: sidecar-${{ matrix.triple }}
122122
path: calm-suite/calm-studio/apps/studio/src-tauri/binaries/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ docs/contributing.md
2929
/shared/package-lock.json
3030

3131
**/.vscode**/
32+
/.vscode/settings.json
3233

3334
node_modules/
3435
**/dist/

.vscode/settings.json

Lines changed: 0 additions & 24 deletions
This file was deleted.

calm-hub-ui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"eslint-plugin-react-refresh": "^0.5.0",
6161
"jsdom": "^26.0.0",
6262
"npm-force-resolutions": "^0.0.10",
63-
"postcss": "^8.4.49",
63+
"postcss": "^8.5.12",
6464
"prettier": "^3.3.3",
6565
"stylelint": "^16.26.0",
6666
"stylelint-config-standard": "^39.0.1",
@@ -74,7 +74,7 @@
7474
"css-what": "7.0.0",
7575
"express": "5.2.1",
7676
"nth-check": "2.1.1",
77-
"postcss": "8.5.8",
77+
"postcss": "8.5.12",
7878
"rollup": "4.60.1"
7979
},
8080
"overrides": {
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { render, screen, fireEvent, act } from '@testing-library/react';
3+
import { MemoryRouter } from 'react-router-dom';
4+
import { GlobalSearchBar } from './GlobalSearchBar.js';
5+
import { SearchService } from '../../service/search-service.js';
6+
import { GroupedSearchResults } from '../../model/search.js';
7+
8+
const emptyResults: GroupedSearchResults = {
9+
architectures: [],
10+
patterns: [],
11+
flows: [],
12+
standards: [],
13+
interfaces: [],
14+
controls: [],
15+
adrs: [],
16+
};
17+
18+
const mockResults: GroupedSearchResults = {
19+
architectures: [
20+
{ namespace: 'finos', id: 1, name: 'Test Architecture', description: 'A test architecture' },
21+
],
22+
patterns: [
23+
{ namespace: 'finos', id: 2, name: 'Test Pattern', description: 'A test pattern' },
24+
],
25+
flows: [],
26+
standards: [],
27+
interfaces: [],
28+
controls: [],
29+
adrs: [],
30+
};
31+
32+
function createMockSearchService(searchFn: (q: string) => Promise<GroupedSearchResults>) {
33+
return { search: searchFn } as unknown as SearchService;
34+
}
35+
36+
function renderSearchBar(searchService?: SearchService) {
37+
return render(
38+
<MemoryRouter>
39+
<GlobalSearchBar searchService={searchService} />
40+
</MemoryRouter>
41+
);
42+
}
43+
44+
describe('GlobalSearchBar', () => {
45+
beforeEach(() => {
46+
vi.useFakeTimers();
47+
});
48+
49+
afterEach(() => {
50+
vi.useRealTimers();
51+
});
52+
53+
it('renders search input', () => {
54+
renderSearchBar();
55+
expect(screen.getByPlaceholderText('Search CALM Hub...')).toBeInTheDocument();
56+
});
57+
58+
it('debounces API calls', async () => {
59+
const searchFn = vi.fn().mockResolvedValue(emptyResults);
60+
const service = createMockSearchService(searchFn);
61+
renderSearchBar(service);
62+
63+
const input = screen.getByPlaceholderText('Search CALM Hub...');
64+
65+
await act(async () => {
66+
fireEvent.change(input, { target: { value: 't' } });
67+
fireEvent.change(input, { target: { value: 'te' } });
68+
fireEvent.change(input, { target: { value: 'tes' } });
69+
fireEvent.change(input, { target: { value: 'test' } });
70+
});
71+
72+
expect(searchFn).not.toHaveBeenCalled();
73+
74+
await act(async () => {
75+
vi.advanceTimersByTime(300);
76+
});
77+
78+
expect(searchFn).toHaveBeenCalledTimes(1);
79+
expect(searchFn).toHaveBeenCalledWith('test');
80+
});
81+
82+
it('displays grouped results', async () => {
83+
const searchFn = vi.fn().mockResolvedValue(mockResults);
84+
const service = createMockSearchService(searchFn);
85+
renderSearchBar(service);
86+
87+
const input = screen.getByPlaceholderText('Search CALM Hub...');
88+
89+
await act(async () => {
90+
fireEvent.change(input, { target: { value: 'test' } });
91+
});
92+
93+
await act(async () => {
94+
await vi.advanceTimersByTimeAsync(300);
95+
});
96+
97+
expect(screen.getByText('Test Architecture')).toBeInTheDocument();
98+
expect(screen.getByText('Test Pattern')).toBeInTheDocument();
99+
expect(screen.getByText('Architectures')).toBeInTheDocument();
100+
expect(screen.getByText('Patterns')).toBeInTheDocument();
101+
});
102+
103+
it('shows no results message when search returns empty', async () => {
104+
const searchFn = vi.fn().mockResolvedValue(emptyResults);
105+
const service = createMockSearchService(searchFn);
106+
renderSearchBar(service);
107+
108+
const input = screen.getByPlaceholderText('Search CALM Hub...');
109+
110+
await act(async () => {
111+
fireEvent.change(input, { target: { value: 'test' } });
112+
});
113+
114+
await act(async () => {
115+
await vi.advanceTimersByTimeAsync(300);
116+
});
117+
118+
expect(screen.getByText('No results found')).toBeInTheDocument();
119+
});
120+
121+
it('navigates with keyboard ArrowDown and Enter', async () => {
122+
const searchFn = vi.fn().mockResolvedValue(mockResults);
123+
const service = createMockSearchService(searchFn);
124+
renderSearchBar(service);
125+
126+
const input = screen.getByPlaceholderText('Search CALM Hub...');
127+
128+
await act(async () => {
129+
fireEvent.change(input, { target: { value: 'test' } });
130+
});
131+
132+
await act(async () => {
133+
await vi.advanceTimersByTimeAsync(300);
134+
});
135+
136+
await act(async () => {
137+
fireEvent.keyDown(input, { key: 'ArrowDown' });
138+
});
139+
140+
const firstOption = screen.getAllByRole('option')[0];
141+
expect(firstOption).toHaveAttribute('aria-selected', 'true');
142+
});
143+
144+
it('closes dropdown on Escape', async () => {
145+
const searchFn = vi.fn().mockResolvedValue(mockResults);
146+
const service = createMockSearchService(searchFn);
147+
renderSearchBar(service);
148+
149+
const input = screen.getByPlaceholderText('Search CALM Hub...');
150+
151+
await act(async () => {
152+
fireEvent.change(input, { target: { value: 'test' } });
153+
});
154+
155+
await act(async () => {
156+
await vi.advanceTimersByTimeAsync(300);
157+
});
158+
159+
await act(async () => {
160+
fireEvent.keyDown(input, { key: 'Escape' });
161+
});
162+
163+
expect(screen.queryByText('Test Architecture')).not.toBeInTheDocument();
164+
});
165+
166+
it('clears search on clear button click', async () => {
167+
const searchFn = vi.fn().mockResolvedValue(mockResults);
168+
const service = createMockSearchService(searchFn);
169+
renderSearchBar(service);
170+
171+
const input = screen.getByPlaceholderText('Search CALM Hub...');
172+
173+
await act(async () => {
174+
fireEvent.change(input, { target: { value: 'test' } });
175+
});
176+
177+
await act(async () => {
178+
await vi.advanceTimersByTimeAsync(300);
179+
});
180+
181+
const clearButton = screen.getByLabelText('Clear search');
182+
183+
await act(async () => {
184+
fireEvent.click(clearButton);
185+
});
186+
187+
expect(input).toHaveValue('');
188+
expect(screen.queryByText('Test Architecture')).not.toBeInTheDocument();
189+
});
190+
191+
it('handles API errors gracefully', async () => {
192+
const searchFn = vi.fn().mockRejectedValue(new Error('Network error'));
193+
const service = createMockSearchService(searchFn);
194+
renderSearchBar(service);
195+
196+
const input = screen.getByPlaceholderText('Search CALM Hub...');
197+
198+
await act(async () => {
199+
fireEvent.change(input, { target: { value: 'test' } });
200+
});
201+
202+
await act(async () => {
203+
vi.advanceTimersByTime(300);
204+
await vi.runAllTimersAsync();
205+
});
206+
207+
expect(screen.getByText('Search failed, please try again')).toBeInTheDocument();
208+
});
209+
210+
it('does not search when input is empty', async () => {
211+
const searchFn = vi.fn().mockResolvedValue(emptyResults);
212+
const service = createMockSearchService(searchFn);
213+
renderSearchBar(service);
214+
215+
const input = screen.getByPlaceholderText('Search CALM Hub...');
216+
217+
await act(async () => {
218+
fireEvent.change(input, { target: { value: ' ' } });
219+
});
220+
221+
await act(async () => {
222+
vi.advanceTimersByTime(300);
223+
});
224+
225+
expect(searchFn).not.toHaveBeenCalled();
226+
});
227+
});

0 commit comments

Comments
 (0)