testmu-playwright-python 0.1.0__tar.gz → 0.1.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.
- {testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info → testmu_playwright_python-0.1.2}/PKG-INFO +14 -21
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/README.md +6 -8
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/pyproject.toml +10 -19
- testmu_playwright_python-0.1.2/testmu/_config.py +13 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_heal_patch.py +13 -3
- testmu_playwright_python-0.1.2/testmu/_helpers/_http.py +20 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/assertion.py +9 -4
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_api.py +7 -7
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_db.py +8 -7
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/kane_cli.py +12 -8
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/network.py +10 -8
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/smartui.py +9 -2
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/vision.py +10 -8
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/wait.py +5 -3
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/__init__.py +9 -6
- testmu_playwright_python-0.1.2/testmu/_reporter/local.py +33 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/lt.py +17 -13
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_session.py +50 -7
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_vars.py +14 -14
- testmu_playwright_python-0.1.2/testmu/playwright_async/__init__.py +67 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2/testmu_playwright_python.egg-info}/PKG-INFO +14 -21
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/SOURCES.txt +20 -3
- testmu_playwright_python-0.1.2/tests/test_capability.py +423 -0
- testmu_playwright_python-0.1.2/tests/test_config.py +65 -0
- testmu_playwright_python-0.1.2/tests/test_configure.py +73 -0
- testmu_playwright_python-0.1.2/tests/test_decorator.py +58 -0
- testmu_playwright_python-0.1.2/tests/test_heal_patch.py +97 -0
- testmu_playwright_python-0.1.2/tests/test_helper_assertion.py +361 -0
- testmu_playwright_python-0.1.2/tests/test_helper_execute_api.py +307 -0
- testmu_playwright_python-0.1.2/tests/test_helper_execute_db.py +269 -0
- testmu_playwright_python-0.1.2/tests/test_helper_execute_js.py +157 -0
- testmu_playwright_python-0.1.2/tests/test_helper_network_math.py +215 -0
- testmu_playwright_python-0.1.2/tests/test_helper_remaining.py +438 -0
- testmu_playwright_python-0.1.2/tests/test_helper_tabs_drag.py +150 -0
- testmu_playwright_python-0.1.2/tests/test_helper_vision.py +476 -0
- testmu_playwright_python-0.1.2/tests/test_helpers_stub.py +39 -0
- testmu_playwright_python-0.1.2/tests/test_integration.py +34 -0
- testmu_playwright_python-0.1.2/tests/test_step.py +54 -0
- testmu_playwright_python-0.1.2/tests/test_test_config.py +85 -0
- testmu_playwright_python-0.1.2/tests/test_vars.py +474 -0
- testmu_playwright_python-0.1.0/LICENSE +0 -21
- testmu_playwright_python-0.1.0/MANIFEST.in +0 -8
- testmu_playwright_python-0.1.0/testmu/_config.py +0 -9
- testmu_playwright_python-0.1.0/testmu/_reporter/local.py +0 -25
- testmu_playwright_python-0.1.0/testmu/playwright_async/__init__.py +0 -36
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/setup.cfg +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/__init__.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_capability.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_configure.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_decorator.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_errors.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/__init__.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/drag.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_js.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/math.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/tabs.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_matchers.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/null.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_step.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_test_config.py +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/dependency_links.txt +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/requires.txt +0 -0
- {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/top_level.txt +0 -0
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testmu-playwright-python
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
5
|
-
Author-email:
|
|
6
|
-
License-Expression:
|
|
7
|
-
Project-URL: Homepage, https://testmu
|
|
8
|
-
Project-URL:
|
|
9
|
-
Project-URL: Repository, https://github.com/testmuai/testmu-bindings
|
|
10
|
-
Keywords: testing,ai,agents,playwright,lambdatest,testmu
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Testmu binding for Playwright Python — thin test runtime for LambdaTest exports
|
|
5
|
+
Author-email: LambdaTest <engineering@lambdatest.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/shravan-lambdatest/testmu-bindings
|
|
8
|
+
Project-URL: Repository, https://github.com/shravan-lambdatest/testmu-bindings
|
|
11
9
|
Classifier: Development Status :: 3 - Alpha
|
|
12
10
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Natural Language :: English
|
|
14
|
-
Classifier: Topic :: Software Development :: Testing
|
|
15
|
-
Classifier: Framework :: Pytest
|
|
16
11
|
Classifier: Programming Language :: Python :: 3
|
|
17
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Framework :: Pytest
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
17
|
Requires-Python: >=3.11
|
|
21
18
|
Description-Content-Type: text/markdown
|
|
22
|
-
License-File: LICENSE
|
|
23
19
|
Requires-Dist: playwright>=1.57.0
|
|
24
20
|
Requires-Dist: pyotp>=2.9.0
|
|
25
21
|
Requires-Dist: aiohttp
|
|
@@ -28,16 +24,18 @@ Requires-Dist: requests>=2.28.0
|
|
|
28
24
|
Provides-Extra: dev
|
|
29
25
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
26
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
31
|
-
Dynamic: license-file
|
|
32
27
|
|
|
33
28
|
# testmu-playwright-python
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
Testmu binding for Playwright Python — a thin test runtime for running LambdaTest exported tests locally or on the LambdaTest grid.
|
|
36
31
|
|
|
37
32
|
## Installation
|
|
38
33
|
|
|
39
34
|
```bash
|
|
40
|
-
|
|
35
|
+
# From TestPyPI (pre-release)
|
|
36
|
+
pip install --index-url https://test.pypi.org/simple/ \
|
|
37
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
38
|
+
testmu-playwright-python
|
|
41
39
|
```
|
|
42
40
|
|
|
43
41
|
## Quick Start
|
|
@@ -75,11 +73,6 @@ testmu.run(my_test)
|
|
|
75
73
|
- Python >= 3.11
|
|
76
74
|
- Playwright >= 1.57.0
|
|
77
75
|
|
|
78
|
-
## Links
|
|
79
|
-
|
|
80
|
-
- Homepage: [testmu.ai](https://testmu.ai)
|
|
81
|
-
- Documentation: [docs.testmu.ai](https://docs.testmu.ai)
|
|
82
|
-
|
|
83
76
|
## License
|
|
84
77
|
|
|
85
|
-
|
|
78
|
+
Proprietary - LambdaTest
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# testmu-playwright-python
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Testmu binding for Playwright Python — a thin test runtime for running LambdaTest exported tests locally or on the LambdaTest grid.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
# From TestPyPI (pre-release)
|
|
9
|
+
pip install --index-url https://test.pypi.org/simple/ \
|
|
10
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
11
|
+
testmu-playwright-python
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
## Quick Start
|
|
@@ -43,11 +46,6 @@ testmu.run(my_test)
|
|
|
43
46
|
- Python >= 3.11
|
|
44
47
|
- Playwright >= 1.57.0
|
|
45
48
|
|
|
46
|
-
## Links
|
|
47
|
-
|
|
48
|
-
- Homepage: [testmu.ai](https://testmu.ai)
|
|
49
|
-
- Documentation: [docs.testmu.ai](https://docs.testmu.ai)
|
|
50
|
-
|
|
51
49
|
## License
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
Proprietary - LambdaTest
|
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=68.0"
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "testmu-playwright-python"
|
|
7
|
-
version = "0.1.
|
|
8
|
-
description = "
|
|
9
|
-
readme = "README.md"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Testmu binding for Playwright Python — thin test runtime for LambdaTest exports"
|
|
10
9
|
requires-python = ">=3.11"
|
|
11
|
-
license = "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{name = "TestMu AI", email = "engineering@testmu.ai"},
|
|
15
|
-
]
|
|
16
|
-
keywords = ["testing", "ai", "agents", "playwright", "lambdatest", "testmu"]
|
|
10
|
+
license = "LicenseRef-Proprietary"
|
|
11
|
+
authors = [{name = "LambdaTest", email = "engineering@lambdatest.com"}]
|
|
12
|
+
readme = "README.md"
|
|
17
13
|
classifiers = [
|
|
18
14
|
"Development Status :: 3 - Alpha",
|
|
19
15
|
"Intended Audience :: Developers",
|
|
20
|
-
"Natural Language :: English",
|
|
21
|
-
"Topic :: Software Development :: Testing",
|
|
22
|
-
"Framework :: Pytest",
|
|
23
16
|
"Programming Language :: Python :: 3",
|
|
24
17
|
"Programming Language :: Python :: 3.11",
|
|
25
18
|
"Programming Language :: Python :: 3.12",
|
|
26
19
|
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Framework :: Pytest",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
27
22
|
]
|
|
28
23
|
dependencies = [
|
|
29
24
|
"playwright>=1.57.0",
|
|
@@ -40,12 +35,8 @@ dev = [
|
|
|
40
35
|
]
|
|
41
36
|
|
|
42
37
|
[project.urls]
|
|
43
|
-
Homepage = "https://testmu
|
|
44
|
-
|
|
45
|
-
Repository = "https://github.com/testmuai/testmu-bindings"
|
|
46
|
-
|
|
47
|
-
[tool.setuptools.packages.find]
|
|
48
|
-
include = ["testmu*"]
|
|
38
|
+
Homepage = "https://github.com/shravan-lambdatest/testmu-bindings"
|
|
39
|
+
Repository = "https://github.com/shravan-lambdatest/testmu-bindings"
|
|
49
40
|
|
|
50
41
|
[tool.pytest.ini_options]
|
|
51
42
|
asyncio_mode = "auto"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Run target, auth, and smart detection.
|
|
2
|
+
|
|
3
|
+
Three orthogonal flags:
|
|
4
|
+
- run_target: "local" (default) or "cloud". Only "cloud" connects to LT CDP.
|
|
5
|
+
- lt_auth: LT creds available. Enables ATMS/automind/downloads/API proxy
|
|
6
|
+
regardless of run target.
|
|
7
|
+
- smart: AI/heal features. Requires lt_auth; validated at testmu.run().
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
run_target = os.getenv("TESTMU_RUN_TARGET", "local").lower()
|
|
12
|
+
lt_auth = bool(os.getenv("LT_USERNAME")) and bool(os.getenv("LT_ACCESS_KEY"))
|
|
13
|
+
smart = os.getenv("TESTMU_SMART", "0") == "1"
|
|
@@ -5,6 +5,16 @@ Installs wrappers on Locator action methods that catch TimeoutError
|
|
|
5
5
|
|
|
6
6
|
The patch only fires inside a testmu.step() block (ContextVar check).
|
|
7
7
|
Outside a step, the wrapper is a pure passthrough.
|
|
8
|
+
|
|
9
|
+
heal_fn contract:
|
|
10
|
+
async def heal_fn(page, description, method_name, *args, **kwargs) -> bool
|
|
11
|
+
|
|
12
|
+
Returns True if heal handled the action (wrapper returns normally).
|
|
13
|
+
Returns False if heal couldn't help (wrapper re-raises original error).
|
|
14
|
+
|
|
15
|
+
Today's default heal does a coordinate lookup via vision API and clicks
|
|
16
|
+
at the returned coordinates for click/hover methods. Other methods are
|
|
17
|
+
not supported until locator re-resolution lands.
|
|
8
18
|
"""
|
|
9
19
|
import functools
|
|
10
20
|
from testmu._step import _current_step
|
|
@@ -41,9 +51,9 @@ def _make_wrapper(name, original, heal_fn):
|
|
|
41
51
|
except TimeoutError:
|
|
42
52
|
if await self.count() > 0:
|
|
43
53
|
raise # element exists but not actionable — heal won't help
|
|
44
|
-
|
|
45
|
-
if
|
|
54
|
+
handled = await heal_fn(self.page, step.description, name, *args, **kwargs)
|
|
55
|
+
if not handled:
|
|
46
56
|
raise
|
|
47
|
-
|
|
57
|
+
# heal performed the action (e.g. via page.mouse) — nothing to return
|
|
48
58
|
|
|
49
59
|
return wrapper
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Shared HTTP session factory for AI API calls.
|
|
2
|
+
|
|
3
|
+
All smart helpers (vision, assertion, wait, network) call LT-hosted
|
|
4
|
+
endpoints that require Basic auth via LT_USERNAME + LT_ACCESS_KEY.
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_session(**kwargs) -> aiohttp.ClientSession:
|
|
13
|
+
"""Create an aiohttp session with LT Basic auth headers."""
|
|
14
|
+
headers = kwargs.pop("headers", {})
|
|
15
|
+
username = os.getenv("LT_USERNAME", "")
|
|
16
|
+
access_key = os.getenv("LT_ACCESS_KEY", "")
|
|
17
|
+
if username and access_key:
|
|
18
|
+
auth = base64.b64encode(f"{username}:{access_key}".encode()).decode()
|
|
19
|
+
headers["Authorization"] = f"Basic {auth}"
|
|
20
|
+
return aiohttp.ClientSession(headers=headers, **kwargs)
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/assertion.py
RENAMED
|
@@ -15,9 +15,14 @@ there are no store_keys (skips the visual fallback).
|
|
|
15
15
|
"""
|
|
16
16
|
import base64
|
|
17
17
|
import logging
|
|
18
|
+
import os
|
|
18
19
|
|
|
19
20
|
import aiohttp
|
|
20
21
|
|
|
22
|
+
from testmu._helpers._http import create_session
|
|
23
|
+
|
|
24
|
+
_AI_API_HOST = os.getenv("TESTMU_AI_API_HOST", "http://localhost:8000")
|
|
25
|
+
|
|
21
26
|
from testmu import _config
|
|
22
27
|
from testmu._vars import _variable_store, var
|
|
23
28
|
|
|
@@ -68,9 +73,9 @@ async def _call_evaluate_api(claim, composite_op, eval_sub_checks):
|
|
|
68
73
|
"composite_operator": composite_op,
|
|
69
74
|
"sub_checks": eval_sub_checks,
|
|
70
75
|
}
|
|
71
|
-
async with
|
|
76
|
+
async with create_session() as session:
|
|
72
77
|
async with session.post(
|
|
73
|
-
"
|
|
78
|
+
f"{_AI_API_HOST}/api/v1/evaluate",
|
|
74
79
|
json=request_body,
|
|
75
80
|
timeout=aiohttp.ClientTimeout(total=60),
|
|
76
81
|
) as response:
|
|
@@ -111,9 +116,9 @@ async def _verify_visual(page, claim, composite_op, sub_checks, assertion_tree):
|
|
|
111
116
|
request_body["verification"] = verification
|
|
112
117
|
|
|
113
118
|
_log.debug("[AssertionAPI] Sending to assertions/verify API...")
|
|
114
|
-
async with
|
|
119
|
+
async with create_session() as session:
|
|
115
120
|
async with session.post(
|
|
116
|
-
"
|
|
121
|
+
f"{_AI_API_HOST}/api/v1/assertions/verify",
|
|
117
122
|
json=request_body,
|
|
118
123
|
timeout=aiohttp.ClientTimeout(total=60),
|
|
119
124
|
) as response:
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_api.py
RENAMED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
Variable resolution: {{var}} templates in URL, headers, body, and params are
|
|
4
4
|
resolved via testmu._vars.var() before the request is made.
|
|
5
5
|
|
|
6
|
-
Proxy routing: when running on HyperExecute (
|
|
7
|
-
requests through the local proxy at 127.0.0.1:22000.
|
|
8
|
-
direct requests.
|
|
6
|
+
Proxy routing: when running on HyperExecute (run_target == "cloud"),
|
|
7
|
+
routes all requests through the local proxy at 127.0.0.1:22000. On local
|
|
8
|
+
run target, makes direct requests.
|
|
9
9
|
|
|
10
10
|
Raises RuntimeError on request failure (network error or bad/unsupported input).
|
|
11
11
|
Returns the response dict directly on success.
|
|
@@ -166,8 +166,8 @@ def _do_request(method, url, headers, body, params, authorization, timeout, veri
|
|
|
166
166
|
_log.debug(f"AWS Signature auth failed: {e}")
|
|
167
167
|
|
|
168
168
|
# -- Proxy ---------------------------------------------------------------
|
|
169
|
-
# Route through HyperExecute local proxy when on
|
|
170
|
-
proxy = "http://127.0.0.1:22000" if _config.
|
|
169
|
+
# Route through HyperExecute local proxy only when running on cloud target.
|
|
170
|
+
proxy = "http://127.0.0.1:22000" if _config.run_target == "cloud" else None
|
|
171
171
|
|
|
172
172
|
# -- Build request kwargs ------------------------------------------------
|
|
173
173
|
_method = method.upper()
|
|
@@ -281,8 +281,8 @@ async def execute_api(
|
|
|
281
281
|
):
|
|
282
282
|
"""Execute an HTTP API request.
|
|
283
283
|
|
|
284
|
-
Routes through the HyperExecute proxy (127.0.0.1:22000) when
|
|
285
|
-
|
|
284
|
+
Routes through the HyperExecute proxy (127.0.0.1:22000) when
|
|
285
|
+
run_target == "cloud"; makes direct requests on local runs.
|
|
286
286
|
|
|
287
287
|
Variable templates ({{var}}) in url, headers, body, and params are resolved
|
|
288
288
|
via testmu._vars.var() before the request is made.
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_db.py
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""execute_db helper — runs a SQL query via the automind /db-query endpoint.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Auth-gated: requires LT_USERNAME + LT_ACCESS_KEY to be set (automind is an
|
|
4
|
+
LT-hosted service). Works in both local and cloud run targets as long as
|
|
5
|
+
creds are present. Raises RuntimeError when creds are missing.
|
|
5
6
|
|
|
6
7
|
Env vars consumed (when not passed explicitly):
|
|
7
8
|
LT_USERNAME — LambdaTest username (used to build auth header)
|
|
@@ -10,7 +11,7 @@ Env vars consumed (when not passed explicitly):
|
|
|
10
11
|
LT_PROXY_TUNNEL_ID — Tunnel ID for network connectivity
|
|
11
12
|
|
|
12
13
|
Raises:
|
|
13
|
-
RuntimeError: When
|
|
14
|
+
RuntimeError: When LT creds are missing, on non-200 response, or on network failure.
|
|
14
15
|
|
|
15
16
|
Returns:
|
|
16
17
|
Query result dict on success.
|
|
@@ -71,7 +72,7 @@ async def execute_db(
|
|
|
71
72
|
):
|
|
72
73
|
"""Execute a database query via the automind /db-query endpoint.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
Auth-gated: raises RuntimeError when LT_USERNAME/LT_ACCESS_KEY are not set.
|
|
75
76
|
|
|
76
77
|
Args:
|
|
77
78
|
query: Base64-encoded SQL query string.
|
|
@@ -86,13 +87,13 @@ async def execute_db(
|
|
|
86
87
|
Query result dict on success.
|
|
87
88
|
|
|
88
89
|
Raises:
|
|
89
|
-
RuntimeError: When
|
|
90
|
+
RuntimeError: When LT creds are missing, on non-200 response,
|
|
90
91
|
or on network/query failure.
|
|
91
92
|
"""
|
|
92
93
|
from testmu import _config
|
|
93
94
|
|
|
94
|
-
if not _config.
|
|
95
|
-
raise RuntimeError("DB query requires
|
|
95
|
+
if not _config.lt_auth:
|
|
96
|
+
raise RuntimeError("DB query requires LT_USERNAME and LT_ACCESS_KEY")
|
|
96
97
|
|
|
97
98
|
# Fall back to environment variables
|
|
98
99
|
if not automind_url:
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/kane_cli.py
RENAMED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"""Kane CLI helper — execute_kane_cli.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
When smart is OFF: logs "branch unresolved", returns None.
|
|
3
|
+
Gated: smart AND run_target == "cloud". kane-cli needs the HyperExecute
|
|
4
|
+
relay proxy to connect back to the running test, which is only available
|
|
5
|
+
in cloud runs. On local or smart-off runs, logs a warning and returns None.
|
|
7
6
|
|
|
8
7
|
Note: relay proxy disconnect/reconnect is NOT ported here — that's a
|
|
9
8
|
deferred gap. The subprocess runs kane-cli directly; if relay proxy is
|
|
10
|
-
absent the test may fail when kane-cli tries to connect
|
|
11
|
-
until relay proxy support is ported.
|
|
9
|
+
absent the test may fail when kane-cli tries to connect.
|
|
12
10
|
"""
|
|
13
11
|
import asyncio
|
|
14
12
|
import logging
|
|
@@ -22,8 +20,8 @@ _log = logging.getLogger("testmu")
|
|
|
22
20
|
async def execute_kane_cli(objective: str):
|
|
23
21
|
"""Execute a test objective via kane-cli subprocess.
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
Gated: smart ON AND run_target == "cloud". Logs a warning and returns
|
|
24
|
+
None without running kane-cli otherwise (branch unresolved).
|
|
27
25
|
|
|
28
26
|
Uses env vars:
|
|
29
27
|
LT_USERNAME — LambdaTest username (required by kane-cli auth)
|
|
@@ -42,6 +40,12 @@ async def execute_kane_cli(objective: str):
|
|
|
42
40
|
"[execute_kane_cli] smart is OFF — branch unresolved, skipping kane-cli"
|
|
43
41
|
)
|
|
44
42
|
return None
|
|
43
|
+
if _config.run_target != "cloud":
|
|
44
|
+
_log.warning(
|
|
45
|
+
"[execute_kane_cli] run_target is not 'cloud' — kane-cli needs the "
|
|
46
|
+
"HyperExecute relay proxy, skipping (branch unresolved)"
|
|
47
|
+
)
|
|
48
|
+
return None
|
|
45
49
|
|
|
46
50
|
username = os.getenv("LT_USERNAME", "")
|
|
47
51
|
access_key = os.getenv("LT_ACCESS_KEY", "")
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/network.py
RENAMED
|
@@ -104,13 +104,14 @@ def evaluate_network_assertion(assertion_tree: dict) -> bool:
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
# ---------------------------------------------------------------------------
|
|
107
|
-
# network_query —
|
|
107
|
+
# network_query — cloud-only, polls HAR server at localhost:8181
|
|
108
108
|
# ---------------------------------------------------------------------------
|
|
109
109
|
|
|
110
110
|
async def network_query(method, url, index, network_log_id="", polling_interval=2, max_polling_time=10):
|
|
111
111
|
"""Poll the HAR server for a matching network entry.
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Cloud-only: the HAR server at localhost:8181 is a HyperExecute sidecar.
|
|
114
|
+
Returns None when run_target is not "cloud".
|
|
114
115
|
|
|
115
116
|
Args:
|
|
116
117
|
method: HTTP method to match (e.g. "GET", "POST").
|
|
@@ -121,16 +122,17 @@ async def network_query(method, url, index, network_log_id="", polling_interval=
|
|
|
121
122
|
max_polling_time: Total seconds before giving up.
|
|
122
123
|
|
|
123
124
|
Returns:
|
|
124
|
-
Decoded HAR entry dict, or None when
|
|
125
|
+
Decoded HAR entry dict, or None when not on cloud / no match found.
|
|
125
126
|
"""
|
|
126
127
|
from testmu import _config
|
|
127
128
|
|
|
128
|
-
if
|
|
129
|
-
_log.info("[network_query]
|
|
129
|
+
if _config.run_target != "cloud":
|
|
130
|
+
_log.info("[network_query] run_target is not 'cloud' — HAR server unavailable, skipping")
|
|
130
131
|
return None
|
|
131
132
|
|
|
132
133
|
import asyncio
|
|
133
134
|
import aiohttp
|
|
135
|
+
from testmu._helpers._http import create_session
|
|
134
136
|
|
|
135
137
|
_har_base = "http://127.0.0.1:8181"
|
|
136
138
|
|
|
@@ -141,7 +143,7 @@ async def network_query(method, url, index, network_log_id="", polling_interval=
|
|
|
141
143
|
|
|
142
144
|
if network_log_id:
|
|
143
145
|
try:
|
|
144
|
-
async with
|
|
146
|
+
async with create_session() as session:
|
|
145
147
|
async with session.get(
|
|
146
148
|
f"{_har_base}/logs/entry?id={network_log_id}",
|
|
147
149
|
timeout=aiohttp.ClientTimeout(total=30),
|
|
@@ -162,7 +164,7 @@ async def network_query(method, url, index, network_log_id="", polling_interval=
|
|
|
162
164
|
num_tries += 1
|
|
163
165
|
_log.info(f"[network_query] polling attempt {num_tries}/{max_tries}")
|
|
164
166
|
try:
|
|
165
|
-
async with
|
|
167
|
+
async with create_session() as session:
|
|
166
168
|
async with session.get(
|
|
167
169
|
f"{_har_base}/logs",
|
|
168
170
|
timeout=aiohttp.ClientTimeout(total=30),
|
|
@@ -187,7 +189,7 @@ async def network_query(method, url, index, network_log_id="", polling_interval=
|
|
|
187
189
|
f" entry_id={entry_id!r}"
|
|
188
190
|
)
|
|
189
191
|
if entry_id:
|
|
190
|
-
async with
|
|
192
|
+
async with create_session() as s2:
|
|
191
193
|
async with s2.get(
|
|
192
194
|
f"{_har_base}/logs/entry?id={entry_id}",
|
|
193
195
|
timeout=aiohttp.ClientTimeout(total=30),
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/smartui.py
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""SmartUI visual comparison screenshot.
|
|
2
2
|
|
|
3
3
|
Captures a screenshot and sends it to LambdaTest's SmartUI via the
|
|
4
|
-
lambdatest_action CDP protocol
|
|
4
|
+
lambdatest_action CDP protocol — cloud only (CDP channel is intercepted
|
|
5
|
+
server-side by LT infrastructure).
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Gated: smart ON AND run_target == "cloud". Warns and no-ops otherwise.
|
|
7
8
|
"""
|
|
8
9
|
import json
|
|
9
10
|
import logging
|
|
@@ -22,6 +23,12 @@ async def smartui_snapshot(page, name):
|
|
|
22
23
|
"""
|
|
23
24
|
if not _config.smart:
|
|
24
25
|
return None
|
|
26
|
+
if _config.run_target != "cloud":
|
|
27
|
+
_log.warning(
|
|
28
|
+
f"SmartUI snapshot '{name}' skipped — requires cloud run target "
|
|
29
|
+
f"(TESTMU_RUN_TARGET=cloud)"
|
|
30
|
+
)
|
|
31
|
+
return None
|
|
25
32
|
|
|
26
33
|
try:
|
|
27
34
|
payload = json.dumps({
|
|
@@ -5,7 +5,7 @@ safe fallbacks immediately without touching any network endpoint.
|
|
|
5
5
|
|
|
6
6
|
When smart is ON, they call the analyzer sidecar via aiohttp.
|
|
7
7
|
|
|
8
|
-
Analyzer base URL is read from
|
|
8
|
+
Analyzer base URL is read from TESTMU_AI_API_HOST env var
|
|
9
9
|
(default: http://localhost:8000).
|
|
10
10
|
"""
|
|
11
11
|
import asyncio
|
|
@@ -17,10 +17,12 @@ import os
|
|
|
17
17
|
import aiohttp
|
|
18
18
|
|
|
19
19
|
from testmu import _config
|
|
20
|
+
from testmu._helpers._http import create_session
|
|
20
21
|
|
|
21
22
|
_log = logging.getLogger("testmu")
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
# TODO: replace default with public LT vision/analyzer endpoint once available.
|
|
25
|
+
_VISION_API_HOST = os.getenv("TESTMU_AI_API_HOST", "http://localhost:8000")
|
|
24
26
|
_ANALYZER_URL = f"{_VISION_API_HOST}/api/v1/analyzer"
|
|
25
27
|
_ANALYZER_TIMEOUT = aiohttp.ClientTimeout(total=120)
|
|
26
28
|
_QUERY_TIMEOUT = aiohttp.ClientTimeout(total=60)
|
|
@@ -204,7 +206,7 @@ async def _check_visibility(session: aiohttp.ClientSession, screenshot_b64: str,
|
|
|
204
206
|
async def _wait_for_visibility(page, query: str) -> str:
|
|
205
207
|
"""Poll visibility API until element is visible; return the last screenshot_b64."""
|
|
206
208
|
last_reasoning = None
|
|
207
|
-
async with
|
|
209
|
+
async with create_session() as session:
|
|
208
210
|
for attempt in range(1, _COORD_MAX_RETRIES + 1):
|
|
209
211
|
screenshot_bytes = await page.screenshot()
|
|
210
212
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
@@ -254,7 +256,7 @@ async def vision_query(page, description: str, return_type: str):
|
|
|
254
256
|
screenshot_bytes = await page.screenshot()
|
|
255
257
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
256
258
|
|
|
257
|
-
async with
|
|
259
|
+
async with create_session() as session:
|
|
258
260
|
result = await _post_analyzer(session, {
|
|
259
261
|
"type": "visual",
|
|
260
262
|
"query": description,
|
|
@@ -297,7 +299,7 @@ async def textual_query(page, description: str, return_type: str):
|
|
|
297
299
|
screenshot_bytes = await page.screenshot()
|
|
298
300
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
299
301
|
|
|
300
|
-
async with
|
|
302
|
+
async with create_session() as session:
|
|
301
303
|
identify_resp = await _post_analyzer(session, {
|
|
302
304
|
"type": "dom",
|
|
303
305
|
"query": description,
|
|
@@ -348,7 +350,7 @@ async def vision_wait(page, description: str, timeout_ms: int = 30000):
|
|
|
348
350
|
|
|
349
351
|
_log.debug(f"[vision_wait] Waiting for: '{description}' (timeout_ms={timeout_ms})")
|
|
350
352
|
|
|
351
|
-
async with
|
|
353
|
+
async with create_session() as session:
|
|
352
354
|
for attempt in range(1, _WAIT_MAX_RETRIES + 1):
|
|
353
355
|
screenshot_bytes = await page.screenshot()
|
|
354
356
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
@@ -397,7 +399,7 @@ async def vision_action(page, description: str, action_type: str, direction: str
|
|
|
397
399
|
width = viewport["width"] if viewport else 1280
|
|
398
400
|
height = viewport["height"] if viewport else 713
|
|
399
401
|
|
|
400
|
-
async with
|
|
402
|
+
async with create_session() as session:
|
|
401
403
|
async with session.post(
|
|
402
404
|
f"{_VISION_API_HOST}/api/v1/vision/coordinates",
|
|
403
405
|
json={
|
|
@@ -464,7 +466,7 @@ async def get_vision_coordinates(page, description: str, action_type: str = "cli
|
|
|
464
466
|
width = viewport["width"] if viewport else 1920
|
|
465
467
|
height = viewport["height"] if viewport else 1080
|
|
466
468
|
|
|
467
|
-
async with
|
|
469
|
+
async with create_session() as session:
|
|
468
470
|
async with session.post(
|
|
469
471
|
f"{_VISION_API_HOST}/api/v1/vision/coordinates",
|
|
470
472
|
json={
|
|
@@ -8,11 +8,13 @@ import logging
|
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
10
|
|
|
11
|
+
from testmu._helpers._http import create_session
|
|
12
|
+
|
|
11
13
|
from testmu import _config
|
|
12
14
|
|
|
13
15
|
_log = logging.getLogger("testmu")
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
_AI_API_HOST_DEFAULT = "http://localhost:8000"
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
async def check_until_condition(page, condition: str) -> bool:
|
|
@@ -37,14 +39,14 @@ async def check_until_condition(page, condition: str) -> bool:
|
|
|
37
39
|
_log.info("[check_until_condition] smart is OFF — returning False (condition skipped)")
|
|
38
40
|
return False
|
|
39
41
|
|
|
40
|
-
vision_api_host = os.getenv("
|
|
42
|
+
vision_api_host = os.getenv("TESTMU_AI_API_HOST", _AI_API_HOST_DEFAULT)
|
|
41
43
|
|
|
42
44
|
_log.info(f"[check_until_condition] checking condition: {condition!r}")
|
|
43
45
|
|
|
44
46
|
screenshot_bytes = await page.screenshot()
|
|
45
47
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
46
48
|
|
|
47
|
-
async with
|
|
49
|
+
async with create_session() as session:
|
|
48
50
|
async with session.post(
|
|
49
51
|
f"{vision_api_host}/api/v1/wait/check",
|
|
50
52
|
json={"screenshot_b64": screenshot_b64, "query": condition},
|
{testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/__init__.py
RENAMED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Reporter protocol and factory.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
|
|
3
|
+
Two implementations:
|
|
4
|
+
- LocalReporter: logs to stdout (used for local runs)
|
|
5
|
+
- LTReporter: lambdatest_action CDP evaluate for dashboard (cloud only)
|
|
6
|
+
|
|
7
|
+
Picking rule: LTReporter only when run_target == "cloud". The LT reporter
|
|
8
|
+
talks via the lambdatest_action CDP channel which is cloud-only, so a local
|
|
9
|
+
browser always uses LocalReporter even when LT creds are present.
|
|
7
10
|
"""
|
|
8
11
|
from typing import Optional, Protocol
|
|
9
12
|
|
|
@@ -20,8 +23,8 @@ class Reporter(Protocol):
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def get_reporter() -> Reporter:
|
|
23
|
-
"""Factory: pick reporter based on
|
|
24
|
-
if _config.
|
|
26
|
+
"""Factory: pick reporter based on run target."""
|
|
27
|
+
if _config.run_target == "cloud":
|
|
25
28
|
from testmu._reporter.lt import LTReporter
|
|
26
29
|
return LTReporter()
|
|
27
30
|
from testmu._reporter.local import LocalReporter
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Local stdout reporter."""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
_log = logging.getLogger("testmu")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocalReporter:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self._step_num = 0
|
|
10
|
+
|
|
11
|
+
async def begin_test(self, name):
|
|
12
|
+
self._step_num = 0
|
|
13
|
+
_log.info("[TEST START] %s", name)
|
|
14
|
+
|
|
15
|
+
async def pass_test(self):
|
|
16
|
+
_log.info("[TEST PASS] (%d steps)", self._step_num)
|
|
17
|
+
|
|
18
|
+
async def fail_test(self, error):
|
|
19
|
+
_log.error("[TEST FAIL] at step %d — %s", self._step_num, error)
|
|
20
|
+
|
|
21
|
+
async def begin_step(self, description):
|
|
22
|
+
self._step_num += 1
|
|
23
|
+
_log.info(" [STEP %d] %s", self._step_num, description)
|
|
24
|
+
|
|
25
|
+
async def end_step(self, description, ok, error=None):
|
|
26
|
+
if ok:
|
|
27
|
+
_log.info(" [STEP %d OK] %s", self._step_num, description)
|
|
28
|
+
else:
|
|
29
|
+
status = f"FAIL: {error}"
|
|
30
|
+
_log.info(" [STEP %d %s] %s", self._step_num, status, description)
|
|
31
|
+
|
|
32
|
+
async def attach_screenshot(self, data):
|
|
33
|
+
_log.info(" [SCREENSHOT] %d bytes", len(data))
|