Nova Flow OS
KDE Developer Platform
KDE Developer Platform
  • KDE Developer Platform
    • Getting started
      • Building KDE software
        • KDE software
        • Where to find the development team
        • Learning more
        • Choose what to work on
        • Source code cross-referencing
        • Installing build dependencies
        • Set up a development environment
        • Building KDE software with kdesrc-build
        • Basic troubleshooting
        • Tips and tricks
        • IDE Configuration
          • Setting up an IDE for KDE development
          • Visual Studio Code
          • Qt Creator
          • Kate
          • KDevelop
          • CLion
          • Sublime Text
        • Building KDE software manually
        • Building KDE software with distrobox and podman
      • Kirigami
        • KDE is ours
        • Setting up and getting started
        • Explaining pages
        • Layouts, ListViews, and Cards
        • Adding actions
        • Adding a dialog
        • Using separate files
        • Next steps
        • Colors and themes in Kirigami
        • Typography
        • Actions based components
        • Page rows and page stacks
        • Scrollable pages and list views
        • Cards
        • Drawers
        • Chips
        • Dialog types
        • Controls and interactive elements
        • Form layouts
        • Inline messages
        • Action toolbars
        • Progress bars and indicators
        • List views
        • Understanding CMakeLists
        • Figuring out main.cpp
        • Connect logic to your QML user interface
        • Connect models to your QML user interface
        • About page
        • Introduction to Kirigami Addons
        • FormCard About pages
        • Form delegates in your settings pages
      • KXmlGui
        • Getting started with KXmlGui
        • Hello World!
        • Creating the main window
        • Using actions
        • Saving and loading
        • Command line interface
      • Python with Kirigami
        • Apps with QML and Python
        • Your first Python + Kirigami application
        • Creating a Python package
        • Creating a Flatpak
      • Common programming mistakes
      • Adding a new KDE project
    • Features
      • Icons
      • Configuration
        • The KConfig Framework
        • Introduction to KConfig
        • Using KConfig XT
        • KDE Frameworks 6 porting guide
        • Settings module (KCM) development
        • KConfigDialog
      • D-Bus
        • What is D-Bus practically useful for?
        • Introduction to D-Bus
        • Accessing D-Bus interfaces
        • Intermediate D-Bus
        • Creating D-Bus interfaces
        • Using custom types with D-Bus
        • D-Bus autostart services
      • Create your own mouse cursor theme
      • Session management
      • Archives
      • Desktop file
      • KAuth
        • Privilege Escalation
        • Using actions in your applications
      • KIdleTime
      • Akonadi: personal information management
        • Debugging Akonadi Resources
        • Using Akonadi in applications
      • Concurrent programming
      • Solid
      • Sonnet
    • Plasma themes and plugins
      • Getting started
      • Plasma Widget tutorial
        • How to create a plasmoid
        • Setup
        • Porting Plasmoids to KF6
        • Testing
        • QML
        • Plasma's QML API
        • Widget Properties
        • Configuration
        • Translations / i18n
        • Examples
        • C++ API
      • KWin Effects
      • Plasma Desktop scripting
        • Javascript Interaction With Plasma Shells
        • Templates
        • Examples
        • API documentation
        • Configuration keys
      • Plasma Style tutorial
        • Creating a Plasma Style quickstart
        • Understanding Plasma Styles
        • SVG elements and Inkscape
        • Background SVG format
        • System and accent colors
        • Theme elements reference
        • Porting themes to Plasma 5
        • Porting themes to Plasma 6
      • Aurorae window decorations
      • KWin scripting tutorial
        • Quick start
        • KWin scripting API
      • Wallpapers
      • Plasma comic
        • Tutorial
        • Testing and debugging
        • Examples
      • Create a custom Window Switcher
      • KRunner C++ Plugin
        • Basic Anatomy of a Runner
        • KRunner metadata format
    • Applications
      • Creating sensor faces
      • Dolphin
        • Creating Dolphin service menus
      • Kate
        • Kate plugin tutorial
      • KMines
        • Making a KMines theme
      • Writing tests
        • Appium automation testing
    • Packaging
      • Android
        • KDE on Android
        • Building applications for Android
        • Packaging and publishing applications for Android
        • Publishing on Google Play
          • Introduction
          • Packaging your app
          • Adding your app to Google Play
          • Publishing your app
          • Releasing new versions of old apps
        • Porting applications to Android
          • Basic porting
          • Making applications run well on Android
          • Metadata
      • Windows
        • Packaging and publishing applications for Windows
        • Publish your app in the Microsoft Store
          • Packaging your app for the Microsoft Store
          • Submitting your app to the Microsoft Store
      • Plasma Mobile
        • KDE on mobile devices
        • Porting a new device to Plasma Mobile
        • KDE Telephony stack
          • General Overview
          • Kernel layer
          • System daemons
            • General overview
            • Developing Telephony functionality
            • ModemManager Telephony functions
          • Session daemons
          • QML declarative plugin layer
          • KDE application layer
        • Execute applications
      • Distributing KDE software as Flatpak
        • Your first Flatpak
        • Extending your package
        • Nightly Flatpaks and Flathub
        • Testing your Flatpak
    • System administration
      • Shell scripting with KDE dialogs
      • Kiosk: Simple configuration management for large deployment
        • Abstract
        • Introduction to Kiosk
        • Kiosk keys
    • Contribute to the documentation
    • About
      • Readme
      • License
        • Creative Commons Attribution-ShareAlike 4.0 International
        • GNU General Public License 3.0 or later
