Testing Graphical User Interfaces

In this chapter, we explore how to generate tests for Graphical User Interfaces (GUIs), abstracting from our previous examples on Web testing. Building on general means to extract user interface elements and activate them, our techniques generalize to arbitrary graphical user interfaces, from rich Web applications to mobile apps, and systematically explore user interfaces through forms and navigation elements.

from bookutils import YouTubeVideo
YouTubeVideo('79-HRgFot4k')

Prerequisites

Synopsis

To use the code provided in this chapter, write

>>> from fuzzingbook.GUIFuzzer import <identifier>

and then make use of the following features.

This chapter demonstrates how to programmatically interact with user interfaces, using Selenium on Web browsers. It provides an experimental GUICoverageFuzzer class that automatically explores a user interface by systematically interacting with all available user interface elements.

The function start_webdriver() starts a headless Web browser in the background and returns a GUI driver as handle for further communication.

>>> gui_driver = start_webdriver()

We let the browser open the URL of the server we want to investigate (in this case, the vulnerable server from the chapter on Web fuzzing) and obtain a screenshot.

>>> gui_driver.get(httpd_url)
>>> Image(gui_driver.get_screenshot_as_png())

The GUICoverageFuzzer class explores the user interface and builds a grammar that encodes all states as well as the user interactions required to move from one state to the next. It is paired with a GUIRunner which interacts with the GUI driver.

>>> gui_fuzzer = GUICoverageFuzzer(gui_driver)
>>> gui_runner = GUIRunner(gui_driver)

The explore_all() method extracts all states and all transitions from a Web user interface.

>>> gui_fuzzer.explore_all(gui_runner)

The grammar embeds a finite state automation and is best visualized as such.

>>> fsm_diagram(gui_fuzzer.grammar)

start <start> state <state> start->state state-1 <state-1> state->state-1 click('terms and conditions') state-2 <state-2> state->state-2 check('terms', <boolean>) fill('city', '<text>') fill('email', '<email>') fill('name', '<text>') fill('zip', '<number>') submit('submit') end <end> state->end state-1->state click('order form') state-1->end state-2->state click('order form') state-2->end

The GUI Fuzzer fuzz() method produces sequences of interactions that follow paths through the finite state machine. Since GUICoverageFuzzer is derived from CoverageFuzzer (see the chapter on coverage-based grammar fuzzing), it automatically covers (a) as many transitions between states as well as (b) as many form elements as possible. In our case, the first set of actions explores the transition via the "order form" link; the second set then goes until the "" state.

>>> gui_driver.get(httpd_url)
>>> actions = gui_fuzzer.fuzz()
>>> print(actions)
check('terms', True)
fill('city', 'J')
fill('email', 'U@si')
fill('name', 'K')
fill('zip', '16')
submit('submit')
click('order form')
check('terms', False)
fill('city', 'c')
fill('email', 'n@H')
fill('name', 'b')
fill('zip', '9')
submit('submit')
click('order form')
check('terms', False)
fill('city', 'y')
fill('email', 'M@C')
fill('name', 'o')
fill('zip', '4')
submit('submit')

These actions can be fed into the GUI runner, which will execute them on the given GUI driver.

>>> gui_driver.get(httpd_url)
>>> result, outcome = gui_runner.run(actions)
>>> Image(gui_driver.get_screenshot_as_png())

Further invocations of fuzz() will further cover the model – for instance, exploring the terms and conditions.

Internally, GUIFuzzer and GUICoverageFuzzer use a subclass GUIGrammarMiner which implements the analysis of the GUI and all its states. Subclassing GUIGrammarMiner allows extending the interpretation of GUIs; the GUIFuzzer constructor allows passing a miner via the miner keyword parameter.

A tool like GUICoverageFuzzer will provide "deep" exploration of user interfaces, even filling out forms to explore what is behind them. Keep in mind, though, that GUICoverageFuzzer is experimental: It only supports a subset of HTML form and link features, and does not take JavaScript into account.

GUIFuzzer GUIFuzzer __init__() restart() run() fsm_last_state_symbol() fsm_path() set_grammar() update_existing_state() update_new_state() update_state() GrammarFuzzer GrammarFuzzer __init__() fuzz() fuzz_tree() GUIFuzzer->GrammarFuzzer Fuzzer Fuzzer __init__() fuzz() run() runs() GrammarFuzzer->Fuzzer GUICoverageFuzzer GUICoverageFuzzer __init__() explore_all() GUICoverageFuzzer->GUIFuzzer GrammarCoverageFuzzer GrammarCoverageFuzzer GUICoverageFuzzer->GrammarCoverageFuzzer SimpleGrammarCoverageFuzzer SimpleGrammarCoverageFuzzer GrammarCoverageFuzzer->SimpleGrammarCoverageFuzzer TrackingGrammarCoverageFuzzer TrackingGrammarCoverageFuzzer __init__() SimpleGrammarCoverageFuzzer->TrackingGrammarCoverageFuzzer TrackingGrammarCoverageFuzzer->GrammarFuzzer GUIRunner GUIRunner DELAY_AFTER_CHECK DELAY_AFTER_CLICK DELAY_AFTER_FILL DELAY_AFTER_SUBMIT __init__() run() do_check() do_click() do_fill() do_submit() find_element() Runner Runner FAIL PASS UNRESOLVED __init__() run() GUIRunner->Runner GUIGrammarMiner GUIGrammarMiner FINAL_STATE GUI_GRAMMAR START_STATE UNEXPLORED_STATE __init__() follow_link() mine_a_element_actions() mine_button_element_actions() mine_input_element_actions() mine_state_actions() mine_state_grammar() new_state_symbol() Legend Legend •  public_method() •  private_method() •  overloaded_method() Hover over names to see doc

