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.
Files changed (63) hide show
  1. {testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info → testmu_playwright_python-0.1.2}/PKG-INFO +14 -21
  2. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/README.md +6 -8
  3. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/pyproject.toml +10 -19
  4. testmu_playwright_python-0.1.2/testmu/_config.py +13 -0
  5. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_heal_patch.py +13 -3
  6. testmu_playwright_python-0.1.2/testmu/_helpers/_http.py +20 -0
  7. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/assertion.py +9 -4
  8. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_api.py +7 -7
  9. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_db.py +8 -7
  10. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/kane_cli.py +12 -8
  11. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/network.py +10 -8
  12. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/smartui.py +9 -2
  13. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/vision.py +10 -8
  14. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/wait.py +5 -3
  15. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/__init__.py +9 -6
  16. testmu_playwright_python-0.1.2/testmu/_reporter/local.py +33 -0
  17. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/lt.py +17 -13
  18. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_session.py +50 -7
  19. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_vars.py +14 -14
  20. testmu_playwright_python-0.1.2/testmu/playwright_async/__init__.py +67 -0
  21. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2/testmu_playwright_python.egg-info}/PKG-INFO +14 -21
  22. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/SOURCES.txt +20 -3
  23. testmu_playwright_python-0.1.2/tests/test_capability.py +423 -0
  24. testmu_playwright_python-0.1.2/tests/test_config.py +65 -0
  25. testmu_playwright_python-0.1.2/tests/test_configure.py +73 -0
  26. testmu_playwright_python-0.1.2/tests/test_decorator.py +58 -0
  27. testmu_playwright_python-0.1.2/tests/test_heal_patch.py +97 -0
  28. testmu_playwright_python-0.1.2/tests/test_helper_assertion.py +361 -0
  29. testmu_playwright_python-0.1.2/tests/test_helper_execute_api.py +307 -0
  30. testmu_playwright_python-0.1.2/tests/test_helper_execute_db.py +269 -0
  31. testmu_playwright_python-0.1.2/tests/test_helper_execute_js.py +157 -0
  32. testmu_playwright_python-0.1.2/tests/test_helper_network_math.py +215 -0
  33. testmu_playwright_python-0.1.2/tests/test_helper_remaining.py +438 -0
  34. testmu_playwright_python-0.1.2/tests/test_helper_tabs_drag.py +150 -0
  35. testmu_playwright_python-0.1.2/tests/test_helper_vision.py +476 -0
  36. testmu_playwright_python-0.1.2/tests/test_helpers_stub.py +39 -0
  37. testmu_playwright_python-0.1.2/tests/test_integration.py +34 -0
  38. testmu_playwright_python-0.1.2/tests/test_step.py +54 -0
  39. testmu_playwright_python-0.1.2/tests/test_test_config.py +85 -0
  40. testmu_playwright_python-0.1.2/tests/test_vars.py +474 -0
  41. testmu_playwright_python-0.1.0/LICENSE +0 -21
  42. testmu_playwright_python-0.1.0/MANIFEST.in +0 -8
  43. testmu_playwright_python-0.1.0/testmu/_config.py +0 -9
  44. testmu_playwright_python-0.1.0/testmu/_reporter/local.py +0 -25
  45. testmu_playwright_python-0.1.0/testmu/playwright_async/__init__.py +0 -36
  46. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/setup.cfg +0 -0
  47. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/__init__.py +0 -0
  48. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_capability.py +0 -0
  49. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_configure.py +0 -0
  50. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_decorator.py +0 -0
  51. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_errors.py +0 -0
  52. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/__init__.py +0 -0
  53. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/drag.py +0 -0
  54. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/execute_js.py +0 -0
  55. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/math.py +0 -0
  56. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_helpers/tabs.py +0 -0
  57. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_matchers.py +0 -0
  58. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_reporter/null.py +0 -0
  59. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_step.py +0 -0
  60. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu/_test_config.py +0 -0
  61. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/dependency_links.txt +0 -0
  62. {testmu_playwright_python-0.1.0 → testmu_playwright_python-0.1.2}/testmu_playwright_python.egg-info/requires.txt +0 -0
  63. {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.0
4
- Summary: TestMu binding for Playwright Python — thin test runtime for TestMu/LambdaTest exports
5
- Author-email: TestMu AI <engineering@testmu.ai>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://testmu.ai
8
- Project-URL: Documentation, https://docs.testmu.ai
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
- TestMu binding for Playwright Python — a thin test runtime for running TestMu exported tests locally or on the LambdaTest grid.
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
- pip install testmu-playwright-python
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
- MIT see [LICENSE](LICENSE)
78
+ Proprietary - LambdaTest
@@ -1,11 +1,14 @@
1
1
  # testmu-playwright-python
2
2
 
3
- TestMu binding for Playwright Python — a thin test runtime for running TestMu exported tests locally or on the LambdaTest grid.
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
- pip install testmu-playwright-python
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
- MIT see [LICENSE](LICENSE)
51
+ Proprietary - LambdaTest
@@ -1,29 +1,24 @@
1
1
  [build-system]
2
- requires = ["setuptools>=68.0", "wheel"]
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.0"
8
- description = "TestMu binding for Playwright Python — thin test runtime for TestMu/LambdaTest exports"
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 = "MIT"
12
- license-files = ["LICENSE"]
13
- authors = [
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.ai"
44
- Documentation = "https://docs.testmu.ai"
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
- healed = await heal_fn(self.page, step.description)
45
- if healed is None:
54
+ handled = await heal_fn(self.page, step.description, name, *args, **kwargs)
55
+ if not handled:
46
56
  raise
47
- return await original(healed, *args, **kwargs)
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)
@@ -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 aiohttp.ClientSession() as session:
76
+ async with create_session() as session:
72
77
  async with session.post(
73
- "http://localhost:8000/api/v1/evaluate",
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 aiohttp.ClientSession() as session:
119
+ async with create_session() as session:
115
120
  async with session.post(
116
- "http://localhost:8000/api/v1/assertions/verify",
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:
@@ -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 (platform is ON), routes all
7
- requests through the local proxy at 127.0.0.1:22000. When local, makes
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 the platform.
170
- proxy = "http://127.0.0.1:22000" if _config.platform else None
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 running on
285
- the LT platform; makes direct requests otherwise.
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.
@@ -1,7 +1,8 @@
1
1
  """execute_db helper — runs a SQL query via the automind /db-query endpoint.
2
2
 
3
- Platform-gated: requires LT_USERNAME + LT_ACCESS_KEY to be set. When the
4
- platform is OFF, raises RuntimeError immediately.
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 platform is off, on non-200 response, or on network failure.
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
- Platform-gated: raises RuntimeError when LT credentials are not configured.
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 platform credentials are missing, on non-200 response,
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.platform:
95
- raise RuntimeError("DB query requires LT platform credentials")
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:
@@ -1,14 +1,12 @@
1
1
  """Kane CLI helper — execute_kane_cli.
2
2
 
3
- Smart AND platform gated: requires LT credentials for kane-cli auth and
4
- TESTMU_SMART=1 for the feature to be active.
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, which is expected
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
- Smart AND platform gated: when _config.smart is OFF, logs a warning
26
- and returns None without running kane-cli.
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", "")
@@ -104,13 +104,14 @@ def evaluate_network_assertion(assertion_tree: dict) -> bool:
104
104
 
105
105
 
106
106
  # ---------------------------------------------------------------------------
107
- # network_query — smart-gated, polls HAR server at localhost:8181
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
- Smart-gated: returns None immediately when _config.smart is OFF.
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 smart is OFF / no match found.
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 not _config.smart:
129
- _log.info("[network_query] smart is OFFskipping HAR lookup")
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as s2:
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),
@@ -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
- Smart-gated: no-op when TESTMU_SMART is OFF.
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 VISION_API_HOST env var
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
- _VISION_API_HOST = os.getenv("VISION_API_HOST", "http://localhost:8000")
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as session:
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 aiohttp.ClientSession() as session:
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
- _VISION_API_HOST_DEFAULT = "http://localhost:8000"
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("VISION_API_HOST", _VISION_API_HOST_DEFAULT)
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 aiohttp.ClientSession() as session:
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},
@@ -1,9 +1,12 @@
1
1
  """Reporter protocol and factory.
2
2
 
3
- Three implementations:
4
- - NullReporter: no-op (default when platform is off)
5
- - LocalReporter: print to stdout
6
- - LTReporter: lambdatest_action evaluate for dashboard
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 config."""
24
- if _config.platform:
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))