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.
- testmu_playwright_python-0.1.0/LICENSE +21 -0
- testmu_playwright_python-0.1.0/MANIFEST.in +8 -0
- testmu_playwright_python-0.1.0/PKG-INFO +85 -0
- testmu_playwright_python-0.1.0/README.md +53 -0
- testmu_playwright_python-0.1.0/pyproject.toml +52 -0
- testmu_playwright_python-0.1.0/setup.cfg +4 -0
- testmu_playwright_python-0.1.0/testmu/__init__.py +58 -0
- testmu_playwright_python-0.1.0/testmu/_capability.py +249 -0
- testmu_playwright_python-0.1.0/testmu/_config.py +9 -0
- testmu_playwright_python-0.1.0/testmu/_configure.py +81 -0
- testmu_playwright_python-0.1.0/testmu/_decorator.py +53 -0
- testmu_playwright_python-0.1.0/testmu/_errors.py +6 -0
- testmu_playwright_python-0.1.0/testmu/_heal_patch.py +49 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/__init__.py +70 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/assertion.py +190 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/drag.py +6 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/execute_api.py +316 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/execute_db.py +116 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/execute_js.py +97 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/kane_cli.py +80 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/math.py +146 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/network.py +246 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/smartui.py +35 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/tabs.py +19 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/vision.py +499 -0
- testmu_playwright_python-0.1.0/testmu/_helpers/wait.py +65 -0
- testmu_playwright_python-0.1.0/testmu/_matchers.py +5 -0
- testmu_playwright_python-0.1.0/testmu/_reporter/__init__.py +44 -0
- testmu_playwright_python-0.1.0/testmu/_reporter/local.py +25 -0
- testmu_playwright_python-0.1.0/testmu/_reporter/lt.py +72 -0
- testmu_playwright_python-0.1.0/testmu/_reporter/null.py +10 -0
- testmu_playwright_python-0.1.0/testmu/_session.py +126 -0
- testmu_playwright_python-0.1.0/testmu/_step.py +60 -0
- testmu_playwright_python-0.1.0/testmu/_test_config.py +112 -0
- testmu_playwright_python-0.1.0/testmu/_vars.py +513 -0
- testmu_playwright_python-0.1.0/testmu/playwright_async/__init__.py +36 -0
- testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/PKG-INFO +85 -0
- testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/SOURCES.txt +39 -0
- testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/dependency_links.txt +1 -0
- testmu_playwright_python-0.1.0/testmu_playwright_python.egg-info/requires.txt +9 -0
- 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,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,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
|