Automated GUI Interaction

In the chapter on Web testing, we have shown how to test Web-based interfaces by directly interacting with a Web server using the HTTP protocol, and processing the retrieved HTML pages to identify user interface elements. While these techniques work well for user interfaces that are based on HTML only, they fail as soon as there are interactive elements that use JavaScript to execute code within the browser, and generate and change the user interface without having to interact with the browser.

In this chapter, we therefore take a different approach to user interface testing. Rather than using HTTP and HTML as the mechanisms for interaction, we leverage a dedicated UI testing framework, which allows us to

  • query the program under test for available user interface elements, and
  • query the UI elements for how they can be interacted with.

Although we will again illustrate our approach using a Web server, the approach easily generalizes to arbitrary user interfaces. In fact, the UI testing framework we use, Selenium, also comes in variants that run for Android apps.

Our Web Server, Again

As in the chapter on Web testing, we run a Web server that allows us to order products.

from typing import Set, FrozenSet, List, Optional, Tuple, Any
import os
import sys
from WebFuzzer import init_db, start_httpd, webbrowser, print_httpd_messages
from WebFuzzer import print_url, ORDERS_DB
import html
db = init_db()

This is the address of our web server:

httpd_process, httpd_url = start_httpd()
print_url(httpd_url)

Using webbrowser(), we can retrieve the HTML of the home page, and use HTML() to render it.

from IPython.display import display, Image
from bookutils import HTML, rich_output
HTML(webbrowser(httpd_url))
127.0.0.1 - - [09/Nov/2024 18:19:29] "GET / HTTP/1.1" 200 -
Fuzzingbook Swag Order Form

Yes! Please send me at your earliest convenience


.

Remote Control with Selenium

Let us take a look at the GUI above. In contrast to the chapter on Web testing, we do not assume we can access the HTML source of the current page. All we assume is that there is a set of user interface elements we can interact with.

Selenium is a framework for testing Web applications by automating interaction in the browser. Selenium provides an API that allows one to launch a Web browser, query the state of the user interface, and interact with individual user interface elements. The Selenium API is available in a number of languages; we use the Selenium API for Python.

A Selenium web driver is the interface between a program and a browser controlled by the program. The following code starts a Web browser in the background, which we then control through the web driver.

from selenium import webdriver

We support both Firefox and Google Chrome.

BROWSER = 'firefox'  # Set to 'chrome' if you prefer Chrome

Setting up Firefox

For Firefox, you have to make sure the geckodriver program is in your path.

import shutil
if BROWSER == 'firefox':
    assert shutil.which('geckodriver') is not None, \
        "Please install the 'geckodriver' executable " \
        "from https://github.com/mozilla/geckodriver/releases"

Setting up Chrome

For Chrome, you may have to make sure the chromedriver program is in your path.

if BROWSER == 'chrome':
    assert shutil.which('chromedriver') is not None, \
        "Please install the 'chromedriver' executable " \
        "from https://chromedriver.chromium.org"

Running a Headless Browser

The browser is headless, meaning that it does not show on the screen.

HEADLESS = True

Note: If the notebook server runs locally (i.e. on the same machine on which you are seeing this), you can also set HEADLESS to False and see what happens right on the screen as you execute the notebook cells. This is very much recommended for interactive sessions.

Starting the Web driver

This code starts the Selenium web driver.

def start_webdriver(browser=BROWSER, headless=HEADLESS, zoom=1.4):
    # Set headless option
    if browser == 'firefox':
        options = webdriver.FirefoxOptions()
        if headless:
            # See https://www.browserstack.com/guide/firefox-headless
            options.add_argument("--headless")
    elif browser == 'chrome':
        options = webdriver.ChromeOptions()
        if headless:
            # See https://www.selenium.dev/blog/2023/headless-is-going-away/
            options.add_argument("--headless=new")
    else:
        assert False, "Select 'firefox' or 'chrome' as browser"

    # Start the browser, and obtain a _web driver_ object such that we can interact with it.
    if browser == 'firefox':
        # For firefox, set a higher resolution for our screenshots
        options.set_preference("layout.css.devPixelsPerPx", repr(zoom))
        gui_driver = webdriver.Firefox(options=options)

        # We set the window size such that it fits our order form exactly;
        # this is useful for not wasting too much space when taking screen shots.
        gui_driver.set_window_size(700, 300)

    elif browser == 'chrome':
        gui_driver = webdriver.Chrome(options=options)
        gui_driver.set_window_size(700, 210 if headless else 340)

    return gui_driver