Powered by GitBook
On this page
  • Introduction
  • The Linux Driver: Selenium AT-SPI
  • Installation
  • Running Tests
  1. KDE Developer Platform
  2. Applications
  3. Writing tests

Appium automation testing

Learn how to run Appium tests for applications on Linux

PreviousWriting testsNextPackaging

Last updated 8 months ago

Introduction

KDE uses manually triggered unit tests as well as autotests to prevent code changes to our applications from introducing usability issues. Oftentimes, this is not enough for graphical applications, so testing the user interface directly is required. To automate this, KDE uses Appium tests.

is an open source tool based on for automating applications on various mobile and desktop platforms.

Selenium itself is used to automate testing web applications. Appium derives from it to allow for application testing in multiple platforms and in multiple languages.

Appium does this leverage of multiple platforms and languages by using a driver. A driver is a component or interface that acts as a bridge between your test scripts and the application you want to automate. With it, developers can write Selenium-style user interface tests for manipulating the user interface, test accessibility, and measure power consumption.

The Linux Driver: Selenium AT-SPI

Appium has the ability to automate Linux apps by using the Linux accessibility API . KDE has a driver for this called , a server for Linux apps that runs the application in a localhost webserver so it can work in non-GUI environments (such as CI jobs), while also being able to run locally in a Wayland compositor (KWin) so the tester can run the tests manually and see the results.

The Linux Driver supports testing applications that implement the AT-SPI2 interface. Applications written in or have good support for the AT-SPI2 interface.

In Qt's case, all you need to do is to follow standard practices.

This tutorial will provide a step-by-step guide to use Selenium AT-SPI to automate system testing of QtQuick applications using Python scripts.

Installation

This tutorial requires Python 3.8+ which is included by default in most distributions.

Installation of (an interactive accessibility explorer) and (screen reader) are recommended if you want to test accessibility with Appium tests.

Building from source

After the development environment is set up, run the command below to build both selenium-webdriver-at-spi and kcalc from source. The command will automatically install all the required dependencies:

kdesrc-build kcalc selenium-webdriver-at-spi

Run the example test script to check that your installation is working.

With kdesrc-build:

kdesrc-build --run --exec selenium-webdriver-at-spi-run selenium-webdriver-at-spi ~/kde/src/selenium-webdriver-at-spi/examples/calculatortest.py

With kde-builder:

kde-builder --run selenium-webdriver-at-spi-run ~/kde/src/selenium-webdriver-at-spi/examples/calculatortest.py

You will see a black window titled "KDE Wayland Compositor" running a KCalc window and having a few buttons getting highlighted (very fast!) as they're activated.

After the test is complete, the window will close itself automatically, and you can see the test result in the console output.

----------------------------------------------------------------------
Ran 6 tests in 38.326s

