gridfleet-testkit 0.8.0__tar.gz → 0.9.1__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.
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/CHANGELOG.md +21 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/PKG-INFO +39 -6
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/README.md +37 -3
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/_example_helpers.py +10 -5
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_android_mobile_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_android_tv_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_firetv_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_ios_simulator_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_roku_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_roku_sideload_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_tvos_screenshot.py +3 -4
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/__init__.py +1 -1
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/allocation.py +25 -15
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/appium.py +34 -34
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/client.py +68 -54
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/pytest_plugin.py +31 -14
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/sessions.py +25 -8
- gridfleet_testkit-0.9.1/gridfleet_testkit/types.py +11 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/pyproject.toml +3 -11
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_allocation.py +21 -7
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_appium.py +9 -19
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_client.py +125 -97
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_client_test_data.py +12 -9
- gridfleet_testkit-0.9.1/tests/test_driver_agnostic_guard.py +57 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_pytest_plugin.py +33 -28
- gridfleet_testkit-0.9.1/tests/test_pytest_plugin_grid_run_id_injection.py +29 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_pytest_plugin_test_data.py +2 -2
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_sessions.py +5 -2
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/uv.lock +4 -6
- gridfleet_testkit-0.8.0/tests/test_driver_agnostic_guard.py +0 -29
- gridfleet_testkit-0.8.0/tests/test_pytest_plugin_grid_run_id_injection.py +0 -56
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/.gitignore +0 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_docs_contract.py +0 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_package_metadata.py +0 -0
- {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_sessions_resolve_device_handle.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.9.1](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.0...gridfleet-testkit-v0.9.1) (2026-05-13)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **testkit:** align release policy with commitlint ([5dd8220](https://github.com/quidow/gridfleet/commit/5dd822010460e994f2e3c5b6676a69bed05678ed))
|
|
11
|
+
|
|
12
|
+
## [0.9.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.8.0...gridfleet-testkit-v0.9.0) (2026-05-12)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **testkit:** add run detail client helper ([c80a8cc](https://github.com/quidow/gridfleet/commit/c80a8cc95093c2f46a7e714c96ff0b33018af5ba))
|
|
18
|
+
* **testkit:** expose allocation device tags ([0bf5e2e](https://github.com/quidow/gridfleet/commit/0bf5e2e04b806fadd0afcd3c95073828d0c2414e))
|
|
19
|
+
* **testkit:** support tag-based device targeting ([db0d0e3](https://github.com/quidow/gridfleet/commit/db0d0e3d3d1231828bb22a707d3bdcab6c0ec717))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
|
|
24
|
+
* **testkit:** document tag-based device targeting ([096841b](https://github.com/quidow/gridfleet/commit/096841b737dec71524d0edfa4c538d9cc69e7c2c))
|
|
25
|
+
|
|
5
26
|
## [0.8.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.7.0...gridfleet-testkit-v0.8.0) (2026-05-12)
|
|
6
27
|
|
|
7
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridfleet-testkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Supported pytest and run-orchestration helpers for GridFleet integrations
|
|
5
5
|
Project-URL: Homepage, https://github.com/quidow/gridfleet
|
|
6
6
|
Project-URL: Repository, https://github.com/quidow/gridfleet
|
|
@@ -22,10 +22,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: <3.15,>=3.10
|
|
25
|
+
Requires-Dist: appium-python-client<6,>=4.5
|
|
25
26
|
Requires-Dist: httpx<1,>=0.27
|
|
26
27
|
Requires-Dist: pytest<10,>=9.0.3
|
|
27
|
-
Provides-Extra: appium
|
|
28
|
-
Requires-Dist: appium-python-client<6,>=4.5; extra == 'appium'
|
|
29
28
|
Provides-Extra: dev
|
|
30
29
|
Requires-Dist: mypy<3,>=1.20.2; extra == 'dev'
|
|
31
30
|
Requires-Dist: pytest<10,>=9.0.3; extra == 'dev'
|
|
@@ -81,19 +80,19 @@ The supported contract is the installable package and documented import pattern.
|
|
|
81
80
|
From PyPI:
|
|
82
81
|
|
|
83
82
|
```bash
|
|
84
|
-
pip install "gridfleet-testkit
|
|
83
|
+
pip install "gridfleet-testkit"
|
|
85
84
|
```
|
|
86
85
|
|
|
87
86
|
From a local checkout:
|
|
88
87
|
|
|
89
88
|
```bash
|
|
90
|
-
uv pip install -e ./testkit
|
|
89
|
+
uv pip install -e ./testkit
|
|
91
90
|
```
|
|
92
91
|
|
|
93
92
|
From a copied `testkit/` directory inside another repository:
|
|
94
93
|
|
|
95
94
|
```bash
|
|
96
|
-
uv pip install -e ./testkit
|
|
95
|
+
uv pip install -e ./testkit
|
|
97
96
|
```
|
|
98
97
|
|
|
99
98
|
From a Git checkout or VCS URL that contains this package:
|
|
@@ -103,6 +102,7 @@ uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
|
|
|
103
102
|
```
|
|
104
103
|
|
|
105
104
|
The package supports Python 3.10 and newer.
|
|
105
|
+
`Appium-Python-Client` is installed as a runtime dependency because the pytest fixtures create real Appium sessions.
|
|
106
106
|
|
|
107
107
|
## Environment
|
|
108
108
|
|
|
@@ -192,6 +192,7 @@ finally:
|
|
|
192
192
|
| `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
|
|
193
193
|
| `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
|
|
194
194
|
| `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
|
|
195
|
+
| `GridFleetClient.get_run(run_id)` | Fetch one run detail row by backend run id |
|
|
195
196
|
| `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
|
|
196
197
|
| `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
|
|
197
198
|
| `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
|
|
@@ -216,6 +217,38 @@ finally:
|
|
|
216
217
|
| `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
|
|
217
218
|
| `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` cleanup callable and return it; stops the heartbeat thread on exit but does not complete or cancel the run by default |
|
|
218
219
|
|
|
220
|
+
### Targeting Devices by Tag
|
|
221
|
+
|
|
222
|
+
GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
@pytest.mark.parametrize(
|
|
226
|
+
"appium_driver",
|
|
227
|
+
[
|
|
228
|
+
{
|
|
229
|
+
"pack_id": "appium-uiautomator2",
|
|
230
|
+
"platform_id": "android_mobile",
|
|
231
|
+
"appium:gridfleet:tag:screen_type": "4k",
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
indirect=True,
|
|
235
|
+
)
|
|
236
|
+
def test_4k_display(appium_driver):
|
|
237
|
+
...
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The same capability works for free sessions:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
driver = create_appium_driver(
|
|
244
|
+
pack_id="appium-uiautomator2",
|
|
245
|
+
platform_id="android_mobile",
|
|
246
|
+
capabilities={"appium:gridfleet:tag:screen_type": "4k"},
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
When an operator edits device tags, GridFleet marks the device for re-verification. The next verification restarts the Appium node and re-registers it with the updated Grid stereotype.
|
|
251
|
+
|
|
219
252
|
### Worker Identity
|
|
220
253
|
|
|
221
254
|
`worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
|
|
@@ -47,19 +47,19 @@ The supported contract is the installable package and documented import pattern.
|
|
|
47
47
|
From PyPI:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
pip install "gridfleet-testkit
|
|
50
|
+
pip install "gridfleet-testkit"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
From a local checkout:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
uv pip install -e ./testkit
|
|
56
|
+
uv pip install -e ./testkit
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
From a copied `testkit/` directory inside another repository:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
uv pip install -e ./testkit
|
|
62
|
+
uv pip install -e ./testkit
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
From a Git checkout or VCS URL that contains this package:
|
|
@@ -69,6 +69,7 @@ uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
The package supports Python 3.10 and newer.
|
|
72
|
+
`Appium-Python-Client` is installed as a runtime dependency because the pytest fixtures create real Appium sessions.
|
|
72
73
|
|
|
73
74
|
## Environment
|
|
74
75
|
|
|
@@ -158,6 +159,7 @@ finally:
|
|
|
158
159
|
| `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
|
|
159
160
|
| `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
|
|
160
161
|
| `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
|
|
162
|
+
| `GridFleetClient.get_run(run_id)` | Fetch one run detail row by backend run id |
|
|
161
163
|
| `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
|
|
162
164
|
| `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
|
|
163
165
|
| `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
|
|
@@ -182,6 +184,38 @@ finally:
|
|
|
182
184
|
| `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
|
|
183
185
|
| `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` cleanup callable and return it; stops the heartbeat thread on exit but does not complete or cancel the run by default |
|
|
184
186
|
|
|
187
|
+
### Targeting Devices by Tag
|
|
188
|
+
|
|
189
|
+
GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
@pytest.mark.parametrize(
|
|
193
|
+
"appium_driver",
|
|
194
|
+
[
|
|
195
|
+
{
|
|
196
|
+
"pack_id": "appium-uiautomator2",
|
|
197
|
+
"platform_id": "android_mobile",
|
|
198
|
+
"appium:gridfleet:tag:screen_type": "4k",
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
indirect=True,
|
|
202
|
+
)
|
|
203
|
+
def test_4k_display(appium_driver):
|
|
204
|
+
...
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The same capability works for free sessions:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
driver = create_appium_driver(
|
|
211
|
+
pack_id="appium-uiautomator2",
|
|
212
|
+
platform_id="android_mobile",
|
|
213
|
+
capabilities={"appium:gridfleet:tag:screen_type": "4k"},
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
When an operator edits device tags, GridFleet marks the device for re-verification. The next verification restarts the Appium node and re-registers it with the updated Grid stereotype.
|
|
218
|
+
|
|
185
219
|
### Worker Identity
|
|
186
220
|
|
|
187
221
|
`worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
|
|
11
|
+
from appium.webdriver.webdriver import WebDriver
|
|
7
12
|
|
|
8
13
|
SCREENSHOT_DIR = Path(__file__).parent / "screenshots"
|
|
9
14
|
ROKU_HELLO_WORLD_APP = Path(__file__).parent / "assets" / "hello-world.zip"
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
def _resolved_connection_target(capabilities:
|
|
17
|
+
def _resolved_connection_target(capabilities: Mapping[str, object]) -> str:
|
|
13
18
|
value = capabilities.get("appium:udid")
|
|
14
19
|
if isinstance(value, str) and value:
|
|
15
20
|
return value
|
|
@@ -19,7 +24,7 @@ def _resolved_connection_target(capabilities: dict[str, Any]) -> str:
|
|
|
19
24
|
return "session"
|
|
20
25
|
|
|
21
26
|
|
|
22
|
-
def print_connection_context(driver:
|
|
27
|
+
def print_connection_context(driver: WebDriver) -> str:
|
|
23
28
|
"""Print the resolved session context and return the connection target string."""
|
|
24
29
|
caps = driver.capabilities
|
|
25
30
|
connection_target = _resolved_connection_target(caps)
|
|
@@ -35,7 +40,7 @@ def print_connection_context(driver: Any) -> str:
|
|
|
35
40
|
return connection_target
|
|
36
41
|
|
|
37
42
|
|
|
38
|
-
def save_and_assert_screenshot(driver:
|
|
43
|
+
def save_and_assert_screenshot(driver: WebDriver, example_name: str) -> Path:
|
|
39
44
|
"""Save a screenshot and assert that the written file is non-empty."""
|
|
40
45
|
caps = driver.capabilities
|
|
41
46
|
connection_target = _resolved_connection_target(caps).replace(":", "_")
|
|
@@ -51,7 +56,7 @@ def save_and_assert_screenshot(driver: Any, example_name: str) -> Path:
|
|
|
51
56
|
return screenshot_path
|
|
52
57
|
|
|
53
58
|
|
|
54
|
-
def install_and_activate_roku_dev_app(driver:
|
|
59
|
+
def install_and_activate_roku_dev_app(driver: WebDriver) -> None:
|
|
55
60
|
"""Install and activate the bundled Roku dev app used by screenshot examples."""
|
|
56
61
|
assert ROKU_HELLO_WORLD_APP.exists(), f"App package not found: {ROKU_HELLO_WORLD_APP}"
|
|
57
62
|
driver.install_app(str(ROKU_HELLO_WORLD_APP.resolve()))
|
{gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_android_mobile_screenshot.py
RENAMED
|
@@ -5,15 +5,14 @@ Requires:
|
|
|
5
5
|
- Selenium Grid hub running on localhost:4444
|
|
6
6
|
- An Android mobile device registered and its Appium node running
|
|
7
7
|
- The supported GridFleet testkit installed
|
|
8
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
8
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
9
9
|
|
|
10
10
|
Run:
|
|
11
11
|
cd testkit && python -m pytest examples/test_android_mobile_screenshot.py -v -s
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
14
|
import pytest
|
|
15
|
+
from appium.webdriver.webdriver import WebDriver
|
|
17
16
|
|
|
18
17
|
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
19
18
|
|
|
@@ -30,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
30
29
|
],
|
|
31
30
|
indirect=True,
|
|
32
31
|
)
|
|
33
|
-
def test_android_mobile_take_screenshot(appium_driver:
|
|
32
|
+
def test_android_mobile_take_screenshot(appium_driver: WebDriver) -> None:
|
|
34
33
|
"""Connect to an Android mobile device through the Grid and take a screenshot."""
|
|
35
34
|
driver = appium_driver
|
|
36
35
|
|
|
@@ -5,15 +5,14 @@ Requires:
|
|
|
5
5
|
- Selenium Grid hub running on localhost:4444
|
|
6
6
|
- An Android TV device registered and its Appium node running
|
|
7
7
|
- The supported GridFleet testkit installed
|
|
8
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
8
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
9
9
|
|
|
10
10
|
Run:
|
|
11
11
|
cd testkit && python -m pytest examples/test_android_tv_screenshot.py -v -s
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
14
|
import pytest
|
|
15
|
+
from appium.webdriver.webdriver import WebDriver
|
|
17
16
|
|
|
18
17
|
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
19
18
|
|
|
@@ -30,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
30
29
|
],
|
|
31
30
|
indirect=True,
|
|
32
31
|
)
|
|
33
|
-
def test_android_tv_take_screenshot(appium_driver:
|
|
32
|
+
def test_android_tv_take_screenshot(appium_driver: WebDriver) -> None:
|
|
34
33
|
"""Connect to an Android TV device through the Grid and take a screenshot."""
|
|
35
34
|
driver = appium_driver
|
|
36
35
|
|
|
@@ -5,15 +5,14 @@ Requires:
|
|
|
5
5
|
- Selenium Grid hub running on localhost:4444
|
|
6
6
|
- A Fire TV device registered and its Appium node running
|
|
7
7
|
- The supported GridFleet testkit installed
|
|
8
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
8
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
9
9
|
|
|
10
10
|
Run:
|
|
11
11
|
cd testkit && python -m pytest examples/test_firetv_screenshot.py -v -s
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
14
|
import pytest
|
|
15
|
+
from appium.webdriver.webdriver import WebDriver
|
|
17
16
|
|
|
18
17
|
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
19
18
|
|
|
@@ -32,7 +31,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
32
31
|
],
|
|
33
32
|
indirect=True,
|
|
34
33
|
)
|
|
35
|
-
def test_firetv_take_screenshot(appium_driver:
|
|
34
|
+
def test_firetv_take_screenshot(appium_driver: WebDriver) -> None:
|
|
36
35
|
"""Connect to a Fire TV device through the Grid and take a screenshot."""
|
|
37
36
|
driver = appium_driver
|
|
38
37
|
|
{gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_ios_simulator_screenshot.py
RENAMED
|
@@ -6,15 +6,14 @@ Requires:
|
|
|
6
6
|
- An iOS simulator registered and its Appium node running
|
|
7
7
|
- The supported GridFleet testkit installed
|
|
8
8
|
- Appium with the XCUITest driver installed (`appium driver install xcuitest`)
|
|
9
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
9
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
10
10
|
|
|
11
11
|
Run:
|
|
12
12
|
cd testkit && python -m pytest examples/test_ios_simulator_screenshot.py -v -s
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
15
|
import pytest
|
|
16
|
+
from appium.webdriver.webdriver import WebDriver
|
|
18
17
|
|
|
19
18
|
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
20
19
|
|
|
@@ -32,7 +31,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
32
31
|
],
|
|
33
32
|
indirect=True,
|
|
34
33
|
)
|
|
35
|
-
def test_ios_take_screenshot(appium_driver:
|
|
34
|
+
def test_ios_take_screenshot(appium_driver: WebDriver) -> None:
|
|
36
35
|
"""Connect to an iOS simulator through the Grid and take a screenshot."""
|
|
37
36
|
driver = appium_driver
|
|
38
37
|
|
|
@@ -6,15 +6,14 @@ Requires:
|
|
|
6
6
|
- A Roku device registered with Roku dev credentials in device config
|
|
7
7
|
- Appium with the Roku driver installed (`appium driver install roku`)
|
|
8
8
|
- The supported GridFleet testkit installed
|
|
9
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
9
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
10
10
|
|
|
11
11
|
Run:
|
|
12
12
|
cd testkit && python -m pytest examples/test_roku_screenshot.py -v -s
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
15
|
import pytest
|
|
16
|
+
from appium.webdriver.webdriver import WebDriver
|
|
18
17
|
|
|
19
18
|
from examples._example_helpers import (
|
|
20
19
|
install_and_activate_roku_dev_app,
|
|
@@ -35,7 +34,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
35
34
|
],
|
|
36
35
|
indirect=True,
|
|
37
36
|
)
|
|
38
|
-
def test_roku_take_screenshot(appium_driver:
|
|
37
|
+
def test_roku_take_screenshot(appium_driver: WebDriver) -> None:
|
|
39
38
|
"""Connect to a Roku device through the Grid and take a screenshot."""
|
|
40
39
|
driver = appium_driver
|
|
41
40
|
|
{gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_roku_sideload_screenshot.py
RENAMED
|
@@ -6,15 +6,14 @@ Requires:
|
|
|
6
6
|
- A Roku device registered with Roku dev credentials in device config
|
|
7
7
|
- Appium with the Roku driver installed (`appium driver install roku`)
|
|
8
8
|
- The supported GridFleet testkit installed
|
|
9
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
9
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
10
10
|
|
|
11
11
|
Run:
|
|
12
12
|
cd testkit && python -m pytest examples/test_roku_sideload_screenshot.py -v -s
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
15
|
import pytest
|
|
16
|
+
from appium.webdriver.webdriver import WebDriver
|
|
18
17
|
|
|
19
18
|
from examples._example_helpers import (
|
|
20
19
|
ROKU_HELLO_WORLD_APP,
|
|
@@ -36,7 +35,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
36
35
|
],
|
|
37
36
|
indirect=True,
|
|
38
37
|
)
|
|
39
|
-
def test_roku_install_app_and_take_screenshot(appium_driver:
|
|
38
|
+
def test_roku_install_app_and_take_screenshot(appium_driver: WebDriver) -> None:
|
|
40
39
|
"""Connect to a Roku device, sideload an app, and take a screenshot."""
|
|
41
40
|
driver = appium_driver
|
|
42
41
|
|
|
@@ -7,15 +7,14 @@ Requires:
|
|
|
7
7
|
- The supported GridFleet testkit installed
|
|
8
8
|
- Appium with the XCUITest driver installed (`appium driver install xcuitest`)
|
|
9
9
|
- tvOS real-device prerequisites already configured on the host, including WebDriverAgent/XCUITest setup
|
|
10
|
-
- Appium-Python-Client installed (`uv pip install -e ./testkit
|
|
10
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit`)
|
|
11
11
|
|
|
12
12
|
Run:
|
|
13
13
|
cd testkit && python -m pytest examples/test_tvos_screenshot.py -v -s
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
16
|
import pytest
|
|
17
|
+
from appium.webdriver.webdriver import WebDriver
|
|
19
18
|
|
|
20
19
|
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
21
20
|
|
|
@@ -32,7 +31,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
32
31
|
],
|
|
33
32
|
indirect=True,
|
|
34
33
|
)
|
|
35
|
-
def test_tvos_take_screenshot(appium_driver:
|
|
34
|
+
def test_tvos_take_screenshot(appium_driver: WebDriver) -> None:
|
|
36
35
|
"""Connect to a tvOS real device through the Grid and take a screenshot."""
|
|
37
36
|
driver = appium_driver
|
|
38
37
|
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass, replace
|
|
6
|
-
from typing import TYPE_CHECKING,
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
+
from appium.webdriver.webdriver import WebDriver
|
|
10
|
+
|
|
9
11
|
from .client import GridFleetClient
|
|
12
|
+
from .types import JsonObject
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
@dataclass(frozen=True)
|
|
@@ -35,10 +38,11 @@ class AllocatedDevice:
|
|
|
35
38
|
connection_type: str
|
|
36
39
|
manufacturer: str | None
|
|
37
40
|
model: str | None
|
|
38
|
-
config:
|
|
39
|
-
live_capabilities:
|
|
40
|
-
test_data:
|
|
41
|
+
config: JsonObject | None
|
|
42
|
+
live_capabilities: JsonObject | None
|
|
43
|
+
test_data: JsonObject | None = None
|
|
41
44
|
unavailable_includes: tuple[UnavailableInclude, ...] = ()
|
|
45
|
+
tags: dict[str, str] | None = None
|
|
42
46
|
|
|
43
47
|
@property
|
|
44
48
|
def is_real_device(self) -> bool:
|
|
@@ -71,23 +75,23 @@ class AllocatedDevice:
|
|
|
71
75
|
return self.platform_label or self.platform_id
|
|
72
76
|
|
|
73
77
|
|
|
74
|
-
def _string_value(payload:
|
|
78
|
+
def _string_value(payload: JsonObject, key: str, *, default: str | None = None) -> str:
|
|
75
79
|
value = payload.get(key, default)
|
|
76
80
|
if isinstance(value, str) and value:
|
|
77
81
|
return value
|
|
78
82
|
raise ValueError(f"Allocated device payload is missing {key}")
|
|
79
83
|
|
|
80
84
|
|
|
81
|
-
def _optional_string_value(payload:
|
|
85
|
+
def _optional_string_value(payload: JsonObject, key: str) -> str | None:
|
|
82
86
|
value = payload.get(key)
|
|
83
87
|
return value if isinstance(value, str) and value else None
|
|
84
88
|
|
|
85
89
|
|
|
86
|
-
def _needs_device_detail(payload:
|
|
90
|
+
def _needs_device_detail(payload: JsonObject) -> bool:
|
|
87
91
|
return any(payload.get(key) is None for key in ("name", "device_type", "connection_type", "manufacturer", "model"))
|
|
88
92
|
|
|
89
93
|
|
|
90
|
-
def _merge_device_detail(payload:
|
|
94
|
+
def _merge_device_detail(payload: JsonObject, detail: JsonObject) -> JsonObject:
|
|
91
95
|
merged = dict(payload)
|
|
92
96
|
for key in ("name", "device_type", "connection_type", "manufacturer", "model"):
|
|
93
97
|
if merged.get(key) is None and detail.get(key) is not None:
|
|
@@ -97,7 +101,7 @@ def _merge_device_detail(payload: dict[str, Any], detail: dict[str, Any]) -> dic
|
|
|
97
101
|
return merged
|
|
98
102
|
|
|
99
103
|
|
|
100
|
-
def _parse_unavailable_includes(payload:
|
|
104
|
+
def _parse_unavailable_includes(payload: JsonObject) -> tuple[UnavailableInclude, ...]:
|
|
101
105
|
raw = payload.get("unavailable_includes")
|
|
102
106
|
if not isinstance(raw, list):
|
|
103
107
|
return ()
|
|
@@ -113,7 +117,7 @@ def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInc
|
|
|
113
117
|
|
|
114
118
|
|
|
115
119
|
def hydrate_allocated_device(
|
|
116
|
-
device_handle:
|
|
120
|
+
device_handle: JsonObject,
|
|
117
121
|
*,
|
|
118
122
|
run_id: str,
|
|
119
123
|
client: GridFleetClient,
|
|
@@ -133,14 +137,14 @@ def hydrate_allocated_device(
|
|
|
133
137
|
connection_target = _optional_string_value(payload, "connection_target")
|
|
134
138
|
inline_config = payload.get("config")
|
|
135
139
|
if isinstance(inline_config, dict):
|
|
136
|
-
config:
|
|
140
|
+
config: JsonObject | None = cast("JsonObject", inline_config)
|
|
137
141
|
elif fetch_config and connection_target and "config" not in unavailable_set:
|
|
138
142
|
config = client.get_device_config(connection_target)
|
|
139
143
|
else:
|
|
140
144
|
config = None
|
|
141
145
|
inline_capabilities = payload.get("live_capabilities")
|
|
142
146
|
if isinstance(inline_capabilities, dict):
|
|
143
|
-
live_capabilities:
|
|
147
|
+
live_capabilities: JsonObject | None = cast("JsonObject", inline_capabilities)
|
|
144
148
|
elif fetch_capabilities and "capabilities" not in unavailable_set:
|
|
145
149
|
live_capabilities = client.get_device_capabilities(device_id)
|
|
146
150
|
else:
|
|
@@ -148,11 +152,16 @@ def hydrate_allocated_device(
|
|
|
148
152
|
|
|
149
153
|
inline_test_data = payload.get("test_data")
|
|
150
154
|
if isinstance(inline_test_data, dict):
|
|
151
|
-
test_data:
|
|
155
|
+
test_data: JsonObject | None = cast("JsonObject", inline_test_data)
|
|
152
156
|
elif fetch_test_data and "test_data" not in unavailable_set:
|
|
153
157
|
test_data = client.get_device_test_data(device_id)
|
|
154
158
|
else:
|
|
155
159
|
test_data = None
|
|
160
|
+
inline_tags = payload.get("tags")
|
|
161
|
+
if isinstance(inline_tags, dict):
|
|
162
|
+
tags: dict[str, str] | None = inline_tags
|
|
163
|
+
else:
|
|
164
|
+
tags = None
|
|
156
165
|
|
|
157
166
|
return AllocatedDevice(
|
|
158
167
|
run_id=run_id,
|
|
@@ -173,19 +182,20 @@ def hydrate_allocated_device(
|
|
|
173
182
|
live_capabilities=live_capabilities,
|
|
174
183
|
test_data=test_data,
|
|
175
184
|
unavailable_includes=unavailable_includes,
|
|
185
|
+
tags=tags,
|
|
176
186
|
)
|
|
177
187
|
|
|
178
188
|
|
|
179
189
|
def hydrate_allocated_device_from_driver(
|
|
180
190
|
allocated: AllocatedDevice,
|
|
181
|
-
driver:
|
|
191
|
+
driver: WebDriver,
|
|
182
192
|
*,
|
|
183
193
|
client: GridFleetClient,
|
|
184
194
|
) -> AllocatedDevice:
|
|
185
195
|
"""Refresh live capabilities from a running Appium driver session."""
|
|
186
196
|
capabilities = getattr(driver, "capabilities", None)
|
|
187
197
|
if isinstance(capabilities, dict):
|
|
188
|
-
live_capabilities = dict(capabilities)
|
|
198
|
+
live_capabilities = cast("JsonObject", dict(capabilities))
|
|
189
199
|
else:
|
|
190
200
|
live_capabilities = client.get_device_capabilities(allocated.device_id)
|
|
191
201
|
return replace(allocated, live_capabilities=live_capabilities)
|