gui_driver = start_webdriver(browser=BROWSER, headless=HEADLESS)

We can now interact with the browser programmatically. First, we have it navigate to the URL of our Web server:

gui_driver.get(httpd_url)

We see that the home page is actually accessed, together with a (failing) request to get a page icon:

print_httpd_messages()
127.0.0.1 - - [09/Nov/2024 18:19:32] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [09/Nov/2024 18:19:32] "GET /favicon.ico HTTP/1.1" 404 -

To see what the "headless" browser displays, we can obtain a screenshot. We see that it actually displays the home page.

Image(gui_driver.get_screenshot_as_png())

Filling out Forms

To interact with the Web page through Selenium and the browser, we can query Selenium for individual elements. For instance, we can access the UI element whose name attribute (as defined in HTML) is "name".

name = gui_driver.find_element(By.NAME, "name")

Once we have an element, we can interact with it. Since name is a text field, we can send it a string using the send_keys() method; the string will be translated into appropriate keystrokes.

name.send_keys("Jane Doe")

In the screenshot, we can see that the name field is now filled:

Image(gui_driver.get_screenshot_as_png())

Similarly, we can fill out the email, city, and ZIP fields:

email = gui_driver.find_element(By.NAME, "email")
email.send_keys("j.doe@example.com")
city = gui_driver.find_element(By.NAME, 'city')
city.send_keys("Seattle")
zip = gui_driver.find_element(By.NAME, 'zip')
zip.send_keys("98104")
Image(gui_driver.get_screenshot_as_png())

The check box for terms and conditions is not filled out, but clicked instead using the click() method.

terms = gui_driver.find_element(By.NAME, 'terms')
terms.click()
Image(gui_driver.get_screenshot_as_png())

The form is now fully filled out. By clicking on the submit button, we can place the order:

submit = gui_driver.find_element(By.NAME, 'submit')
submit.click()

We see that the order is being processed, and that the Web browser has switched to the confirmation page.

print_httpd_messages()
127.0.0.1 - - [09/Nov/2024 18:19:33] INSERT INTO orders VALUES ('tshirt', 'Jane Doe', 'j.doe@example.com', 'Seattle', '98104')
127.0.0.1 - - [09/Nov/2024 18:19:33] "GET /order?item=tshirt&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=98104&terms=on&submit=Place+order HTTP/1.1" 200 -
Image(gui_driver.get_screenshot_as_png())

Just as we fill out forms, we can also navigate through a website by clicking on links. Let us go back to the home page:

gui_driver.back()
Image(gui_driver.get_screenshot_as_png())

We can query the web driver for all elements of a particular type. Querying for HTML anchor elements (<a>) for instance, gives us all links on a page.

links = gui_driver.find_elements(By.TAG_NAME, "a")

We can query the attributes of UI elements – for instance, the URL the first anchor on the page links to:

links[0].get_attribute('href')
'http://127.0.0.1:8800/terms'

What happens if we click on it? Very simple: We switch to the Web page being referenced.

links[0].click()
print_httpd_messages()
127.0.0.1 - - [09/Nov/2024 18:19:33] "GET /terms HTTP/1.1" 200 -
Image(gui_driver.get_screenshot_as_png())

Okay. Let's get back to our home page again.

gui_driver.back()
print_httpd_messages()
Image(gui_driver.get_screenshot_as_png())

Writing Test Cases

The above calls, interacting with a user interface automatically, are typically used in Selenium tests – that is, code snippets that interact with a website, occasionally checking whether everything works as expected. The following code, for instance, places an order just as above. It then retrieves the title element and checks whether the title contains a "Thank you" message, indicating success.

def test_successful_order(driver, url):
    name = "Walter White"
    email = "white@jpwynne.edu"
    city = "Albuquerque"
    zip_code = "87101"

    driver.get(url)
    driver.find_element(By.NAME, "name").send_keys(name)
    driver.find_element(By.NAME, "email").send_keys(email)
    driver.find_element(By.NAME, 'city').send_keys(city)
    driver.find_element(By.NAME, 'zip').send_keys(zip_code)
    driver.find_element(By.NAME, 'terms').click()
    driver.find_element(By.NAME, 'submit').click()

    title = driver.find_element(By.ID, 'title')
    assert title is not None
    assert title.text.find("Thank you") >= 0

    confirmation = driver.find_element(By.ID, "confirmation")
    assert confirmation is not None

    assert confirmation.text.find(name) >= 0
    assert confirmation.text.find(email) >= 0
    assert confirmation.text.find(city) >= 0
    assert confirmation.text.find(zip_code) >= 0

    return True