OK
I, [2023-10-17T20:37:55.194322 #32112]  INFO -- : tests done
I, [2023-10-17T20:37:55.217687 #32112]  INFO -- : run.rb exiting true

Building manually

To build Selenium manually, you will need to install its dependencies first:

Then clone the repository and build it:

git clone https://invent.kde.org/sdk/selenium-webdriver-at-spi.git
cd selenium-webdriver-at-spi
cmake -B build/
cmake --build build/
sudo cmake --install build/

This will install it in your host root.

Running Tests

Since this tutorial will focus on test scripts, at first we will be using QML code snippets instead of a full project to exemplify their use. First, we should see how an Appium test looks like.

Anatomy of an Appium test

Writing an Appium test consists of three things:

  • a class that is initialized with Appium options

  • class members that start with the name "test_"

  • an entrypoint

At minimum, an Appium test should contain the following:

import unittest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.options.common.base import AppiumOptions
from selenium.webdriver.support.ui import WebDriverWait

class YourTestClass(unittest.TestCase):
    @classmethod
    def setUpClass(self) -> None:
        options = AppiumOptions()
        options.set_capability("app", "/path/to/program")
        self.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
        self.driver.implicitly_wait = 10

    @classmethod
    def tearDownClass(self) -> None:
        self.driver.quit()

    @classmethod
    def test_something(self) -> None:
        # Do something here

if __name__ == '__main__':
    unittest.main()

The "app" capability points to the application that will be tested, and it can be:

  • The absolute path to an executable and optionally its command line parameters.

  • The relative path to an executable and optionally its command line parameters (used for autotests when the application is being built).

  • The desktop file that runs the application, without the file path and including the .desktop extension.

The second class method functions like a destructor that makes sure the driver will quit once the tests are done. It also lets you terminate any other external processes you might have started for your tests, like a small web server.

Next we use some boilerplate code that allows Appium to run at a localhost web server at a certain port, passing the options and capabilities we have defined. Then, we set a timeout of 10 seconds to allow the test to prepare and run the application when it is executed.

As mentioned before, we need a class method that starts with the name "test_" and that will perform the actual test.

Lastly we create the main entrypoint for our test.

GUI interaction via Appium tests

For QML applications, interaction with Appium tests is simple and consists of two steps:

  1. Defining a property to a QML element that makes it locatable.

  2. Using a Selenium Locator to find that QML element.

Essential Locators

For example, if we have a QtQuick.Controls.Button:

Controls.Button {
    text: "Hello World!"
    Accessible.name: "Button"
}

A test that clicks said button can be defined like the following:

def test_click_button(self) -> None:
    self.driver.find_element(by=AppiumBy.NAME, value="Button").click()
    # Or the less direct but more legible:
    # button = self.driver.find_element(by=AppiumBy.NAME, value="Button")
    # button.click()

A string can be directly passed to find_element() as well. This is useful to access the Accessible.description of an element:

Controls.Button {
    text: "Hello World!"
    Accessible.description: "A clickable button"
}

In a test, we'd use:

def test_click_button(self) -> None:
    self.driver.find_element(by="description", value="A clickable button").click()
Controls.TextField {
    placeholderText: "Hello World!"
    Accessible.name: "Text field"
}

You would simply do:

def test_input(self) -> None:
    text_input = self.driver.find_element(by=AppiumBy.NAME, value="Text field")
    text_input.send_keys("Hello Appium")
from selenium.webdriver.common.keys import Keys

# ...

def test_dialog_confirm(self) -> None:
    edit_field = self.driver.find_element(by=AppiumBy.NAME, value="Text field")
    edit_field.send_keys("myquery")
    edit_field.send_keys(Keys.ENTER)
searchFocused = searchField.get_attribute('focused')

There are many other convenience functions:

Other ways to use locators

Item {
    objectName: "Button"
}

The locator would be used like so:

def test_click_button(self) -> None:
    self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value="Button").click()

While the accessibility ID is a unique identifier, the Role on the other hand is a generic identifier for an element type, especially used for keyboard navigation. It is used to determine how to traverse focus, for example, in a menu bar, a tool bar, a check box, and other elements. In code terms, this means a QtQuick.Controls.Button and a Kirigami.Action attached to a button would share the same role of "push button", as they are both functionally buttons.

To inspect roles in KDE applications, you first need to open Accerciser, and then run the desired KDE application by passing the environment variable QT_LINUX_ACCESSIBILITY_ALWAYS_ON, like so:

QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1 plasma-discover

Discover for example has a test that first waits for the loading screen to vanish before searching for certain user interface elements. To do so, it first searches for the "Loading..." label's Role:

Kirigami.Action {
    text: {
        if (!root.isStateAvailable) {
            return i18nc("State being fetched", "Loading…")
        }
        // Rest of code here
    }
}
def test_search_install_uninstall(self):
    WebDriverWait(self.driver, 30).until(
        EC.invisibility_of_element_located((AppiumBy.CLASS_NAME, "[label | Loading…]"))
    )
    # Rest of code

Testing and asserting

So far, we have seen how to interact with GUI elements, but we haven't tested anything yet.

# a == b
assertEqual(a, b)
# a != b
assertNotEqual(a, b)
# bool(x) is True
assertTrue(x)
# bool(x) is False
assertFalse(x)
# member is in container
assertIn(member,container)
# member is not in container
assertNotIn(member, container)

A common pattern used in KDE software is to write a getresults() function that retrieves a property from a certain element. This is useful for when multiple UI elements are handled and the result culminates into a single element. The standard calculatortest.py provided with Selenium AT-SPI is an example of this, with the input of multiple calculator keys being probed before checking the result in the text field:

def getresults(self):
    displaytext = self.driver.find_element(by='description', value='Result Display').text
    return displaytext

Another assert pattern is used to test whether the values you get from interacting with the application's UI matches an expected result. This can be done with an assertResult(self, actual, expected) method. Generally you want for the driver to have finished loading and having run the getresults() method, so a timeout is needed.

def assertResult(self, actual, expected):
    wait = WebDriverWait(self.driver, 20)
    wait.until(lambda x: self.getresults() == expected)
    self.assertEqual(self.getresults(), expected)

def test_addition(self):
    self.driver.find_element(by=AppiumBy.NAME, value="1").click()
    self.driver.find_element(by=AppiumBy.NAME, value="+").click()
    self.driver.find_element(by=AppiumBy.NAME, value="7").click()
    self.driver.find_element(by=AppiumBy.NAME, value="=").click()
    self.assertResult(self.getresults(), "8")
def tearDownClass(self) -> None:
    if not self._outcome.result.wasSuccessful():
        self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))

