gridfleet-testkit 0.9.0__tar.gz → 0.9.2__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.9.0 → gridfleet_testkit-0.9.2}/CHANGELOG.md +14 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/PKG-INFO +6 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/README.md +4 -3
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/_example_helpers.py +10 -5
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_android_mobile_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_android_tv_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_firetv_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_ios_simulator_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_roku_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_roku_sideload_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_tvos_screenshot.py +3 -6
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/__init__.py +1 -1
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/allocation.py +18 -15
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/appium.py +34 -34
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/client.py +60 -56
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/pytest_plugin.py +31 -14
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/sessions.py +25 -8
- gridfleet_testkit-0.9.2/gridfleet_testkit/types.py +11 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/pyproject.toml +3 -11
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_allocation.py +10 -7
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_appium.py +9 -19
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_client.py +105 -98
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_client_test_data.py +12 -9
- gridfleet_testkit-0.9.2/tests/test_driver_agnostic_guard.py +57 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_pytest_plugin.py +6 -28
- gridfleet_testkit-0.9.2/tests/test_pytest_plugin_grid_run_id_injection.py +29 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_pytest_plugin_test_data.py +2 -2
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_sessions.py +5 -2
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/uv.lock +4 -6
- gridfleet_testkit-0.9.0/tests/test_driver_agnostic_guard.py +0 -29
- gridfleet_testkit-0.9.0/tests/test_pytest_plugin_grid_run_id_injection.py +0 -56
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/.gitignore +0 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_docs_contract.py +0 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_package_metadata.py +0 -0
- {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_sessions_resolve_device_handle.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.9.2](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.1...gridfleet-testkit-v0.9.2) (2026-05-16)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **testkit:** remove pytest_plugins shim and stale --extra appium docs ([#270](https://github.com/quidow/gridfleet/issues/270)) ([1165fc1](https://github.com/quidow/gridfleet/commit/1165fc1e7326853e7467500c41d8b1c197e7046f))
|
|
11
|
+
|
|
12
|
+
## [0.9.1](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.0...gridfleet-testkit-v0.9.1) (2026-05-13)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **testkit:** align release policy with commitlint ([5dd8220](https://github.com/quidow/gridfleet/commit/5dd822010460e994f2e3c5b6676a69bed05678ed))
|
|
18
|
+
|
|
5
19
|
## [0.9.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.8.0...gridfleet-testkit-v0.9.0) (2026-05-12)
|
|
6
20
|
|
|
7
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridfleet-testkit
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
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
|
|
|
@@ -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
|
|
|
@@ -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.9.0 → gridfleet_testkit-0.9.2}/examples/test_android_mobile_screenshot.py
RENAMED
|
@@ -5,20 +5,17 @@ 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
|
|
|
20
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
21
|
-
|
|
22
19
|
|
|
23
20
|
@pytest.mark.parametrize(
|
|
24
21
|
"appium_driver",
|
|
@@ -30,7 +27,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
30
27
|
],
|
|
31
28
|
indirect=True,
|
|
32
29
|
)
|
|
33
|
-
def test_android_mobile_take_screenshot(appium_driver:
|
|
30
|
+
def test_android_mobile_take_screenshot(appium_driver: WebDriver) -> None:
|
|
34
31
|
"""Connect to an Android mobile device through the Grid and take a screenshot."""
|
|
35
32
|
driver = appium_driver
|
|
36
33
|
|
|
@@ -5,20 +5,17 @@ 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
|
|
|
20
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
21
|
-
|
|
22
19
|
|
|
23
20
|
@pytest.mark.parametrize(
|
|
24
21
|
"appium_driver",
|
|
@@ -30,7 +27,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
30
27
|
],
|
|
31
28
|
indirect=True,
|
|
32
29
|
)
|
|
33
|
-
def test_android_tv_take_screenshot(appium_driver:
|
|
30
|
+
def test_android_tv_take_screenshot(appium_driver: WebDriver) -> None:
|
|
34
31
|
"""Connect to an Android TV device through the Grid and take a screenshot."""
|
|
35
32
|
driver = appium_driver
|
|
36
33
|
|
|
@@ -5,20 +5,17 @@ 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
|
|
|
20
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
21
|
-
|
|
22
19
|
|
|
23
20
|
@pytest.mark.parametrize(
|
|
24
21
|
"appium_driver",
|
|
@@ -32,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
32
29
|
],
|
|
33
30
|
indirect=True,
|
|
34
31
|
)
|
|
35
|
-
def test_firetv_take_screenshot(appium_driver:
|
|
32
|
+
def test_firetv_take_screenshot(appium_driver: WebDriver) -> None:
|
|
36
33
|
"""Connect to a Fire TV device through the Grid and take a screenshot."""
|
|
37
34
|
driver = appium_driver
|
|
38
35
|
|
{gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_ios_simulator_screenshot.py
RENAMED
|
@@ -6,20 +6,17 @@ 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
|
|
|
21
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
22
|
-
|
|
23
20
|
|
|
24
21
|
@pytest.mark.parametrize(
|
|
25
22
|
"appium_driver",
|
|
@@ -32,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
32
29
|
],
|
|
33
30
|
indirect=True,
|
|
34
31
|
)
|
|
35
|
-
def test_ios_take_screenshot(appium_driver:
|
|
32
|
+
def test_ios_take_screenshot(appium_driver: WebDriver) -> None:
|
|
36
33
|
"""Connect to an iOS simulator through the Grid and take a screenshot."""
|
|
37
34
|
driver = appium_driver
|
|
38
35
|
|
|
@@ -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,
|
|
@@ -22,8 +21,6 @@ from examples._example_helpers import (
|
|
|
22
21
|
save_and_assert_screenshot,
|
|
23
22
|
)
|
|
24
23
|
|
|
25
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
26
|
-
|
|
27
24
|
|
|
28
25
|
@pytest.mark.parametrize(
|
|
29
26
|
"appium_driver",
|
|
@@ -35,7 +32,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
35
32
|
],
|
|
36
33
|
indirect=True,
|
|
37
34
|
)
|
|
38
|
-
def test_roku_take_screenshot(appium_driver:
|
|
35
|
+
def test_roku_take_screenshot(appium_driver: WebDriver) -> None:
|
|
39
36
|
"""Connect to a Roku device through the Grid and take a screenshot."""
|
|
40
37
|
driver = appium_driver
|
|
41
38
|
|
{gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/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,
|
|
@@ -23,8 +22,6 @@ from examples._example_helpers import (
|
|
|
23
22
|
save_and_assert_screenshot,
|
|
24
23
|
)
|
|
25
24
|
|
|
26
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
27
|
-
|
|
28
25
|
|
|
29
26
|
@pytest.mark.parametrize(
|
|
30
27
|
"appium_driver",
|
|
@@ -36,7 +33,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
36
33
|
],
|
|
37
34
|
indirect=True,
|
|
38
35
|
)
|
|
39
|
-
def test_roku_install_app_and_take_screenshot(appium_driver:
|
|
36
|
+
def test_roku_install_app_and_take_screenshot(appium_driver: WebDriver) -> None:
|
|
40
37
|
"""Connect to a Roku device, sideload an app, and take a screenshot."""
|
|
41
38
|
driver = appium_driver
|
|
42
39
|
|
|
@@ -7,20 +7,17 @@ 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
|
|
|
22
|
-
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
23
|
-
|
|
24
21
|
|
|
25
22
|
@pytest.mark.parametrize(
|
|
26
23
|
"appium_driver",
|
|
@@ -32,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
|
32
29
|
],
|
|
33
30
|
indirect=True,
|
|
34
31
|
)
|
|
35
|
-
def test_tvos_take_screenshot(appium_driver:
|
|
32
|
+
def test_tvos_take_screenshot(appium_driver: WebDriver) -> None:
|
|
36
33
|
"""Connect to a tvOS real device through the Grid and take a screenshot."""
|
|
37
34
|
driver = appium_driver
|
|
38
35
|
|
|
@@ -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,9 +38,9 @@ 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, ...] = ()
|
|
42
45
|
tags: dict[str, str] | None = None
|
|
43
46
|
|
|
@@ -72,23 +75,23 @@ class AllocatedDevice:
|
|
|
72
75
|
return self.platform_label or self.platform_id
|
|
73
76
|
|
|
74
77
|
|
|
75
|
-
def _string_value(payload:
|
|
78
|
+
def _string_value(payload: JsonObject, key: str, *, default: str | None = None) -> str:
|
|
76
79
|
value = payload.get(key, default)
|
|
77
80
|
if isinstance(value, str) and value:
|
|
78
81
|
return value
|
|
79
82
|
raise ValueError(f"Allocated device payload is missing {key}")
|
|
80
83
|
|
|
81
84
|
|
|
82
|
-
def _optional_string_value(payload:
|
|
85
|
+
def _optional_string_value(payload: JsonObject, key: str) -> str | None:
|
|
83
86
|
value = payload.get(key)
|
|
84
87
|
return value if isinstance(value, str) and value else None
|
|
85
88
|
|
|
86
89
|
|
|
87
|
-
def _needs_device_detail(payload:
|
|
90
|
+
def _needs_device_detail(payload: JsonObject) -> bool:
|
|
88
91
|
return any(payload.get(key) is None for key in ("name", "device_type", "connection_type", "manufacturer", "model"))
|
|
89
92
|
|
|
90
93
|
|
|
91
|
-
def _merge_device_detail(payload:
|
|
94
|
+
def _merge_device_detail(payload: JsonObject, detail: JsonObject) -> JsonObject:
|
|
92
95
|
merged = dict(payload)
|
|
93
96
|
for key in ("name", "device_type", "connection_type", "manufacturer", "model"):
|
|
94
97
|
if merged.get(key) is None and detail.get(key) is not None:
|
|
@@ -98,7 +101,7 @@ def _merge_device_detail(payload: dict[str, Any], detail: dict[str, Any]) -> dic
|
|
|
98
101
|
return merged
|
|
99
102
|
|
|
100
103
|
|
|
101
|
-
def _parse_unavailable_includes(payload:
|
|
104
|
+
def _parse_unavailable_includes(payload: JsonObject) -> tuple[UnavailableInclude, ...]:
|
|
102
105
|
raw = payload.get("unavailable_includes")
|
|
103
106
|
if not isinstance(raw, list):
|
|
104
107
|
return ()
|
|
@@ -114,7 +117,7 @@ def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInc
|
|
|
114
117
|
|
|
115
118
|
|
|
116
119
|
def hydrate_allocated_device(
|
|
117
|
-
device_handle:
|
|
120
|
+
device_handle: JsonObject,
|
|
118
121
|
*,
|
|
119
122
|
run_id: str,
|
|
120
123
|
client: GridFleetClient,
|
|
@@ -134,14 +137,14 @@ def hydrate_allocated_device(
|
|
|
134
137
|
connection_target = _optional_string_value(payload, "connection_target")
|
|
135
138
|
inline_config = payload.get("config")
|
|
136
139
|
if isinstance(inline_config, dict):
|
|
137
|
-
config:
|
|
140
|
+
config: JsonObject | None = cast("JsonObject", inline_config)
|
|
138
141
|
elif fetch_config and connection_target and "config" not in unavailable_set:
|
|
139
142
|
config = client.get_device_config(connection_target)
|
|
140
143
|
else:
|
|
141
144
|
config = None
|
|
142
145
|
inline_capabilities = payload.get("live_capabilities")
|
|
143
146
|
if isinstance(inline_capabilities, dict):
|
|
144
|
-
live_capabilities:
|
|
147
|
+
live_capabilities: JsonObject | None = cast("JsonObject", inline_capabilities)
|
|
145
148
|
elif fetch_capabilities and "capabilities" not in unavailable_set:
|
|
146
149
|
live_capabilities = client.get_device_capabilities(device_id)
|
|
147
150
|
else:
|
|
@@ -149,7 +152,7 @@ def hydrate_allocated_device(
|
|
|
149
152
|
|
|
150
153
|
inline_test_data = payload.get("test_data")
|
|
151
154
|
if isinstance(inline_test_data, dict):
|
|
152
|
-
test_data:
|
|
155
|
+
test_data: JsonObject | None = cast("JsonObject", inline_test_data)
|
|
153
156
|
elif fetch_test_data and "test_data" not in unavailable_set:
|
|
154
157
|
test_data = client.get_device_test_data(device_id)
|
|
155
158
|
else:
|
|
@@ -185,14 +188,14 @@ def hydrate_allocated_device(
|
|
|
185
188
|
|
|
186
189
|
def hydrate_allocated_device_from_driver(
|
|
187
190
|
allocated: AllocatedDevice,
|
|
188
|
-
driver:
|
|
191
|
+
driver: WebDriver,
|
|
189
192
|
*,
|
|
190
193
|
client: GridFleetClient,
|
|
191
194
|
) -> AllocatedDevice:
|
|
192
195
|
"""Refresh live capabilities from a running Appium driver session."""
|
|
193
196
|
capabilities = getattr(driver, "capabilities", None)
|
|
194
197
|
if isinstance(capabilities, dict):
|
|
195
|
-
live_capabilities = dict(capabilities)
|
|
198
|
+
live_capabilities = cast("JsonObject", dict(capabilities))
|
|
196
199
|
else:
|
|
197
200
|
live_capabilities = client.get_device_capabilities(allocated.device_id)
|
|
198
201
|
return replace(allocated, live_capabilities=live_capabilities)
|
|
@@ -3,35 +3,43 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
from typing import TYPE_CHECKING,
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
from appium import webdriver
|
|
9
|
+
from appium.options.common import AppiumOptions
|
|
10
10
|
|
|
11
11
|
from .client import GridFleetClient, _default_grid_url
|
|
12
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable, Mapping
|
|
15
|
+
|
|
16
|
+
from appium.webdriver.webdriver import WebDriver
|
|
17
|
+
|
|
18
|
+
from .types import JsonObject
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
|
|
21
|
+
def _catalog_payload(catalog_client: object | None) -> JsonObject:
|
|
15
22
|
if catalog_client is None:
|
|
16
23
|
catalog_client = GridFleetClient()
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
catalog_getter = getattr(catalog_client, "get_driver_pack_catalog", None)
|
|
25
|
+
if callable(catalog_getter):
|
|
26
|
+
payload = catalog_getter()
|
|
19
27
|
elif callable(catalog_client):
|
|
20
|
-
payload = catalog_client()
|
|
28
|
+
payload = cast("Callable[[], object]", catalog_client)()
|
|
21
29
|
else:
|
|
22
30
|
payload = catalog_client
|
|
23
31
|
if isinstance(payload, dict):
|
|
24
|
-
return payload
|
|
32
|
+
return cast("JsonObject", payload)
|
|
25
33
|
if isinstance(payload, list):
|
|
26
|
-
return {"packs": payload}
|
|
34
|
+
return cast("JsonObject", {"packs": payload})
|
|
27
35
|
raise ValueError("Driver pack catalog client returned an invalid payload")
|
|
28
36
|
|
|
29
37
|
|
|
30
|
-
def _enabled_platform_matches(catalog:
|
|
38
|
+
def _enabled_platform_matches(catalog: JsonObject, platform_id: str) -> list[tuple[JsonObject, JsonObject]]:
|
|
31
39
|
packs = catalog.get("packs")
|
|
32
40
|
if not isinstance(packs, list):
|
|
33
41
|
raise ValueError("Driver pack catalog payload must include a packs list")
|
|
34
|
-
matches: list[tuple[
|
|
42
|
+
matches: list[tuple[JsonObject, JsonObject]] = []
|
|
35
43
|
for pack in packs:
|
|
36
44
|
if not isinstance(pack, dict) or pack.get("state") != "enabled":
|
|
37
45
|
continue
|
|
@@ -48,8 +56,8 @@ def _resolve_pack_platform(
|
|
|
48
56
|
*,
|
|
49
57
|
pack_id: str | None,
|
|
50
58
|
platform_id: str | None,
|
|
51
|
-
catalog_client:
|
|
52
|
-
) -> tuple[str,
|
|
59
|
+
catalog_client: object | None,
|
|
60
|
+
) -> tuple[str, JsonObject]:
|
|
53
61
|
resolved_pack_id = pack_id or os.getenv("GRIDFLEET_TESTKIT_PACK_ID")
|
|
54
62
|
resolved_platform_id = platform_id or os.getenv("GRIDFLEET_TESTKIT_PLATFORM_ID")
|
|
55
63
|
if not resolved_platform_id:
|
|
@@ -77,7 +85,7 @@ def _resolve_pack_platform(
|
|
|
77
85
|
raise ValueError(f"Enabled driver pack platform for platform_id {resolved_platform_id!r} was not found")
|
|
78
86
|
|
|
79
87
|
|
|
80
|
-
def _required_platform_string(platform:
|
|
88
|
+
def _required_platform_string(platform: JsonObject, key: str) -> str:
|
|
81
89
|
value = platform.get(key)
|
|
82
90
|
if not isinstance(value, str) or not value:
|
|
83
91
|
raise ValueError(f"Driver pack platform is missing {key}")
|
|
@@ -88,15 +96,11 @@ def build_appium_options(
|
|
|
88
96
|
*,
|
|
89
97
|
pack_id: str | None = None,
|
|
90
98
|
platform_id: str | None = None,
|
|
91
|
-
capabilities: Mapping[str,
|
|
99
|
+
capabilities: Mapping[str, object] | None = None,
|
|
92
100
|
test_name: str | None = None,
|
|
93
|
-
catalog_client:
|
|
94
|
-
) ->
|
|
101
|
+
catalog_client: object | None = None,
|
|
102
|
+
) -> AppiumOptions:
|
|
95
103
|
"""Build Appium options from driver-pack catalog platform metadata."""
|
|
96
|
-
# appium is an optional dep (extra "appium"); imported lazily so consumers
|
|
97
|
-
# without the extra can still use the rest of testkit.
|
|
98
|
-
from appium.options.common import AppiumOptions # noqa: PLC0415
|
|
99
|
-
|
|
100
104
|
params = dict(capabilities or {})
|
|
101
105
|
params.setdefault("gridfleet:run_id", os.environ.get("GRIDFLEET_RUN_ID", "free"))
|
|
102
106
|
explicit_platform_name = params.get("platformName")
|
|
@@ -126,16 +130,12 @@ def create_appium_driver(
|
|
|
126
130
|
*,
|
|
127
131
|
pack_id: str | None = None,
|
|
128
132
|
platform_id: str | None = None,
|
|
129
|
-
capabilities: Mapping[str,
|
|
133
|
+
capabilities: Mapping[str, object] | None = None,
|
|
130
134
|
test_name: str | None = None,
|
|
131
135
|
grid_url: str | None = None,
|
|
132
|
-
catalog_client:
|
|
133
|
-
) ->
|
|
136
|
+
catalog_client: object | None = None,
|
|
137
|
+
) -> WebDriver:
|
|
134
138
|
"""Create an Appium remote driver through Selenium Grid."""
|
|
135
|
-
# appium is an optional dep (extra "appium"); imported lazily so consumers
|
|
136
|
-
# without the extra can still use the rest of testkit.
|
|
137
|
-
from appium import webdriver # noqa: PLC0415
|
|
138
|
-
|
|
139
139
|
options = build_appium_options(
|
|
140
140
|
pack_id=pack_id,
|
|
141
141
|
platform_id=platform_id,
|
|
@@ -146,9 +146,9 @@ def create_appium_driver(
|
|
|
146
146
|
return webdriver.Remote(grid_url or _default_grid_url(), options=options)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
def get_connection_target_from_driver(driver:
|
|
149
|
+
def get_connection_target_from_driver(driver: WebDriver) -> str:
|
|
150
150
|
"""Return the runtime connection target from a live Appium driver."""
|
|
151
|
-
capabilities = driver.capabilities
|
|
151
|
+
capabilities = cast("JsonObject", driver.capabilities) if isinstance(driver.capabilities, dict) else {}
|
|
152
152
|
connection_target = capabilities.get("appium:udid")
|
|
153
153
|
if not isinstance(connection_target, str) or not connection_target:
|
|
154
154
|
raise ValueError("Could not determine device connection target from session capabilities")
|
|
@@ -156,20 +156,20 @@ def get_connection_target_from_driver(driver: Any) -> str:
|
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def get_device_config_for_driver(
|
|
159
|
-
driver:
|
|
159
|
+
driver: WebDriver,
|
|
160
160
|
*,
|
|
161
161
|
gridfleet_client: GridFleetClient | None = None,
|
|
162
|
-
) ->
|
|
162
|
+
) -> JsonObject:
|
|
163
163
|
"""Fetch device config for a live Appium driver using its runtime connection target."""
|
|
164
164
|
client = gridfleet_client or GridFleetClient()
|
|
165
165
|
return client.get_device_config(get_connection_target_from_driver(driver))
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
def get_device_test_data_for_driver(
|
|
169
|
-
driver:
|
|
169
|
+
driver: WebDriver,
|
|
170
170
|
*,
|
|
171
171
|
gridfleet_client: GridFleetClient | None = None,
|
|
172
|
-
) ->
|
|
172
|
+
) -> JsonObject:
|
|
173
173
|
"""Fetch operator-attached test_data for a live Appium driver session."""
|
|
174
174
|
client = gridfleet_client or GridFleetClient()
|
|
175
175
|
connection_target = get_connection_target_from_driver(driver)
|