testmu-playwright-python 0.1.0__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 (41) hide show
  1. testmu_playwright_python-0.1.0/LICENSE +21 -0
  2. testmu_playwright_python-0.1.0/MANIFEST.in +8 -0
  3. testmu_playwright_python-0.1.0/PKG-INFO +85 -0
  4. testmu_playwright_python-0.1.0/README.md +53 -0
  5. testmu_playwright_python-0.1.0/pyproject.toml +52 -0
  6. testmu_playwright_python-0.1.0/setup.cfg +4 -0
  7. testmu_playwright_python-0.1.0/testmu/__init__.py +58 -0
  8. testmu_playwright_python-0.1.0/testmu/_capability.py +249 -0
  9. testmu_playwright_python-0.1.0/testmu/_config.py +9 -0
  10. testmu_playwright_python-0.1.0/testmu/_configure.py +81 -0
  11. testmu_playwright_python-0.1.0/testmu/_decorator.py +53 -0
  12. testmu_playwright_python-0.1.0/testmu/_errors.py +6 -0
  13. testmu_playwright_python-0.1.0/testmu/_heal_patch.py +49 -0
  14. testmu_playwright_python-0.1.0/testmu/_helpers/__init__.py +70 -0
  15. testmu_playwright_python-0.1.0/testmu/_helpers/assertion.py +190 -0
  16. testmu_playwright_python-0.1.0/testmu/_helpers/drag.py +6 -0
  17. testmu_playwright_python-0.1.0/testmu/_helpers/execute_api.py +316 -0
  18. testmu_playwright_python-0.1.0/testmu/_helpers/execute_db.py +116 -0
  19. testmu_playwright_python-0.1.0/testmu/_helpers/execute_js.py +97 -0
  20. testmu_playwright_python-0.1.0/testmu/_helpers/kane_cli.py +80 -0
  21. testmu_playwright_python-0.1.0/testmu/_helpers/math.py +146 -0
  22. testmu_playwright_python-0.1.0/testmu/_helpers/network.py +246 -0
  23. testmu_playwright_python-0.1.0/testmu/_helpers/smartui.py +35 -0
  24. testmu_playwright_python-0.1.0/testmu/_helpers/tabs.py +19 -0
  25. testmu_playwright_python-0.1.0/testmu/_helpers/vision.py +499 -0
  26. testmu_playwright_python-0.1.0/testmu/_helpers/wait.py +65 -0
  27. testmu_playwright_python-0.1.0/testmu/_matchers.py +5 -0
  28. testmu_playwright_python-0.1.0/testmu/_reporter/__init__.py +44 -0
  29. testmu_playwright_python-0.1.0/testmu/_reporter/local.py +25 -0
  30. testmu_playwright_python-0.1.0/testmu/_reporter/lt.py +72 -0
  31. testmu_playwright_python-0.1.0/testmu/_reporter/null.py +10 -0
  32. testmu_playwright_python-0.1.0/testmu/_session.py +126 -0
  33. testmu_playwright_python-0.1.0/testmu/_step.py +60 -0
  34. testmu_playwright_python-0.1.0/testmu/_test_config.py +112 -0
  35. testmu_playwright_python-0.1.0/testmu/_vars.py +513 -0
  36. testmu_playwright_python-0.1.0/testmu/playwright_async/__init__.py +36 -0
  37. testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/PKG-INFO +85 -0
  38. testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/SOURCES.txt +39 -0
  39. testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/dependency_links.txt +1 -0
  40. testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/requires.txt +9 -0
  41. testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 TestMu AI (LambdaTest)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ recursive-include testmu *.py