Now you should know everything you need to write your own Appium tests! 🎉

,

,

is the recommended tool to build selenium-webdriver-at-spi from source. Learn .

,

The class can have any name you'd like, conventionally using uppercase, and it must derive from .

The first class method is like a constructor where we perform some setup and can have any name, but it is conventionally called "setUpClass()". AppiumOptions() provides the default settings for our tests. set_capability() takes a , analogous to or .

In this case, the first string is the key, the second string is the value. The list of keys and values available can be seen in the . As can be seen in the documentation, the string "app" used above corresponds to the capability "appium:app".

The Qt API that is required to make a QML element locatable is . Typically what is used is or .

To find that QML element, we'd use the aptly named Appium function , to which we pass a .

The locator that is most commonly used for tests in KDE software is . It matches the Accessible.name set in the QML code.

Another particularly useful Appium function we can use is , which allows to send keyboard input to the application. With a simple QtQuick.Controls.TextField for example:

If you need to enter certain non-typable keys, such as Enter/Return, you can use for that:

Another commonly used function is . It can be used to determine whether an element currently has an Accessible attribute such as or enabled.

touchscreen

mouse

These can be seen at the .

Additional commands specific to using Python scripts can be seen at the .

In the introductory section , you became aware that Linux has a standard driver for handling accessibility. This driver is used, for example, in the Qt and GTK toolkits, and it specifies things such as accessibility IDs or Roles.

For the accessibility ID, which serves as a unique identifier for a user interface element, we can use the Appium-provided locator , which should match the QML .

The accessibility ID can be used in a generic manner like NAME or "description" as shown above, but its primary use is with QtWidgets applications. You can see a live example of this in .

You can view the available roles in an application by using , an application that allows you to explore accessibility properties in other programs. KDE also has its own accessibility explorer, , which has not yet been packaged by distros.

Then it uses the Selenium function together with the Selenium locator to be sure the "Loading..." element is no longer there.

Generally, tests assert whether the performed action was successful or not. This is done by using the . The most common methods are:

Whenever a test fails, it is customary to take a screenshot of the result so that even tests done in the can be viewed by developers as downloadable artifacts. This can be achieved with in the tearDownClass() destructor:

sudo apt install accerciser orca
sudo pacman -S accerciser orca
sudo zypper install accerciser orca
sudo dnf install accerciser orca
sudo apt install extra-cmake-modules libkf5windowsystem-dev libkf5wayland-dev libkpipewire-dev kwin-dev libwayland-dev
sudo pacman -S extra-cmake-modules kwindowsystem5 kwayland5 kpipewire kwin wayland
sudo zypper install extra-cmake-modules kwindowsystem-devel kwayland-devel kpipewire-devel kwin5-devel wayland-devel
sudo dnf install extra-cmake-modules kf5-kwindowsystem-devel kf5-kwayland-devel kpipewire-devel kwin-devel wayland-deve
Appium
Selenium
AT-SPI2
Selenium AT-SPI
WebDriver
Qt5/Qt6
GTK3/GTK4
Qt Accessibility
Accerciser
Orca
kdesrc-build
how to set up a KDE development environment using kdesrc-build
Python's standard unittest library
Python dictionary
C++ std::map
Qt's QMap
Appium Documentation for Capabilities
Accessible
Accessible.name
Accessible.description
find_element()
Selenium Locator
NAME
send_keys()
Selenium Keys
get_attribute()
focused
selected
get_clipboard_text()
double_click()
tap()
move_to()
get_screenshot_as_file()
Appium Commands documentation
Selenium Python documentation
ACCESSIBILITY_ID
objectName
KDebugSettings
Accerciser
Accessibility Inspector
invisibility_of_element_located()
CLASS_NAME
Python unittest module's assert methods
KDE CI
get_screenshot_as_file(filename)
The Linux Driver: Selenium AT-SPI
Kubuntu
KDE Neon
Manjaro
Arch
OpenSUSE
Fedora
Kubuntu
KDE Neon
Manjaro
Arch
OpenSUSE
Fedora