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.
Files changed (58) hide show
  1. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/PKG-INFO +1 -1
  2. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/pyproject.toml +1 -1
  3. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_decorator.py +10 -7
  4. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/_http.py +14 -1
  5. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/assertion.py +4 -6
  6. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_api.py +2 -2
  7. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_db.py +1 -1
  8. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/execute_js.py +2 -2
  9. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/vision.py +49 -64
  10. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/wait.py +1 -1
  11. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/__init__.py +1 -1
  12. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/local.py +6 -8
  13. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/lt.py +19 -20
  14. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_reporter/null.py +1 -1
  15. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/PKG-INFO +1 -1
  16. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_decorator.py +4 -2
  17. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_vision.py +9 -8
  18. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/README.md +0 -0
  19. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/setup.cfg +0 -0
  20. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/__init__.py +0 -0
  21. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_capability.py +0 -0
  22. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_config.py +0 -0
  23. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_configure.py +0 -0
  24. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_errors.py +0 -0
  25. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_heal_patch.py +0 -0
  26. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/__init__.py +0 -0
  27. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/drag.py +0 -0
  28. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/kane_cli.py +0 -0
  29. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/math.py +0 -0
  30. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/network.py +0 -0
  31. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/smartui.py +0 -0
  32. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_helpers/tabs.py +0 -0
  33. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_matchers.py +0 -0
  34. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_session.py +0 -0
  35. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_step.py +0 -0
  36. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_test_config.py +0 -0
  37. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/_vars.py +0 -0
  38. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu/playwright_async/__init__.py +0 -0
  39. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/SOURCES.txt +0 -0
  40. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/dependency_links.txt +0 -0
  41. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/requires.txt +0 -0
  42. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/testmu_playwright_python.egg-info/top_level.txt +0 -0
  43. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_capability.py +0 -0
  44. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_config.py +0 -0
  45. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_configure.py +0 -0
  46. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_heal_patch.py +0 -0
  47. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_assertion.py +0 -0
  48. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_api.py +0 -0
  49. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_db.py +0 -0
  50. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_execute_js.py +0 -0
  51. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_network_math.py +0 -0
  52. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_remaining.py +0 -0
  53. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helper_tabs_drag.py +0 -0
  54. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_helpers_stub.py +0 -0
  55. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_integration.py +0 -0
  56. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_step.py +0 -0
  57. {testmu_playwright_python-0.1.2 → testmu_playwright_python-0.1.4}/tests/test_test_config.py +0 -0
  58. {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.2
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.2"
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
- page = _find_page(args, kwargs)
38
- if page is not None:
39
- try:
40
- screenshot = await page.screenshot()
41
- await rep.attach_screenshot(screenshot)
42
- except Exception:
43
- pass
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)
@@ -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.debug(f"[AssertionAPI] Deterministic evaluation: '{claim}'")
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.debug(f"[AssertionAPI] result={result!r}")
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.debug(f"[AssertionAPI] Visual verification: '{claim}'")
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.debug(f"[AssertionAPI] Result: {result.get('status', 'unknown')}")
128
+ _log.info(" [assertion] result status=%s", result.get("status", "unknown"))
131
129
  return result
132
130
 
133
131
 
@@ -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.debug(f"execute_api response: status={response.status_code}, time={elapsed_ms:.0f}ms")
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.debug(f"execute_api: {method} {url}")
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,
@@ -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.debug(f"execute_db: db_id={db_id!r} db_name={db_name!r}")
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
@@ -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.debug(f"execute_js: {preview}")
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.debug(f"execute_js result: {str(result)[:100]}")
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.debug(
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.debug(f"[vision_query] Extracting: '{description}'")
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.debug(f"[vision_query] Extracted: {extracted!r}")
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.debug(f"[textual_query] Extracting: '{description}'")
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.debug(f"[textual_query] Extracted: {extracted!r}")
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.debug(f"[vision_wait] Waiting for: '{description}' (timeout_ms={timeout_ms})")
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
- _log.debug(
361
- f"[vision_wait] waiting={result.get('waiting')}, "
362
- f"confidence={result.get('confidence')}, "
363
- f"reasoning={result.get('reasoning')}"
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.debug("[vision_wait] Max retries exhausted, continuing")
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 with fallback to raw (x, y).
438
+ """Get coordinates for an element via vision API.
443
439
 
444
- Smart-gated: returns {"x": x, "y": y} immediately when _config.smart is OFF.
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: Fallback X coordinate if API fails or smart is OFF.
451
- y: Fallback Y coordinate if API fails or smart is OFF.
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.debug(f"[get_vision_coordinates] Looking up: '{description}' (action_type: {action_type})")
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
- try:
463
- screenshot_b64 = await _wait_for_visibility(page, description)
461
+ screenshot_b64 = await _wait_for_visibility(page, description)
464
462
 
465
- viewport = page.viewport_size
466
- width = viewport["width"] if viewport else 1920
467
- height = viewport["height"] if viewport else 1080
463
+ viewport = page.viewport_size
464
+ width = viewport["width"] if viewport else 1920
465
+ height = viewport["height"] if viewport else 1080
468
466
 
469
- async with create_session() as session:
470
- async with session.post(
471
- f"{_VISION_API_HOST}/api/v1/vision/coordinates",
472
- json={
473
- "screenshot_b64": screenshot_b64,
474
- "action_instruction": description,
475
- "action_type": action_type,
476
- "width": width,
477
- "height": height,
478
- },
479
- timeout=aiohttp.ClientTimeout(total=30),
480
- ) as resp:
481
- if resp.status == 200:
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
- except Exception as e:
497
- _log.debug(f"[get_vision_coordinates] Exception: {type(e).__name__}: {e}")
498
- if x is not None and y is not None:
499
- _log.debug("[get_vision_coordinates] Using fallback due to error")
500
- return {"x": x, "y": y}
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(f"[check_until_condition] checking condition: {condition!r}")
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")
@@ -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.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))
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 test status
4
- and page.evaluate('lambdatest_executor: ...') for step context.
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._evaluate_executor("stepcontext", {
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.info(" [STEP %d OK] %s", self._step_num, description)
54
- else:
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
- _log.info(" [SCREENSHOT] %d bytes", len(data))
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
- if self._page is None:
71
- return
72
- try:
73
- payload = json.dumps({"action": action, "arguments": arguments})
74
- await self._page.evaluate("_ => {}", f"lambdatest_executor: {payload}")
75
- except Exception as e:
76
- _log.warning("lambdatest_executor failed: %s", e)
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)
@@ -7,4 +7,4 @@ class NullReporter:
7
7
  async def fail_test(self, error): pass
8
8
  async def begin_step(self, description): pass
9
9
  async def end_step(self, description, ok, error=None): pass
10
- async def attach_screenshot(self, data): pass
10
+ # async def attach_screenshot(self, data): pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testmu-playwright-python
3
- Version: 0.1.2
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 test_screenshot_captured_on_failure(self):
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.assert_awaited_once()
53
+ mock_page.screenshot.assert_not_called()
52
54
 
53
55
  def test_sync_function_detected(self):
54
56
  @decorator_test
@@ -385,13 +385,14 @@ class TestGetVisionCoordinates:
385
385
  assert result == {"x": 500, "y": 300}
386
386
 
387
387
  @pytest.mark.asyncio
388
- async def test_falls_back_to_xy_on_exception(self):
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
- result = await get_vision_coordinates(page, "button", "click", x=42, y=99)
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 test_falls_back_when_not_found_and_has_fallback(self):
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
- result = await get_vision_coordinates(page, "button", "click", x=77, y=88)
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
  # ---------------------------------------------------------------------------