This is Appium driver for automating linux applications using Linux's AtSpi2 framework. The driver operates in scope of W3C WebDriver protocol with several custom extensions to cover operating-system specific scenarios. Yaoshi technical consultant sponses this project and contributed the underlying linux automation framework. The automation framework is distributed as debian installer.
Running a linux automated test requires to install the following three components:
- The forked version of Appium: https://github.com/fantonglang/appium
- This linux driver: https://github.com/fantonglang/appium-linux-driver
- Yaoshi's automation framework
git clone https://github.com/fantonglang/appium
cd appium
yarn install
node ./
The linux driver is referenced as dependency in appium, so no need to install it explicitly.
It has been tested on Ubuntu 20.04/18.04 LTS. Here is the download link for installer. Install it with the following instructions:
sudo apt-get install gdebi-core
sudo gdebi stdspalinux-ubuntu_18_04.deb
We have plan to publish the installer for deepin, and also docker image for test automation in the cloud native way.
Any requests, such as support another linux distribution; extend user interaction gestures; build a commercial product with better performance and more capabilities, please always feel free to contact me:
Name | ||
---|---|---|
Fan Tong | zhao_sun@hotmail.com | S597653509 |
A custom UI inspector is provided for users. It can:
- List all the running windows with their names. Window name is used as parameter to switch between windows.
- Show XML UI hierachy of a window, and validate XPath. The user experience is inspired by Appium Inspector.
Capability Name | Description |
---|---|
platformName | Should be set to linux |
automationName | Must always be set to atspi2 . Values of automationName are compared case-insensitively. |
appium:appName | The path of the application to automate, for example /bin/yelp , or short-hand yelp because /bin is in the path environment variable. This is a required capability. If the given application is not installed then an error will be thrown on session startup. If the application is already running then it will be killed and restarted. |
Linux driver supports the following element attributes:
Name | Description | Example |
---|---|---|
rect | Coordinates of bounding element rectangle | {x: 1, y: 2, width: 100, height: 200} |
size | Size of bounding element rectangle | {width: 100, height: 200} |
name | Element's name value. Could be empty | 'my name' |
text | Element's text value. Could be empty | 'my text' |
More attributes could be visited via the WebElement.get_property(name: str)
API. Here is an example source of UI element: <tree-item name="package.json" pid="2575" toolkit="Chromium__1.0" states="[ENABLED,FOCUSABLE,SELECTABLE,SELECTED,SENSITIVE,SHOWING,VISIBLE]" rect="[48,746,368,22]" text-align="left" explicit-name="true" class="monaco-list-row focused selected" display="block" posinset="9" level="2" xml-roles="treeitem" tag="div" id="list_id_2_37" setsize="11" />
.
To get the states of an element, use the property states
. e.g. to judge if the element is selected or not, write:
if contains(el.get_property('states'), 'SELECTED'):
print('element is selected')
Element attribute values could be retrieved from the XML page source. UI inspector can also visualize element's attributes. All attributes which present in the XML page source could be used for elements location, including but not limited to the attributes in the table.
Linux driver supports the following location strategies:
Name | Description | Example |
---|---|---|
name | The name strategy uses element's name attribute for lookup. |
driver.find_element(AppiumBy.NAME, 'Find') |
xpath | For elements lookup Xpath strategy the driver uses the same XML tree that is generated by page source API. Only Xpath 1.0 is supported (based on xpath.js). | driver.find_element(AppiumBy.XPATH, "//document-web") |
Check the integration tests for more examples on different location strategies usage.
Enter text or value in the input element
Name | Type | Required | Description | Example |
---|---|---|---|---|
keys | string | yes | The string value to enter in the input element. | 'example text' |
el.set_text('example text')
el.set_value(12)
Perform click gesture on an element
No Arguments
el.click()
Perform press key code gesture. Key code like 'F1', 'F2', 'Delete', 'Return', 'Up', 'Down', 'Left', 'Right'. Combination key can be performed by combining the metastate like 'CONTROL', 'ALT', 'SHIFT'.
The press key code gesture can also simulate press alphabat keys like 'a', 'b', 'c', ..., 'z'. But then the keycode field should use negative value. e.g. the ascii code for 'a' is 97, then the keycode value is -97.
Name | Type | Required | Description | Example |
---|---|---|---|---|
keycode | int | yes | Refer to keycode enumeration | 65535(DELETE), -97(a) |
metastate | int | no | Refer to metastate enumeration | 0(None), 64(META), 8(ALT),4(CONTROL), 1(SHIFT) |
driver.press_keycode(65473, 8) # alt + f4
driver.press_keycode(-118, 4) # control + v
name | keycode | name | keycode |
---|---|---|---|
NOT_A_KEY | 9999 | a | -97 |
BACKSPACE | 65288 | b | -98 |
DELETE | 65535 | c | -99 |
RETURN | 65293 | d | -100 |
TAB | 65289 | e | -101 |
ESCAPE | 65307 | f | -102 |
UP | 65362 | g | -103 |
DOWN | 65364 | h | -104 |
RIGHT | 65363 | i | -105 |
LEFT | 65361 | j | -106 |
HOME | 65360 | k | -107 |
END | 65367 | l | -108 |
PAGEUP | 65365 | m | -109 |
PAGEDOWN | 65366 | n | -110 |
F1 | 65470 | o | -111 |
F2 | 65471 | p | -112 |
F3 | 65472 | q | -113 |
F4 | 65473 | r | -114 |
F5 | 65474 | s | -115 |
F6 | 65475 | t | -116 |
F7 | 65476 | u | -117 |
F8 | 65477 | v | -118 |
F9 | 65478 | w | -119 |
F10 | 65479 | x | -120 |
F11 | 65480 | y | -121 |
F12 | 65481 | z | -122 |
F13 | 65482 | ||
F14 | 65483 | ||
F15 | 65484 | ||
F16 | 65485 | ||
F17 | 65486 | ||
F18 | 65487 | ||
F19 | 65488 | ||
F20 | 65489 | ||
F21 | 65490 | ||
F22 | 65491 | ||
F23 | 65492 | ||
F24 | 65493 | ||
META | 65515 | ||
LMETA | 65515 | ||
RMETA | 65516 | ||
ALT | 65513 | ||
LALT | 65513 | ||
RALT | 65514 | ||
CONTROL | 65507 | ||
LCONTROL | 65507 | ||
RCONTROL | 65508 | ||
SHIFT | 65505 | ||
LSHIFT | 65505 | ||
RSHIFT | 65506 | ||
CAPSLOCK | 65510 | ||
SPACE | 32 | ||
INSERT | 65379 | ||
PRINTSCREEN | 65377 | ||
MENU | 9999 | ||
NUMPAD_0 | 65456 | ||
NUMPAD_1 | 65457 | ||
NUMPAD_2 | 65458 | ||
NUMPAD_3 | 65459 | ||
NUMPAD_4 | 65460 | ||
NUMPAD_5 | 65461 | ||
NUMPAD_6 | 65462 | ||
NUMPAD_7 | 65463 | ||
NUMPAD_8 | 65464 | ||
NUMPAD_9 | 65465 | ||
NUMPAD_LOCK | 65407 | ||
NUMPAD_DECIMAL | 65454 | ||
NUMPAD_PLUS | 78 | ||
NUMPAD_MINUS | 74 | ||
NUMPAD_MUL | 55 | ||
NUMPAD_DIV | 98 | ||
NUMPAD_CLEAR | 9999 | ||
NUMPAD_ENTER | 96 | ||
NUMPAD_EQUAL | 61 | ||
AUDIO_VOLUME_MUTE | 269025042 | ||
AUDIO_VOLUME_DOWN | 269025041 | ||
AUDIO_VOLUME_UP | 269025043 | ||
AUDIO_PLAY | 269025044 | ||
AUDIO_STOP | 269025045 | ||
AUDIO_PAUSE | 269025073 | ||
AUDIO_PREV | 269025046 | ||
AUDIO_NEXT | 269025047 | ||
AUDIO_REWIND | 269025086 | ||
AUDIO_FORWARD | 269025175 | ||
AUDIO_REPEAT | 269025176 | ||
AUDIO_RANDOM | 269025177 | ||
LIGHTS_MON_UP | 269025026 | ||
LIGHTS_MON_DOWN | 269025027 | ||
LIGHTS_KBD_TOGGLE | 269025028 | ||
LIGHTS_KBD_UP | 269025029 | ||
LIGHTS_KBD_DOWN | 269025030 |
name | metastate |
---|---|
None | 0 |
META | 64 |
ALT | 8 |
CONTROL | 4 |
SHIFT | 1 |
The same with press key code, except for pressing the key for a longer time (0.5s)
Get the element's property value.
Name | Type | Required | Description | Example |
---|---|---|---|---|
name | string | yes | the property name | 'states' |
name = el.get_property('name') # get the name property value
name = el.get_attribute('name') # get the name property value, equivalent to get_property
Clear the (input) element's text
No arguments
el.clear()
Take screenshot of the element
Name | Type | Required | Description | Example |
---|---|---|---|---|
filename | string | yes | the path to save the screenshot | '.pics/a.png' |
el.screenshot('.pics/a.png')
Take screenshot of the window
Name | Type | Required | Description | Example |
---|---|---|---|---|
filename | string | yes | the path to save the screenshot | '.pics/a.png' |
driver.get_screenshot_as_file('.pics/a.png')
Beside of standard W3C APIs the driver provides the following custom command extensions to execute platform specific scenarios:
Get the width and height of the display
No arguments
size = driver.execute_script('linux: getDisplaySize')
print(size) # outputs: {'width': 1440, 'height': 900}
Move the mouse to a given absolute coordinates.
Name | Type | Required | Description | Example |
---|---|---|---|---|
x | int | yes | the x coordinate | 100 |
y | int | yes | the y coordinate | 100 |
driver.execute_script('linux: mouseMove', {"x": 100, "y": 100})
Mouse down at (sx, sy), and move to (ex, ey), and then mouse up.
Effectively swipe from (sx, sy) to (ex, ey)
Name | Type | Required | Description | Example |
---|---|---|---|---|
sx | int | yes | swipe from x coordinate | 500 |
sy | int | yes | swipe from y coordinate | 100 |
ex | int | yes | swipe to x coordinate | 500 |
ey | int | yes | swipe to y coordinate | 700 |
driver.execute_script('linux: mouseSwipe', {"sx": 500, "sy": 100, "ex": 500, "ey": 700})
Right click on a given absolute coordinates.
Name | Type | Required | Description | Example |
---|---|---|---|---|
x | int | yes | the x coordinate | 100 |
y | int | yes | the y coordinate | 100 |
driver.execute_script('linux: rightClick', {"x": 100, "y": 100})
Double click on a given absolute coordinates.
Name | Type | Required | Description | Example |
---|---|---|---|---|
x | int | yes | the x coordinate | 100 |
y | int | yes | the y coordinate | 100 |
driver.execute_script('linux: doubleClick', {"x": 100, "y": 100})
Simulate mouse scroll horizontally and vertically
Name | Type | Required | Description | Example |
---|---|---|---|---|
moveLeftSteps | int | no | how much scrolls left, when the value is negative, scrolls right, when the value is 0, doesn't scroll horizontally | |
moveUpSteps | int | no | how much scrolls up, when the value is negative, scrolls down, when the value is 0, doesn't scroll vertically. |
driver.execute_script('linux: mouseScroll', {"moveLeftSteps": 0, "moveUpSteps": -1}) # scrolls down
Copy a given string to the system clipboard
Name | Type | Required | Description | Example |
---|---|---|---|---|
str | string | yes | the string to be copied | 'sample copy text' |
driver.execute_script('linux: copy', {"str": 'sample copy text'})
Get the clipboard content
No Arguments
content = driver.execute_script('linux: getClipboard')
The Linux driver has the concept of Application Under Test
. This is the app, whose path has been passed as appName
capability.
If this application is unexpectedly terminated during test session execution then an exception is going to be thrown upon any following session command invocation. In such case the driver assumes the application under test is crashed and it is impossible to proceed.
Also, the Application Under Test
is going to be terminated when the testing session quits.
# Python3 + PyTest
import pytest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
@pytest.fixture()
def driver():
drv = webdriver.Remote('http://localhost:4723/wd/hub', {
# automationName capability presence is mandatory for this Linux Driver to be selected
'automationName': 'atspi2',
'platformName': 'linux',
'bundleId': 'yelp',
})
yield drv
drv.quit()
def test_edit_text(driver):
find_el = driver.find_element(by=AppiumBy.NAME, value='Find')
find_el.click()
edit_field = driver.find_element(by=AppiumBy.XPATH, value='(//text[@name="Search"])[1]')
edit_field.send_keys('hello world')
edit_field = driver.find_element(by=AppiumBy.XPATH, value='(//text[@name="Search"])[1]')
assert edit_field.text == 'hello world'
edit_field.clear()
edit_field = driver.find_element(by=AppiumBy.XPATH, value='(//text[@name="Search"])[1]')
assert edit_field.text == ''
Parallel execution of multiple Linux driver instances is highly discouraged. Only one UI test must be running at the same time.
This module uses the same development tools as the other Appium drivers.
Check out the source. Then run:
npm install
gulp watch
Execute npm run test
to run unit tests and npm run e2e-test
to run integration tests.