test_successful_order(gui_driver, httpd_url)
True

In a similar vein, we can set up automated test cases for unsuccessful orders, canceling orders, changing orders, and many more. All these test cases would be automatically run after any change to the program code, ensuring the Web application still works.

Of course, writing such tests is quite some effort. Hence, in the remainder of this chapter, we will again explore how to automatically generate them.

Retrieving User Interface Actions

To automatically interact with a user interface, we first need to find out which elements there are, and which user interactions (or short actions) they support.

User Interface Elements

We start with finding available user elements. Let us get back to the order form.

gui_driver.get(httpd_url)
Image(gui_driver.get_screenshot_as_png())

Using find_elements(By.TAG_NAME, ) (and other similar find_elements_...() functions), we can retrieve all elements of a particular type, such as HTML input elements.

ui_elements = gui_driver.find_elements(By.TAG_NAME, "input")

For each element, we can retrieve its HTML attributes, using get_attribute(). We can thus retrieve the name and type of each input element (if defined).

for element in ui_elements:
    print("Name: %-10s | Type: %-10s | Text: %s" %
          (element.get_attribute('name'),
           element.get_attribute('type'),
           element.text))
Name: name       | Type: text       | Text: 
Name: email      | Type: email      | Text: 
Name: city       | Type: text       | Text: 
Name: zip        | Type: number     | Text: 
Name: terms      | Type: checkbox   | Text: 
Name: submit     | Type: submit     | Text: 
ui_elements = gui_driver.find_elements(By.TAG_NAME, "a")
for element in ui_elements:
    print("Name: %-10s | Type: %-10s | Text: %s" %
          (element.get_attribute('name'),
           element.get_attribute('type'),
           element.text))
Name:            | Type:            | Text: terms and conditions

User Interface Actions

Similarly to what we did in the chapter on Web fuzzing, our idea is now to mine a grammar for the user interface – first for an individual user interface page (i.e., a single Web page), later for all pages offered by the application. The idea is that a grammar defines legal sequences of actions – clicks and keystrokes – that can be applied on the application.

We assume the following actions:

  1. fill(<name>, <text>) – fill the UI input element named <name> with the text <text>.
  2. check(<name>, <value>) – set the UI checkbox <name> to the given value <value> (True or False)
  3. submit(<name>) – submit the form by clicking on the UI element <name>.
  4. click(<name>) – click on the UI element <name>, typically for following a link.

This sequence of actions, for instance would fill out the order form:

fill('name', "Walter White")
fill('email', "white@jpwynne.edu")
fill('city', "Albuquerque")
fill('zip', "87101")
check('terms', True)
submit('submit')

Our set of actions is deliberately defined to be small – for real user interfaces, one would also have to define interactions such as swipes, double clicks, long clicks, right button clicks, modifier keys, and more. Selenium supports all of this; but in the interest of simplicity, we focus on the most important set of interactions.

Retrieving Actions

As a first step in mining an action grammar, we need to be able to retrieve possible interactions. We introduce a class GUIGrammarMiner, which is set to do precisely that.

class GUIGrammarMiner:
    """Retrieve a grammar of possible GUI interaction sequences"""

    def __init__(self, driver, stay_on_host: bool = True) -> None:
        """Constructor.
        `driver` - a web driver as produced by Selenium.
        `stay_on_host` - if True (default), no not follow links to other hosts.
        """
        self.driver = driver
        self.stay_on_host = stay_on_host
        self.grammar: Grammar = {}
Implementing Retrieving Actions

Our first task is to obtain the set of possible interactions. Given a single UI page, the method mine_input_actions() of GUIGrammarMiner returns a set of actions as defined above. It first gets all input elements, followed by button elements, finally followed by links (a elements), and merges them into a set. (We use a frozenset here since we want to use the set as an index later.)

class GUIGrammarMiner(GUIGrammarMiner):
    def mine_state_actions(self) -> FrozenSet[str]:
        """Return a set of all possible actions on the current Web site.
        Can be overloaded in subclasses."""
        return frozenset(self.mine_input_element_actions()
                         | self.mine_button_element_actions()
                         | self.mine_a_element_actions())

    def mine_input_element_actions(self) -> Set[str]:
        return set()  # to be defined later

    def mine_button_element_actions(self) -> Set[str]:
        return set()  # to be defined later

    def mine_a_element_actions(self) -> Set[str]:
        return set()  # to be defined later
Input Element Actions

Mining input actions goes through the set of input elements, and returns an action depending on the input type. If the input field is a text, for instance, the associated action is fill(); for checkboxes, the action is check().

The respective values are placeholders depending on the type; if the input field is a number, for instance, the value becomes <number>. As these actions later become part of the grammar, they will be expanded into actual values during grammar expansion.

