robotframework-maestrolibrary 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robotframework_maestrolibrary-0.2.0/LICENSE +21 -0
- robotframework_maestrolibrary-0.2.0/PKG-INFO +131 -0
- robotframework_maestrolibrary-0.2.0/README.md +112 -0
- robotframework_maestrolibrary-0.2.0/pyproject.toml +39 -0
- robotframework_maestrolibrary-0.2.0/setup.cfg +4 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/__init__.py +231 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/__init__.py +8 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_applicationmanagement.py +203 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_device.py +92 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_element.py +221 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_keyevent.py +34 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_runonfailure.py +16 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_screenshot.py +170 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_touch.py +75 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_waiting.py +58 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/locators.py +59 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/mcp.py +126 -0
- robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/py.typed +0 -0
- robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/PKG-INFO +131 -0
- robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/SOURCES.txt +21 -0
- robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/dependency_links.txt +1 -0
- robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/requires.txt +2 -0
- robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The robotframework-maestrolibrary authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-maestrolibrary
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Robot Framework mobile keywords on top of Maestro, with an AppiumLibrary-style API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Framework :: Robot Framework
|
|
7
|
+
Classifier: Framework :: Robot Framework :: Library
|
|
8
|
+
Classifier: Topic :: Software Development :: Testing
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: robotframework>=7.5
|
|
17
|
+
Requires-Dist: robotframework-pythonlibcore>=4.6.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# robotframework-maestrolibrary
|
|
21
|
+
|
|
22
|
+
Robot Framework keywords for mobile apps, driven by [Maestro](https://maestro.dev).
|
|
23
|
+
If you know [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary),
|
|
24
|
+
you already know the keyword names, the arguments and the `strategy=value` locators.
|
|
25
|
+
There's no Appium server and no capabilities to manage. Maestro waits for the UI on every action.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
- Maestro CLI on PATH (`maestro --version`) and Java 17+
|
|
29
|
+
- A running emulator/simulator or a connected device
|
|
30
|
+
- Android: `adb` (platform-tools) on PATH. `Open Application` uses it to wait for the app's
|
|
31
|
+
window, and `Execute Adb Shell` needs it. Without it, Open Application warns and returns as
|
|
32
|
+
soon as Maestro does.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
```
|
|
36
|
+
pip install robotframework-maestrolibrary
|
|
37
|
+
```
|
|
38
|
+
For development: `pip install -e .`
|
|
39
|
+
|
|
40
|
+
A version tag (`v0.2.0`, matching `__version__`) builds the package in CI and uploads it to the
|
|
41
|
+
project's registry, and to PyPI when the `PYPI_TOKEN` CI variable is set.
|
|
42
|
+
MIT: see LICENSE.
|
|
43
|
+
|
|
44
|
+
## Example
|
|
45
|
+
```robotframework
|
|
46
|
+
*** Settings ***
|
|
47
|
+
Library MaestroLibrary timeout=10s
|
|
48
|
+
|
|
49
|
+
*** Test Cases ***
|
|
50
|
+
Search Settings
|
|
51
|
+
Open Application com.android.settings
|
|
52
|
+
Click Text Search settings exact_match=True
|
|
53
|
+
Input Text id=com.google.android.settings.intelligence:id/open_search_view_edit_text wifi
|
|
54
|
+
Wait Until Page Contains Wi-Fi
|
|
55
|
+
[Teardown] Close All Applications
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Locators
|
|
59
|
+
`text=`, `accessibility_id=`, `id=` (literal, whole value), `regex=`, `id_regex=`
|
|
60
|
+
(Maestro regular expressions), `point=50%,50%`. A bare value means text. Flutter apps
|
|
61
|
+
get ids from `Semantics(identifier: ...)`. xpath, class, android, ios, predicate, chain,
|
|
62
|
+
css, name and identifier fail with a clear message.
|
|
63
|
+
|
|
64
|
+
## Coming from AppiumLibrary
|
|
65
|
+
Same names and arguments: Close/Activate/Terminate Application, Close All
|
|
66
|
+
Applications, Go Back, Go To Url, Get Source, Log Source, Execute Adb Shell, Click
|
|
67
|
+
Element, Click Text, Input Text, Input Password, Input Text Into Current Element, Clear
|
|
68
|
+
Text, Hide Keyboard, Page Should (Not) Contain Text/Element, Element Should Be
|
|
69
|
+
Visible/Enabled/Disabled, Text Should Be Visible, Element Text Should Be, Element Should
|
|
70
|
+
(Not) Contain Text, Get Text, Get Element Attribute, Scroll Element Into View, Expect
|
|
71
|
+
Element, Expect Text, the five Wait Until keywords, Swipe, Swipe By Percent, Scroll
|
|
72
|
+
Down/Up, Tap, Long Press, Press Keycode, Capture Page Screenshot, Register Keyword To Run
|
|
73
|
+
On Failure, Set Location, Landscape, Portrait.
|
|
74
|
+
|
|
75
|
+
Differences:
|
|
76
|
+
- `Open Application <app id> clear_state=False stop_app=True`: no remote URL or
|
|
77
|
+
capabilities. Like Appium, it returns once the app has window focus.
|
|
78
|
+
- Set/Get Appium Timeout become Set/Get Maestro Timeout.
|
|
79
|
+
- Press Keycode supports the keycodes that Maestro can send. `Press Key` takes Maestro key names.
|
|
80
|
+
- Should/Get keywords check the settled screen once. Wait/Expect keywords and every action wait.
|
|
81
|
+
- Screenshots are JPEG (`maestro-screenshot-<n>.jpg`, or `EMBED`).
|
|
82
|
+
- Swipe `duration` takes a time (`300ms`); a bare number is milliseconds, as in AppiumLibrary.
|
|
83
|
+
- `speed=0.5s` (import argument) pauses before every Maestro command, like SeleniumLibrary's
|
|
84
|
+
speed. Use it when a slow or overloaded emulator stops responding.
|
|
85
|
+
- Start Screen Recording takes `time_limit` (AppiumLibrary: `timeLimit`), 3 minutes by default.
|
|
86
|
+
Stop Screen Recording saves `filename` as given (AppiumLibrary appends `.mp4`). Both are
|
|
87
|
+
Android only (adb `screenrecord`): Maestro ends its own recording with each flow, and every
|
|
88
|
+
keyword is its own flow. Put Stop in a teardown.
|
|
89
|
+
- Set Location ignores `altitude`.
|
|
90
|
+
|
|
91
|
+
Maestro-only keywords:
|
|
92
|
+
- `Run Flow`: a YAML file, inline commands, or a directory of flows with `include_tags` /
|
|
93
|
+
`exclude_tags`, plus env variables.
|
|
94
|
+
- App state: `Clear Application State`, `Kill Application` (the system killing a background app),
|
|
95
|
+
`Set Application Permissions` (`camera=deny`, `all=allow`), `Open Application permissions=...`.
|
|
96
|
+
- Device: `Travel` (move the location along points), `Set Orientation`, `Set Airplane Mode`,
|
|
97
|
+
`Set Dark Mode`, `Add Media` (put images or videos in the gallery).
|
|
98
|
+
- Visual: `Capture Element Screenshot` (PNG), `Screenshot Should Match` (compare with a reference
|
|
99
|
+
PNG, whole screen or one element, with a threshold).
|
|
100
|
+
- `Wait For Animation To End`, `Is Keyboard Shown`.
|
|
101
|
+
|
|
102
|
+
Not ported: webview contexts, xpath, multiple app aliases, touch id, WebElement keywords,
|
|
103
|
+
Drag And Drop, Flick, sleep-between-wait-loop settings.
|
|
104
|
+
|
|
105
|
+
## Maestro CLI coverage
|
|
106
|
+
The library talks to `maestro mcp`. What the other `maestro` subcommands do, and where it lives here:
|
|
107
|
+
|
|
108
|
+
| `maestro` | Here |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `test` (flows, tags, `-e`) | `Run Flow` |
|
|
111
|
+
| `print-hierarchy`, `query` | `Get Source`, `Get Text`, `Get Element Attribute`, the Should keywords |
|
|
112
|
+
| `list-devices` | the `device` import argument; the first connected device by default |
|
|
113
|
+
| `record` | `Start/Stop Screen Recording` |
|
|
114
|
+
| `check-syntax` | `Run Flow` fails with Maestro's parse error and its line |
|
|
115
|
+
| `start-device` | not a test step: boot the emulator before the run, e.g. `maestro start-device --platform android` in CI |
|
|
116
|
+
| `studio`, `chat`, `driver`, `cloud`, `login`, ... | interactive or Maestro Cloud tools, not used |
|
|
117
|
+
|
|
118
|
+
Flow commands without a keyword, still reachable with `Run Flow`: the AI assertions (need a
|
|
119
|
+
Maestro Cloud key), `inputRandom*`, clipboard commands (the copied text stays inside one flow),
|
|
120
|
+
`repeat`/`retry` (use Robot's FOR and `Wait Until Keyword Succeeds`), `runScript`/`evalScript`,
|
|
121
|
+
`toggleAirplaneMode`, `assertDarkMode`/`assertLightMode`, `clearKeychain` (iOS), `doubleTapOn`.
|
|
122
|
+
|
|
123
|
+
Each `maestro mcp` process reinstalls Maestro's driver app on the device once, when it first
|
|
124
|
+
connects (Maestro hard-codes it). The library keeps one process for the whole run.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
```
|
|
128
|
+
python -m unittest discover -s utest # no device needed
|
|
129
|
+
robot --pythonpath src -d results atest # needs a running Android emulator
|
|
130
|
+
python -m robot.libdoc --pythonpath src MaestroLibrary results/MaestroLibrary.html
|
|
131
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# robotframework-maestrolibrary
|
|
2
|
+
|
|
3
|
+
Robot Framework keywords for mobile apps, driven by [Maestro](https://maestro.dev).
|
|
4
|
+
If you know [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary),
|
|
5
|
+
you already know the keyword names, the arguments and the `strategy=value` locators.
|
|
6
|
+
There's no Appium server and no capabilities to manage. Maestro waits for the UI on every action.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
- Maestro CLI on PATH (`maestro --version`) and Java 17+
|
|
10
|
+
- A running emulator/simulator or a connected device
|
|
11
|
+
- Android: `adb` (platform-tools) on PATH. `Open Application` uses it to wait for the app's
|
|
12
|
+
window, and `Execute Adb Shell` needs it. Without it, Open Application warns and returns as
|
|
13
|
+
soon as Maestro does.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
```
|
|
17
|
+
pip install robotframework-maestrolibrary
|
|
18
|
+
```
|
|
19
|
+
For development: `pip install -e .`
|
|
20
|
+
|
|
21
|
+
A version tag (`v0.2.0`, matching `__version__`) builds the package in CI and uploads it to the
|
|
22
|
+
project's registry, and to PyPI when the `PYPI_TOKEN` CI variable is set.
|
|
23
|
+
MIT: see LICENSE.
|
|
24
|
+
|
|
25
|
+
## Example
|
|
26
|
+
```robotframework
|
|
27
|
+
*** Settings ***
|
|
28
|
+
Library MaestroLibrary timeout=10s
|
|
29
|
+
|
|
30
|
+
*** Test Cases ***
|
|
31
|
+
Search Settings
|
|
32
|
+
Open Application com.android.settings
|
|
33
|
+
Click Text Search settings exact_match=True
|
|
34
|
+
Input Text id=com.google.android.settings.intelligence:id/open_search_view_edit_text wifi
|
|
35
|
+
Wait Until Page Contains Wi-Fi
|
|
36
|
+
[Teardown] Close All Applications
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Locators
|
|
40
|
+
`text=`, `accessibility_id=`, `id=` (literal, whole value), `regex=`, `id_regex=`
|
|
41
|
+
(Maestro regular expressions), `point=50%,50%`. A bare value means text. Flutter apps
|
|
42
|
+
get ids from `Semantics(identifier: ...)`. xpath, class, android, ios, predicate, chain,
|
|
43
|
+
css, name and identifier fail with a clear message.
|
|
44
|
+
|
|
45
|
+
## Coming from AppiumLibrary
|
|
46
|
+
Same names and arguments: Close/Activate/Terminate Application, Close All
|
|
47
|
+
Applications, Go Back, Go To Url, Get Source, Log Source, Execute Adb Shell, Click
|
|
48
|
+
Element, Click Text, Input Text, Input Password, Input Text Into Current Element, Clear
|
|
49
|
+
Text, Hide Keyboard, Page Should (Not) Contain Text/Element, Element Should Be
|
|
50
|
+
Visible/Enabled/Disabled, Text Should Be Visible, Element Text Should Be, Element Should
|
|
51
|
+
(Not) Contain Text, Get Text, Get Element Attribute, Scroll Element Into View, Expect
|
|
52
|
+
Element, Expect Text, the five Wait Until keywords, Swipe, Swipe By Percent, Scroll
|
|
53
|
+
Down/Up, Tap, Long Press, Press Keycode, Capture Page Screenshot, Register Keyword To Run
|
|
54
|
+
On Failure, Set Location, Landscape, Portrait.
|
|
55
|
+
|
|
56
|
+
Differences:
|
|
57
|
+
- `Open Application <app id> clear_state=False stop_app=True`: no remote URL or
|
|
58
|
+
capabilities. Like Appium, it returns once the app has window focus.
|
|
59
|
+
- Set/Get Appium Timeout become Set/Get Maestro Timeout.
|
|
60
|
+
- Press Keycode supports the keycodes that Maestro can send. `Press Key` takes Maestro key names.
|
|
61
|
+
- Should/Get keywords check the settled screen once. Wait/Expect keywords and every action wait.
|
|
62
|
+
- Screenshots are JPEG (`maestro-screenshot-<n>.jpg`, or `EMBED`).
|
|
63
|
+
- Swipe `duration` takes a time (`300ms`); a bare number is milliseconds, as in AppiumLibrary.
|
|
64
|
+
- `speed=0.5s` (import argument) pauses before every Maestro command, like SeleniumLibrary's
|
|
65
|
+
speed. Use it when a slow or overloaded emulator stops responding.
|
|
66
|
+
- Start Screen Recording takes `time_limit` (AppiumLibrary: `timeLimit`), 3 minutes by default.
|
|
67
|
+
Stop Screen Recording saves `filename` as given (AppiumLibrary appends `.mp4`). Both are
|
|
68
|
+
Android only (adb `screenrecord`): Maestro ends its own recording with each flow, and every
|
|
69
|
+
keyword is its own flow. Put Stop in a teardown.
|
|
70
|
+
- Set Location ignores `altitude`.
|
|
71
|
+
|
|
72
|
+
Maestro-only keywords:
|
|
73
|
+
- `Run Flow`: a YAML file, inline commands, or a directory of flows with `include_tags` /
|
|
74
|
+
`exclude_tags`, plus env variables.
|
|
75
|
+
- App state: `Clear Application State`, `Kill Application` (the system killing a background app),
|
|
76
|
+
`Set Application Permissions` (`camera=deny`, `all=allow`), `Open Application permissions=...`.
|
|
77
|
+
- Device: `Travel` (move the location along points), `Set Orientation`, `Set Airplane Mode`,
|
|
78
|
+
`Set Dark Mode`, `Add Media` (put images or videos in the gallery).
|
|
79
|
+
- Visual: `Capture Element Screenshot` (PNG), `Screenshot Should Match` (compare with a reference
|
|
80
|
+
PNG, whole screen or one element, with a threshold).
|
|
81
|
+
- `Wait For Animation To End`, `Is Keyboard Shown`.
|
|
82
|
+
|
|
83
|
+
Not ported: webview contexts, xpath, multiple app aliases, touch id, WebElement keywords,
|
|
84
|
+
Drag And Drop, Flick, sleep-between-wait-loop settings.
|
|
85
|
+
|
|
86
|
+
## Maestro CLI coverage
|
|
87
|
+
The library talks to `maestro mcp`. What the other `maestro` subcommands do, and where it lives here:
|
|
88
|
+
|
|
89
|
+
| `maestro` | Here |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `test` (flows, tags, `-e`) | `Run Flow` |
|
|
92
|
+
| `print-hierarchy`, `query` | `Get Source`, `Get Text`, `Get Element Attribute`, the Should keywords |
|
|
93
|
+
| `list-devices` | the `device` import argument; the first connected device by default |
|
|
94
|
+
| `record` | `Start/Stop Screen Recording` |
|
|
95
|
+
| `check-syntax` | `Run Flow` fails with Maestro's parse error and its line |
|
|
96
|
+
| `start-device` | not a test step: boot the emulator before the run, e.g. `maestro start-device --platform android` in CI |
|
|
97
|
+
| `studio`, `chat`, `driver`, `cloud`, `login`, ... | interactive or Maestro Cloud tools, not used |
|
|
98
|
+
|
|
99
|
+
Flow commands without a keyword, still reachable with `Run Flow`: the AI assertions (need a
|
|
100
|
+
Maestro Cloud key), `inputRandom*`, clipboard commands (the copied text stays inside one flow),
|
|
101
|
+
`repeat`/`retry` (use Robot's FOR and `Wait Until Keyword Succeeds`), `runScript`/`evalScript`,
|
|
102
|
+
`toggleAirplaneMode`, `assertDarkMode`/`assertLightMode`, `clearKeychain` (iOS), `doubleTapOn`.
|
|
103
|
+
|
|
104
|
+
Each `maestro mcp` process reinstalls Maestro's driver app on the device once, when it first
|
|
105
|
+
connects (Maestro hard-codes it). The library keeps one process for the whole run.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
```
|
|
109
|
+
python -m unittest discover -s utest # no device needed
|
|
110
|
+
robot --pythonpath src -d results atest # needs a running Android emulator
|
|
111
|
+
python -m robot.libdoc --pythonpath src MaestroLibrary results/MaestroLibrary.html
|
|
112
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=84"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "robotframework-maestrolibrary"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Robot Framework mobile keywords on top of Maestro, with an AppiumLibrary-style API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"robotframework>=7.5",
|
|
15
|
+
"robotframework-pythonlibcore>=4.6.0",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Framework :: Robot Framework",
|
|
19
|
+
"Framework :: Robot Framework :: Library",
|
|
20
|
+
"Topic :: Software Development :: Testing",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.dynamic]
|
|
28
|
+
version = {attr = "MaestroLibrary.__version__"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
MaestroLibrary = ["py.typed"]
|
|
35
|
+
|
|
36
|
+
[tool.bandit]
|
|
37
|
+
# B404/B603: the library drives `maestro` and `adb` through subprocess on purpose, always with
|
|
38
|
+
# list arguments and no shell; Execute Adb Shell runs the caller's adb command by design.
|
|
39
|
+
skips = ["B404", "B603"]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
|
|
9
|
+
from robot.api import logger
|
|
10
|
+
from robotlibcore import DynamicCore
|
|
11
|
+
|
|
12
|
+
from .keywords import ApplicationManagementKeywords, DeviceKeywords, ElementKeywords, KeyeventKeywords, RunOnFailureKeywords, ScreenshotKeywords, TouchKeywords, WaitingKeywords
|
|
13
|
+
from .locators import matches, walk
|
|
14
|
+
from .mcp import MaestroError, MaestroMCP
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
|
+
NO_APP = "maestro.no.app"
|
|
18
|
+
SETTLE_TIMEOUT_MS = 3000
|
|
19
|
+
APP_LAUNCH_TIMEOUT_S = 20 # Appium's default appWaitDuration
|
|
20
|
+
ADB_TIMEOUT_S = 10
|
|
21
|
+
MCP_TIMEOUT_S = 300 # how long to wait for one Maestro command flow beyond its own timeouts
|
|
22
|
+
FLOW_TIMEOUT_S = 3600 # a Run Flow file or directory can hold any number of commands
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MaestroLibrary(DynamicCore):
|
|
26
|
+
"""MaestroLibrary drives Android and iOS apps through [https://maestro.dev|Maestro].
|
|
27
|
+
|
|
28
|
+
It mirrors [https://github.com/serhatbolsu/robotframework-appiumlibrary|AppiumLibrary]:
|
|
29
|
+
the keyword names, the argument order and the `strategy=value` locators are the same,
|
|
30
|
+
so most AppiumLibrary suites read the same with this library. There's no Appium server
|
|
31
|
+
and no capabilities, and Maestro waits for the UI to settle after every action.
|
|
32
|
+
|
|
33
|
+
= Locating elements =
|
|
34
|
+
|
|
35
|
+
| *Strategy* | *Example* | *Matches* |
|
|
36
|
+
| text | ``text=Log in`` | whole text, content-desc or hint, literally |
|
|
37
|
+
| accessibility_id | ``accessibility_id=Back`` | same as text (Maestro matches content-desc as text) |
|
|
38
|
+
| id | ``id=login_button`` | whole resource-id or Flutter ``Semantics.identifier`` |
|
|
39
|
+
| regex | ``regex=Log.*`` | text, as a Maestro (Java) regular expression |
|
|
40
|
+
| id_regex | ``id_regex=.*login`` | id, as a regular expression |
|
|
41
|
+
| point | ``point=50%,90%`` | a screen position, in percent or pixels |
|
|
42
|
+
|
|
43
|
+
A locator without a strategy is treated as text, because Flutter widgets expose
|
|
44
|
+
text far more often than ids. xpath, class, android, ios, predicate, chain, css, name and identifier
|
|
45
|
+
have no Maestro equivalent and fail immediately.
|
|
46
|
+
|
|
47
|
+
Actions match on the device with Java regular expressions. The `Should` and `Get`
|
|
48
|
+
keywords match the inspected screen with Python's ``re``. The two behave the same for
|
|
49
|
+
literal locators and for common patterns. Keep ``regex=`` and ``id_regex=`` to
|
|
50
|
+
syntax that both engines share (no possessive quantifiers, ``\\p{...}`` classes or inline flags).
|
|
51
|
+
|
|
52
|
+
= Timeouts =
|
|
53
|
+
|
|
54
|
+
`timeout` (default 5 seconds) is the default for the `Wait Until` keywords and `Expect`
|
|
55
|
+
keywords, the same as AppiumLibrary. The `Should` keywords check the current screen once
|
|
56
|
+
and don't wait. Every action waits for its element on its own.
|
|
57
|
+
|
|
58
|
+
= Anything else =
|
|
59
|
+
|
|
60
|
+
`Run Flow` runs raw Maestro YAML or a flow file, for the Maestro commands that have
|
|
61
|
+
no keyword yet.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
ROBOT_LIBRARY_SCOPE = "GLOBAL"
|
|
65
|
+
ROBOT_LIBRARY_VERSION = __version__
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
timeout: timedelta = timedelta(seconds=5),
|
|
70
|
+
run_on_failure: str = "Capture Page Screenshot",
|
|
71
|
+
device: str | None = None,
|
|
72
|
+
maestro: str = "maestro",
|
|
73
|
+
speed: timedelta = timedelta(0),
|
|
74
|
+
):
|
|
75
|
+
"""`device` is a Maestro device id such as ``emulator-5554``. By default the first
|
|
76
|
+
connected device is used. `maestro` is the Maestro CLI executable.
|
|
77
|
+
|
|
78
|
+
`speed` pauses before every Maestro command, like SeleniumLibrary's speed. Raise it
|
|
79
|
+
(for example ``speed=0.5s``) when a slow or overloaded emulator stops responding.
|
|
80
|
+
"""
|
|
81
|
+
self.timeout = timeout
|
|
82
|
+
self.speed = speed
|
|
83
|
+
self.run_on_failure_keyword = None if run_on_failure.upper() in ("NOTHING", "NONE", "") else run_on_failure
|
|
84
|
+
self.device = device
|
|
85
|
+
self.platform = None
|
|
86
|
+
self.app_id = None
|
|
87
|
+
self.mcp = MaestroMCP([maestro, "mcp"])
|
|
88
|
+
self._running_on_failure = False
|
|
89
|
+
atexit.register(self.mcp.close)
|
|
90
|
+
DynamicCore.__init__(self, [
|
|
91
|
+
ApplicationManagementKeywords(self),
|
|
92
|
+
ElementKeywords(self),
|
|
93
|
+
WaitingKeywords(self),
|
|
94
|
+
TouchKeywords(self),
|
|
95
|
+
DeviceKeywords(self),
|
|
96
|
+
KeyeventKeywords(self),
|
|
97
|
+
ScreenshotKeywords(self),
|
|
98
|
+
RunOnFailureKeywords(self),
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
def run_keyword(self, name, args, kwargs=None):
|
|
102
|
+
try:
|
|
103
|
+
return DynamicCore.run_keyword(self, name, args, kwargs)
|
|
104
|
+
except Exception:
|
|
105
|
+
self.run_on_failure()
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
def run_on_failure(self):
|
|
109
|
+
if not self.run_on_failure_keyword or self._running_on_failure:
|
|
110
|
+
return
|
|
111
|
+
self._running_on_failure = True
|
|
112
|
+
try:
|
|
113
|
+
from robot.libraries.BuiltIn import BuiltIn
|
|
114
|
+
BuiltIn().run_keyword(self.run_on_failure_keyword)
|
|
115
|
+
except Exception as err:
|
|
116
|
+
logger.warn(f"Keyword '{self.run_on_failure_keyword}' could not be run on failure: {err}")
|
|
117
|
+
finally:
|
|
118
|
+
self._running_on_failure = False
|
|
119
|
+
|
|
120
|
+
def device_id(self):
|
|
121
|
+
if not self.platform:
|
|
122
|
+
content = self.mcp.call_tool("list_devices", {})
|
|
123
|
+
connected = [d for d in json.loads(content[0]["text"])["devices"] if d.get("connected")]
|
|
124
|
+
if self.device:
|
|
125
|
+
connected = [d for d in connected if d["device_id"] == self.device]
|
|
126
|
+
if not connected:
|
|
127
|
+
raise MaestroError(f"Device '{self.device or 'any'}' is not connected. Start an emulator or "
|
|
128
|
+
"simulator, or connect a device.")
|
|
129
|
+
self.device, self.platform = connected[0]["device_id"], connected[0]["platform"]
|
|
130
|
+
logger.info(f"Using {self.platform} device '{self.device}'.")
|
|
131
|
+
return self.device
|
|
132
|
+
|
|
133
|
+
def wait_for_app_focus(self, app_id):
|
|
134
|
+
"""Waits until `app_id` has window focus, like Appium waiting for the app's activity.
|
|
135
|
+
|
|
136
|
+
Maestro's launchApp returns while the launch transition is still running, so the
|
|
137
|
+
first inspect_screen can show the previous app. Android only: other platforms return at once.
|
|
138
|
+
"""
|
|
139
|
+
self.device_id()
|
|
140
|
+
if self.platform != "android":
|
|
141
|
+
return
|
|
142
|
+
deadline = time.monotonic() + APP_LAUNCH_TIMEOUT_S
|
|
143
|
+
while time.monotonic() < deadline:
|
|
144
|
+
focus = self.adb_shell("dumpsys", "window", purpose="waiting for the app's window")
|
|
145
|
+
if focus is None:
|
|
146
|
+
return
|
|
147
|
+
if re.search(rf"mCurrentFocus=.*\s{re.escape(app_id)}/", focus):
|
|
148
|
+
# Focus arrives before the first frames are drawn; a gesture sent then can be lost.
|
|
149
|
+
self.run_commands({"waitForAnimationToEnd": {"timeout": SETTLE_TIMEOUT_MS}}, log=False)
|
|
150
|
+
return
|
|
151
|
+
# dumpsys window is a heavy call; focus arrived within 2 s in measurements, so 1 s is enough.
|
|
152
|
+
time.sleep(1)
|
|
153
|
+
logger.warn(f"App '{app_id}' did not get window focus within {APP_LAUNCH_TIMEOUT_S} s.")
|
|
154
|
+
|
|
155
|
+
def adb_shell(self, *args, purpose):
|
|
156
|
+
"""Runs `adb shell args` on the current Android device and returns its output.
|
|
157
|
+
|
|
158
|
+
Returns None, with a warning naming `purpose`, when adb is missing or doesn't answer:
|
|
159
|
+
Maestro itself needs no adb, so the checks built on it degrade instead of failing.
|
|
160
|
+
"""
|
|
161
|
+
adb = shutil.which("adb")
|
|
162
|
+
if not adb:
|
|
163
|
+
logger.warn(f"adb is not on PATH, so {purpose} is skipped. Install Android platform-tools.")
|
|
164
|
+
return None
|
|
165
|
+
try:
|
|
166
|
+
return subprocess.run([adb, "-s", self.device, "shell", *args], capture_output=True,
|
|
167
|
+
text=True, errors="replace", timeout=ADB_TIMEOUT_S).stdout
|
|
168
|
+
except subprocess.TimeoutExpired:
|
|
169
|
+
logger.warn(f"adb did not answer within {ADB_TIMEOUT_S} s while {purpose}; is the device responsive?")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def run_commands(self, *commands, app_id=None, log=True):
|
|
173
|
+
"""Runs Maestro commands (dicts or strings) as one flow on the current device."""
|
|
174
|
+
header = f"appId: {json.dumps(app_id or self.app_id or NO_APP)}\n---\n"
|
|
175
|
+
body = "\n".join(f"- {json.dumps(command)}" for command in commands)
|
|
176
|
+
if log:
|
|
177
|
+
logger.debug(f"Maestro flow:\n{body}")
|
|
178
|
+
if self.speed:
|
|
179
|
+
time.sleep(self.speed.total_seconds())
|
|
180
|
+
# A wait or scroll may legitimately take longer than the default; allow its own timeout.
|
|
181
|
+
waits = [c[k]["timeout"] for c in commands if isinstance(c, dict) for k in c
|
|
182
|
+
if isinstance(c[k], dict) and isinstance(c[k].get("timeout"), int)]
|
|
183
|
+
self._run({"yaml": header + body}, None, max([MCP_TIMEOUT_S] + [w / 1000 + 60 for w in waits]))
|
|
184
|
+
|
|
185
|
+
def run_yaml(self, yaml, env=None):
|
|
186
|
+
self._run({"yaml": yaml}, env, FLOW_TIMEOUT_S)
|
|
187
|
+
|
|
188
|
+
def run_files(self, files, env=None):
|
|
189
|
+
self._run({"files": files}, env, FLOW_TIMEOUT_S)
|
|
190
|
+
|
|
191
|
+
def run_dir(self, path, env=None, include_tags=None, exclude_tags=None):
|
|
192
|
+
flow = {"dir": path}
|
|
193
|
+
if include_tags is not None:
|
|
194
|
+
flow["include_tags"] = include_tags
|
|
195
|
+
if exclude_tags is not None:
|
|
196
|
+
flow["exclude_tags"] = exclude_tags
|
|
197
|
+
self._run(flow, env, FLOW_TIMEOUT_S)
|
|
198
|
+
|
|
199
|
+
def _run(self, flow, env, timeout):
|
|
200
|
+
arguments = {"device_id": self.device_id(), **flow}
|
|
201
|
+
if env:
|
|
202
|
+
arguments["env"] = {k: str(v) for k, v in env.items()}
|
|
203
|
+
self.mcp.call_tool("run", arguments, timeout=timeout)
|
|
204
|
+
|
|
205
|
+
def screen(self):
|
|
206
|
+
"""Returns the settled current screen's element tree, from Maestro's inspect_screen.
|
|
207
|
+
|
|
208
|
+
Actions return before transitions finish, so inspecting at once can see the
|
|
209
|
+
previous screen. Waiting for animations first makes snapshot checks deterministic.
|
|
210
|
+
"""
|
|
211
|
+
self.run_commands({"waitForAnimationToEnd": {"timeout": SETTLE_TIMEOUT_MS}}, log=False)
|
|
212
|
+
content = self.mcp.call_tool("inspect_screen", {"device_id": self.device_id()})
|
|
213
|
+
return json.loads(content[0]["text"])["elements"]
|
|
214
|
+
|
|
215
|
+
def elements(self):
|
|
216
|
+
"""Returns every element on the current screen, flattened."""
|
|
217
|
+
return list(walk(self.screen()))
|
|
218
|
+
|
|
219
|
+
def timeout_ms(self, timeout=None):
|
|
220
|
+
return int((self.timeout if timeout is None else timeout).total_seconds() * 1000)
|
|
221
|
+
|
|
222
|
+
def wait_until(self, selector, visible=True, timeout=None, error=None):
|
|
223
|
+
"""Waits for a selector to become visible (or not visible) using Maestro's extendedWaitUntil."""
|
|
224
|
+
state = "visible" if visible else "notVisible"
|
|
225
|
+
try:
|
|
226
|
+
self.run_commands({"extendedWaitUntil": {state: selector, "timeout": self.timeout_ms(timeout)}})
|
|
227
|
+
except MaestroError as err:
|
|
228
|
+
raise AssertionError(error or str(err)) from None
|
|
229
|
+
|
|
230
|
+
def find(self, selector):
|
|
231
|
+
return [e for e in self.elements() if matches(e, selector)]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ._applicationmanagement import ApplicationManagementKeywords
|
|
2
|
+
from ._element import ElementKeywords
|
|
3
|
+
from ._runonfailure import RunOnFailureKeywords
|
|
4
|
+
from ._screenshot import ScreenshotKeywords
|
|
5
|
+
from ._waiting import WaitingKeywords
|
|
6
|
+
from ._touch import TouchKeywords
|
|
7
|
+
from ._keyevent import KeyeventKeywords
|
|
8
|
+
from ._device import DeviceKeywords
|