#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)## Copyright (C) 2026 Red Hat, Inc.# SPDX-License-Identifier: LGPL-2.1-or-laterimport timeimport netlibimport testlibfrom dialoglib import DialogHelpersfrom lib.constants import TEST_OS_DEFAULT@testlib.nondestructive@testlib.onlyImage("hostapd/NM-wifi not installed on our test image", TEST_OS_DEFAULT)class TestNetworkingWifi(netlib.NetworkCase):def setUp(self):super().setUp()self.setup_wireless_networks()def testWifi(self):m = self.machineb = self.browserd = DialogHelpers(b, "#network-wifi-connect-dialog")self.login_and_go("/network")b.wait_visible("#networking")self.wait_for_iface(self.iface, active=False)# Details column shows correct number of networks (hidden networks not counted)with b.wait_timeout(30):b.wait_text(f"tr[data-interface='{self.iface}'] [data-label='Details']", "3 networks")# Not connected to anythingb.wait_not_present(f"tr[data-interface='{self.iface}'] .pf-m-success")self.select_iface(self.iface)b.wait_visible("#network-interface")self.wait_for_iface_setting("Status", "Inactive")# detects our three visible wifis (plus one hidden)b.wait_in_text("table[aria-label='Available networks'] tbody:first-of-type", "ZE WIFI!")b.wait_text("tr[data-ssid='ZE WIFI!'] th", "ZE WIFI!")self.assertEqual(b.get_pf_progress_value("tr[data-ssid='ZE WIFI!'] [data-label='Signal']"), 100)# sadly, mac80211_hwsim's data rate reporting is flaky, so just ensure at least one of our networks is correctb.wait_in_text("table[aria-label='Available networks']", "54 Mbps")b.wait_visible("tr[data-ssid='ZE WIFI!'] [data-label='Mode'] [aria-label='secured']")b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")# No known network yetb.wait_not_present(".nm-icon-known")b.wait_visible("tr[data-ssid='PrivateNet'] [data-label='Mode'] [aria-label='secured']")b.wait_visible("tr[data-ssid='PrivateNet'] button[aria-label='Connect']")# only one OpenNet row for the two APsb.wait_count("tr[data-ssid='OpenNet']", 1)b.wait_visible("tr[data-ssid='OpenNet'] [data-label='Mode'] [aria-label='open']")b.wait_visible("tr[data-ssid='OpenNet'] button[aria-label='Connect']")# Filter networksb.set_input_text("#network-interface-wifi-networks input[placeholder='Filter']", "wifi")# Should show only "ZE WIFI!"b.wait_visible("tr[data-ssid='ZE WIFI!']")b.wait_not_present("tr[data-ssid='PrivateNet']")b.wait_not_present("tr[data-ssid='OpenNet']")# Search for network that doesn't existb.set_input_text("#network-interface-wifi-networks input[placeholder='Filter']", "NoSuchNetwork")b.wait_not_present("tr[data-ssid='ZE WIFI!']")b.wait_not_present("tr[data-ssid='PrivateNet']")b.wait_not_present("tr[data-ssid='OpenNet']")# Clear searchb.click("#network-interface-wifi-networks button[aria-label='Reset']")b.wait_visible("tr[data-ssid='ZE WIFI!']")b.wait_visible("tr[data-ssid='PrivateNet']")b.wait_visible("tr[data-ssid='OpenNet']")# Connect to OpenNetb.click("tr[data-ssid='OpenNet'] button[aria-label='Connect']")b.wait_visible("tr[data-ssid='OpenNet'] button[aria-label='Disconnect']")# gets a "connected" iconb.wait_visible("tr[data-ssid='OpenNet'] .nm-icon-connected")# gets an IPself.wait_for_iface_setting("Status", "10.0.42")# moves to top of tableb.wait_in_text("table[aria-label='Available networks'] tbody:first-of-type", "OpenNet")# Overview shows connected networkb.click("#network-interface nav a:contains('Networking')")b.wait_visible("#networking")b.wait_in_text(f"tr[data-interface='{self.iface}'] [data-label='Details']", "3 networks")b.wait_in_text(f"tr[data-interface='{self.iface}'] [data-label='Details'] .pf-v6-c-label.pf-m-success", "OpenNet")self.select_iface(self.iface)# Disconnectb.click("tr[data-ssid='OpenNet'] button[aria-label='Disconnect']")b.wait_visible("tr[data-ssid='OpenNet'] button[aria-label='Connect']")# icon moves to "known" stateb.wait_visible("tr[data-ssid='OpenNet'] .nm-icon-known")b.wait_not_present("tr[data-ssid='OpenNet'] .nm-icon-connected")# loses IPself.wait_for_iface_setting("Status", "Inactive")# still a known network, stays at the top of the tableb.wait_in_text("table[aria-label='Available networks'] tbody:nth-of-type(1)", "OpenNet")# Overview shows disconnectedb.click("#network-interface nav a:contains('Networking')")b.wait_visible("#networking")b.wait_text(f"tr[data-interface='{self.iface}'] [data-label='Details']", "3 networks")b.wait_not_present(f"tr[data-interface='{self.iface}'] .pf-v6-c-label.pf-m-success")self.select_iface(self.iface)# Connect to ZE WIFI!b.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_in_text("#network-wifi-connect-dialog", "Connect to ZE WIFI!")# Validation: Empty passwordd.wait_TextInput("password", "")b.click(d.apply_button())b.wait_visible(d.apply_button() + ":disabled")# Test password reveald.set_TextInput("password", "wrong123")b.wait_visible(d.apply_button() + ":not(:disabled)")self.assertEqual(b.attr(d.field("password"), "type"), "password")b.click("#network-wifi-connect-dialog button[aria-label='Show password']")self.assertEqual(b.attr(d.field("password"), "type"), "text")self.assertEqual(d.get_TextInput("password"), "wrong123")b.click("#network-wifi-connect-dialog button[aria-label='Hide password']")self.assertEqual(b.attr(d.field("password"), "type"), "password")# Submit with wrong passwordb.click(d.apply_button())b.wait_visible(d.apply_button() + ".pf-m-in-progress")b.wait_visible(d.apply_button() + ":disabled")b.wait_in_text("#network-wifi-connect-dialog", "Check your password")# correct passwordd.set_TextInput("password", "12345678")b.click(d.apply_button())b.wait_not_present("#network-wifi-connect-dialog")# connects, action changesb.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")# gets a "connected" iconb.wait_visible("tr[data-ssid='ZE WIFI!'] .nm-icon-connected")# gets an IPself.wait_for_iface_setting("Status", "10.0.42")# Disconnectb.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")# icon moves to "known" stateb.wait_visible("tr[data-ssid='ZE WIFI!'] .nm-icon-known")b.wait_not_present("tr[data-ssid='ZE WIFI!'] .nm-icon-connected")# loses IPself.wait_for_iface_setting("Status", "Inactive")# NM now knows the connection and can re-connect without password queryb.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")# icon moves to "connected" stateb.wait_visible("tr[data-ssid='ZE WIFI!'] .nm-icon-connected")b.wait_not_present("tr[data-ssid='ZE WIFI!'] .nm-icon-known")b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")self.wait_for_iface_setting("Status", "10.0.42")# OpenNet is known but not connectedb.wait_visible("tr[data-ssid='OpenNet'] .nm-icon-known")# Test connection failure when password is not stored# Disconnect firstb.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")# Remove the password from the connection file and restart NMm.execute("sed -i '/^psk=/d' /etc/NetworkManager/system-connections/'ZE WIFI!.nmconnection'")m.execute("systemctl restart NetworkManager")b.reload()b.enter_page("/network")# Try to connect - shows error about missing passwordb.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_in_text("#error-popup", "Failed to connect to ZE WIFI!")b.wait_in_text("#error-popup", "Network password is not stored")b.click("#error-popup button:contains('Close')")b.wait_not_present("#error-popup")# Restore the connection with password for subsequent testsb.click("tr[data-ssid='ZE WIFI!'] button.pf-v6-c-menu-toggle")b.click(".pf-v6-c-menu button[aria-label='Forget']")b.wait_not_present("tr[data-ssid='ZE WIFI!'] .nm-icon-known")b.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_in_text("#network-wifi-connect-dialog", "Connect to ZE WIFI!")d.set_TextInput("password", "12345678")b.click(d.apply_button())b.wait_not_present("#network-wifi-connect-dialog")b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")b.wait_visible("tr[data-ssid='ZE WIFI!'] .nm-icon-connected")# connecting to OpenNet disconnects from ZE WIFI!self.assertIn("ZE WIFI!", m.execute("nmcli d show wlan1 | grep GENERAL.CONNECTION"))b.click("tr[data-ssid='OpenNet'] button[aria-label='Connect']")b.wait_visible("tr[data-ssid='OpenNet'] button[aria-label='Disconnect']")# OpenNet moves to top of the tableb.wait_in_text("table[aria-label='Available networks'] tbody:first-of-type", "OpenNet")# OpenNet icon moves to "connected" stateb.wait_visible("tr[data-ssid='OpenNet'] .nm-icon-connected")b.wait_not_present("tr[data-ssid='OpenNet'] .nm-icon-known")self.wait_for_iface_setting("Status", "10.0.42")# ZE WIFI! moves to "known" stateb.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_visible("tr[data-ssid='ZE WIFI!'] .nm-icon-known")b.wait_not_present("tr[data-ssid='ZE WIFI!'] .nm-icon-connected")self.assertIn("OpenNet", m.execute("nmcli d show wlan1 | grep GENERAL.CONNECTION"))b.click("tr[data-ssid='OpenNet'] button[aria-label='Disconnect']")self.wait_for_iface_setting("Status", "Inactive")# Forget ZE WIFI! networkb.click("tr[data-ssid='ZE WIFI!'] button.pf-v6-c-menu-toggle")b.click(".pf-v6-c-menu button[aria-label='Forget']")# Known icon disappearsb.wait_not_present("tr[data-ssid='ZE WIFI!'] .nm-icon-known")b.wait_not_present("tr[data-ssid='ZE WIFI!'] button.pf-v6-c-menu-toggle")# Try to connect again - should ask for password; cancelb.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_in_text("#network-wifi-connect-dialog", "Connect to ZE WIFI!")b.click(d.cancel_button())b.wait_not_present("#network-wifi-connect-dialog")# Cancelling does not create a known connectionb.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_not_present("tr[data-ssid='ZE WIFI!'] button.pf-v6-c-menu-toggle")# Cancel during connection attemptb.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_in_text("#network-wifi-connect-dialog", "Connect to ZE WIFI!")# Use a wrong password. Otherwise connecting might happen faster than we can click "cancel".d.set_TextInput("password", "12345679")b.click(d.apply_button())b.wait_visible(d.apply_button() + ".pf-m-in-progress")b.wait_visible(d.cancel_button() + ":not(:disabled)")b.click(d.cancel_button())b.wait_not_present("#network-wifi-connect-dialog")# Cancelling did not create or deleted the known connection# This tests a negative -- ensure the connection does not get created in the backgroundtime.sleep(3)b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")b.wait_not_present("tr[data-ssid='ZE WIFI!'] button.pf-v6-c-menu-toggle")self.wait_for_iface_setting("Status", "Inactive")# WiFi kill switch: detected networks disappearm.execute("nmcli radio wifi off")self.addCleanup(m.execute, "nmcli radio wifi on")self.wait_for_iface_setting("Status", "Not available")b.wait_not_present("table[aria-label='Available networks']")b.click("#network-interface nav a:contains('Networking')")b.wait_not_present(f"tr[data-interface='{self.iface}'] [data-label='Details']")m.execute("nmcli radio wifi on")# in theory, NM will now connect to OpenNet automatically; however, kill switch messes up# mac80211_hwsim/hostapd's state, and they become unpredictable. So just assert that we get any life signb.wait_in_text(f"tr[data-interface='{self.iface}'] [data-label='Details']", "networks")self.select_iface(self.iface)b.wait_visible("table[aria-label='Available networks']")def testHidden(self):b = self.browserd = DialogHelpers(b, "#network-wifi-connect-dialog")self.login_and_go("/network")b.wait_visible("#networking")self.wait_for_iface(self.iface, active=False)self.select_iface(self.iface)b.wait_visible("#network-interface")# Hidden networks are aggregated at the bottom as "N hidden networks" with no actions# They are not broadcast very often especially after initial hostapd setupwith b.wait_timeout(60):b.wait_text("tr[data-hidden]", "1 hidden network")# Connect to hidden network via manual connection dialogb.click("button:contains('Connect to hidden network')")b.wait_in_text("#network-wifi-connect-dialog", "Connect to hidden network")# Validation: SSID and password are requiredb.click(d.apply_button())b.wait_visible(d.apply_button() + ":disabled")b.wait_in_text(d.helper_text("ssid"), "SSID can not be empty")b.wait_in_text(d.helper_text("password"), "Password can not be empty")d.set_TextInput("ssid", "SomeNetwork")d.set_TextInput("password", "foobarfoo")b.wait_visible(d.apply_button() + ":not(:disabled)")b.wait_not_present(d.helper_text("ssid"))b.wait_not_present(d.helper_text("password"))d.set_TextInput("password", "")b.wait_visible(d.apply_button() + ":disabled")b.wait_in_text(d.helper_text("password"), "Password can not be empty")# Validation: No password required for open networksd.set_DropdownSelect("security", "none")b.wait_not_present(d.field("password"))b.wait_visible(d.apply_button() + ":not(:disabled)")# Validation: Password required for WPA networksd.set_DropdownSelect("security", "wpa-psk")b.wait_visible(d.field("password"))b.wait_visible(d.apply_button() + ":disabled")# Error case 1: Wrong SSIDd.set_TextInput("ssid", "WrongSSID")self.assertEqual(d.get_DropdownSelect("security"), "wpa-psk")d.set_TextInput("password", "password")b.click(d.apply_button())b.wait_visible(d.apply_button() + ".pf-m-in-progress")with b.wait_timeout(30):b.wait_in_text("#network-wifi-connect-dialog", "Failed to connect")b.wait_not_present(d.apply_button() + ".pf-m-in-progress")# Error case 2: Wrong security (open instead of WPA)d.set_TextInput("ssid", "HiddenNet")d.set_DropdownSelect("security", "none")b.wait_not_present(d.field("password"))b.click(d.apply_button())b.wait_visible(d.apply_button() + ".pf-m-in-progress")with b.wait_timeout(30):b.wait_in_text("#network-wifi-connect-dialog", "Failed to connect")b.wait_not_present(d.apply_button() + ".pf-m-in-progress")# Error case 3: Wrong passwordd.set_DropdownSelect("security", "wpa-psk")b.wait_visible(d.field("password"))d.set_TextInput("password", "wrongpassword")b.click(d.apply_button())b.wait_visible(d.apply_button() + ".pf-m-in-progress")b.wait_in_text("#network-wifi-connect-dialog", "Failed to connect")b.wait_not_present(d.apply_button() + ".pf-m-in-progress")# Success case: Correct credentialsd.set_TextInput("password", "hidden99")b.click(d.apply_button())b.wait_not_present("#network-wifi-connect-dialog")# Hidden network connects and shows its real SSIDb.wait_visible("tr[data-ssid='HiddenNet'] button[aria-label='Disconnect']")b.wait_visible("tr[data-ssid='HiddenNet'] .nm-icon-connected")self.wait_for_iface_setting("Status", "10.0.42")# Should not show (hidden network) anymore, only HiddenNetb.wait_not_present("tr[data-hidden]")# Disconnect from hidden network - it remains known with its SSIDb.click("tr[data-ssid='HiddenNet'] button[aria-label='Disconnect']")self.wait_for_iface_setting("Status", "Inactive")b.wait_visible("tr[data-ssid='HiddenNet'] button[aria-label='Connect']")b.wait_visible("tr[data-ssid='HiddenNet'] .nm-icon-known")b.wait_not_present("tr[data-ssid='HiddenNet'] .nm-icon-connected")# Forget hidden network - it disappearsb.click("tr[data-ssid='HiddenNet'] button.pf-v6-c-menu-toggle")b.click(".pf-v6-c-menu button[aria-label='Forget']")b.wait_not_present("tr[data-ssid='HiddenNet']")b.wait_not_present("tr[data-ssid='HiddenNet'] .nm-icon-known")# ... and becomes unknown again in the hidden networks rowb.wait_text("tr[data-hidden]", "1 hidden network")if __name__ == '__main__':testlib.test_main()