from selenium.common.exceptions import StaleElementReferenceException
class GUIGrammarMiner(GUIGrammarMiner):
    def mine_input_element_actions(self) -> Set[str]:
        """Determine all input actions on the current Web page"""

        actions = set()

        for elem in self.driver.find_elements(By.TAG_NAME, "input"):
            try:
                input_type = elem.get_attribute("type")
                input_name = elem.get_attribute("name")
                if input_name is None:
                    input_name = elem.text

                if input_type in ["checkbox", "radio"]:
                    actions.add("check('%s', <boolean>)" % html.escape(input_name))
                elif input_type in ["text", "number", "email", "password"]:
                    actions.add("fill('%s', '<%s>')" % (html.escape(input_name), html.escape(input_type)))
                elif input_type in ["button", "submit"]:
                    actions.add("submit('%s')" % html.escape(input_name))
                elif input_type in ["hidden"]:
                    pass
                else:
                    # TODO: Handle more types here
                    actions.add("fill('%s', <%s>)" % (html.escape(input_name), html.escape(input_type)))
            except StaleElementReferenceException:
                pass

        return actions

Applied on our order form, we see that the method gets us all input actions:

gui_grammar_miner = GUIGrammarMiner(gui_driver)
gui_grammar_miner.mine_input_element_actions()
{"check('terms', <boolean>)",
 "fill('city', '<text>')",
 "fill('email', '<email>')",
 "fill('name', '<text>')",
 "fill('zip', '<number>')",
 "submit('submit')"}
Button Element Actions

Mining buttons works similarly:

class GUIGrammarMiner(GUIGrammarMiner):
    def mine_button_element_actions(self) -> Set[str]:
        """Determine all button actions on the current Web page"""

        actions = set()

        for elem in self.driver.find_elements(By.TAG_NAME, "button"):
            try:
                button_type = elem.get_attribute("type")
                button_name = elem.get_attribute("name")
                if button_name is None:
                    button_name = elem.text
                if button_type == "submit":
                    actions.add("submit('%s')" % html.escape(button_name))
                elif button_type != "reset":
                    actions.add("click('%s')" % html.escape(button_name))
            except StaleElementReferenceException:
                pass

        return actions

Our order form has no button elements. (The submit button is an input element, and was handled above).

gui_grammar_miner = GUIGrammarMiner(gui_driver)
gui_grammar_miner.mine_button_element_actions()
set()

When following links, we need to make sure that we stay on the current host – we want to explore a single website only, not all the Internet. To this end, we check the href attribute of the link to check whether it still points to the same host. If it does not, we give it a special action ignore(), which, as the name suggests, will later be ignored as it comes to executing these actions. We still return an action, though, as we use the set of actions to characterize a state in the application.

from urllib.parse import urljoin, urlsplit
class GUIGrammarMiner(GUIGrammarMiner):
    def mine_a_element_actions(self) -> Set[str]:
        """Determine all link actions on the current Web page"""

        actions = set()

        for elem in self.driver.find_elements(By.TAG_NAME, "a"):
            try:
                a_href = elem.get_attribute("href")
                if a_href is not None:
                    if self.follow_link(a_href):
                        actions.add("click('%s')" % html.escape(elem.text))
                    else:
                        actions.add("ignore('%s')" % html.escape(elem.text))
            except StaleElementReferenceException:
                pass

        return actions

To check whether we can follow a link, the method follow_link() checks the URL:

class GUIGrammarMiner(GUIGrammarMiner):
    def follow_link(self, link: str) -> bool:
        """Return True iff we are allowed to follow the `link` URL"""

        if not self.stay_on_host:
            return True

        current_url = self.driver.current_url
        target_url = urljoin(current_url, link)
        return urlsplit(current_url).hostname == urlsplit(target_url).hostname

In our application, we would not be allowed to follow a link to foo.bar:

gui_grammar_miner = GUIGrammarMiner(gui_driver)
gui_grammar_miner.follow_link("ftp://foo.bar/")
False

Following a link to localhost, though, works well:

gui_grammar_miner.follow_link("https://127.0.0.1/")
True

When adapting this for other user interfaces, similar measures would be taken to ensure we stay in the same application.

Running this method on our page gets us the set of links:

gui_grammar_miner = GUIGrammarMiner(gui_driver)
gui_grammar_miner.mine_a_element_actions()
{"click('terms and conditions')"}

Let us show GUIGrammarMiner in action, using its mine_state_actions() method to retrieve all elements from our current page. We see that we obtain input element actions, button element actions, and link element actions.

gui_grammar_miner = GUIGrammarMiner(gui_driver)
gui_grammar_miner.mine_state_actions()
frozenset({"check('terms', <boolean>)",
           "click('terms and conditions')",
           "fill('city', '<text>')",
           "fill('email', '<email>')",
           "fill('name', '<text>')",
           "fill('zip', '<number>')",
           "submit('submit')"})

