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.
Files changed (23) hide show
  1. robotframework_maestrolibrary-0.2.0/LICENSE +21 -0
  2. robotframework_maestrolibrary-0.2.0/PKG-INFO +131 -0
  3. robotframework_maestrolibrary-0.2.0/README.md +112 -0
  4. robotframework_maestrolibrary-0.2.0/pyproject.toml +39 -0
  5. robotframework_maestrolibrary-0.2.0/setup.cfg +4 -0
  6. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/__init__.py +231 -0
  7. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/__init__.py +8 -0
  8. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_applicationmanagement.py +203 -0
  9. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_device.py +92 -0
  10. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_element.py +221 -0
  11. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_keyevent.py +34 -0
  12. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_runonfailure.py +16 -0
  13. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_screenshot.py +170 -0
  14. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_touch.py +75 -0
  15. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/keywords/_waiting.py +58 -0
  16. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/locators.py +59 -0
  17. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/mcp.py +126 -0
  18. robotframework_maestrolibrary-0.2.0/src/MaestroLibrary/py.typed +0 -0
  19. robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/PKG-INFO +131 -0
  20. robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/SOURCES.txt +21 -0
  21. robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/dependency_links.txt +1 -0
  22. robotframework_maestrolibrary-0.2.0/src/robotframework_maestrolibrary.egg-info/requires.txt +2 -0
  23. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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