[WifiPasspointTest] UI for boingo passpoint
CHERRY PICKED CHANGES FROM git_master and aosp_master
Bug: 148611705
Test: Verified changes on all devices
Merged-In: Ieb6b7df5598e3242a4e65c50e8e2eb410918fbc8
Change-Id: I7e1115031520b5dff3c577fd245847560482b610
diff --git a/acts/framework/acts/test_utils/net/ui_utils.py b/acts/framework/acts/test_utils/net/ui_utils.py
new file mode 100644
index 0000000..4dadda1
--- /dev/null
+++ b/acts/framework/acts/test_utils/net/ui_utils.py
@@ -0,0 +1,270 @@
+"""Utils for adb-based UI operations."""
+
+import collections
+import logging
+import os
+import re
+import time
+
+from xml.dom import minidom
+from acts.controllers.android_lib.errors import AndroidDeviceError
+
+
+class Point(collections.namedtuple('Point', ['x', 'y'])):
+
+ def __repr__(self):
+ return '{x},{y}'.format(x=self.x, y=self.y)
+
+
+class Bounds(collections.namedtuple('Bounds', ['start', 'end'])):
+
+ def __repr__(self):
+ return '[{start}][{end}]'.format(start=str(self.start), end=str(self.end))
+
+ def calculate_middle_point(self):
+ return Point((self.start.x + self.end.x) // 2,
+ (self.start.y + self.end.y) // 2)
+
+
+def get_key_value_pair_strings(kv_pairs):
+ return ' '.join(['%s="%s"' % (k, v) for k, v in kv_pairs.items()])
+
+
+def parse_bound(bounds_string):
+ """Parse UI bound string.
+
+ Args:
+ bounds_string: string, In the format of the UI element bound.
+ e.g '[0,0][1080,2160]'
+
+ Returns:
+ Bounds, The bound of UI element.
+ """
+ bounds_pattern = re.compile(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]')
+ points = bounds_pattern.match(bounds_string).groups()
+ points = list(map(int, points))
+ return Bounds(Point(*points[:2]), Point(*points[-2:]))
+
+
+def _find_point_in_bounds(bounds_string):
+ """Finds a point that resides within the given bounds.
+
+ Args:
+ bounds_string: string, In the format of the UI element bound.
+
+ Returns:
+ A tuple of integers, representing X and Y coordinates of a point within
+ the given boundary.
+ """
+ return parse_bound(bounds_string).calculate_middle_point()
+
+
+def get_screen_dump_xml(device):
+ """Gets an XML dump of the current device screen.
+
+ This only works when there is no instrumentation process running. A running
+ instrumentation process will disrupt calls for `adb shell uiautomator dump`.
+
+ Args:
+ device: AndroidDevice object.
+
+ Returns:
+ XML Document of the screen dump.
+ """
+ os.makedirs(device.log_path, exist_ok=True)
+ device.adb.shell('uiautomator dump')
+ device.adb.pull('/sdcard/window_dump.xml %s' % device.log_path)
+ return minidom.parse('%s/window_dump.xml' % device.log_path)
+
+
+def match_node(node, **matcher):
+ """Determine if a mode matches with the given matcher.
+
+ Args:
+ node: Is a XML node to be checked against matcher.
+ **matcher: Is a dict representing mobly AdbUiDevice matchers.
+
+ Returns:
+ True if all matchers match the given node.
+ """
+ match_list = []
+ for k, v in matcher.items():
+ if k == 'class_name':
+ key = k.replace('class_name', 'class')
+ elif k == 'text_contains':
+ key = k.replace('text_contains', 'text')
+ else:
+ key = k.replace('_', '-')
+ try:
+ if k == 'text_contains':
+ match_list.append(v in node.attributes[key].value)
+ else:
+ match_list.append(node.attributes[key].value == v)
+ except KeyError:
+ match_list.append(False)
+ return all(match_list)
+
+
+def _find_node(screen_dump_xml, **kwargs):
+ """Finds an XML node from an XML DOM.
+
+ Args:
+ screen_dump_xml: XML doc, parsed from adb ui automator dump.
+ **kwargs: key/value pairs to match in an XML node's attributes. Value of
+ each key has to be string type. Below lists keys which can be used:
+ index
+ text
+ text_contains (matching a part of text attribute)
+ resource_id
+ class_name (representing "class" attribute)
+ package
+ content_desc
+ checkable
+ checked
+ clickable
+ enabled
+ focusable
+ focused
+ scrollable
+ long_clickable
+ password
+ selected
+
+ Returns:
+ XML node of the UI element or None if not found.
+ """
+ nodes = screen_dump_xml.getElementsByTagName('node')
+ for node in nodes:
+ if match_node(node, **kwargs):
+ logging.debug('Found a node matching conditions: %s',
+ get_key_value_pair_strings(kwargs))
+ return node
+
+
+def wait_and_get_xml_node(device, timeout, child=None, sibling=None, **kwargs):
+ """Waits for a node to appear and return it.
+
+ Args:
+ device: AndroidDevice object.
+ timeout: float, The number of seconds to wait for before giving up.
+ child: dict, a dict contains child XML node's attributes. It is extra set of
+ conditions to match an XML node that is under the XML node which is found
+ by **kwargs.
+ sibling: dict, a dict contains sibling XML node's attributes. It is extra
+ set of conditions to match an XML node that is under parent of the XML
+ node which is found by **kwargs.
+ **kwargs: Key/value pairs to match in an XML node's attributes.
+
+ Returns:
+ The XML node of the UI element.
+
+ Raises:
+ AndroidDeviceError: if the UI element does not appear on screen within
+ timeout or extra sets of conditions of child and sibling are used in a call.
+ """
+ if child and sibling:
+ raise AndroidDeviceError(
+ device, 'Only use one extra set of conditions: child or sibling.')
+ start_time = time.time()
+ threshold = start_time + timeout
+ while time.time() < threshold:
+ time.sleep(1)
+ screen_dump_xml = get_screen_dump_xml(device)
+ node = _find_node(screen_dump_xml, **kwargs)
+ if node and child:
+ node = _find_node(node, **child)
+ if node and sibling:
+ node = _find_node(node.parentNode, **sibling)
+ if node:
+ return node
+ msg = ('Timed out after %ds waiting for UI node matching conditions: %s.'
+ % (timeout, get_key_value_pair_strings(kwargs)))
+ if child:
+ msg = ('%s extra conditions: %s'
+ % (msg, get_key_value_pair_strings(child)))
+ if sibling:
+ msg = ('%s extra conditions: %s'
+ % (msg, get_key_value_pair_strings(sibling)))
+ raise AndroidDeviceError(device, msg)
+
+
+def has_element(device, **kwargs):
+ """Checks a UI element whether appears or not in the current screen.
+
+ Args:
+ device: AndroidDevice object.
+ **kwargs: Key/value pairs to match in an XML node's attributes.
+
+ Returns:
+ True if the UI element appears in the current screen else False.
+ """
+ timeout_sec = kwargs.pop('timeout', 30)
+ try:
+ wait_and_get_xml_node(device, timeout_sec, **kwargs)
+ return True
+ except AndroidDeviceError:
+ return False
+
+
+def get_element_attributes(device, **kwargs):
+ """Gets a UI element's all attributes.
+
+ Args:
+ device: AndroidDevice object.
+ **kwargs: Key/value pairs to match in an XML node's attributes.
+
+ Returns:
+ XML Node Attributes.
+ """
+ timeout_sec = kwargs.pop('timeout', 30)
+ node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
+ return node.attributes
+
+
+def wait_and_click(device, duration_ms=None, **kwargs):
+ """Wait for a UI element to appear and click on it.
+
+ This function locates a UI element on the screen by matching attributes of
+ nodes in XML DOM, calculates a point's coordinates within the boundary of the
+ element, and clicks on the point marked by the coordinates.
+
+ Args:
+ device: AndroidDevice object.
+ duration_ms: int, The number of milliseconds to long-click.
+ **kwargs: A set of `key=value` parameters that identifies a UI element.
+ """
+ timeout_sec = kwargs.pop('timeout', 30)
+ button_node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
+ x, y = _find_point_in_bounds(button_node.attributes['bounds'].value)
+ args = []
+ if duration_ms is None:
+ args = 'input tap %s %s' % (str(x), str(y))
+ else:
+ # Long click.
+ args = 'input swipe %s %s %s %s %s' % \
+ (str(x), str(y), str(x), str(y), str(duration_ms))
+ device.adb.shell(args)
+
+def wait_and_input_text(device, input_text, duration_ms=None, **kwargs):
+ """Wait for a UI element text field that can accept text entry.
+
+ This function located a UI element using wait_and_click. Once the element is
+ clicked, the text is input into the text field.
+
+ Args:
+ device: AndroidDevice, Mobly's Android controller object.
+ input_text: Text string to be entered in to the text field.
+ duration_ms: duration in milliseconds.
+ **kwargs: A set of `key=value` parameters that identifies a UI element.
+ """
+ wait_and_click(device, duration_ms, **kwargs)
+ # Replace special characters.
+ # The command "input text <string>" requires special treatment for
+ # characters ' ' and '&'. They need to be escaped. for example:
+ # "hello world!!&" needs to transform to "hello\ world!!\&"
+ special_chars = ' &'
+ for c in special_chars:
+ input_text = input_text.replace(c, '\\%s' % c)
+ input_text = "'" + input_text + "'"
+ args = 'input text %s' % input_text
+ device.adb.shell(args)
diff --git a/acts/tests/google/net/CaptivePortalTest.py b/acts/tests/google/net/CaptivePortalTest.py
index 0611640..7df4372 100644
--- a/acts/tests/google/net/CaptivePortalTest.py
+++ b/acts/tests/google/net/CaptivePortalTest.py
@@ -17,10 +17,10 @@
from acts import asserts
from acts import base_test
-from acts.libs.uicd.uicd_cli import UicdCli
from acts.test_decorators import test_tracker_info
from acts.test_utils.net import connectivity_const as cconst
from acts.test_utils.net import connectivity_test_utils as cutils
+from acts.test_utils.net import ui_utils as uutils
from acts.test_utils.wifi import wifi_test_utils as wutils
from acts.test_utils.wifi.WifiBaseTest import WifiBaseTest
@@ -28,6 +28,7 @@
IFACE = "InterfaceName"
TIME_OUT = 20
WLAN = "wlan0"
+ACCEPT_CONTINUE = "Accept and Continue"
class CaptivePortalTest(base_test.BaseTestClass):
@@ -45,35 +46,37 @@
4. uic_zip: Zip file location of UICD application
"""
self.dut = self.android_devices[0]
- wutils.wifi_test_device_init(self.dut)
- wutils.wifi_toggle_state(self.dut, True)
- req_params = ["rk_captive_portal",
- "gg_captive_portal",
- "uicd_workflows",
- "uicd_zip"]
+ req_params = ["rk_captive_portal", "gg_captive_portal"]
self.unpack_userparams(req_param_names=req_params,)
- self.ui = UicdCli(self.uicd_zip, self.uicd_workflows)
- self.rk_workflow_config = "rk_captive_portal_%s" % self.dut.model
- self.gg_workflow_config = "gg_captive_portal_%s" % self.dut.model
+ wutils.wifi_test_device_init(self.dut)
def teardown_class(self):
"""Reset devices."""
cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OPPORTUNISTIC)
+ wutils.reset_wifi(self.dut)
+ self.dut.droid.telephonyToggleDataConnection(True)
def setup_test(self):
"""Setup device."""
- self.dut.unlock_screen()
-
- def teardown_test(self):
- """Reset to default state after each test."""
wutils.reset_wifi(self.dut)
+ self.dut.unlock_screen()
+ self._go_to_wifi_settings()
def on_fail(self, test_name, begin_time):
self.dut.take_bug_report(test_name, begin_time)
### Helper methods ###
- def _verify_captive_portal(self, network, uicd_workflow):
+ def _go_to_wifi_settings(self):
+ """Go to wifi settings to perform UI actions for Captive portal."""
+ self.dut.adb.shell("am start -a android.settings.SETTINGS")
+ asserts.assert_true(
+ uutils.has_element(self.dut, text="Network & internet"),
+ "Failed to find 'Network & internet' icon")
+ uutils.wait_and_click(self.dut, text="Network & internet")
+ uutils.wait_and_click(self.dut, text="Not connected")
+
+ def _verify_captive_portal(self, network, click_accept=ACCEPT_CONTINUE):
"""Connect to captive portal network using uicd workflow.
Steps:
@@ -83,15 +86,16 @@
Args:
network: captive portal network to connect to
- uicd_workflow: ui workflow to accept captive portal conn
+ click_accept: Notification to select to accept captive portal
"""
# connect to captive portal wifi network
- wutils.start_wifi_connection_scan_and_ensure_network_found(
- self.dut, network[WifiEnums.SSID_KEY])
- wutils.wifi_connect(self.dut, network, check_connectivity=False)
+ wutils.connect_to_wifi_network(
+ self.dut, network, check_connectivity=False)
- # run uicd
- self.ui.run(self.dut.serial, uicd_workflow)
+ # run ui automator
+ uutils.wait_and_click(self.dut, text="%s" % network["SSID"])
+ if uutils.has_element(self.dut, text="%s" % click_accept):
+ uutils.wait_and_click(self.dut, text="%s" % click_accept)
# wait for sometime for captive portal connection to go through
curr_time = time.time()
@@ -103,11 +107,9 @@
time.sleep(2)
# verify connectivity
- try:
- asserts.assert_true(wutils.validate_connection(self.dut),
- "Failed to verify internet connectivity")
- except Exception as e:
- asserts.fail("Failed to connect to captive portal: %s" % e)
+ asserts.assert_true(
+ wutils.validate_connection(self.dut, ping_gateway=False),
+ "Failed to connect to internet. Captive portal test failed")
### Test Cases ###
@@ -125,8 +127,7 @@
cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OPPORTUNISTIC)
# verify connection to captive portal network
- self._verify_captive_portal(self.rk_captive_portal,
- self.rk_workflow_config)
+ self._verify_captive_portal(self.rk_captive_portal)
@test_tracker_info(uuid="8ea18d80-0170-41b1-8945-fe14bcd4feab")
@WifiBaseTest.wifi_test_wrap
@@ -142,8 +143,7 @@
cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OFF)
# verify connection to captive portal network
- self._verify_captive_portal(self.rk_captive_portal,
- self.rk_workflow_config)
+ self._verify_captive_portal(self.rk_captive_portal)
@test_tracker_info(uuid="e8e05907-55f7-40e5-850c-b3111ceb31a4")
@WifiBaseTest.wifi_test_wrap
@@ -161,8 +161,7 @@
cconst.DNS_GOOGLE)
# verify connection to captive portal network
- self._verify_captive_portal(self.rk_captive_portal,
- self.rk_workflow_config)
+ self._verify_captive_portal(self.rk_captive_portal)
@test_tracker_info(uuid="76e49800-f141-4fd2-9969-562585eb1e7a")
def test_guestgate_captive_portal_default(self):
@@ -177,7 +176,7 @@
cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OPPORTUNISTIC)
# verify connection to captive portal network
- self._verify_captive_portal(self.gg_captive_portal, "gg_captive_portal")
+ self._verify_captive_portal(self.gg_captive_portal)
@test_tracker_info(uuid="0aea0cac-0f42-406b-84ba-62c1ef74adfc")
def test_guestgate_captive_portal_private_dns_off(self):
@@ -192,7 +191,7 @@
cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OFF)
# verify connection to captive portal network
- self._verify_captive_portal(self.gg_captive_portal, "gg_captive_portal")
+ self._verify_captive_portal(self.gg_captive_portal)
@test_tracker_info(uuid="39124dcc-2fd3-4d33-b129-a1c8150b7f2a")
def test_guestgate_captive_portal_private_dns_strict(self):
@@ -209,4 +208,4 @@
cconst.DNS_GOOGLE)
# verify connection to captive portal network
- self._verify_captive_portal(self.gg_captive_portal, "gg_captive_portal")
+ self._verify_captive_portal(self.gg_captive_portal)
diff --git a/acts/tests/google/wifi/WifiPasspointTest.py b/acts/tests/google/wifi/WifiPasspointTest.py
index 962d9db..8e51eab 100755
--- a/acts/tests/google/wifi/WifiPasspointTest.py
+++ b/acts/tests/google/wifi/WifiPasspointTest.py
@@ -20,14 +20,13 @@
import time
import acts.base_test
+from acts.test_utils.net import ui_utils as uutils
import acts.test_utils.wifi.wifi_test_utils as wutils
import WifiManagerTest
from acts import asserts
from acts import signals
-from acts.libs.uicd.uicd_cli import UicdCli
-from acts.libs.uicd.uicd_cli import UicdError
from acts.test_decorators import test_tracker_info
from acts.test_utils.tel.tel_test_utils import get_operator_name
from acts.utils import force_airplane_mode
@@ -49,6 +48,12 @@
UNKNOWN_FQDN = "@#@@!00fffffx"
+# Constants for Boingo UI automator
+EDIT_TEXT_CLASS_NAME = "android.widget.EditText"
+PASSWORD_TEXT = "Password"
+PASSPOINT_BUTTON = "Get Passpoint"
+BOINGO_UI_TEXT = "Online Sign Up"
+
class WifiPasspointTest(acts.base_test.BaseTestClass):
"""Tests for APIs in Android's WifiManager class.
@@ -61,19 +66,15 @@
def setup_class(self):
self.dut = self.android_devices[0]
wutils.wifi_test_device_init(self.dut)
- req_params = ["passpoint_networks", "uicd_workflows", "uicd_zip"]
- opt_param = []
- self.unpack_userparams(
- req_param_names=req_params, opt_param_names=opt_param)
- self.unpack_userparams(req_params)
+ req_params = ["passpoint_networks",
+ "boingo_username",
+ "boingo_password",]
+ self.unpack_userparams(req_param_names=req_params,)
asserts.assert_true(
len(self.passpoint_networks) > 0,
"Need at least one Passpoint network.")
wutils.wifi_toggle_state(self.dut, True)
self.unknown_fqdn = UNKNOWN_FQDN
- # Setup Uicd cli object for UI interation.
- self.ui = UicdCli(self.uicd_zip[0], self.uicd_workflows)
- self.passpoint_workflow = "passpoint-login_%s" % self.dut.model
def setup_test(self):
@@ -146,6 +147,36 @@
raise signals.TestFailure("Failed to delete Passpoint configuration"
" with FQDN = %s" % passpoint_config[0])
+ def ui_automator_boingo(self):
+ """Run UI automator for boingo passpoint."""
+ # Verify the boingo login page shows
+ asserts.assert_true(
+ uutils.has_element(self.dut, text=BOINGO_UI_TEXT),
+ "Failed to launch boingohotspot login page")
+
+ # Go to the bottom of the page
+ for _ in range(3):
+ self.dut.adb.shell("input swipe 300 900 300 300")
+
+ # Enter username
+ uutils.wait_and_input_text(self.dut,
+ input_text=self.boingo_username,
+ text="",
+ class_name=EDIT_TEXT_CLASS_NAME)
+ self.dut.adb.shell("input keyevent 111") # collapse keyboard
+ self.dut.adb.shell("input swipe 300 900 300 750") # swipe up to show text
+
+ # Enter password
+ uutils.wait_and_input_text(self.dut,
+ input_text=self.boingo_password,
+ text=PASSWORD_TEXT)
+ self.dut.adb.shell("input keyevent 111") # collapse keyboard
+ self.dut.adb.shell("input swipe 300 900 300 750") # swipe up to show text
+
+ # Login
+ uutils.wait_and_click(self.dut, text=PASSPOINT_BUTTON)
+
+
def start_subscription_provisioning(self, state):
"""Start subscription provisioning with a default provider."""
@@ -183,7 +214,7 @@
"Passpoint Provisioning status %s" % dut_event['data'][
'status'])
if int(dut_event['data']['status']) == 7:
- self.ui.run(self.dut.serial, self.passpoint_workflow)
+ self.ui_automator_boingo()
# Clear all previous events.
self.dut.ed.clear_all_events()