We assume that we can identify a user interface state from the set of interactive elements it contains – that is, the current Web page is identified by the set above. This is in contrast to Web fuzzing, where we assumed the URL to uniquely characterize a page – but with JavaScript, the URL can stay unchanged although the page contents change, and UIs other than the Web may have no concept of unique URLs. Therefore, we say that the way a UI can be interacted with uniquely defines its state.

Models for User Interfaces

User Interfaces as Finite State Machines

Now that we can retrieve UI elements from a page, let us go and systematically explore a user interface. The idea is to represent the user interface as a finite state machine – that is, a sequence of states that can be reached by interacting with the individual user interface elements.

Let us illustrate such a finite state machine by looking at our Web server. The following diagram shows the states our server can be in:

\<start\> <start> \<Order Form\> <Order Form> \<start\>->\<Order Form\> \<Terms and Conditions\> <Terms and Conditions> \<Order Form\>->\<Terms and Conditions\> click('Terms and conditions') \<Thank You\> <Thank You> \<Order Form\>->\<Thank You\> fill(...) submit('submit') \<Terms and Conditions\>->\<Order Form\> click('order form') \<Thank You\>->\<Order Form\> click('order form')

Initially, we are in the <Order Form> state. From here, we can click on Terms and Conditions, and we'll be in the Terms and Conditions state, showing the page with the same title. We can also fill out the form and place the order, having us end in the Thank You state (again showing the page with the same title). From both <Terms and Conditions> and <Thank You>, we can return to the order form by clicking on the order form link.

State Machines as Grammars

To systematically explore a user interface, we must retrieve its finite state machine, and eventually cover all states and transitions. In the presence of forms, such an exploration is difficult, as we need a special mechanism to fill out forms and submit the values to get to the next state. There is a trick, though, which allows us to have a single representation for both states and (form) values. We can embed the finite state machine into a grammar, which is then used for both states and form values.

To embed a finite state machine into a grammar, we proceed as follows:

  1. Every state $\langle s \rangle$ in the finite state machine becomes a symbol $\langle s \rangle$ in the grammar.
  2. Every transition in the finite state machine from $\langle s \rangle$ to $\langle t \rangle$ and actions $a_1, a_2, \dots$ becomes an alternative of $\langle s \rangle$ in the form $a_1, a_2, dots$ $\langle t \rangle$ in the grammar.

The above finite state machine thus gets encoded into the grammar

<start> ::= <Order Form>
<Order Form> ::= click('Terms and Conditions') <Terms and Conditions> | 
                 fill(...) submit('submit') <Thank You>
<Terms and Conditions> ::= click('order form') <Order Form>
<Thank You> ::= click('order form') <Order Form>

Expanding this grammar gets us a stream of actions, navigating through the user interface:

fill(...) submit('submit') click('order form') click('Terms and Conditions') click('order form') ...

This stream is actually infinite (as one can interact with the UI forever); to have it end, one can introduce an alternative <end> that simply expands to the empty string, without having any expansion (state) follow.

Retrieving State Grammars

Let us extend GUIGrammarMiner such that it retrieves a grammar from the user interface in its current state.

Implementing Extracting State Grammars

We first define a constant GUI_GRAMMAR that serves as template for all sorts of input types. We will use this to fill out forms.

\todo{}: Have a common base class GrammarMiner with __init__() and mine_grammar()

from Grammars import new_symbol
from Grammars import nonterminals, START_SYMBOL
from Grammars import extend_grammar, unreachable_nonterminals, crange, srange
from Grammars import syntax_diagram, is_valid_grammar, Grammar
class GUIGrammarMiner(GUIGrammarMiner):
    START_STATE = "<state>"
    UNEXPLORED_STATE = "<unexplored>"
    FINAL_STATE = "<end>"

    GUI_GRAMMAR: Grammar = ({
        START_SYMBOL: [START_STATE],
        UNEXPLORED_STATE: [""],
        FINAL_STATE: [""],

        "<text>": ["<string>"],
        "<string>": ["<character>", "<string><character>"],
        "<character>": ["<letter>", "<digit>", "<special>"],
        "<letter>": crange('a', 'z') + crange('A', 'Z'),

        "<number>": ["<digits>"],
        "<digits>": ["<digit>", "<digits><digit>"],
        "<digit>": crange('0', '9'),

        "<special>": srange(". !"),

        "<email>": ["<letters>@<letters>"],
        "<letters>": ["<letter>", "<letters><letter>"],

        "<boolean>": ["True", "False"],

        # Use a fixed password in case we need to repeat it
        "<password>": ["abcABC.123"],

        "<hidden>": ["<string>"],
    })
syntax_diagram(GUIGrammarMiner.GUI_GRAMMAR)
start
state
unexplored
end
text
string
string
character string character
character
letter digit special
letter
e d c b a f g h i j o n m l k p q r s t y x w v u z A B C D I H G F E J K L M N S R Q P O T U V W X Y Z
number
digits
digits
digit digits digit
digit
0 1 2 3 4 5 6 7 8 9
special
. !
email
letters @ letters
letters
letter letters letter
boolean
True False
password
abcABC.123
hidden
string

