Skip to content

Commit 4b7187e

Browse files
committed
refactor(web): improve WifiConnectionForm
Also include missing files in previous commits
1 parent cbeb72e commit 4b7187e

File tree

4 files changed

+256
-63
lines changed

4 files changed

+256
-63
lines changed

web/src/components/network/WifiConnectionForm.test.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,21 @@ import WifiConnectionForm from "./WifiConnectionForm";
2727
import { WifiNetworkStatus } from "~/types/network";
2828

2929
const mockUpdateConnection = jest.fn();
30+
const mockUseWifiNetworks = jest.fn();
31+
3032
jest.mock("~/hooks/model/config/network", () => ({
3133
...jest.requireActual("~/hooks/model/config/network"),
3234
useConnectionMutation: () => ({
3335
mutateAsync: mockUpdateConnection,
3436
}),
3537
}));
3638

39+
jest.mock("~/hooks/model/system/network", () => ({
40+
...jest.requireActual("~/hooks/model/system/network"),
41+
useNetworkChanges: jest.fn(),
42+
useWifiNetworks: () => mockUseWifiNetworks(),
43+
}));
44+
3745
const visibleNetwork = {
3846
ssid: "Visible Network",
3947
hidden: false,
@@ -50,15 +58,17 @@ const publicNetwork = {
5058
security: [],
5159
};
5260

53-
jest.mock("~/hooks/model/system/network", () => ({
54-
...jest.requireActual("~/hooks/model/system/network"),
55-
useWifiNetworks: () => [visibleNetwork, publicNetwork],
56-
}));
57-
5861
describe("WifiConnectionForm", () => {
5962
beforeEach(() => {
6063
jest.clearAllMocks();
6164
mockUpdateConnection.mockResolvedValue(undefined);
65+
mockUseWifiNetworks.mockReturnValue([visibleNetwork, publicNetwork]);
66+
});
67+
68+
it("renders an empty state when no networks are found", () => {
69+
mockUseWifiNetworks.mockReturnValue([]);
70+
installerRender(<WifiConnectionForm />, { withL10n: true });
71+
screen.getByText("No Wi-Fi networks were found");
6272
});
6373

6474
it("renders the network selector", () => {
@@ -69,16 +79,14 @@ describe("WifiConnectionForm", () => {
6979
describe("when a public network is selected", () => {
7080
it("warns the user about connecting to an unprotected network", async () => {
7181
const { user } = installerRender(<WifiConnectionForm />, { withL10n: true });
72-
const selector = screen.getByRole("combobox", { name: "Network" });
73-
await user.selectOptions(selector, "Public Network");
82+
await user.selectOptions(screen.getByRole("combobox", { name: "Network" }), "Public Network");
7483
screen.getByText("Warning alert:");
7584
screen.getByText("Not protected network");
7685
});
7786

7887
it("does not render the security selector", async () => {
7988
const { user } = installerRender(<WifiConnectionForm />, { withL10n: true });
80-
const selector = screen.getByRole("combobox", { name: "Network" });
81-
await user.selectOptions(selector, "Public Network");
89+
await user.selectOptions(screen.getByRole("combobox", { name: "Network" }), "Public Network");
8290
expect(screen.queryByRole("combobox", { name: "Security" })).toBeNull();
8391
});
8492
});
@@ -118,7 +126,6 @@ describe("WifiConnectionForm", () => {
118126
const passwordInput = screen.getByLabelText("WPA Password");
119127
await user.type(passwordInput, "wifi-password");
120128
await user.click(screen.getByRole("button", { name: "Connect" }));
121-
122129
await waitFor(() => {
123130
expect(mockUpdateConnection).toHaveBeenCalledWith(
124131
expect.objectContaining({
@@ -130,7 +137,6 @@ describe("WifiConnectionForm", () => {
130137
}),
131138
);
132139
});
133-
134140
expect(mockNavigateFn).toHaveBeenCalledWith("/network");
135141
});
136142
});

web/src/components/network/WifiConnectionForm.tsx

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ import {
2626
ActionGroup,
2727
Alert,
2828
Content,
29+
EmptyState,
2930
Form,
3031
FormGroup,
3132
FormSelect,
3233
FormSelectOption,
3334
} from "@patternfly/react-core";
35+
import Icon from "~/components/layout/Icon";
3436
import { Page, PasswordInput } from "~/components/core";
3537
import { Connection, WifiNetwork, Wireless } from "~/types/network";
3638
import { useWifiNetworks } from "~/hooks/model/system/network";
@@ -98,7 +100,7 @@ const formReducer = (state: FormState, action: FormAction): FormState => {
98100
}
99101
};
100102

101-
export default function WifiConnectionForm() {
103+
function WifiConnectionFormContent() {
102104
const navigate = useNavigate();
103105
const networks = useWifiNetworks();
104106
const [form, dispatch] = useReducer(formReducer, { ssid: "", security: "", password: "" });
@@ -127,6 +129,69 @@ export default function WifiConnectionForm() {
127129
navigate(PATHS.root);
128130
};
129131

132+
if (networks.length === 0)
133+
return (
134+
<EmptyState
135+
titleText={_("No Wi-Fi networks were found")}
136+
icon={() => <Icon name="error" />}
137+
/>
138+
);
139+
140+
return (
141+
<Form id="wifiConnectionForm" onSubmit={accept} aria-label={_("Wi-Fi connection form")}>
142+
<FormGroup fieldId="ssid" label={_("Network")}>
143+
<WifiNetworksSelector
144+
id="ssid"
145+
value={form.ssid}
146+
onChange={(_, v) => dispatch({ type: "SET_SSID", ssid: v, networks })}
147+
/>
148+
</FormGroup>
149+
150+
{isPublicNetwork && <PublicNetworkAlert />}
151+
{/* TRANSLATORS: Wifi security configuration (password protected or not) */}
152+
{!isEmpty(network?.security) && (
153+
<FormGroup fieldId="security" label={_("Security")}>
154+
<FormSelect
155+
id="security"
156+
aria-label={_("Security")}
157+
value={form.security}
158+
onChange={(_, v) => dispatch({ type: "SET_SECURITY", security: v })}
159+
>
160+
{securityOptions.map((security) => (
161+
<FormSelectOption
162+
key={security.value}
163+
value={security.value}
164+
/* eslint-disable agama-i18n/string-literals */
165+
label={_(security.label)}
166+
/>
167+
))}
168+
</FormSelect>
169+
</FormGroup>
170+
)}
171+
{form.security === "wpa-psk" && (
172+
// TRANSLATORS: WiFi password
173+
<FormGroup fieldId="password" label={_("WPA Password")}>
174+
<PasswordInput
175+
id="password"
176+
name="password"
177+
aria-label={_("Password")}
178+
value={form.password}
179+
onChange={(_, v) => dispatch({ type: "SET_PASSWORD", password: v })}
180+
/>
181+
</FormGroup>
182+
)}
183+
<ActionGroup>
184+
<Page.Submit form="wifiConnectionForm">
185+
{/* TRANSLATORS: button label, connect to a Wi-Fi network */}
186+
{_("Connect")}
187+
</Page.Submit>
188+
<Page.Back>{_("Cancel")}</Page.Back>
189+
</ActionGroup>
190+
</Form>
191+
);
192+
}
193+
194+
export default function wifiConnectionForm() {
130195
return (
131196
<Page
132197
breadcrumbs={[
@@ -136,57 +201,7 @@ export default function WifiConnectionForm() {
136201
progress={{ scope: "network", ensureRefetched: "system" }}
137202
>
138203
<Page.Content>
139-
{/** TRANSLATORS: accessible name for the WiFi connection form */}
140-
<Form id="wifiConnectionForm" onSubmit={accept} aria-label={_("Wi-Fi connection form")}>
141-
<FormGroup fieldId="ssid" label={_("Network")}>
142-
<WifiNetworksSelector
143-
id="ssid"
144-
value={form.ssid}
145-
onChange={(_, v) => dispatch({ type: "SET_SSID", ssid: v, networks })}
146-
/>
147-
</FormGroup>
148-
149-
{isPublicNetwork && <PublicNetworkAlert />}
150-
{/* TRANSLATORS: Wifi security configuration (password protected or not) */}
151-
{!isEmpty(network?.security) && (
152-
<FormGroup fieldId="security" label={_("Security")}>
153-
<FormSelect
154-
id="security"
155-
aria-label={_("Security")}
156-
value={form.security}
157-
onChange={(_, v) => dispatch({ type: "SET_SECURITY", security: v })}
158-
>
159-
{securityOptions.map((security) => (
160-
<FormSelectOption
161-
key={security.value}
162-
value={security.value}
163-
/* eslint-disable agama-i18n/string-literals */
164-
label={_(security.label)}
165-
/>
166-
))}
167-
</FormSelect>
168-
</FormGroup>
169-
)}
170-
{form.security === "wpa-psk" && (
171-
// TRANSLATORS: WiFi password
172-
<FormGroup fieldId="password" label={_("WPA Password")}>
173-
<PasswordInput
174-
id="password"
175-
name="password"
176-
aria-label={_("Password")}
177-
value={form.password}
178-
onChange={(_, v) => dispatch({ type: "SET_PASSWORD", password: v })}
179-
/>
180-
</FormGroup>
181-
)}
182-
<ActionGroup>
183-
<Page.Submit form="wifiConnectionForm">
184-
{/* TRANSLATORS: button label, connect to a Wi-Fi network */}
185-
{_("Connect")}
186-
</Page.Submit>
187-
<Page.Back>{_("Cancel")}</Page.Back>
188-
</ActionGroup>
189-
</Form>
204+
<WifiConnectionFormContent />
190205
</Page.Content>
191206
</Page>
192207
);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) [2026] SUSE LLC
3+
*
4+
* All Rights Reserved.
5+
*
6+
* This program is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License as published by the Free
8+
* Software Foundation; either version 2 of the License, or (at your option)
9+
* any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14+
* more details.
15+
*
16+
* You should have received a copy of the GNU General Public License along
17+
* with this program; if not, contact SUSE LLC.
18+
*
19+
* To contact SUSE LLC about this file by physical or electronic mail, you may
20+
* find current contact information at www.suse.com.
21+
*/
22+
23+
import React from "react";
24+
import { screen } from "@testing-library/react";
25+
import { installerRender } from "~/test-utils";
26+
import WifiNetworksSelector from "./WifiNetworksSelector";
27+
import { WifiNetworkStatus } from "~/types/network";
28+
29+
const connectedNetwork = {
30+
ssid: "Connected Network",
31+
hidden: false,
32+
deviceName: "wlan0",
33+
status: WifiNetworkStatus.CONNECTED,
34+
hwAddress: "00:EB:D8:17:6B:56",
35+
security: ["WPA2"],
36+
strength: 75,
37+
};
38+
39+
const configuredNetwork = {
40+
ssid: "Configured Network",
41+
hidden: false,
42+
deviceName: "wlan0",
43+
status: WifiNetworkStatus.CONFIGURED,
44+
hwAddress: "00:EB:D8:17:6B:57",
45+
security: ["WPA2"],
46+
strength: 50,
47+
};
48+
49+
const notConfiguredNetwork = {
50+
ssid: "Not Configured Network",
51+
hidden: false,
52+
deviceName: "wlan0",
53+
status: WifiNetworkStatus.NOT_CONFIGURED,
54+
hwAddress: "00:EB:D8:17:6B:58",
55+
security: [],
56+
strength: 25,
57+
};
58+
59+
const weakNetwork = {
60+
ssid: "Weak Network",
61+
hidden: false,
62+
deviceName: "wlan0",
63+
status: WifiNetworkStatus.NOT_CONFIGURED,
64+
hwAddress: "00:EB:D8:17:6B:59",
65+
security: [],
66+
strength: 10,
67+
};
68+
69+
const mockUseWifiNetworks = jest.fn();
70+
71+
jest.mock("~/hooks/model/system/network", () => ({
72+
...jest.requireActual("~/hooks/model/system/network"),
73+
useNetworkChanges: jest.fn(),
74+
useWifiNetworks: () => mockUseWifiNetworks(),
75+
}));
76+
77+
describe("WifiNetworksSelector", () => {
78+
beforeEach(() => {
79+
mockUseWifiNetworks.mockReturnValue([
80+
notConfiguredNetwork,
81+
weakNetwork,
82+
configuredNetwork,
83+
connectedNetwork,
84+
]);
85+
});
86+
87+
it("renders all available networks as options", () => {
88+
installerRender(<WifiNetworksSelector onChange={jest.fn()} />);
89+
const selector = screen.getByRole("combobox");
90+
expect(selector).toBeInTheDocument();
91+
screen.getByRole("option", { name: "Connected Network" });
92+
screen.getByRole("option", { name: "Configured Network" });
93+
screen.getByRole("option", { name: "Not Configured Network" });
94+
screen.getByRole("option", { name: "Weak Network" });
95+
});
96+
97+
it("sorts networks by status first, then by signal strength", () => {
98+
installerRender(<WifiNetworksSelector onChange={jest.fn()} />);
99+
const options = screen.getAllByRole("option").map((o) => o.textContent);
100+
expect(options).toEqual([
101+
"Connected Network",
102+
"Configured Network",
103+
"Not Configured Network",
104+
"Weak Network",
105+
]);
106+
});
107+
108+
it("renders with the given value selected", () => {
109+
installerRender(<WifiNetworksSelector value="Configured Network" onChange={jest.fn()} />);
110+
expect(screen.getByRole("combobox")).toHaveValue("Configured Network");
111+
});
112+
});

0 commit comments

Comments
 (0)