Skip to content

Commit cd99b3a

Browse files
upcoming: [UIE-10427] - Reserved IP: Implement IP Address Selection component (#13582)
* upcoming: [UIE-10427] - Reserved IP: Implement IP Address Selection component. * Address review comments. * Address review comments.
1 parent 0b1e0f0 commit cd99b3a

2 files changed

Lines changed: 529 additions & 0 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import userEvent from '@testing-library/user-event';
2+
import React from 'react';
3+
4+
import { reservedIPsFactory } from 'src/factories';
5+
import { makeResourcePage } from 'src/mocks/serverHandlers';
6+
import { http, HttpResponse, server } from 'src/mocks/testServer';
7+
import { renderWithTheme } from 'src/utilities/testHelpers';
8+
9+
import { IPAddressSelection } from './IPAddressSelection';
10+
11+
import type { IPAddress } from '@linode/api-v4';
12+
13+
describe('IPAddressSelection', () => {
14+
describe('Component Rendering', () => {
15+
it('should render the IP Address label', () => {
16+
const { getByText } = renderWithTheme(<IPAddressSelection />);
17+
expect(getByText('IP Address')).toBeInTheDocument();
18+
});
19+
20+
it('should render Auto-assigned radio button by default', () => {
21+
const { getAllByRole } = renderWithTheme(<IPAddressSelection />);
22+
const autoRadio = getAllByRole('radio', { name: /Auto-assigned/i })[0];
23+
expect(autoRadio).toBeInTheDocument();
24+
expect(autoRadio).toBeChecked();
25+
});
26+
27+
it('should render Reserved radio button', () => {
28+
const { getAllByRole } = renderWithTheme(<IPAddressSelection />);
29+
const reservedRadio = getAllByRole('radio', { name: /Reserved/i })[0];
30+
expect(reservedRadio).toBeInTheDocument();
31+
expect(reservedRadio).not.toBeChecked();
32+
});
33+
34+
it('should render tooltips for both radio options', async () => {
35+
const { findByText, getAllByTestId } = renderWithTheme(
36+
<IPAddressSelection />
37+
);
38+
const tooltipIcons = getAllByTestId('tooltip-info-icon');
39+
expect(tooltipIcons).toHaveLength(2);
40+
await userEvent.hover(tooltipIcons[0]);
41+
expect(
42+
await findByText(
43+
"A public IPv4 address automatically assigned to your Linode. Use this for standard web traffic that doesn't require a permanent, static IP."
44+
)
45+
).toBeInTheDocument();
46+
await userEvent.unhover(tooltipIcons[0]);
47+
await userEvent.hover(tooltipIcons[1]);
48+
expect(
49+
await findByText(
50+
"A reserved IPv4 address is a static public IP that can be assigned to Linodes in the same region. Use it for services that require a consistent IP address. Charges apply while the IP is reserved, even if it's not assigned to a Linode."
51+
)
52+
).toBeInTheDocument();
53+
});
54+
55+
it('should not show reserved IP dropdown by default', () => {
56+
const { queryByLabelText } = renderWithTheme(<IPAddressSelection />);
57+
expect(queryByLabelText('Reserved IP Address')).not.toBeInTheDocument();
58+
});
59+
});
60+
61+
describe('Mode Selection', () => {
62+
it('should call onIPModeChange when mode changes', async () => {
63+
const onIPModeChange = vi.fn();
64+
const { getAllByRole } = renderWithTheme(
65+
<IPAddressSelection onIPModeChange={onIPModeChange} />
66+
);
67+
68+
const reservedRadio = getAllByRole('radio', { name: /Reserved/i })[0];
69+
await userEvent.click(reservedRadio);
70+
71+
expect(onIPModeChange).toHaveBeenCalledWith('reserved');
72+
});
73+
74+
it('should call onReservedIPSelect with null when switching to auto mode', async () => {
75+
const onReservedIPSelect = vi.fn();
76+
let currentValue: 'auto' | 'reserved' = 'reserved';
77+
const handleIPModeChange = (mode: 'auto' | 'reserved') => {
78+
currentValue = mode;
79+
// Simulate clearing the selected IP when switching to auto
80+
onReservedIPSelect(null);
81+
};
82+
83+
const { getAllByRole, rerender } = renderWithTheme(
84+
<IPAddressSelection
85+
mode={currentValue}
86+
onIPModeChange={handleIPModeChange}
87+
onReservedIPSelect={onReservedIPSelect}
88+
/>
89+
);
90+
91+
const autoRadio = getAllByRole('radio', { name: /Auto-assigned/i })[0];
92+
await userEvent.click(autoRadio);
93+
94+
// Re-render with updated value
95+
rerender(
96+
<IPAddressSelection
97+
mode={currentValue}
98+
onIPModeChange={handleIPModeChange}
99+
onReservedIPSelect={onReservedIPSelect}
100+
/>
101+
);
102+
103+
expect(onReservedIPSelect).toHaveBeenCalledWith(null);
104+
});
105+
});
106+
107+
describe('Reserved IP Dropdown', () => {
108+
it('should show helper text when no region is selected', async () => {
109+
const { getByLabelText, getByText } = renderWithTheme(
110+
<IPAddressSelection mode="reserved" />
111+
);
112+
113+
const dropdown = getByLabelText('Reserved IP Address');
114+
expect(dropdown).toBeInTheDocument();
115+
expect(
116+
getByText('Select a region to see available reserved IPs.')
117+
).toBeInTheDocument();
118+
});
119+
120+
it('should fetch and display unassigned reserved IPs for the selected region', async () => {
121+
const reservedIPs: IPAddress[] = [
122+
reservedIPsFactory.build({
123+
address: '192.0.2.1',
124+
assigned_entity: null,
125+
region: 'us-east',
126+
}),
127+
reservedIPsFactory.build({
128+
address: '192.0.2.2',
129+
assigned_entity: null,
130+
region: 'us-east',
131+
}),
132+
reservedIPsFactory.build({
133+
address: '192.0.2.3',
134+
assigned_entity: {
135+
id: 123,
136+
label: 'test-linode',
137+
type: 'linode',
138+
url: '/linodes/123',
139+
},
140+
region: 'us-east',
141+
}),
142+
];
143+
144+
server.use(
145+
http.get('*/v4beta/networking/reserved/ips', () => {
146+
return HttpResponse.json(makeResourcePage(reservedIPs));
147+
})
148+
);
149+
150+
const { getByLabelText, getByText, queryByText } = renderWithTheme(
151+
<IPAddressSelection mode="reserved" regionId="us-east" />
152+
);
153+
154+
const dropdown = getByLabelText('Reserved IP Address');
155+
await userEvent.click(dropdown);
156+
157+
// Should show unassigned IPs
158+
expect(getByText('192.0.2.1')).toBeInTheDocument();
159+
expect(getByText('192.0.2.2')).toBeInTheDocument();
160+
161+
// Should not show assigned IP
162+
expect(queryByText('192.0.2.3')).not.toBeInTheDocument();
163+
});
164+
165+
it('should filter reserved IPs by region', async () => {
166+
const reservedIPs: IPAddress[] = [
167+
reservedIPsFactory.build({
168+
address: '192.0.2.1',
169+
assigned_entity: null,
170+
region: 'us-east',
171+
}),
172+
reservedIPsFactory.build({
173+
address: '192.0.2.2',
174+
assigned_entity: null,
175+
region: 'us-west',
176+
}),
177+
];
178+
179+
server.use(
180+
http.get('*/v4beta/networking/reserved/ips', () => {
181+
return HttpResponse.json(makeResourcePage(reservedIPs));
182+
})
183+
);
184+
185+
const { getByLabelText, getByText, queryByText } = renderWithTheme(
186+
<IPAddressSelection mode="reserved" regionId="us-east" />
187+
);
188+
189+
const dropdown = getByLabelText('Reserved IP Address');
190+
await userEvent.click(dropdown);
191+
192+
// Should only show IPs in us-east
193+
expect(getByText('192.0.2.1')).toBeInTheDocument();
194+
expect(queryByText('192.0.2.2')).not.toBeInTheDocument();
195+
});
196+
197+
it('should show "no options" message when no reserved IPs are available', async () => {
198+
server.use(
199+
http.get('*/v4beta/networking/reserved/ips', () => {
200+
return HttpResponse.json(makeResourcePage([]));
201+
})
202+
);
203+
204+
const { getByLabelText, getByText } = renderWithTheme(
205+
<IPAddressSelection mode="reserved" regionId="us-east" />
206+
);
207+
208+
const dropdown = getByLabelText('Reserved IP Address');
209+
await userEvent.click(dropdown);
210+
211+
expect(
212+
getByText('There are no available reserved IPs in the selected region.')
213+
).toBeInTheDocument();
214+
});
215+
216+
it('should call onReservedIPSelect when an IP is selected', async () => {
217+
const onReservedIPSelect = vi.fn();
218+
const reservedIP = reservedIPsFactory.build({
219+
address: '192.0.2.1',
220+
assigned_entity: null,
221+
region: 'us-east',
222+
});
223+
224+
server.use(
225+
http.get('*/v4beta/networking/reserved/ips', () => {
226+
return HttpResponse.json(makeResourcePage([reservedIP]));
227+
})
228+
);
229+
230+
const { getByLabelText, getByText } = renderWithTheme(
231+
<IPAddressSelection
232+
mode="reserved"
233+
onReservedIPSelect={onReservedIPSelect}
234+
regionId="us-east"
235+
/>
236+
);
237+
238+
const dropdown = getByLabelText('Reserved IP Address');
239+
await userEvent.click(dropdown);
240+
241+
const ipOption = getByText('192.0.2.1');
242+
await userEvent.click(ipOption);
243+
244+
expect(onReservedIPSelect).toHaveBeenCalledWith({
245+
...reservedIP,
246+
label: '192.0.2.1',
247+
});
248+
});
249+
});
250+
251+
describe('Reserve IP Button', () => {
252+
it('should show Reserve IP button when in reserved mode', async () => {
253+
const { getAllByRole, getByText } = renderWithTheme(
254+
<IPAddressSelection mode="reserved" regionId="us-east" />
255+
);
256+
257+
const reservedRadio = getAllByRole('radio', { name: /Reserved/i })[0];
258+
await userEvent.click(reservedRadio);
259+
260+
expect(getByText('Reserve IP')).toBeInTheDocument();
261+
});
262+
263+
it('should not show Reserve IP button in auto mode', () => {
264+
const { queryByText } = renderWithTheme(<IPAddressSelection />);
265+
expect(queryByText('Reserve IP')).not.toBeInTheDocument();
266+
});
267+
268+
it('should open Reserve IP drawer when button is clicked', async () => {
269+
const { getByText, getByRole } = renderWithTheme(
270+
<IPAddressSelection mode="reserved" regionId="us-east" />
271+
);
272+
273+
const reserveButton = getByText('Reserve IP');
274+
await userEvent.click(reserveButton);
275+
276+
expect(
277+
getByRole('heading', { name: 'Reserve an IP Address' })
278+
).toBeInTheDocument();
279+
});
280+
});
281+
282+
describe('Loading States', () => {
283+
it('should show loading state while fetching reserved IPs', async () => {
284+
server.use(
285+
http.get('*/v4beta/networking/reserved/ips', async () => {
286+
await new Promise((resolve) => setTimeout(resolve, 100));
287+
return HttpResponse.json(makeResourcePage([]));
288+
})
289+
);
290+
291+
const { getByLabelText } = renderWithTheme(
292+
<IPAddressSelection mode="reserved" regionId="us-east" />
293+
);
294+
295+
const dropdown = getByLabelText('Reserved IP Address');
296+
await userEvent.click(dropdown);
297+
298+
// Component should show loading indicator (exact implementation may vary)
299+
expect(dropdown).toBeInTheDocument();
300+
});
301+
});
302+
303+
describe('Accessibility', () => {
304+
it('should have proper ARIA labels', () => {
305+
const { getByRole } = renderWithTheme(
306+
<IPAddressSelection mode="reserved" />
307+
);
308+
309+
const radioGroup = getByRole('radiogroup', { name: 'IP Address' });
310+
expect(radioGroup).toBeInTheDocument();
311+
});
312+
313+
it('should have proper radio group labeling', () => {
314+
const { getByRole } = renderWithTheme(<IPAddressSelection />);
315+
316+
const radioGroup = getByRole('radiogroup', { name: 'IP Address' });
317+
expect(radioGroup).toBeInTheDocument();
318+
});
319+
320+
it('should have accessible radio buttons', () => {
321+
const { getAllByRole } = renderWithTheme(<IPAddressSelection />);
322+
323+
const radios = getAllByRole('radio');
324+
expect(radios).toHaveLength(2);
325+
expect(radios[0]).toHaveAccessibleName(/Auto-assigned/i);
326+
expect(radios[1]).toHaveAccessibleName(/Reserved/i);
327+
});
328+
});
329+
});

0 commit comments

Comments
 (0)