gridfleet-testkit 0.1.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.
@@ -0,0 +1,52 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .venv/
7
+ venv/
8
+ *.egg-info/
9
+ *.egg
10
+
11
+ # Testing
12
+ .pytest_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # Frontend
17
+ frontend/node_modules/
18
+ frontend/dist/
19
+
20
+ # IDE
21
+ .idea/
22
+ .vscode/
23
+ *.swp
24
+ *.swo
25
+
26
+ # Environment
27
+ .env
28
+ .env.local
29
+ .env.*.local
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Playwright
36
+ .playwright-mcp/
37
+
38
+
39
+ # Build output
40
+ dist/
41
+
42
+ # Misc
43
+ *.log
44
+ .idea
45
+ testing/screenshots
46
+ frontend/test-results
47
+
48
+ # Worktrees
49
+ .worktrees/
50
+
51
+ # Superpowers working docs
52
+ .superpowers/
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: gridfleet-testkit
3
+ Version: 0.1.0
4
+ Summary: Supported pytest and run-orchestration helpers for GridFleet integrations
5
+ Project-URL: Homepage, https://github.com/quidow/gridfleet
6
+ Project-URL: Repository, https://github.com/quidow/gridfleet
7
+ Project-URL: Documentation, https://github.com/quidow/gridfleet/tree/main/docs/reference/testkit.md
8
+ Project-URL: Issues, https://github.com/quidow/gridfleet/issues
9
+ Project-URL: Security, https://github.com/quidow/gridfleet/security/advisories/new
10
+ Author: GridFleet contributors
11
+ License-Expression: Apache-2.0
12
+ Keywords: appium,gridfleet,pytest,selenium,testing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Pytest
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: httpx<1,>=0.27
26
+ Requires-Dist: pytest>=9.0.3
27
+ Provides-Extra: appium
28
+ Requires-Dist: appium-python-client>=4.5; extra == 'appium'
29
+ Provides-Extra: dev
30
+ Requires-Dist: mypy>=1.20.2; extra == 'dev'
31
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
32
+ Requires-Dist: ruff>=0.15.12; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # GridFleet Testkit
36
+
37
+ `testkit/` is the supported Python integration surface for external pytest/Appium suites that run through GridFleet.
38
+
39
+ ## What This Package Owns
40
+
41
+ - Stable import root: `gridfleet_testkit`
42
+ - Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
43
+ - Supported public helpers:
44
+ - `build_appium_options`
45
+ - `create_appium_driver`
46
+ - `get_connection_target_from_driver`
47
+ - `get_device_config_for_driver`
48
+ - `GridFleetClient`
49
+ - `HeartbeatThread`
50
+ - `register_run_cleanup`
51
+ - Manual hardware examples under `testkit/examples/`
52
+
53
+ ## What It Does Not Own
54
+
55
+ - Appium server installation or host-level driver setup
56
+ - Selenium Grid lifecycle
57
+ - Device registration, verification, or readiness setup
58
+ - CI orchestration beyond the documented client helpers
59
+
60
+ The supported contract is the installable package and documented import pattern. The example scripts are onboarding aids, not CI-backed conformance tests.
61
+
62
+ ## Install
63
+
64
+ From PyPI:
65
+
66
+ ```bash
67
+ pip install "gridfleet-testkit[appium]"
68
+ ```
69
+
70
+ From a local checkout:
71
+
72
+ ```bash
73
+ uv pip install -e ./testkit[appium]
74
+ ```
75
+
76
+ From a copied `testkit/` directory inside another repository:
77
+
78
+ ```bash
79
+ uv pip install -e ./testkit[appium]
80
+ ```
81
+
82
+ From a Git checkout or VCS URL that contains this package:
83
+
84
+ ```bash
85
+ uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
86
+ ```
87
+
88
+ The package supports Python 3.10 and newer.
89
+
90
+ ## Environment
91
+
92
+ | Variable | Default | Meaning |
93
+ | --- | --- | --- |
94
+ | `GRID_URL` | `http://localhost:4444` | Selenium Grid hub URL used by the pytest Appium fixture |
95
+ | `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
96
+ | `GRIDFLEET_TESTKIT_USERNAME` | unset | Machine-auth username sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_USERNAME`. |
97
+ | `GRIDFLEET_TESTKIT_PASSWORD` | unset | Machine-auth password sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_PASSWORD`. |
98
+ | `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
99
+ | `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
100
+
101
+ The package assumes a running GridFleet API, a reachable Selenium Grid hub, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
102
+
103
+ ## Pytest Plugin
104
+
105
+ Load the supported plugin from your test project:
106
+
107
+ ```python
108
+ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
109
+ ```
110
+
111
+ Minimal usage:
112
+
113
+ ```python
114
+ import pytest
115
+
116
+ @pytest.mark.parametrize(
117
+ "appium_driver",
118
+ [{"pack_id": "appium-uiautomator2", "platform_id": "android_mobile"}],
119
+ indirect=True,
120
+ )
121
+ def test_session_starts(appium_driver):
122
+ assert appium_driver.session_id is not None
123
+ ```
124
+
125
+ The plugin resolves `pack_id` and `platform_id` against the enabled driver-pack catalog, then injects Appium `platformName`, `appium:automationName`, `appium:platform`, and `gridfleet:testName`.
126
+
127
+ When exactly one enabled pack provides a platform id, `platform_id` alone is accepted. For environment-portable tests, set `GRIDFLEET_TESTKIT_PACK_ID` and `GRIDFLEET_TESTKIT_PLATFORM_ID`, then parametrize with `{}`.
128
+
129
+ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then pass `platformName` as a normal capability key.
130
+
131
+ ### Plugin Lifecycle
132
+
133
+ - Creates an Appium session through `GRID_URL`
134
+ - Injects `gridfleet:testName` with the pytest test name
135
+ - Reports final session status back to `GRIDFLEET_API_URL`
136
+ - Exposes `device_config` for post-session config lookup using the runtime connection target
137
+ - Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
138
+
139
+ ## Direct Appium Usage
140
+
141
+ If you need to create a driver outside pytest, use the public Appium helpers:
142
+
143
+ ```python
144
+ from gridfleet_testkit import create_appium_driver, get_device_config_for_driver
145
+
146
+ driver = create_appium_driver(
147
+ pack_id="appium-uiautomator2",
148
+ platform_id="firetv_real",
149
+ test_name="manual-smoke",
150
+ )
151
+
152
+ try:
153
+ assert driver.session_id is not None
154
+ device_config = get_device_config_for_driver(driver)
155
+ finally:
156
+ driver.quit()
157
+ ```
158
+
159
+ `create_appium_driver(...)` reuses the same driver-pack catalog resolver as the pytest fixture. Managed nodes still get their host-scoped runtime allocations from the manager, so callers should not hard-code `systemPort`, `chromedriverPort`, `mjpegServerPort`, `wdaLocalPort`, or `derivedDataPath`. `get_device_config_for_driver(...)` is the non-pytest equivalent of the `device_config` fixture. If you only need the options object, use `build_appium_options(...)`.
160
+
161
+ ## Client Helpers
162
+
163
+ | Helper | Purpose |
164
+ | --- | --- |
165
+ | `GridFleetClient.get_device_config(connection_target, reveal=True)` | Look up a device by runtime connection target and fetch its config |
166
+ | `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
167
+ | `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
168
+ | `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
169
+ | `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
170
+ | `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
171
+ | `GridFleetClient.report_preparation_failure(run_id, device_id, message, source="ci_preparation")` | Exclude one reserved device after setup fails |
172
+ | `GridFleetClient.complete_run(run_id)` | Complete a run |
173
+ | `GridFleetClient.cancel_run(run_id)` | Cancel a run |
174
+ | `GridFleetClient.start_heartbeat(run_id, interval=30)` | Start a background heartbeat thread |
175
+ | `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` and signal cleanup that completes or cancels a run |
176
+
177
+ ### Reservation Flow
178
+
179
+ ```python
180
+ from gridfleet_testkit import GridFleetClient, register_run_cleanup
181
+
182
+ client = GridFleetClient("http://manager-ip:8000/api")
183
+
184
+ run = client.reserve_devices(
185
+ name="my-test-run",
186
+ requirements=[
187
+ {
188
+ "pack_id": "appium-uiautomator2",
189
+ "platform_id": "firetv_real",
190
+ "os_version": "8",
191
+ "allocation": "all_available",
192
+ "min_count": 1,
193
+ }
194
+ ],
195
+ ttl_minutes=45,
196
+ created_by="local-dev",
197
+ )
198
+
199
+ run_id = run["id"]
200
+ worker_count = len(run["devices"])
201
+ heartbeat_thread = client.start_heartbeat(run_id, interval=30)
202
+ register_run_cleanup(client, run_id, heartbeat_thread)
203
+
204
+ # If one reserved device fails setup:
205
+ client.report_preparation_failure(
206
+ run_id,
207
+ device_id="device-123",
208
+ message="Driver bootstrap timed out during CI setup",
209
+ source="local-dev",
210
+ )
211
+
212
+ client.signal_ready(run_id)
213
+ client.signal_active(run_id)
214
+ ```
215
+
216
+ Use `count` for exact reservations. Use `allocation: "all_available"` when CI should reserve every currently eligible matching device and size its worker pool from `len(run["devices"])`.
217
+
218
+ ## Examples
219
+
220
+ Baseline screenshot examples:
221
+
222
+ - `examples/test_android_mobile_screenshot.py`
223
+ - `examples/test_android_tv_screenshot.py`
224
+ - `examples/test_firetv_screenshot.py`
225
+ - `examples/test_ios_simulator_screenshot.py`
226
+ - `examples/test_tvos_screenshot.py`
227
+ - `examples/test_roku_screenshot.py`
228
+
229
+ Advanced example:
230
+
231
+ - `examples/test_roku_sideload_screenshot.py`
232
+
233
+ The baseline examples share the same flow:
234
+
235
+ 1. Create a session through Selenium Grid
236
+ 2. Print the resolved connection context
237
+ 3. Save a screenshot
238
+ 4. Assert that the screenshot file exists and is non-empty
239
+
240
+ ## Platform Notes
241
+
242
+ - Android Mobile / Android TV / Fire TV:
243
+ - require the UiAutomator2 driver
244
+ - rely on Grid routing hints generated from GridFleet metadata
245
+ - Fire TV:
246
+ - baseline example supports optional `appium:os_version` filtering when you need a specific Fire OS release
247
+ - iOS simulator:
248
+ - baseline example intentionally targets the simulator lane with `appium:device_type=simulator`
249
+ - tvOS:
250
+ - baseline example intentionally targets a real device and assumes the host already satisfies XCUITest and WebDriverAgent prerequisites
251
+ - Roku:
252
+ - screenshot examples install and activate the bundled sample dev app before capture
253
+ - both Roku examples depend on Roku dev credentials
@@ -0,0 +1,219 @@
1
+ # GridFleet Testkit
2
+
3
+ `testkit/` is the supported Python integration surface for external pytest/Appium suites that run through GridFleet.
4
+
5
+ ## What This Package Owns
6
+
7
+ - Stable import root: `gridfleet_testkit`
8
+ - Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
9
+ - Supported public helpers:
10
+ - `build_appium_options`
11
+ - `create_appium_driver`
12
+ - `get_connection_target_from_driver`
13
+ - `get_device_config_for_driver`
14
+ - `GridFleetClient`
15
+ - `HeartbeatThread`
16
+ - `register_run_cleanup`
17
+ - Manual hardware examples under `testkit/examples/`
18
+
19
+ ## What It Does Not Own
20
+
21
+ - Appium server installation or host-level driver setup
22
+ - Selenium Grid lifecycle
23
+ - Device registration, verification, or readiness setup
24
+ - CI orchestration beyond the documented client helpers
25
+
26
+ The supported contract is the installable package and documented import pattern. The example scripts are onboarding aids, not CI-backed conformance tests.
27
+
28
+ ## Install
29
+
30
+ From PyPI:
31
+
32
+ ```bash
33
+ pip install "gridfleet-testkit[appium]"
34
+ ```
35
+
36
+ From a local checkout:
37
+
38
+ ```bash
39
+ uv pip install -e ./testkit[appium]
40
+ ```
41
+
42
+ From a copied `testkit/` directory inside another repository:
43
+
44
+ ```bash
45
+ uv pip install -e ./testkit[appium]
46
+ ```
47
+
48
+ From a Git checkout or VCS URL that contains this package:
49
+
50
+ ```bash
51
+ uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
52
+ ```
53
+
54
+ The package supports Python 3.10 and newer.
55
+
56
+ ## Environment
57
+
58
+ | Variable | Default | Meaning |
59
+ | --- | --- | --- |
60
+ | `GRID_URL` | `http://localhost:4444` | Selenium Grid hub URL used by the pytest Appium fixture |
61
+ | `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
62
+ | `GRIDFLEET_TESTKIT_USERNAME` | unset | Machine-auth username sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_USERNAME`. |
63
+ | `GRIDFLEET_TESTKIT_PASSWORD` | unset | Machine-auth password sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_PASSWORD`. |
64
+ | `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
65
+ | `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
66
+
67
+ The package assumes a running GridFleet API, a reachable Selenium Grid hub, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
68
+
69
+ ## Pytest Plugin
70
+
71
+ Load the supported plugin from your test project:
72
+
73
+ ```python
74
+ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
75
+ ```
76
+
77
+ Minimal usage:
78
+
79
+ ```python
80
+ import pytest
81
+
82
+ @pytest.mark.parametrize(
83
+ "appium_driver",
84
+ [{"pack_id": "appium-uiautomator2", "platform_id": "android_mobile"}],
85
+ indirect=True,
86
+ )
87
+ def test_session_starts(appium_driver):
88
+ assert appium_driver.session_id is not None
89
+ ```
90
+
91
+ The plugin resolves `pack_id` and `platform_id` against the enabled driver-pack catalog, then injects Appium `platformName`, `appium:automationName`, `appium:platform`, and `gridfleet:testName`.
92
+
93
+ When exactly one enabled pack provides a platform id, `platform_id` alone is accepted. For environment-portable tests, set `GRIDFLEET_TESTKIT_PACK_ID` and `GRIDFLEET_TESTKIT_PLATFORM_ID`, then parametrize with `{}`.
94
+
95
+ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then pass `platformName` as a normal capability key.
96
+
97
+ ### Plugin Lifecycle
98
+
99
+ - Creates an Appium session through `GRID_URL`
100
+ - Injects `gridfleet:testName` with the pytest test name
101
+ - Reports final session status back to `GRIDFLEET_API_URL`
102
+ - Exposes `device_config` for post-session config lookup using the runtime connection target
103
+ - Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
104
+
105
+ ## Direct Appium Usage
106
+
107
+ If you need to create a driver outside pytest, use the public Appium helpers:
108
+
109
+ ```python
110
+ from gridfleet_testkit import create_appium_driver, get_device_config_for_driver
111
+
112
+ driver = create_appium_driver(
113
+ pack_id="appium-uiautomator2",
114
+ platform_id="firetv_real",
115
+ test_name="manual-smoke",
116
+ )
117
+
118
+ try:
119
+ assert driver.session_id is not None
120
+ device_config = get_device_config_for_driver(driver)
121
+ finally:
122
+ driver.quit()
123
+ ```
124
+
125
+ `create_appium_driver(...)` reuses the same driver-pack catalog resolver as the pytest fixture. Managed nodes still get their host-scoped runtime allocations from the manager, so callers should not hard-code `systemPort`, `chromedriverPort`, `mjpegServerPort`, `wdaLocalPort`, or `derivedDataPath`. `get_device_config_for_driver(...)` is the non-pytest equivalent of the `device_config` fixture. If you only need the options object, use `build_appium_options(...)`.
126
+
127
+ ## Client Helpers
128
+
129
+ | Helper | Purpose |
130
+ | --- | --- |
131
+ | `GridFleetClient.get_device_config(connection_target, reveal=True)` | Look up a device by runtime connection target and fetch its config |
132
+ | `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
133
+ | `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
134
+ | `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
135
+ | `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
136
+ | `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
137
+ | `GridFleetClient.report_preparation_failure(run_id, device_id, message, source="ci_preparation")` | Exclude one reserved device after setup fails |
138
+ | `GridFleetClient.complete_run(run_id)` | Complete a run |
139
+ | `GridFleetClient.cancel_run(run_id)` | Cancel a run |
140
+ | `GridFleetClient.start_heartbeat(run_id, interval=30)` | Start a background heartbeat thread |
141
+ | `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` and signal cleanup that completes or cancels a run |
142
+
143
+ ### Reservation Flow
144
+
145
+ ```python
146
+ from gridfleet_testkit import GridFleetClient, register_run_cleanup
147
+
148
+ client = GridFleetClient("http://manager-ip:8000/api")
149
+
150
+ run = client.reserve_devices(
151
+ name="my-test-run",
152
+ requirements=[
153
+ {
154
+ "pack_id": "appium-uiautomator2",
155
+ "platform_id": "firetv_real",
156
+ "os_version": "8",
157
+ "allocation": "all_available",
158
+ "min_count": 1,
159
+ }
160
+ ],
161
+ ttl_minutes=45,
162
+ created_by="local-dev",
163
+ )
164
+
165
+ run_id = run["id"]
166
+ worker_count = len(run["devices"])
167
+ heartbeat_thread = client.start_heartbeat(run_id, interval=30)
168
+ register_run_cleanup(client, run_id, heartbeat_thread)
169
+
170
+ # If one reserved device fails setup:
171
+ client.report_preparation_failure(
172
+ run_id,
173
+ device_id="device-123",
174
+ message="Driver bootstrap timed out during CI setup",
175
+ source="local-dev",
176
+ )
177
+
178
+ client.signal_ready(run_id)
179
+ client.signal_active(run_id)
180
+ ```
181
+
182
+ Use `count` for exact reservations. Use `allocation: "all_available"` when CI should reserve every currently eligible matching device and size its worker pool from `len(run["devices"])`.
183
+
184
+ ## Examples
185
+
186
+ Baseline screenshot examples:
187
+
188
+ - `examples/test_android_mobile_screenshot.py`
189
+ - `examples/test_android_tv_screenshot.py`
190
+ - `examples/test_firetv_screenshot.py`
191
+ - `examples/test_ios_simulator_screenshot.py`
192
+ - `examples/test_tvos_screenshot.py`
193
+ - `examples/test_roku_screenshot.py`
194
+
195
+ Advanced example:
196
+
197
+ - `examples/test_roku_sideload_screenshot.py`
198
+
199
+ The baseline examples share the same flow:
200
+
201
+ 1. Create a session through Selenium Grid
202
+ 2. Print the resolved connection context
203
+ 3. Save a screenshot
204
+ 4. Assert that the screenshot file exists and is non-empty
205
+
206
+ ## Platform Notes
207
+
208
+ - Android Mobile / Android TV / Fire TV:
209
+ - require the UiAutomator2 driver
210
+ - rely on Grid routing hints generated from GridFleet metadata
211
+ - Fire TV:
212
+ - baseline example supports optional `appium:os_version` filtering when you need a specific Fire OS release
213
+ - iOS simulator:
214
+ - baseline example intentionally targets the simulator lane with `appium:device_type=simulator`
215
+ - tvOS:
216
+ - baseline example intentionally targets a real device and assumes the host already satisfies XCUITest and WebDriverAgent prerequisites
217
+ - Roku:
218
+ - screenshot examples install and activate the bundled sample dev app before capture
219
+ - both Roku examples depend on Roku dev credentials
@@ -0,0 +1 @@
1
+ """Manual hardware examples for the supported GridFleet testkit."""
@@ -0,0 +1,58 @@
1
+ """Shared helpers for copyable manual screenshot examples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ SCREENSHOT_DIR = Path(__file__).parent / "screenshots"
9
+ ROKU_HELLO_WORLD_APP = Path(__file__).parent / "assets" / "hello-world.zip"
10
+
11
+
12
+ def _resolved_connection_target(capabilities: dict[str, Any]) -> str:
13
+ value = capabilities.get("appium:udid")
14
+ if isinstance(value, str) and value:
15
+ return value
16
+ session_id = capabilities.get("sessionId")
17
+ if isinstance(session_id, str) and session_id:
18
+ return session_id
19
+ return "session"
20
+
21
+
22
+ def print_connection_context(driver: Any) -> str:
23
+ """Print the resolved session context and return the connection target string."""
24
+ caps = driver.capabilities
25
+ connection_target = _resolved_connection_target(caps)
26
+ platform_name = caps.get("platformName", "")
27
+ automation_name = caps.get("appium:automationName") or caps.get("automationName")
28
+ print(
29
+ "\nConnected to device: "
30
+ f"connection_target={connection_target}, "
31
+ f"platform={platform_name}, "
32
+ f"automationName={automation_name}"
33
+ )
34
+ print(f"Session ID: {driver.session_id}")
35
+ return connection_target
36
+
37
+
38
+ def save_and_assert_screenshot(driver: Any, example_name: str) -> Path:
39
+ """Save a screenshot and assert that the written file is non-empty."""
40
+ caps = driver.capabilities
41
+ connection_target = _resolved_connection_target(caps).replace(":", "_")
42
+ SCREENSHOT_DIR.mkdir(exist_ok=True)
43
+ screenshot_path = SCREENSHOT_DIR / f"{example_name}_{connection_target}.png"
44
+ saved = driver.save_screenshot(str(screenshot_path))
45
+
46
+ assert saved, "save_screenshot returned False"
47
+ assert screenshot_path.exists(), f"Screenshot file not found at {screenshot_path}"
48
+ assert screenshot_path.stat().st_size > 0, "Screenshot file is empty"
49
+
50
+ print(f"Screenshot saved: {screenshot_path} ({screenshot_path.stat().st_size} bytes)")
51
+ return screenshot_path
52
+
53
+
54
+ def install_and_activate_roku_dev_app(driver: Any) -> None:
55
+ """Install and activate the bundled Roku dev app used by screenshot examples."""
56
+ assert ROKU_HELLO_WORLD_APP.exists(), f"App package not found: {ROKU_HELLO_WORLD_APP}"
57
+ driver.install_app(str(ROKU_HELLO_WORLD_APP.resolve()))
58
+ driver.activate_app("dev")
@@ -0,0 +1,40 @@
1
+ """
2
+ Manual baseline example: connect to an Android mobile device through Selenium Grid and take a screenshot.
3
+
4
+ Requires:
5
+ - Selenium Grid hub running on localhost:4444
6
+ - An Android mobile device registered and its Appium node running
7
+ - The supported GridFleet testkit installed
8
+ - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
9
+
10
+ Run:
11
+ cd testkit && python -m pytest examples/test_android_mobile_screenshot.py -v -s
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ import pytest
17
+
18
+ from examples._example_helpers import print_connection_context, save_and_assert_screenshot
19
+
20
+ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
21
+
22
+
23
+ @pytest.mark.parametrize(
24
+ "appium_driver",
25
+ [
26
+ {
27
+ "pack_id": "appium-uiautomator2",
28
+ "platform_id": "android_mobile",
29
+ }
30
+ ],
31
+ indirect=True,
32
+ )
33
+ def test_android_mobile_take_screenshot(appium_driver: Any) -> None:
34
+ """Connect to an Android mobile device through the Grid and take a screenshot."""
35
+ driver = appium_driver
36
+
37
+ assert driver.session_id is not None, "Failed to create Appium session"
38
+
39
+ print_connection_context(driver)
40
+ save_and_assert_screenshot(driver, "android_mobile")
@@ -0,0 +1,40 @@
1
+ """
2
+ Manual baseline example: connect to an Android TV device through Selenium Grid and take a screenshot.
3
+
4
+ Requires:
5
+ - Selenium Grid hub running on localhost:4444
6
+ - An Android TV device registered and its Appium node running
7
+ - The supported GridFleet testkit installed
8
+ - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
9
+
10
+ Run:
11
+ cd testkit && python -m pytest examples/test_android_tv_screenshot.py -v -s
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ import pytest
17
+
18
+ from examples._example_helpers import print_connection_context, save_and_assert_screenshot
19
+
20
+ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
21
+
22
+
23
+ @pytest.mark.parametrize(
24
+ "appium_driver",
25
+ [
26
+ {
27
+ "pack_id": "appium-uiautomator2",
28
+ "platform_id": "android_tv",
29
+ }
30
+ ],
31
+ indirect=True,
32
+ )
33
+ def test_android_tv_take_screenshot(appium_driver: Any) -> None:
34
+ """Connect to an Android TV device through the Grid and take a screenshot."""
35
+ driver = appium_driver
36
+
37
+ assert driver.session_id is not None, "Failed to create Appium session"
38
+
39
+ print_connection_context(driver)
40
+ save_and_assert_screenshot(driver, "android_tv")