testmu-playwright-python 0.1.2__tar.gz → 0.1.4__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.2 → testmu_playwright_python-0.1.4}/PKG-INFO +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/pyproject.toml +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_decorator.py +10 -7
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/_http.py +14 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/assertion.py +4 -6
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_api.py +2 -2
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_db.py +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_js.py +2 -2
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/vision.py +49 -64
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/wait.py +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/__init__.py +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/local.py +6 -8
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/lt.py +19 -20
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/null.py +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/PKG-INFO +1 -1
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_decorator.py +4 -2
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_vision.py +9 -8
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/README.md +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/setup.cfg +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/__init__.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_capability.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_config.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_configure.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_errors.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_heal_patch.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/__init__.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/drag.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/kane_cli.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/math.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/network.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/smartui.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/tabs.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_matchers.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_session.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_step.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_test_config.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_vars.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/playwright_async/__init__.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/SOURCES.txt +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/dependency_links.txt +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/requires.txt +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/top_level.txt +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_capability.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_config.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_configure.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_heal_patch.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_assertion.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_api.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_db.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_js.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_network_math.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_remaining.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_tabs_drag.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helpers_stub.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_integration.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_step.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_test_config.py +0 -0
- {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_vars.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testmu-playwright-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Testmu binding for Playwright Python — thin test runtime for LambdaTest exports
|
|
5
5
|
Author-email: LambdaTest <engineering@lambdatest.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "testmu-playwright-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Testmu binding for Playwright Python — thin test runtime for LambdaTest exports"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
@@ -34,13 +34,16 @@ def test(fn):
|
|
|
34
34
|
await rep.pass_test()
|
|
35
35
|
return result
|
|
36
36
|
except Exception as e:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
# Screenshot-on-failure disabled — attach_screenshot is a stub
|
|
38
|
+
# that doesn't upload bytes anywhere. Re-enable once LT CDP upload
|
|
39
|
+
# is wired up.
|
|
40
|
+
# page = _find_page(args, kwargs)
|
|
41
|
+
# if page is not None:
|
|
42
|
+
# try:
|
|
43
|
+
# screenshot = await page.screenshot()
|
|
44
|
+
# await rep.attach_screenshot(screenshot)
|
|
45
|
+
# except Exception:
|
|
46
|
+
# pass
|
|
44
47
|
await rep.fail_test(e)
|
|
45
48
|
raise
|
|
46
49
|
return async_wrapper
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All smart helpers (vision, assertion, wait, network) call LT-hosted
|
|
4
4
|
endpoints that require Basic auth via LT_USERNAME + LT_ACCESS_KEY.
|
|
5
|
+
|
|
6
|
+
Also threads session tracking headers (x-session-id, x-source) so the
|
|
7
|
+
LT backend can distinguish local runs from HyperExecute runs. Forge
|
|
8
|
+
sets TESTMUAI_SOURCE=hyperexecute in all cloud YAML paths; locally we
|
|
9
|
+
default to "local".
|
|
5
10
|
"""
|
|
6
11
|
import base64
|
|
7
12
|
import os
|
|
@@ -10,11 +15,19 @@ import aiohttp
|
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
def create_session(**kwargs) -> aiohttp.ClientSession:
|
|
13
|
-
"""Create an aiohttp session with LT Basic auth headers."""
|
|
18
|
+
"""Create an aiohttp session with LT Basic auth + tracking headers."""
|
|
14
19
|
headers = kwargs.pop("headers", {})
|
|
20
|
+
|
|
15
21
|
username = os.getenv("LT_USERNAME", "")
|
|
16
22
|
access_key = os.getenv("LT_ACCESS_KEY", "")
|
|
17
23
|
if username and access_key:
|
|
18
24
|
auth = base64.b64encode(f"{username}:{access_key}".encode()).decode()
|
|
19
25
|
headers["Authorization"] = f"Basic {auth}"
|
|
26
|
+
|
|
27
|
+
session_id = os.getenv("TESTMUAI_SESSION_ID", "")
|
|
28
|
+
if session_id:
|
|
29
|
+
headers["x-session-id"] = session_id
|
|
30
|
+
|
|
31
|
+
headers["x-source"] = os.getenv("TESTMUAI_SOURCE", "local")
|
|
32
|
+
|
|
20
33
|
return aiohttp.ClientSession(headers=headers, **kwargs)
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/assertion.py
RENAMED
|
@@ -87,17 +87,16 @@ async def _call_evaluate_api(claim, composite_op, eval_sub_checks):
|
|
|
87
87
|
|
|
88
88
|
async def _evaluate_deterministic(claim, composite_op, sub_checks):
|
|
89
89
|
"""Deterministic path: resolve variables then call /api/v1/evaluate."""
|
|
90
|
-
_log.
|
|
90
|
+
_log.info(" [assertion] deterministic claim=%s", claim[:80])
|
|
91
91
|
eval_sub_checks = await _resolve_sub_checks(sub_checks)
|
|
92
|
-
_log.debug(f"[AssertionAPI] resolved sub_checks={eval_sub_checks!r}")
|
|
93
92
|
result = await _call_evaluate_api(claim, composite_op, eval_sub_checks)
|
|
94
|
-
_log.
|
|
93
|
+
_log.info(" [assertion] result status=%s", result.get("status", "unknown"))
|
|
95
94
|
return result
|
|
96
95
|
|
|
97
96
|
|
|
98
97
|
async def _verify_visual(page, claim, composite_op, sub_checks, assertion_tree):
|
|
99
98
|
"""Visual path: screenshot + LLM-based assertion via /api/v1/assertions/verify."""
|
|
100
|
-
_log.
|
|
99
|
+
_log.info(" [assertion] visual claim=%s", claim[:80])
|
|
101
100
|
|
|
102
101
|
screenshot_bytes = await page.screenshot()
|
|
103
102
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
@@ -115,7 +114,6 @@ async def _verify_visual(page, claim, composite_op, sub_checks, assertion_tree):
|
|
|
115
114
|
if verification:
|
|
116
115
|
request_body["verification"] = verification
|
|
117
116
|
|
|
118
|
-
_log.debug("[AssertionAPI] Sending to assertions/verify API...")
|
|
119
117
|
async with create_session() as session:
|
|
120
118
|
async with session.post(
|
|
121
119
|
f"{_AI_API_HOST}/api/v1/assertions/verify",
|
|
@@ -127,7 +125,7 @@ async def _verify_visual(page, claim, composite_op, sub_checks, assertion_tree):
|
|
|
127
125
|
raise Exception(f"Assertion API error {response.status}: {text}")
|
|
128
126
|
result = await response.json()
|
|
129
127
|
|
|
130
|
-
_log.
|
|
128
|
+
_log.info(" [assertion] result status=%s", result.get("status", "unknown"))
|
|
131
129
|
return result
|
|
132
130
|
|
|
133
131
|
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_api.py
RENAMED
|
@@ -223,7 +223,7 @@ def _do_request(method, url, headers, body, params, authorization, timeout, veri
|
|
|
223
223
|
raise RuntimeError(f"Unsupported HTTP method: {_method!r}")
|
|
224
224
|
|
|
225
225
|
elapsed_ms = (time.time() - start) * 1000
|
|
226
|
-
_log.
|
|
226
|
+
_log.info(" [execute_api] status=%d time=%.0fms", response.status_code, elapsed_ms)
|
|
227
227
|
|
|
228
228
|
# -- Build response dict -------------------------------------------------
|
|
229
229
|
resp = {
|
|
@@ -304,7 +304,7 @@ async def execute_api(
|
|
|
304
304
|
Raises:
|
|
305
305
|
RuntimeError: On invalid URL, unsupported method, or request failure.
|
|
306
306
|
"""
|
|
307
|
-
_log.
|
|
307
|
+
_log.info(" [execute_api] %s %s", method, url[:80])
|
|
308
308
|
try:
|
|
309
309
|
return await asyncio.to_thread(
|
|
310
310
|
_do_request, method, url, headers, body, params,
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_db.py
RENAMED
|
@@ -106,7 +106,7 @@ async def execute_db(
|
|
|
106
106
|
if not tunnel_id:
|
|
107
107
|
tunnel_id = os.environ.get("LT_PROXY_TUNNEL_ID", "")
|
|
108
108
|
|
|
109
|
-
_log.
|
|
109
|
+
_log.info(" [execute_db] db_id=%r db_name=%r", db_id, db_name)
|
|
110
110
|
try:
|
|
111
111
|
return await asyncio.to_thread(
|
|
112
112
|
_do_query, query, db_id, db_name, timeout, tunnel_id, auth_header, automind_url
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_js.py
RENAMED
|
@@ -35,7 +35,7 @@ async def execute_js(page, script: str):
|
|
|
35
35
|
from testmu._vars import _variable_store
|
|
36
36
|
|
|
37
37
|
preview = script[:100] + ("..." if len(script) > 100 else "")
|
|
38
|
-
_log.
|
|
38
|
+
_log.info(" [execute_js] %s", preview)
|
|
39
39
|
|
|
40
40
|
try:
|
|
41
41
|
user_js_code = script + "\n"
|
|
@@ -88,7 +88,7 @@ async def execute_js(page, script: str):
|
|
|
88
88
|
if result is None or result == "":
|
|
89
89
|
result = "null"
|
|
90
90
|
|
|
91
|
-
_log.
|
|
91
|
+
_log.info(" [execute_js] result=%s", str(result)[:100])
|
|
92
92
|
return result
|
|
93
93
|
|
|
94
94
|
except RuntimeError:
|
|
@@ -211,13 +211,8 @@ async def _wait_for_visibility(page, query: str) -> str:
|
|
|
211
211
|
screenshot_bytes = await page.screenshot()
|
|
212
212
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
213
213
|
|
|
214
|
-
_log.debug(f"[vision] Visibility check attempt {attempt}/{_COORD_MAX_RETRIES}")
|
|
215
214
|
result = await _check_visibility(session, screenshot_b64, query)
|
|
216
|
-
_log.
|
|
217
|
-
f"[vision] visible={result.get('visible')}, "
|
|
218
|
-
f"confidence={result.get('confidence')}, "
|
|
219
|
-
f"reasoning={result.get('reasoning')}"
|
|
220
|
-
)
|
|
215
|
+
_log.info(" [visibility] attempt %d/%d visible=%s", attempt, _COORD_MAX_RETRIES, result.get("visible"))
|
|
221
216
|
|
|
222
217
|
if result.get("visible", False):
|
|
223
218
|
return screenshot_b64
|
|
@@ -251,7 +246,7 @@ async def vision_query(page, description: str, return_type: str):
|
|
|
251
246
|
if not _config.smart:
|
|
252
247
|
return None
|
|
253
248
|
|
|
254
|
-
_log.
|
|
249
|
+
_log.info(" [vision_query] query=%s", description[:80])
|
|
255
250
|
|
|
256
251
|
screenshot_bytes = await page.screenshot()
|
|
257
252
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
@@ -264,7 +259,7 @@ async def vision_query(page, description: str, return_type: str):
|
|
|
264
259
|
}, timeout=_QUERY_TIMEOUT)
|
|
265
260
|
|
|
266
261
|
extracted = result.get("extracted_value", "")
|
|
267
|
-
_log.
|
|
262
|
+
_log.info(" [vision_query] result=%s", repr(extracted)[:120])
|
|
268
263
|
return extracted
|
|
269
264
|
|
|
270
265
|
|
|
@@ -287,7 +282,7 @@ async def textual_query(page, description: str, return_type: str):
|
|
|
287
282
|
if not _config.smart:
|
|
288
283
|
return None
|
|
289
284
|
|
|
290
|
-
_log.
|
|
285
|
+
_log.info(" [textual_query] query=%s", description[:80])
|
|
291
286
|
|
|
292
287
|
cdp = await page.context.new_cdp_session(page)
|
|
293
288
|
try:
|
|
@@ -326,7 +321,7 @@ async def textual_query(page, description: str, return_type: str):
|
|
|
326
321
|
})
|
|
327
322
|
|
|
328
323
|
extracted = extract_resp.get("extracted_value", "")
|
|
329
|
-
_log.
|
|
324
|
+
_log.info(" [textual_query] result=%s", repr(extracted)[:120])
|
|
330
325
|
return extracted
|
|
331
326
|
|
|
332
327
|
finally:
|
|
@@ -348,29 +343,24 @@ async def vision_wait(page, description: str, timeout_ms: int = 30000):
|
|
|
348
343
|
await page.wait_for_timeout(timeout_ms)
|
|
349
344
|
return
|
|
350
345
|
|
|
351
|
-
_log.
|
|
346
|
+
_log.info(" [vision_wait] query=%s", description[:80])
|
|
352
347
|
|
|
353
348
|
async with create_session() as session:
|
|
354
349
|
for attempt in range(1, _WAIT_MAX_RETRIES + 1):
|
|
355
350
|
screenshot_bytes = await page.screenshot()
|
|
356
351
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
357
352
|
|
|
358
|
-
_log.debug(f"[vision_wait] Check attempt {attempt}/{_WAIT_MAX_RETRIES}")
|
|
359
353
|
result = await _check_wait_condition(session, screenshot_b64, description)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
if not result.get("waiting", True):
|
|
367
|
-
_log.debug(f"[vision_wait] Condition met: {result.get('description', 'done')}")
|
|
354
|
+
waiting = result.get("waiting", True)
|
|
355
|
+
_log.info(" [vision_wait] attempt %d/%d waiting=%s", attempt, _WAIT_MAX_RETRIES, waiting)
|
|
356
|
+
|
|
357
|
+
if not waiting:
|
|
368
358
|
return
|
|
369
359
|
|
|
370
360
|
if attempt < _WAIT_MAX_RETRIES:
|
|
371
361
|
await asyncio.sleep(_WAIT_RETRY_INTERVAL)
|
|
372
362
|
|
|
373
|
-
_log.
|
|
363
|
+
_log.info(" [vision_wait] max retries exhausted, continuing")
|
|
374
364
|
|
|
375
365
|
|
|
376
366
|
async def vision_action(page, description: str, action_type: str, direction: str = "down", amount: int = 300) -> bool:
|
|
@@ -392,6 +382,8 @@ async def vision_action(page, description: str, action_type: str, direction: str
|
|
|
392
382
|
if not _config.smart:
|
|
393
383
|
return None
|
|
394
384
|
|
|
385
|
+
_log.info(" [vision_action] action=%s query=%s", action_type, description[:60])
|
|
386
|
+
|
|
395
387
|
screenshot_bytes = await page.screenshot()
|
|
396
388
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
397
389
|
|
|
@@ -412,13 +404,17 @@ async def vision_action(page, description: str, action_type: str, direction: str
|
|
|
412
404
|
timeout=aiohttp.ClientTimeout(total=30),
|
|
413
405
|
) as resp:
|
|
414
406
|
if resp.status != 200:
|
|
407
|
+
_log.info(" [vision_action] API error status=%d", resp.status)
|
|
415
408
|
return False
|
|
416
409
|
result = await resp.json()
|
|
417
410
|
if not result.get("found", False):
|
|
411
|
+
_log.info(" [vision_action] element not found")
|
|
418
412
|
return False
|
|
419
413
|
coord_x = result["x"]
|
|
420
414
|
coord_y = result["y"]
|
|
421
415
|
|
|
416
|
+
_log.info(" [vision_action] executing %s at (%d, %d)", action_type, coord_x, coord_y)
|
|
417
|
+
|
|
422
418
|
if action_type == "click":
|
|
423
419
|
await page.mouse.click(coord_x, coord_y)
|
|
424
420
|
elif action_type == "scroll":
|
|
@@ -439,16 +435,20 @@ async def vision_action(page, description: str, action_type: str, direction: str
|
|
|
439
435
|
|
|
440
436
|
|
|
441
437
|
async def get_vision_coordinates(page, description: str, action_type: str = "click", x: int = None, y: int = None) -> dict:
|
|
442
|
-
"""Get coordinates for an element via vision API
|
|
438
|
+
"""Get coordinates for an element via vision API.
|
|
443
439
|
|
|
444
|
-
|
|
440
|
+
When smart is ON: always uses vision API. No fallback to (x, y) — if the
|
|
441
|
+
API can't find the element or errors, raises instead of silently using
|
|
442
|
+
stale coordinates that may click the wrong thing.
|
|
443
|
+
|
|
444
|
+
When smart is OFF: returns {"x": x, "y": y} immediately.
|
|
445
445
|
|
|
446
446
|
Args:
|
|
447
447
|
page: Playwright page object.
|
|
448
448
|
description: Description of the action/element to find.
|
|
449
449
|
action_type: Type of action ("click", "type", etc.).
|
|
450
|
-
x:
|
|
451
|
-
y:
|
|
450
|
+
x: X coordinate used only when smart is OFF.
|
|
451
|
+
y: Y coordinate used only when smart is OFF.
|
|
452
452
|
|
|
453
453
|
Returns:
|
|
454
454
|
dict with 'x' and 'y' keys.
|
|
@@ -456,46 +456,31 @@ async def get_vision_coordinates(page, description: str, action_type: str = "cli
|
|
|
456
456
|
if not _config.smart:
|
|
457
457
|
return {"x": x, "y": y}
|
|
458
458
|
|
|
459
|
-
_log.
|
|
460
|
-
_log.debug(f"[get_vision_coordinates] Fallback: x={x}, y={y}")
|
|
459
|
+
_log.info(" [get_vision_coordinates] query=%s", description[:60])
|
|
461
460
|
|
|
462
|
-
|
|
463
|
-
screenshot_b64 = await _wait_for_visibility(page, description)
|
|
461
|
+
screenshot_b64 = await _wait_for_visibility(page, description)
|
|
464
462
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
463
|
+
viewport = page.viewport_size
|
|
464
|
+
width = viewport["width"] if viewport else 1920
|
|
465
|
+
height = viewport["height"] if viewport else 1080
|
|
468
466
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
result = await resp.json()
|
|
483
|
-
if result.get("found", False):
|
|
484
|
-
coords = {"x": result["x"], "y": result["y"]}
|
|
485
|
-
_log.debug(f"[get_vision_coordinates] Found: {coords}")
|
|
486
|
-
return coords
|
|
487
|
-
if x is not None and y is not None:
|
|
488
|
-
_log.debug("[get_vision_coordinates] Not found, using fallback")
|
|
489
|
-
return {"x": x, "y": y}
|
|
490
|
-
raise Exception("Vision API: element not found and no fallback provided")
|
|
491
|
-
if x is not None and y is not None:
|
|
492
|
-
_log.debug(f"[get_vision_coordinates] API error {resp.status}, using fallback")
|
|
493
|
-
return {"x": x, "y": y}
|
|
467
|
+
async with create_session() as session:
|
|
468
|
+
async with session.post(
|
|
469
|
+
f"{_VISION_API_HOST}/api/v1/vision/coordinates",
|
|
470
|
+
json={
|
|
471
|
+
"screenshot_b64": screenshot_b64,
|
|
472
|
+
"action_instruction": description,
|
|
473
|
+
"action_type": action_type,
|
|
474
|
+
"width": width,
|
|
475
|
+
"height": height,
|
|
476
|
+
},
|
|
477
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
478
|
+
) as resp:
|
|
479
|
+
if resp.status != 200:
|
|
494
480
|
raise Exception(f"Vision API error: {resp.status}")
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
_log.
|
|
500
|
-
return
|
|
501
|
-
raise
|
|
481
|
+
result = await resp.json()
|
|
482
|
+
if not result.get("found", False):
|
|
483
|
+
raise Exception(f"Vision API: element not found for query={description!r}")
|
|
484
|
+
coords = {"x": result["x"], "y": result["y"]}
|
|
485
|
+
_log.info(" [get_vision_coordinates] found (%d, %d)", coords["x"], coords["y"])
|
|
486
|
+
return coords
|
|
@@ -41,7 +41,7 @@ async def check_until_condition(page, condition: str) -> bool:
|
|
|
41
41
|
|
|
42
42
|
vision_api_host = os.getenv("TESTMU_AI_API_HOST", _AI_API_HOST_DEFAULT)
|
|
43
43
|
|
|
44
|
-
_log.info(
|
|
44
|
+
_log.info(" [check_until_condition] condition=%s", condition[:80])
|
|
45
45
|
|
|
46
46
|
screenshot_bytes = await page.screenshot()
|
|
47
47
|
screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/__init__.py
RENAMED
|
@@ -19,7 +19,7 @@ class Reporter(Protocol):
|
|
|
19
19
|
async def fail_test(self, error: Exception) -> None: ...
|
|
20
20
|
async def begin_step(self, description: str) -> None: ...
|
|
21
21
|
async def end_step(self, description: str, ok: bool, error: Optional[Exception] = None) -> None: ...
|
|
22
|
-
async def attach_screenshot(self, data: bytes) -> None: ...
|
|
22
|
+
# async def attach_screenshot(self, data: bytes) -> None: ...
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def get_reporter() -> Reporter:
|
|
@@ -23,11 +23,9 @@ class LocalReporter:
|
|
|
23
23
|
_log.info(" [STEP %d] %s", self._step_num, description)
|
|
24
24
|
|
|
25
25
|
async def end_step(self, description, ok, error=None):
|
|
26
|
-
if ok:
|
|
27
|
-
_log.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
async def attach_screenshot(self, data):
|
|
33
|
-
_log.info(" [SCREENSHOT] %d bytes", len(data))
|
|
26
|
+
if not ok:
|
|
27
|
+
_log.error(" [STEP %d FAIL] %s", self._step_num, error)
|
|
28
|
+
|
|
29
|
+
# async def attach_screenshot(self, data):
|
|
30
|
+
# # Stub — screenshot bytes aren't uploaded anywhere.
|
|
31
|
+
# _log.info(" [SCREENSHOT] %d bytes", len(data))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""LambdaTest dashboard reporter.
|
|
2
2
|
|
|
3
|
-
Uses page.evaluate('lambdatest_action: ...') for
|
|
4
|
-
|
|
3
|
+
Uses page.evaluate('lambdatest_action: ...') for:
|
|
4
|
+
- setTestStatus (overall test verdict)
|
|
5
|
+
- testCaseStart / testCaseEnd (per-step annotations on LT timeline)
|
|
5
6
|
|
|
6
7
|
All events are ALSO logged to stdout so HyperExecute console
|
|
7
8
|
shows progress — the CDP calls alone are invisible in HE logs.
|
|
@@ -43,19 +44,16 @@ class LTReporter:
|
|
|
43
44
|
async def begin_step(self, description):
|
|
44
45
|
self._step_num += 1
|
|
45
46
|
_log.info(" [STEP %d] %s", self._step_num, description)
|
|
46
|
-
await self.
|
|
47
|
-
"data": description,
|
|
48
|
-
"type": "info",
|
|
49
|
-
})
|
|
47
|
+
await self._evaluate_action("testCaseStart", {"name": description})
|
|
50
48
|
|
|
51
49
|
async def end_step(self, description, ok, error=None):
|
|
52
|
-
if ok:
|
|
53
|
-
_log.
|
|
54
|
-
|
|
55
|
-
_log.error(" [STEP %d FAIL] %s — %s", self._step_num, description, error)
|
|
50
|
+
if not ok:
|
|
51
|
+
_log.error(" [STEP %d FAIL] %s", self._step_num, error)
|
|
52
|
+
await self._evaluate_action("testCaseEnd", {"name": description})
|
|
56
53
|
|
|
57
|
-
async def attach_screenshot(self, data):
|
|
58
|
-
|
|
54
|
+
# async def attach_screenshot(self, data):
|
|
55
|
+
# # Stub — screenshot bytes aren't uploaded to LT dashboard yet.
|
|
56
|
+
# _log.info(" [SCREENSHOT] %d bytes", len(data))
|
|
59
57
|
|
|
60
58
|
async def _evaluate_action(self, action, arguments):
|
|
61
59
|
if self._page is None:
|
|
@@ -66,11 +64,12 @@ class LTReporter:
|
|
|
66
64
|
except Exception as e:
|
|
67
65
|
_log.warning("lambdatest_action failed: %s", e)
|
|
68
66
|
|
|
69
|
-
async def _evaluate_executor(self, action, arguments):
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
# async def _evaluate_executor(self, action, arguments):
|
|
68
|
+
# # Unused — stepcontext was replaced by testCaseStart/End.
|
|
69
|
+
# if self._page is None:
|
|
70
|
+
# return
|
|
71
|
+
# try:
|
|
72
|
+
# payload = json.dumps({"action": action, "arguments": arguments})
|
|
73
|
+
# await self._page.evaluate("_ => {}", f"lambdatest_executor: {payload}")
|
|
74
|
+
# except Exception as e:
|
|
75
|
+
# _log.warning("lambdatest_executor failed: %s", e)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testmu-playwright-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Testmu binding for Playwright Python — thin test runtime for LambdaTest exports
|
|
5
5
|
Author-email: LambdaTest <engineering@lambdatest.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -37,7 +37,9 @@ class TestDecorator:
|
|
|
37
37
|
await failing_test(page=mock_page)
|
|
38
38
|
|
|
39
39
|
@pytest.mark.asyncio
|
|
40
|
-
async def
|
|
40
|
+
async def test_screenshot_not_captured_on_failure(self):
|
|
41
|
+
"""Screenshot-on-failure is currently disabled (stub) — page.screenshot
|
|
42
|
+
should NOT be called by the decorator. Re-enable when LT upload is wired."""
|
|
41
43
|
mock_page = AsyncMock()
|
|
42
44
|
mock_page.screenshot = AsyncMock(return_value=b"png_data")
|
|
43
45
|
|
|
@@ -48,7 +50,7 @@ class TestDecorator:
|
|
|
48
50
|
with pytest.raises(RuntimeError):
|
|
49
51
|
await failing_test(page=mock_page)
|
|
50
52
|
|
|
51
|
-
mock_page.screenshot.
|
|
53
|
+
mock_page.screenshot.assert_not_called()
|
|
52
54
|
|
|
53
55
|
def test_sync_function_detected(self):
|
|
54
56
|
@decorator_test
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_vision.py
RENAMED
|
@@ -385,13 +385,14 @@ class TestGetVisionCoordinates:
|
|
|
385
385
|
assert result == {"x": 500, "y": 300}
|
|
386
386
|
|
|
387
387
|
@pytest.mark.asyncio
|
|
388
|
-
async def
|
|
388
|
+
async def test_raises_on_exception_when_smart_on_even_with_xy(self):
|
|
389
|
+
"""When smart is ON, no fallback to (x, y) — raise instead.
|
|
390
|
+
Silent fallback to stale coordinates can click the wrong element."""
|
|
389
391
|
with patch.object(_config, "smart", True), \
|
|
390
392
|
patch("testmu._helpers.vision._wait_for_visibility", AsyncMock(side_effect=Exception("network down"))):
|
|
391
393
|
page = _make_page()
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
assert result == {"x": 42, "y": 99}
|
|
394
|
+
with pytest.raises(Exception, match="network down"):
|
|
395
|
+
await get_vision_coordinates(page, "button", "click", x=42, y=99)
|
|
395
396
|
|
|
396
397
|
@pytest.mark.asyncio
|
|
397
398
|
async def test_raises_when_no_fallback_and_exception(self):
|
|
@@ -402,7 +403,8 @@ class TestGetVisionCoordinates:
|
|
|
402
403
|
await get_vision_coordinates(page, "button", "click")
|
|
403
404
|
|
|
404
405
|
@pytest.mark.asyncio
|
|
405
|
-
async def
|
|
406
|
+
async def test_raises_when_not_found_even_with_fallback(self):
|
|
407
|
+
"""When smart is ON, API returning 'not found' must raise — no fallback."""
|
|
406
408
|
visibility_resp_cm = _mock_post_response(200, {"visible": True})
|
|
407
409
|
coord_resp_cm = _mock_post_response(200, {"found": False})
|
|
408
410
|
|
|
@@ -420,9 +422,8 @@ class TestGetVisionCoordinates:
|
|
|
420
422
|
with patch.object(_config, "smart", True), \
|
|
421
423
|
patch("testmu._helpers.vision.aiohttp.ClientSession", return_value=session_cm):
|
|
422
424
|
page = _make_page()
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
assert result == {"x": 77, "y": 88}
|
|
425
|
+
with pytest.raises(Exception, match="element not found"):
|
|
426
|
+
await get_vision_coordinates(page, "button", "click", x=77, y=88)
|
|
426
427
|
|
|
427
428
|
|
|
428
429
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/kane_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/network.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/smartui.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_assertion.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_api.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_db.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_js.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_network_math.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_remaining.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_tabs_drag.py
RENAMED
|
File without changes
|
{testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helpers_stub.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|