5
+
6
+ # Exclude tests and dev artifacts from sdist
7
+ prune tests
8
+ prune .github
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
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
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Topic :: Software Development :: Testing
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: playwright>=1.57.0
24
+ Requires-Dist: pyotp>=2.9.0
25
+ Requires-Dist: aiohttp
26
+ Requires-Dist: httpx>=0.27.0
27
+ Requires-Dist: requests>=2.28.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # testmu-playwright-python
34
+
35
+ TestMu binding for Playwright Python — a thin test runtime for running TestMu exported tests locally or on the LambdaTest grid.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install testmu-playwright-python
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ import testmu
47
+
48
+ testmu.configure(
49
+ username="YOUR_LT_USERNAME",
50
+ access_key="YOUR_LT_ACCESS_KEY",
51
+ )
52
+
53
+ @testmu.test
54
+ async def my_test(page):
55
+ async with testmu.step("Open site"):
56
+ await page.goto("https://example.com")
57
+
58
+ async with testmu.step("Verify title"):
59
+ testmu.expect(page).to_have_title("Example Domain")
60
+
61
+ testmu.run(my_test)
62
+ ```
63
+
64
+ ## Features
65
+
66
+ - **Test decorator** — `@testmu.test` wraps async Playwright tests with session lifecycle
67
+ - **Step tracking** — `testmu.step(...)` context manager for structured test steps
68
+ - **Variable system** — `var()` / `set_var()` for template-based test data
69
+ - **Helpers** — vision queries, API/DB/JS execution, network assertions, SmartUI snapshots
70
+ - **Reporters** — local console and LambdaTest cloud reporting
71
+ - **Self-healing locators** — automatic locator recovery via heal patch
72
+
73
+ ## Requirements
74
+
75
+ - Python >= 3.11
76
+ - Playwright >= 1.57.0
77
+
78
+ ## Links
79
+
80
+ - Homepage: [testmu.ai](https://testmu.ai)
81
+ - Documentation: [docs.testmu.ai](https://docs.testmu.ai)
82
+
83
+ ## License
84
+
85
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,53 @@
1
+ # testmu-playwright-python
2
+
3
+ TestMu binding for Playwright Python — a thin test runtime for running TestMu exported tests locally or on the LambdaTest grid.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install testmu-playwright-python
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import testmu
15
+
16
+ testmu.configure(
17
+ username="YOUR_LT_USERNAME",
18
+ access_key="YOUR_LT_ACCESS_KEY",
19
+ )
20
+
21
+ @testmu.test
22
+ async def my_test(page):
23
+ async with testmu.step("Open site"):
24
+ await page.goto("https://example.com")
25
+
26
+ async with testmu.step("Verify title"):
27
+ testmu.expect(page).to_have_title("Example Domain")
28
+
29
+ testmu.run(my_test)
30
+ ```
31
+
32
+ ## Features
33
+
34
+ - **Test decorator** — `@testmu.test` wraps async Playwright tests with session lifecycle
35
+ - **Step tracking** — `testmu.step(...)` context manager for structured test steps
36
+ - **Variable system** — `var()` / `set_var()` for template-based test data
37
+ - **Helpers** — vision queries, API/DB/JS execution, network assertions, SmartUI snapshots
38
+ - **Reporters** — local console and LambdaTest cloud reporting
39
+ - **Self-healing locators** — automatic locator recovery via heal patch
40
+
41
+ ## Requirements
42
+
43
+ - Python >= 3.11
44
+ - Playwright >= 1.57.0
45
+
46
+ ## Links
47
+
48
+ - Homepage: [testmu.ai](https://testmu.ai)
49
+ - Documentation: [docs.testmu.ai](https://docs.testmu.ai)
50
+
51
+ ## License
52
+
53
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
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"
10
+ 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"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Natural Language :: English",
21
+ "Topic :: Software Development :: Testing",
22
+ "Framework :: Pytest",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ ]
28
+ dependencies = [
29
+ "playwright>=1.57.0",
30
+ "pyotp>=2.9.0",
31
+ "aiohttp",
32
+ "httpx>=0.27.0",
33
+ "requests>=2.28.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=7.0",
39
+ "pytest-asyncio>=0.21",
40
+ ]
41
+
42
+ [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*"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,58 @@
1
+ """Testmu binding for Playwright Python.
2
+
3
+ Public API:
4
+ testmu.test — decorator for test functions
5
+ testmu.step(...) — step context manager
6
+ testmu.run(fn) — session lifecycle runner
7
+ var(template) — variable/template resolver
8
+ set_var(name, value) — store variable
9
+ expect — PW expect with custom matchers
10
+ testmu.<helper>(...) — helper functions (execute_js, vision_query, etc.)
11
+ """
12
+
13
+ from testmu._configure import configure
14
+ from testmu._decorator import test
15
+ from testmu._step import step
16
+ from testmu._session import run
17
+ from testmu._vars import var, set_var
18
+ from testmu._matchers import expect
19
+ from testmu._errors import TestmuConfigError
20
+
21
+ # Helpers — vision functions ported; remaining stubs will be replaced in Layer B
22
+ from testmu._helpers import (
23
+ execute_js,
24
+ execute_api,
25
+ execute_db,
26
+ vision_query,
27
+ textual_query,
28
+ vision_wait,
29
+ vision_action,
30
+ verify_assertion,
31
+ evaluate_branch,
32
+ network_query,
33
+ evaluate_network_assertion,
34
+ evaluate_math,
35
+ smartui_snapshot,
36
+ switch_tab,
37
+ click_drag,
38
+ execute_kane_cli,
39
+ check_until_condition,
40
+ get_vision_coordinates,
41
+ )
42
+
43
+ # Install PW-specific heal patch
44
+ import testmu.playwright_async # noqa: F401 — triggers _install_heal()
45
+
46
+ __all__ = [
47
+ "configure",
48
+ "test", "step", "run", "var", "set_var", "expect",
49
+ "execute_js", "execute_api", "execute_db",
50
+ "vision_query", "textual_query", "vision_wait", "vision_action",
51
+ "verify_assertion", "evaluate_branch",
52
+ "network_query", "evaluate_network_assertion",
53
+ "evaluate_math", "smartui_snapshot",
54
+ "switch_tab", "click_drag",
55
+ "execute_kane_cli", "check_until_condition",
56
+ "get_vision_coordinates",
57
+ "TestmuConfigError",
58
+ ]
@@ -0,0 +1,249 @@
1
+ """LambdaTest capability builder for Playwright Python.
2
+
3
+ Configuration is sourced from testmu.configure() data first, then falls back
4
+ to environment variables (no sys.argv / test-run JSON).
5
+ This module is the env-var-only equivalent of the capability.py template produced by
6
+ create_capabilities_file_playwright() in auteur-code-export.
7
+ """
8
+ import json
9
+ import os
10
+ import urllib.parse
11
+ from pathlib import Path
12
+
13
+ from testmu import _configure
14
+
15
+ try:
16
+ from importlib.metadata import version as _pkg_version
17
+ _PW_VERSION = _pkg_version("playwright")
18
+ except Exception:
19
+ _PW_VERSION = "unknown"
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Internal helpers (match template helpers exactly)
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _get_download_folder() -> Path:
27
+ """Return the system Downloads folder, creating it if needed."""
28
+ if os.name == "nt":
29
+ downloads_path = Path(os.path.join(os.environ.get("USERPROFILE", ""), "Downloads"))
30
+ else:
31
+ downloads_path = Path(os.path.expanduser("~/Downloads"))
32
+ downloads_path.mkdir(parents=True, exist_ok=True)
33
+ return downloads_path
34
+
35
+
36
+ def _get_cap_value(cap_name: str, default_value=None):
37
+ """Return env var value or default — mirrors get_cap_value(None, ...) path."""
38
+ return os.getenv(cap_name, default_value)
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Public API
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def get_capabilities(
46
+ browser: str = None,
47
+ browser_version: str = None,
48
+ resolution: str = None,
49
+ platform: str = None,
50
+ *,
51
+ # baked-in values (set at code-export time, overridable by env)
52
+ build: str = None,
53
+ name: str = None,
54
+ tc_id: str = None,
55
+ network_caps: bool = False,
56
+ multiple_profiles: bool = False,
57
+ chrome_options: list = None,
58
+ custom_headers: dict = None,
59
+ ) -> dict:
60
+ """Build and return the LambdaTest capabilities dict.
61
+
62
+ All parameters fall back to env vars when not supplied, mirroring the
63
+ template's get_cap_value(test_config, ...) with test_config=None path.
64
+
65
+ After building, sets smart_os / smart_os_version / smart_browser_name /
66
+ smart_browser_version env vars (same as the template).
67
+ """
68
+ if chrome_options is None:
69
+ chrome_options = _configure.get("chrome_options", [])
70
+ if custom_headers is None:
71
+ custom_headers = _configure.get("custom_headers", {})
72
+
73
+ # Resolve top-level fields from env or defaults
74
+ _browser = browser or os.getenv("LT_BROWSER", "Chrome")
75
+ _browser_version = browser_version or os.getenv("LT_BROWSER_VERSION", "latest")
76
+ _resolution = resolution or os.getenv("LT_RESOLUTION", "1920x1080")
77
+ _platform = platform or os.getenv("LT_PLATFORM", "linux")
78
+
79
+ username = os.getenv("LT_USERNAME")
80
+ access_key = os.getenv("LT_ACCESS_KEY")
81
+
82
+ # HE test-run config — checked after configure(), before env vars
83
+ from testmu._test_config import get_cap_value as _he_cap
84
+
85
+ # video/visual/console/network coerce env strings to bool-like; template
86
+ # passes True/False from get_cap_value so booleans are fine here.
87
+ def _bool_env(key: str, default: bool) -> bool:
88
+ val = os.getenv(key)
89
+ if val is None:
90
+ return default
91
+ return val.lower() in ("1", "true", "yes")
92
+
93
+ def _bool_he(key: str, default: bool) -> bool:
94
+ """Check test_config first, then env var, then default."""
95
+ val = _he_cap(key, None)
96
+ if val is None:
97
+ return default
98
+ if isinstance(val, bool):
99
+ return val
100
+ return str(val).lower() in ("1", "true", "yes")
101
+
102
+ # Build + name: configure() > test_config > env > default
103
+ _build = _configure.get("build") or _he_cap("BUILD", os.getenv("BUILD", build or ""))
104
+ _name = _configure.get("name") or _he_cap("TEST_NAME", os.getenv("TEST_NAME", name or ""))
105
+ _tc_id = _configure.get("tc_id") or tc_id or os.getenv("LT_TC_ID", "")
106
+
107
+ # For bool fields: configure() wins when explicitly set, then test_config > env > default.
108
+ _cfg_network = _configure.get("network")
109
+ if _configure.was_set("network"):
110
+ _network = bool(_cfg_network)
111
+ else:
112
+ _network = _bool_he("NETWORK", network_caps)
113
+
114
+ _cfg_multiple_profiles = _configure.get("multiple_profiles")
115
+ if _configure.was_set("multiple_profiles"):
116
+ multiple_profiles = _cfg_multiple_profiles
117
+
118
+ # Bool caps: configure() has no explicit setters for these, so test_config > env > default.
119
+ _video = _bool_he("VIDEO", True)
120
+ _visual = _bool_he("VISUAL", True)
121
+ _console = _bool_he("CONSOLE", True)
122
+ _tunnel = _bool_he("TUNNEL", False)
123
+ _performance = _bool_he("PERFORMANCE", False)
124
+ _dedicated_proxy = _bool_he("DEDICATED_PROXY", False)
125
+ _accessibility = _bool_he("ACCESSIBILITY", False)
126
+ _idle_timeout = int(_he_cap("IDLE_TIMEOUT", os.getenv("IDLE_TIMEOUT", "1800")))
127
+
128
+ capabilities = {
129
+ "browserName": _browser,
130
+ "browserVersion": _browser_version,
131
+ "LT:Options": {
132
+ "platform": _platform,
133
+ "user": username,
134
+ "accessKey": access_key,
135
+ "video": _video,
136
+ "resolution": _resolution,
137
+ "network": _network,
138
+ "network.full.har": _network,
139
+ "build": _build,
140
+ "project": "Auteur-Code-Export",
141
+ "name": _name,
142
+ "w3c": True,
143
+ "plugin": "python-python",
144
+ "visual": _visual,
145
+ "console": _console,
146
+ "tms.tc_id": _tc_id,
147
+ "loadExtensions": [os.getenv("EXTENSION")],
148
+ "playwrightClientVersion": _PW_VERSION,
149
+ "tunnel": _tunnel,
150
+ "performance": _performance,
151
+ "dedicatedProxy": _dedicated_proxy,
152
+ "idleTimeout": _idle_timeout,
153
+ "accessibility": _accessibility,
154
+ "hideInternalCommandLogs": True,
155
+ "dependentTestsInScenario": multiple_profiles,
156
+ },
157
+ }
158
+
159
+ # Geolocation — conditional (matches: if os.getenv("GEO_LOCATION", False))
160
+ if os.getenv("GEO_LOCATION", False):
161
+ capabilities["LT:Options"]["geoLocation"] = os.getenv("GEO_LOCATION")
162
+
163
+ # Timezone — configure() > test_config (dict with "region") > env var
164
+ _timezone = _configure.get("timezone") or ""
165
+ if not _timezone:
166
+ from testmu._test_config import load_test_config
167
+ _tc = load_test_config()
168
+ if _tc:
169
+ _tz_val = _tc.get("timezone", {})
170
+ if isinstance(_tz_val, dict) and _tz_val.get("region"):
171
+ _timezone = _tz_val["region"]
172
+ if not _timezone:
173
+ _timezone = os.getenv("TIMEZONE", "")
174
+ if _timezone:
175
+ capabilities["LT:Options"]["timezone"] = _timezone
176
+
177
+ # Custom headers — conditional
178
+ if custom_headers:
179
+ capabilities["LT:Options"]["customHeaders"] = custom_headers
180
+
181
+ # Browser-specific options
182
+ if _browser.lower() in ("chrome", "pw-chromium"):
183
+ default_args = [
184
+ "--enable-logging",
185
+ "--disable-notifications",
186
+ "--no-sandbox",
187
+ "--log-level=0",
188
+ "--ignore-certificate-errors",
189
+ "--disable-blink-features=AutomationControlled",
190
+ "--password-store=basic",
191
+ "--safebrowsing-disable-auto-update",
192
+ "--disable-sync",
193
+ ]
194
+ capabilities["LT:Options"]["goog:chromeOptions"] = default_args
195
+ for op in chrome_options:
196
+ op_key = op.get("key", "")
197
+ op_type = op.get("type", "")
198
+ if op_type == "file":
199
+ op_value = os.path.join(str(_get_download_folder()), op.get("value", ""))
200
+ else:
201
+ op_value = op.get("value", "")
202
+ if op_type == "no-args":
203
+ capabilities["LT:Options"]["goog:chromeOptions"].append(f"{op_key}")
204
+ else:
205
+ capabilities["LT:Options"]["goog:chromeOptions"].append(f"{op_key}={op_value}")
206
+ elif _browser.lower() == "microsoftedge":
207
+ default_args = [
208
+ "--enable-logging",
209
+ "--disable-notifications",
210
+ "--log-level=0",
211
+ "--disable-blink-features=AutomationControlled",
212
+ "--password-store=basic",
213
+ "--safebrowsing-disable-auto-update",
214
+ "--disable-sync",
215
+ ]
216
+ capabilities["LT:Options"]["ms:edgeOptions"] = default_args
217
+ elif _browser.lower() in ("firefox", "pw-firefox"):
218
+ capabilities["LT:Options"]["firefoxUserPrefs"] = {
219
+ "dom.webnotifications.enabled": False,
220
+ "signon.rememberSignons": False,
221
+ "signon.autofillForms": False,
222
+ "permissions.default.desktop-notification": 2,
223
+ }
224
+
225
+ # Set smart env vars (same as template)
226
+ os.environ["smart_os"] = capabilities["LT:Options"]["platform"]
227
+ os.environ["smart_os_version"] = "latest"
228
+ os.environ["smart_browser_name"] = capabilities["browserName"]
229
+ os.environ["smart_browser_version"] = capabilities["browserVersion"]
230
+
231
+ return capabilities
232
+
233
+
234
+ def get_cdp_url(capabilities: dict) -> str:
235
+ """Build the LambdaTest CDP WebSocket URL from a capabilities dict."""
236
+ return (
237
+ "wss://cdp.lambdatest.com/playwright?capabilities="
238
+ + urllib.parse.quote(json.dumps(capabilities))
239
+ )
240
+
241
+
242
+ def get_viewport(resolution: str = None) -> dict:
243
+ """Parse a resolution string like '1920x1080' into a viewport dict."""
244
+ _res = resolution or os.getenv("LT_RESOLUTION", "1920x1080")
245
+ try:
246
+ width_str, height_str = _res.lower().split("x")
247
+ return {"width": int(width_str), "height": int(height_str)}
248
+ except (ValueError, AttributeError):
249
+ return {"width": 1920, "height": 1080}
@@ -0,0 +1,9 @@
1
+ """Platform and smart mode detection from environment variables.
2
+
3
+ platform: True when LT_USERNAME + LT_ACCESS_KEY are set (LT infrastructure accessible)
4
+ smart: True when platform is on AND TESTMU_SMART=1 (AI/heal features enabled)
5
+ """
6
+ import os
7
+
8
+ platform = bool(os.getenv("LT_USERNAME")) and bool(os.getenv("LT_ACCESS_KEY"))
9
+ smart = platform and os.getenv("TESTMU_SMART", "0") == "1"
@@ -0,0 +1,81 @@
1
+ """Export-time configuration for testmu.
2
+
3
+ testmu.configure() is called at the top of generated test files to
4
+ pass values that were known at export time (test name, build ID,
5
+ variables, chrome options, etc.) into the binding runtime.
6
+
7
+ This replaces the old pattern of baking values into capability.py
8
+ and test.py as string literals.
9
+ """
10
+ import logging
11
+
12
+ _log = logging.getLogger("testmu")
13
+
14
+ # All config fields with defaults
15
+ _config_data = {
16
+ "build": "",
17
+ "name": "",
18
+ "tc_id": "",
19
+ "network": False,
20
+ "timezone": "",
21
+ "chrome_options": [],
22
+ "custom_headers": {},
23
+ "multiple_profiles": False,
24
+ "variables": {},
25
+ "test_params": {},
26
+ "global_variables": [],
27
+ "uploaded_files": [],
28
+ }
29
+
30
+ # Keys explicitly set via configure() — used to distinguish "not configured"
31
+ # from "configured to the default value" (important for bool fields).
32
+ _configured_keys: set = set()
33
+
34
+
35
+ def configure(**kwargs):
36
+ """Set export-time configuration values.
37
+
38
+ Called once at the top of generated test files before @testmu.test.
39
+ Values are consumed by _capability.py, _vars.py, and _session.py.
40
+ """
41
+ for key, value in kwargs.items():
42
+ if key not in _config_data:
43
+ _log.warning(f"testmu.configure: unknown key '{key}', ignoring")
44
+ continue
45
+ _config_data[key] = value
46
+ _configured_keys.add(key)
47
+
48
+ # Populate variable stores immediately
49
+ from testmu._vars import set_var, _test_params, _global_variables
50
+ for var_name, var_value in _config_data.get("variables", {}).items():
51
+ set_var(var_name, var_value)
52
+ _test_params.update(_config_data.get("test_params", {}))
53
+ _global_variables.clear()
54
+ _global_variables.extend(_config_data.get("global_variables", []))
55
+
56
+
57
+ def get(key, default=None):
58
+ """Read a config value."""
59
+ return _config_data.get(key, default)
60
+
61
+
62
+ def was_set(key) -> bool:
63
+ """Return True if key was explicitly passed to configure()."""
64
+ return key in _configured_keys
65
+
66
+
67
+ def _reset():
68
+ """Reset all config and variable stores (for testing)."""
69
+ for key in _config_data:
70
+ if isinstance(_config_data[key], dict):
71
+ _config_data[key] = {}
72
+ elif isinstance(_config_data[key], list):
73
+ _config_data[key] = []
74
+ elif isinstance(_config_data[key], bool):
75
+ _config_data[key] = False
76
+ else:
77
+ _config_data[key] = ""
78
+ _configured_keys.clear()
79
+ # Also reset variable stores populated by configure()
80
+ from testmu._vars import _reset_store
81
+ _reset_store()
@@ -0,0 +1,53 @@
1
+ """@testmu.test decorator.
2
+
3
+ Wraps test functions for lifecycle reporting:
4
+ - begin_test / pass_test / fail_test
5
+ - screenshot on failure (best-effort)
6
+
7
+ Does NOT own browser launch (that's testmu.run).
8
+ Does NOT override PW methods (that's the heal patch).
9
+ """
10
+ import asyncio
11
+ import functools
12
+
13
+ from testmu._reporter import reporter
14
+
15
+
16
+ def _find_page(args, kwargs):
17
+ """Extract the page argument from args/kwargs."""
18
+ if "page" in kwargs:
19
+ return kwargs["page"]
20
+ for arg in args:
21
+ if hasattr(arg, "screenshot") and hasattr(arg, "locator"):
22
+ return arg
23
+ return None
24
+
25
+
26
+ def test(fn):
27
+ if asyncio.iscoroutinefunction(fn):
28
+ @functools.wraps(fn)
29
+ async def async_wrapper(*args, **kwargs):
30
+ rep = reporter()
31
+ await rep.begin_test(fn.__name__)
32
+ try:
33
+ result = await fn(*args, **kwargs)
34
+ await rep.pass_test()
35
+ return result
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
44
+ await rep.fail_test(e)
45
+ raise
46
+ return async_wrapper
47
+ else:
48
+ @functools.wraps(fn)
49
+ def sync_wrapper(*args, **kwargs):
50
+ raise NotImplementedError(
51
+ "Sync test functions are not yet supported. Use 'async def'."
52
+ )
53
+ return sync_wrapper