Skip to content

Commit 1dd9575

Browse files
authored
Merge pull request finos#1363 from niamhg-ms/Issue-1354
[Issue finos#1354] - Implementing new sidebar navigation
2 parents d0ddd14 + 4b22c32 commit 1dd9575

File tree

7 files changed

+470
-267
lines changed

7 files changed

+470
-267
lines changed

calm-hub-ui/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"serve": "vite preview",
1010
"prod": "npm run build && mkdir -p ../calm-hub/src/main/resources/META-INF/resources && rsync -a build/* ../calm-hub/src/main/resources/META-INF/resources",
1111
"test": "vitest run",
12+
"test-coverage": "vitest run --coverage",
1213
"watch-test": "vitest watch",
1314
"lint": "eslint .",
1415
"format": "prettier --write .",
@@ -20,6 +21,9 @@
2021
"@types/react": "^18.3.12",
2122
"@types/react-dom": "^18.3.1",
2223
"@vitejs/plugin-react-swc": "^3.7.2",
24+
"@vitest/coverage-v8": "^3.1.4",
25+
"axios": "^1.9.0",
26+
"axios-mock-adapter": "^2.1.0",
2327
"copyfiles": "^2.4.1",
2428
"cytoscape": "^3.30.3",
2529
"cytoscape-cola": "^2.5.1",
@@ -29,14 +33,12 @@
2933
"react-dom": "^19.1.0",
3034
"react-icons": "^5.5.0",
3135
"react-json-view-lite": "^2.4.1",
36+
"react-markdown": "^10.1.0",
3237
"react-router-dom": "^7.6.0",
3338
"typescript": "^4.9.5",
3439
"vite-plugin-svgr": "^4.3.0",
3540
"vite-tsconfig-paths": "^5.1.4",
36-
"web-vitals": "^5.0.1",
37-
"react-markdown": "^10.1.0",
38-
"axios": "^1.9.0",
39-
"axios-mock-adapter": "^2.1.0"
41+
"web-vitals": "^5.0.1"
4042
},
4143
"devDependencies": {
4244
"@eslint/js": "^9.25.0",

calm-hub-ui/src/hub/Hub.test.tsx

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { BrowserRouter } from 'react-router-dom';
4+
import Hub from './Hub.js';
5+
import { vi, describe, it, expect } from 'vitest';
6+
7+
vi.mock('./components/value-table/ValueTable', () => ({
8+
ValueTable: ({
9+
header,
10+
values,
11+
callback,
12+
}: {
13+
header: string;
14+
values: string[];
15+
callback: (v: string) => void;
16+
}) => (
17+
<div data-testid="value-table">
18+
<div>{header}</div>
19+
{values.map((v: string) => (
20+
<button key={v} onClick={() => callback(v)}>
21+
{v}
22+
</button>
23+
))}
24+
</div>
25+
),
26+
}));
27+
28+
vi.mock('./components/json-renderer/JsonRenderer', () => ({
29+
JsonRenderer: ({ json }: { json: unknown }) => (
30+
<div data-testid="json-renderer">{json ? 'JSON' : ''}</div>
31+
),
32+
}));
33+
34+
vi.mock('./components/adr-renderer/AdrRenderer', () => ({
35+
AdrRenderer: ({ adrDetails }: { adrDetails: { id?: string } }) => (
36+
<div data-testid="adr-renderer">ADR: {adrDetails?.id}</div>
37+
),
38+
}));
39+
40+
vi.mock('../components/navbar/Navbar', () => ({
41+
Navbar: () => <nav data-testid="navbar">Navbar</nav>,
42+
}));
43+
44+
// Mock calm-service and adr-service
45+
vi.mock('../service/calm-service', () => ({
46+
fetchNamespaces: (cb: (v: string[]) => void) => cb(['namespace1', 'namespace2']),
47+
fetchPatternIDs: (namespace: string, cb: (v: string[]) => void) => cb(['pattern1', 'pattern2']),
48+
fetchFlowIDs: (namespace: string, cb: (v: string[]) => void) => cb(['flow1']),
49+
fetchArchitectureIDs: (namespace: string, cb: (v: string[]) => void) => cb(['arch1']),
50+
fetchPatternVersions: (namespace: string, id: string, cb: (v: string[]) => void) =>
51+
cb(['1.0', '2.0']),
52+
fetchFlowVersions: (namespace: string, id: string, cb: (v: string[]) => void) => cb(['1.1']),
53+
fetchArchitectureVersions: (namespace: string, id: string, cb: (v: string[]) => void) =>
54+
cb(['2.1']),
55+
fetchPattern: (
56+
namespace: string,
57+
id: string,
58+
v: string,
59+
cb: (data: { id: string; v: string }) => void
60+
) => cb({ id, v }),
61+
fetchFlow: (
62+
namespace: string,
63+
id: string,
64+
v: string,
65+
cb: (data: { id: string; v: string }) => void
66+
) => cb({ id, v }),
67+
fetchArchitecture: (
68+
namespace: string,
69+
id: string,
70+
v: string,
71+
cb: (data: { id: string; v: string }) => void
72+
) => cb({ id, v }),
73+
}));
74+
75+
vi.mock('../service/adr-service/adr-service', () => {
76+
class MockAdrService {
77+
fetchAdrIDs = vi.fn(async () => ['adr1', 'adr2']);
78+
fetchAdrRevisions = vi.fn(async () => ['rev1', 'rev2']);
79+
fetchAdr = vi.fn(async () => ({ id: 'adr1', revision: 'rev1' }));
80+
}
81+
return { AdrService: MockAdrService };
82+
});
83+
84+
// Helper to render with router
85+
const renderWithRouter = (ui: React.ReactElement) => {
86+
return render(<BrowserRouter>{ui}</BrowserRouter>);
87+
};
88+
89+
describe('Hub', () => {
90+
it('renders Navbar and initial namespaces', async () => {
91+
renderWithRouter(<Hub />);
92+
expect(screen.getByTestId('navbar')).toBeInTheDocument();
93+
expect(screen.getAllByText('Namespaces').length).toBeGreaterThanOrEqual(1);
94+
expect(screen.getByRole('button', { name: 'namespace1' })).toBeInTheDocument();
95+
expect(screen.getByRole('button', { name: 'namespace2' })).toBeInTheDocument();
96+
});
97+
98+
it('shows Calm Type options after namespace selection', async () => {
99+
renderWithRouter(<Hub />);
100+
fireEvent.click(screen.getByText('namespace1'));
101+
expect(await screen.findByText('Calm Type')).toBeInTheDocument();
102+
expect(screen.getByText('Architectures')).toBeInTheDocument();
103+
expect(screen.getByText('Patterns')).toBeInTheDocument();
104+
expect(screen.getByText('Flows')).toBeInTheDocument();
105+
expect(screen.getByText('ADRs')).toBeInTheDocument();
106+
});
107+
108+
it('shows resource IDs after Calm Type selection', async () => {
109+
renderWithRouter(<Hub />);
110+
fireEvent.click(screen.getByText('namespace1'));
111+
fireEvent.click(screen.getByText('Patterns'));
112+
expect(screen.getAllByText('Patterns').length).toBeGreaterThanOrEqual(1);
113+
expect(screen.getByRole('button', { name: 'pattern1' })).toBeInTheDocument();
114+
expect(screen.getByRole('button', { name: 'pattern2' })).toBeInTheDocument();
115+
});
116+
117+
it('shows versions after pattern resource selection', async () => {
118+
renderWithRouter(<Hub />);
119+
fireEvent.click(screen.getByText('namespace1'));
120+
fireEvent.click(screen.getByText('Patterns'));
121+
fireEvent.click(screen.getByText('pattern1'));
122+
expect(await screen.findByText('Versions')).toBeInTheDocument();
123+
expect(await screen.findByRole('button', { name: '1.0' })).toBeInTheDocument();
124+
expect(screen.getByRole('button', { name: '2.0' })).toBeInTheDocument();
125+
});
126+
127+
it('shows versions after architecture resource selection', async () => {
128+
renderWithRouter(<Hub />);
129+
fireEvent.click(screen.getByText('namespace1'));
130+
fireEvent.click(screen.getByText('Architectures'));
131+
fireEvent.click(screen.getByText('arch1'));
132+
expect(await screen.findByText('Versions')).toBeInTheDocument();
133+
expect(await screen.findByRole('button', { name: '2.1' })).toBeInTheDocument();
134+
});
135+
136+
it('shows versions after flow resource selection', async () => {
137+
renderWithRouter(<Hub />);
138+
fireEvent.click(screen.getByText('namespace1'));
139+
fireEvent.click(screen.getByText('Flows'));
140+
fireEvent.click(screen.getByText('flow1'));
141+
expect(await screen.findByText('Versions')).toBeInTheDocument();
142+
expect(await screen.findByRole('button', { name: '1.1' })).toBeInTheDocument();
143+
});
144+
145+
it('renders JsonRenderer after pattern version selection', async () => {
146+
renderWithRouter(<Hub />);
147+
fireEvent.click(screen.getByText('namespace1'));
148+
fireEvent.click(screen.getByText('Patterns'));
149+
fireEvent.click(screen.getByText('pattern1'));
150+
fireEvent.click(screen.getByText('1.0'));
151+
152+
expect(await screen.findByTestId('json-renderer')).toBeInTheDocument();
153+
});
154+
155+
it('renders JsonRenderer after architecture version selection', async () => {
156+
renderWithRouter(<Hub />);
157+
fireEvent.click(screen.getByText('namespace1'));
158+
fireEvent.click(screen.getByText('Architectures'));
159+
fireEvent.click(screen.getByText('arch1'));
160+
fireEvent.click(screen.getByText('2.1'));
161+
162+
expect(await screen.findByTestId('json-renderer')).toBeInTheDocument();
163+
});
164+
165+
it('renders JsonRenderer after flow version selection', async () => {
166+
renderWithRouter(<Hub />);
167+
fireEvent.click(screen.getByText('namespace1'));
168+
fireEvent.click(screen.getByText('Flows'));
169+
fireEvent.click(screen.getByText('flow1'));
170+
fireEvent.click(screen.getByText('1.1'));
171+
172+
expect(await screen.findByTestId('json-renderer')).toBeInTheDocument();
173+
});
174+
175+
it('shows ADRs and revisions, and renders AdrRenderer', async () => {
176+
renderWithRouter(<Hub />);
177+
fireEvent.click(screen.getByText('namespace1'));
178+
fireEvent.click(screen.getByText('ADRs'));
179+
180+
expect(await screen.findByText('adr1')).toBeInTheDocument();
181+
fireEvent.click(screen.getByText('adr1'));
182+
183+
expect(await screen.findByText('rev1')).toBeInTheDocument();
184+
fireEvent.click(screen.getByText('rev1'));
185+
186+
expect(await screen.findByTestId('adr-renderer')).toBeInTheDocument();
187+
});
188+
189+
it('does not render ADR Renderer before revision selection', async () => {
190+
renderWithRouter(<Hub />);
191+
fireEvent.click(screen.getByText('namespace1'));
192+
fireEvent.click(screen.getByText('ADRs'));
193+
194+
expect(await screen.findByText('adr1')).toBeInTheDocument();
195+
fireEvent.click(screen.getByText('adr1'));
196+
197+
expect(screen.queryByTestId('adr-renderer')).not.toBeInTheDocument();
198+
expect(screen.getByText('Please select an ADR to load')).toBeInTheDocument();
199+
});
200+
201+
it('shows breadcrumbs and resets when namespaces is clicked', async () => {
202+
renderWithRouter(<Hub />);
203+
fireEvent.click(screen.getByText('namespace1'));
204+
fireEvent.click(screen.getByText('Patterns'));
205+
fireEvent.click(screen.getByText('pattern1'));
206+
expect(screen.getByText('Namespaces')).toBeInTheDocument();
207+
fireEvent.click(screen.getByText('Namespaces'));
208+
expect(await screen.findByText('namespace1')).toBeInTheDocument();
209+
expect(screen.queryByText('pattern1')).not.toBeInTheDocument();
210+
});
211+
212+
it('shows breadcrumbs and resets to show CALM types when namespace is clicked', async () => {
213+
renderWithRouter(<Hub />);
214+
fireEvent.click(screen.getByText('namespace1'));
215+
fireEvent.click(screen.getByText('Patterns'));
216+
fireEvent.click(screen.getByText('pattern1'));
217+
fireEvent.click(screen.getByText('namespace1'));
218+
219+
expect(await screen.findByText('namespace1')).toBeInTheDocument();
220+
expect(screen.queryByText('Calm Type')).toBeInTheDocument();
221+
expect(screen.queryByText('pattern1')).not.toBeInTheDocument();
222+
});
223+
224+
it('shows breadcrumbs and resets to show the list of Patterns when Patterns is clicked', async () => {
225+
renderWithRouter(<Hub />);
226+
fireEvent.click(screen.getByText('namespace1'));
227+
fireEvent.click(screen.getByText('Patterns'));
228+
fireEvent.click(screen.getByText('pattern1'));
229+
fireEvent.click(screen.getByText('Patterns'));
230+
231+
expect(await screen.findByText('namespace1')).toBeInTheDocument();
232+
expect(screen.queryByText('pattern1')).toBeInTheDocument();
233+
expect(screen.queryByText('Calm Type')).not.toBeInTheDocument();
234+
});
235+
});

0 commit comments

Comments
 (0)