The method mine_state_grammar() goes through the actions mined from the page (using mine_state_actions()) and creates a grammar for the current state. For each click() and submit() action, it assumes a new state follows, and introduces an appropriate state symbol into the grammar – a state symbol that now will be marked as <unexplored>, but will be expanded later as the appropriate state is seen.

class GUIGrammarMiner(GUIGrammarMiner):
    def new_state_symbol(self, grammar: Grammar) -> str:
        """Return a new symbol for some state in `grammar`"""
        return new_symbol(grammar, self.START_STATE)

    def mine_state_grammar(self, grammar: Grammar = {},
                           state_symbol: Optional[str] = None) -> Grammar:
        """Return a state grammar for the actions on the current Web site.
        Can be overloaded in subclasses."""

        grammar = extend_grammar(self.GUI_GRAMMAR, grammar)

        if state_symbol is None:
            state_symbol = self.new_state_symbol(grammar)
            grammar[state_symbol] = []

        alternatives = []
        form = ""
        submit = None

        for action in self.mine_state_actions():
            if action.startswith("submit"):
                submit = action

            elif action.startswith("click"):
                link_target = self.new_state_symbol(grammar)
                grammar[link_target] = [self.UNEXPLORED_STATE]
                alternatives.append(action + '\n' + link_target)

            elif action.startswith("ignore"):
                pass

            else:  # fill(), check() actions
                if len(form) > 0:
                    form += '\n'
                form += action

        if submit is not None:
            if len(form) > 0:
                form += '\n'
            form += submit

        if len(form) > 0:
            form_target = self.new_state_symbol(grammar)
            grammar[form_target] = [self.UNEXPLORED_STATE]
            alternatives.append(form + '\n' + form_target)

        alternatives += [self.FINAL_STATE]

        grammar[state_symbol] = alternatives

        # Remove unused parts
        for nonterminal in unreachable_nonterminals(grammar):
            del grammar[nonterminal]

        assert is_valid_grammar(grammar)

        return grammar

To better see the state structure, the function fsm_diagram() shows the resulting state grammar as a finite state machine. (This assumes that the grammar actually encodes a state machine.)

from collections import deque
from bookutils import unicode_escape
def fsm_diagram(grammar: Grammar, start_symbol: str = START_SYMBOL) -> Any:
    """Produce a FSM diagram for the state grammar `grammar`.
    `start_symbol` - the start symbol (default: START_SYMBOL)"""

    from graphviz import Digraph
    from IPython.display import display

    def left_align(label: str) -> str:
        """Render `label` as left-aligned in dot"""
        return dot_escape(label.replace('\n', r'\l')).replace(r'\\l', '\\l')

    dot = Digraph(comment="Grammar as Finite State Machine")

    symbols = deque([start_symbol])
    symbols_seen = set()

    while len(symbols) > 0:
        symbol = symbols.popleft()
        symbols_seen.add(symbol)
        dot.node(symbol, dot_escape(unicode_escape(symbol)))

        for expansion in grammar[symbol]:
            assert type(expansion) == str  # no opts() here

            nts = nonterminals(expansion)
            if len(nts) > 0:
                target_symbol = nts[-1]
                if target_symbol not in symbols_seen:
                    symbols.append(target_symbol)

                label = expansion.replace(target_symbol, '')
                dot.edge(symbol, target_symbol, left_align(unicode_escape(label)))

    return display(dot)

Let us show GUIGrammarMiner() in action. Its method mine_state_grammar() extracts the grammar for the current Web page:

gui_grammar_miner = GUIGrammarMiner(gui_driver)
state_grammar = gui_grammar_miner.mine_state_grammar()
state_grammar
{'<start>': ['<state>'],
 '<unexplored>': [''],
 '<end>': [''],
 '<text>': ['<string>'],
 '<string>': ['<character>', '<string><character>'],
 '<character>': ['<letter>', '<digit>', '<special>'],
 '<letter>': ['a',
  'b',
  'c',
  'd',
  'e',
  'f',
  'g',
  'h',
  'i',
  'j',
  'k',
  'l',
  'm',
  'n',
  'o',
  'p',
  'q',
  'r',
  's',
  't',
  'u',
  'v',
  'w',
  'x',
  'y',
  'z',
  'A',
  'B',
  'C',
  'D',
  'E',
  'F',
  'G',
  'H',
  'I',
  'J',
  'K',
  'L',
  'M',
  'N',
  'O',
  'P',
  'Q',
  'R',
  'S',
  'T',
  'U',
  'V',
  'W',
  'X',
  'Y',
  'Z'],
 '<number>': ['<digits>'],
 '<digits>': ['<digit>', '<digits><digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<special>': ['.', ' ', '!'],
 '<email>': ['<letters>@<letters>'],
 '<letters>': ['<letter>', '<letters><letter>'],
 '<boolean>': ['True', 'False'],
 '<state>': ["click('terms and conditions')\n<state-1>",
  "check('terms', <boolean>)\nfill('city', '<text>')\nfill('email', '<email>')\nfill('name', '<text>')\nfill('zip', '<number>')\nsubmit('submit')\n<state-2>",
  '<end>'],
 '<state-1>': ['<unexplored>'],
 '<state-2>': ['<unexplored>']}

To better see the structure of the state grammar, we can visualize it as a state machine. We see that it nicely reflects what we can see from our Web server's home page:

fsm_diagram(state_grammar)
start <start> state <state> start->state state-1 <state-1> state->state-1 click('terms and conditions') state-2 <state-2> state->state-2 check('terms', <boolean>) fill('city', '<text>') fill('email', '<email>') fill('name', '<text>') fill('zip', '<number>') submit('submit') end <end> state->end unexplored <unexplored> state-1->unexplored state-2->unexplored

From the start state (<state>), we can go and either click on "terms and conditions", ending in <state-1>, or fill out the form, ending in <state-2>.

state_grammar[GUIGrammarMiner.START_STATE]
["click('terms and conditions')\n<state-1>",
 "check('terms', <boolean>)\nfill('city', '<text>')\nfill('email', '<email>')\nfill('name', '<text>')\nfill('zip', '<number>')\nsubmit('submit')\n<state-2>",
 '<end>']

Both these states are yet unexplored:

state_grammar['<state-1>']
['<unexplored>']
state_grammar['<state-2>']
['<unexplored>']
state_grammar['<unexplored>']
['']

Given the grammar, we can use any of our grammar fuzzers to create valid input sequences:

from GrammarFuzzer import GrammarFuzzer
gui_fuzzer = GrammarFuzzer(state_grammar)
while True:
    action = gui_fuzzer.fuzz()
    if action.find('submit(') > 0:
        break
print(action)
check('terms', False)
fill('city', 'xo')
fill('email', 'W@L')
fill('name', '.')
fill('zip', '95')
submit('submit')

These actions, however, must also be executed such that we can explore the user interface. This is what we do in the next section.

Executing User Interface Actions

To execute actions, we introduce a Runner class, conveniently named GUIRunner. Its run() method executes the actions as given in an action string.

from Fuzzer import Runner
class GUIRunner(Runner):
    """Execute the actions in a given action string"""

    def __init__(self, driver) -> None:
        """Constructor. `driver` is a Selenium Web driver"""
        self.driver = driver
Implementing Executing UI Actions

The way we implement run() is fairly simple: We introduce four methods named fill(), check(), submit() and click(), and run exec() on the action string to have the Python interpreter invoke these methods.

Running exec() on third-party input is dangerous, as the names of UI elements may contain valid Python code. We restrict access to the four functions defined above, and also set __builtins__ to the empty dictionary such that built-in Python functions are not available during exec(). This will prevent accidents, but as we will see in the chapter on information flow, it is still possible to inject Python code. To prevent such injection attacks, we use html.escape() to quote angle and quote characters in all third-party strings.

class GUIRunner(GUIRunner):
    def run(self, inp: str) -> Tuple[str, str]:
        """Execute the action string `inp` on the current Web site.
        Return a pair (`inp`, `outcome`)."""

        def fill(name, value):
            self.do_fill(html.unescape(name), html.unescape(value))

        def check(name, state):
            self.do_check(html.unescape(name), state)

        def submit(name):
            self.do_submit(html.unescape(name))

        def click(name):
            self.do_click(html.unescape(name))

        exec(inp, {'__builtins__': {}},
                  {
                      'fill': fill,
                      'check': check,
                      'submit': submit,
                      'click': click,
                  })

        return inp, self.PASS

To identify elements in an action, we first search them by their name, and then by the displayed link text.

from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import ElementClickInterceptedException, ElementNotInteractableException
class GUIRunner(GUIRunner):
    def find_element(self, name: str) -> Any:
        """Search for an element named `name` on the current Web site.
        Matches can occur by name or by link text."""

        try:
            return self.driver.find_element(By.NAME, name)
        except NoSuchElementException:
            return self.driver.find_element(By.LINK_TEXT, name)

The implementations of the actions simply defer to the appropriate Selenium methods, introducing explicit delays such that the page can reload and refresh.

from selenium.webdriver.support.ui import WebDriverWait
class GUIRunner(GUIRunner):
    # Delays (in seconds)
    DELAY_AFTER_FILL = 0.1
    DELAY_AFTER_CHECK = 0.1
    DELAY_AFTER_SUBMIT = 1.5
    DELAY_AFTER_CLICK = 1.5
class GUIRunner(GUIRunner):
    def do_fill(self, name: str, value: str) -> None:
        """Fill the text element `name` with `value`"""

        element = self.find_element(