prettyplay 0.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of prettyplay might be problematic. Click here for more details.
- prettyplay/.usages/lifecycle.md +50 -0
- prettyplay/.usages/steps.md +56 -0
- prettyplay/CODEMANIFEST +182 -0
- prettyplay/__init__.py +13 -0
- prettyplay/cache/.usages/addressing.md +31 -0
- prettyplay/cache/.usages/budgets.md +21 -0
- prettyplay/cache/.usages/storage.md +32 -0
- prettyplay/cache/CODEMANIFEST +152 -0
- prettyplay/cache/__init__.py +8 -0
- prettyplay/cache/budgets.py +73 -0
- prettyplay/cache/models.py +60 -0
- prettyplay/cache/store.py +260 -0
- prettyplay/cache/text.py +35 -0
- prettyplay/config/.usages/configuration.md +87 -0
- prettyplay/config/CODEMANIFEST +132 -0
- prettyplay/config/__init__.py +6 -0
- prettyplay/config/loader.py +192 -0
- prettyplay/config/models.py +92 -0
- prettyplay/driver/.usages/facade.md +66 -0
- prettyplay/driver/CODEMANIFEST +162 -0
- prettyplay/driver/__init__.py +6 -0
- prettyplay/driver/page.py +350 -0
- prettyplay/driver/session.py +288 -0
- prettyplay/engine/.usages/generation.md +45 -0
- prettyplay/engine/.usages/healing.md +31 -0
- prettyplay/engine/CODEMANIFEST +215 -0
- prettyplay/engine/__init__.py +8 -0
- prettyplay/engine/classification.py +69 -0
- prettyplay/engine/execution.py +25 -0
- prettyplay/engine/generator.py +318 -0
- prettyplay/engine/healer.py +116 -0
- prettyplay/engine/text.py +19 -0
- prettyplay/executor.py +109 -0
- prettyplay/failures/.usages/taxonomy.md +40 -0
- prettyplay/failures/CODEMANIFEST +117 -0
- prettyplay/failures/__init__.py +17 -0
- prettyplay/failures/errors.py +147 -0
- prettyplay/llm/.usages/classification.md +25 -0
- prettyplay/llm/.usages/providers.md +33 -0
- prettyplay/llm/CODEMANIFEST +125 -0
- prettyplay/llm/__init__.py +8 -0
- prettyplay/llm/_request.py +216 -0
- prettyplay/llm/anthropic_provider.py +213 -0
- prettyplay/llm/models.py +22 -0
- prettyplay/llm/openai_provider.py +187 -0
- prettyplay/llm/provider.py +115 -0
- prettyplay/reporting/.usages/hooks.md +41 -0
- prettyplay/reporting/CODEMANIFEST +79 -0
- prettyplay/reporting/__init__.py +6 -0
- prettyplay/reporting/hooks.py +37 -0
- prettyplay/reporting/reporter.py +70 -0
- prettyplay/runtime.py +107 -0
- prettyplay/scenario.py +291 -0
- prettyplay-0.0.0.dist-info/METADATA +236 -0
- prettyplay-0.0.0.dist-info/RECORD +58 -0
- prettyplay-0.0.0.dist-info/WHEEL +5 -0
- prettyplay-0.0.0.dist-info/licenses/LICENSE +28 -0
- prettyplay-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Loading of the ``[tool.prettyplay]`` section of pyproject.toml with layered overrides.
|
|
2
|
+
|
|
3
|
+
The pyproject.toml path is either given explicitly or auto-searched upward from
|
|
4
|
+
the current working directory. Environment overrides win over the file
|
|
5
|
+
whenever the variable is set — including when it is set to an empty string:
|
|
6
|
+
``PRETTYPLAY_<SETTING_UPPER>`` for every setting except the browser
|
|
7
|
+
(``PRETTYPLAY_BROWSER_NAME``) and headless (``PRETTYPLAY_BROWSER_HEADLESS``).
|
|
8
|
+
The removed legacy name ``PRETTYPLAY_BROWSER`` fails loudly before merging.
|
|
9
|
+
Explicitly set programmatic values (``PrettyConfig`` fields passed at
|
|
10
|
+
construction, non-empty for strings) win over the pyproject+env layer.
|
|
11
|
+
LLM API keys are never read here: they come only from the provider clients
|
|
12
|
+
themselves.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pydantic import ValidationError
|
|
20
|
+
|
|
21
|
+
from ..failures.errors import PrettyplayError
|
|
22
|
+
from .models import Config
|
|
23
|
+
|
|
24
|
+
if sys.version_info >= (3, 11):
|
|
25
|
+
import tomllib
|
|
26
|
+
else:
|
|
27
|
+
import tomli as tomllib
|
|
28
|
+
|
|
29
|
+
#: The removed legacy env name; set — the load fails loudly before merging.
|
|
30
|
+
_LEGACY_BROWSER_ENV = "PRETTYPLAY_BROWSER"
|
|
31
|
+
|
|
32
|
+
#: Env variable name of every overridable setting; the two browser entries
|
|
33
|
+
#: keep their historical special names.
|
|
34
|
+
_ENV_NAMES: dict[str, str] = {
|
|
35
|
+
field: f"PRETTYPLAY_{field.upper()}"
|
|
36
|
+
for field in (
|
|
37
|
+
"provider",
|
|
38
|
+
"model",
|
|
39
|
+
"generation_model",
|
|
40
|
+
"classification_model",
|
|
41
|
+
"base_url",
|
|
42
|
+
"cache_root",
|
|
43
|
+
"generation_prompt",
|
|
44
|
+
"browser_endpoint",
|
|
45
|
+
"generation_attempts",
|
|
46
|
+
"healing_attempts",
|
|
47
|
+
"send_screenshots",
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
_ENV_NAMES["browser"] = "PRETTYPLAY_BROWSER_NAME"
|
|
51
|
+
_ENV_NAMES["headless"] = "PRETTYPLAY_BROWSER_HEADLESS"
|
|
52
|
+
|
|
53
|
+
#: The allowed-values text of the settings the validation render names.
|
|
54
|
+
_ALLOWED_TEXT: dict[str, str] = {
|
|
55
|
+
"provider": "openai, anthropic",
|
|
56
|
+
"browser": "chromium, firefox, webkit, chrome, msedge",
|
|
57
|
+
"generation_attempts": "a positive integer",
|
|
58
|
+
"healing_attempts": "a positive integer",
|
|
59
|
+
"headless": "a boolean",
|
|
60
|
+
"send_screenshots": "a boolean",
|
|
61
|
+
"model": "a non-empty string",
|
|
62
|
+
"generation_model": "a non-empty string",
|
|
63
|
+
"classification_model": "a non-empty string",
|
|
64
|
+
"base_url": "a non-empty string",
|
|
65
|
+
"cache_root": "a non-empty string",
|
|
66
|
+
"generation_prompt": "a non-empty string",
|
|
67
|
+
"browser_endpoint": "a valid ws/wss URL",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ConfigurationError(PrettyplayError):
|
|
72
|
+
"""An invalid prettyplay configuration: the loaded settings failed validation.
|
|
73
|
+
|
|
74
|
+
Raised by :func:`load_config` with the original pydantic
|
|
75
|
+
``ValidationError`` chained; catchable with the single library except
|
|
76
|
+
clause. Never carries a verdict — a configuration failure is not a step
|
|
77
|
+
failure.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
message: the rendered actionable text — one line per invalid setting.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _find_pyproject() -> Path:
|
|
85
|
+
"""Return the first existing pyproject.toml upward from the cwd.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Absolute path of the found pyproject.toml.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
FileNotFoundError: when no pyproject.toml exists upward from the cwd.
|
|
92
|
+
"""
|
|
93
|
+
for directory in (Path.cwd(), *Path.cwd().parents):
|
|
94
|
+
candidate = directory / "pyproject.toml"
|
|
95
|
+
if candidate.is_file():
|
|
96
|
+
return candidate
|
|
97
|
+
|
|
98
|
+
raise FileNotFoundError("pyproject.toml not found upward from the current directory")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _collect_env_overrides() -> dict[str, str]:
|
|
102
|
+
"""Collect the set environment overrides of all supported settings."""
|
|
103
|
+
overrides: dict[str, str] = {}
|
|
104
|
+
|
|
105
|
+
for field, env_name in _ENV_NAMES.items():
|
|
106
|
+
value = os.environ.get(env_name)
|
|
107
|
+
if value is not None:
|
|
108
|
+
overrides[field] = value
|
|
109
|
+
|
|
110
|
+
return overrides
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _render_validation(error: ValidationError) -> str:
|
|
114
|
+
"""Render the pydantic error as actionable per-setting lines.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
error: the pydantic validation failure of the merged settings.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
One line per invalid setting — the setting name, the received value
|
|
121
|
+
and the allowed values or range — joined with newlines; no pydantic
|
|
122
|
+
internals in the user-visible text.
|
|
123
|
+
"""
|
|
124
|
+
lines: list[str] = []
|
|
125
|
+
|
|
126
|
+
for entry in error.errors():
|
|
127
|
+
field = entry["loc"][0]
|
|
128
|
+
received = entry.get("input")
|
|
129
|
+
allowed = _ALLOWED_TEXT.get(str(field), entry["msg"])
|
|
130
|
+
lines.append(f"{field}: received {received!r} — allowed: {allowed}")
|
|
131
|
+
|
|
132
|
+
return "\n".join(lines)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_config(pyproject_path: str | None = None, overrides: Config | None = None) -> Config:
|
|
136
|
+
"""Load validated settings layered pyproject → env → explicit programmatic values.
|
|
137
|
+
|
|
138
|
+
The section is optional: a missing ``[tool.prettyplay]`` yields defaults.
|
|
139
|
+
Environment variables override the file value whenever they are set, empty
|
|
140
|
+
string included: ``PRETTYPLAY_BROWSER_NAME`` for the browser,
|
|
141
|
+
``PRETTYPLAY_BROWSER_HEADLESS`` for headless and
|
|
142
|
+
``PRETTYPLAY_<SETTING_UPPER>`` for every other setting. An empty
|
|
143
|
+
``cache_root`` resolves to ``<pyproject_dir>/.prettyplay/cache``.
|
|
144
|
+
|
|
145
|
+
The programmatic layer wins last: a field of ``overrides`` participates
|
|
146
|
+
when it was passed at construction (``model_fields_set``) and is non-empty
|
|
147
|
+
for strings — explicitly set values win, untouched model defaults and
|
|
148
|
+
empty strings never overwrite the pyproject+env values.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
pyproject_path: explicit pyproject.toml path; ``None`` auto-searches
|
|
152
|
+
upward from the current working directory.
|
|
153
|
+
overrides: the programmatic layer, typically a ``PrettyConfig`` built
|
|
154
|
+
by the integrator; ``None`` (or an instance with no explicitly set
|
|
155
|
+
fields) returns the validated file layer as is.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Validated configuration.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
FileNotFoundError: when ``pyproject_path`` is ``None`` and no
|
|
162
|
+
pyproject.toml is found upward from the cwd.
|
|
163
|
+
tomllib.TOMLDecodeError: when the file is not valid TOML.
|
|
164
|
+
ConfigurationError: when merged settings fail validation — the
|
|
165
|
+
original ``ValidationError`` chained as the cause.
|
|
166
|
+
"""
|
|
167
|
+
path = Path(pyproject_path) if pyproject_path is not None else _find_pyproject()
|
|
168
|
+
|
|
169
|
+
with path.open("rb") as stream:
|
|
170
|
+
data = tomllib.load(stream)
|
|
171
|
+
section = data.get("tool", {}).get("prettyplay", {})
|
|
172
|
+
|
|
173
|
+
if _LEGACY_BROWSER_ENV in os.environ:
|
|
174
|
+
raise ConfigurationError(f"{_LEGACY_BROWSER_ENV} is no longer supported: use PRETTYPLAY_BROWSER_NAME")
|
|
175
|
+
|
|
176
|
+
merged = {**section, **_collect_env_overrides()}
|
|
177
|
+
|
|
178
|
+
if not merged.get("cache_root"):
|
|
179
|
+
merged["cache_root"] = str(path.parent / ".prettyplay" / "cache")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
file_config = Config(**merged)
|
|
183
|
+
except ValidationError as error:
|
|
184
|
+
raise ConfigurationError(_render_validation(error)) from error
|
|
185
|
+
|
|
186
|
+
if overrides is None:
|
|
187
|
+
return file_config
|
|
188
|
+
|
|
189
|
+
explicit = overrides.model_fields_set
|
|
190
|
+
update = {name: value for name, value in overrides if name in explicit and (value or not isinstance(value, str))}
|
|
191
|
+
|
|
192
|
+
return file_config.model_copy(update=update)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Validated project settings of prettyplay.
|
|
2
|
+
|
|
3
|
+
The single source of the immutable configuration part. Secrets (LLM API keys)
|
|
4
|
+
never live here: they come only from environment variables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, PositiveInt, field_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Config(BaseModel):
|
|
14
|
+
"""Validated project settings loaded from ``[tool.prettyplay]``.
|
|
15
|
+
|
|
16
|
+
Every field has an empty default so the model can be constructed before a
|
|
17
|
+
pyproject.toml section exists; ``load_config`` fills and resolves it.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
provider: the LLM provider of the {openai, anthropic} set; default openai.
|
|
21
|
+
browser: browser of the {chromium, firefox, webkit, chrome, msedge} set;
|
|
22
|
+
chrome and msedge launch the locally installed browser through the
|
|
23
|
+
driver channel mechanism; default chromium.
|
|
24
|
+
model: main LLM model name.
|
|
25
|
+
generation_model: optional generation override; empty falls back to ``model``.
|
|
26
|
+
classification_model: optional classification override; empty falls back to ``model``.
|
|
27
|
+
base_url: optional custom LLM API endpoint.
|
|
28
|
+
cache_root: cache root; empty means the default resolved at load.
|
|
29
|
+
generation_prompt: user instructions for generation requests; non-empty
|
|
30
|
+
renders a separate USER INSTRUCTIONS block in generation and
|
|
31
|
+
regeneration requests; empty means no block.
|
|
32
|
+
browser_endpoint: ws endpoint of a remote browser; empty means the local
|
|
33
|
+
launch; on a remote connect headless is ignored.
|
|
34
|
+
generation_attempts: generation attempt budget per step per test; default 3.
|
|
35
|
+
healing_attempts: healing attempt budget per step per test; default 2.
|
|
36
|
+
send_screenshots: whether screenshots are attached to LLM requests.
|
|
37
|
+
headless: run the browser without a visible window; default True.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(kw_only=True)
|
|
41
|
+
|
|
42
|
+
provider: Literal["openai", "anthropic"] = "openai"
|
|
43
|
+
browser: Literal["chromium", "firefox", "webkit", "chrome", "msedge"] = "chromium"
|
|
44
|
+
model: str = ""
|
|
45
|
+
generation_model: str = ""
|
|
46
|
+
classification_model: str = ""
|
|
47
|
+
base_url: str = ""
|
|
48
|
+
cache_root: str = ""
|
|
49
|
+
generation_prompt: str = ""
|
|
50
|
+
browser_endpoint: str = ""
|
|
51
|
+
generation_attempts: PositiveInt = 3
|
|
52
|
+
healing_attempts: PositiveInt = 2
|
|
53
|
+
send_screenshots: bool = False
|
|
54
|
+
headless: bool = True
|
|
55
|
+
|
|
56
|
+
@field_validator("browser_endpoint")
|
|
57
|
+
@classmethod
|
|
58
|
+
def _validate_browser_endpoint(cls, value: str) -> str:
|
|
59
|
+
"""Check that a non-empty endpoint is a ws/wss URL.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
value: the raw endpoint setting; empty means the local launch.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The unchanged endpoint when empty or a valid ws/wss URL.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: when a non-empty endpoint is not a ws/wss URL.
|
|
69
|
+
"""
|
|
70
|
+
if not value:
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
parsed = urlparse(value)
|
|
74
|
+
|
|
75
|
+
if parsed.scheme not in {"ws", "wss"} or not parsed.netloc:
|
|
76
|
+
raise ValueError("must be a valid ws/wss URL")
|
|
77
|
+
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def effective_generation_model(self) -> str:
|
|
82
|
+
"""Return generation_model when non-empty, otherwise model."""
|
|
83
|
+
return self.generation_model if self.generation_model else self.model
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def effective_classification_model(self) -> str:
|
|
87
|
+
"""Return classification_model when non-empty, otherwise model."""
|
|
88
|
+
return self.classification_model if self.classification_model else self.model
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
#: The public name of ``Config``: the integrator-facing settings model.
|
|
92
|
+
PrettyConfig = Config
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Driver facade
|
|
2
|
+
|
|
3
|
+
Domain: the browser facade of prettyplay. Audience: consumers of the page API — the step generation engine and engineers reading or hand-writing step code.
|
|
4
|
+
|
|
5
|
+
The facade wraps the Playwright sync API. Step code receives a `PageFacade` and works only through it and `LocatorFacade` — never through raw Playwright objects. The method set is a backward-compatibility contract: cached step code keeps working across library upgrades.
|
|
6
|
+
|
|
7
|
+
## Surface
|
|
8
|
+
|
|
9
|
+
| Call | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| page.open(url) | navigate and wait for load |
|
|
12
|
+
| page.find_by_role(role, name) | element by aria role and accessible name |
|
|
13
|
+
| page.find_by_label(label) | element by associated label |
|
|
14
|
+
| page.find_by_text(text) | element by visible text |
|
|
15
|
+
| page.find_by_attribute(name, value) | element by attribute value — data-* attributes |
|
|
16
|
+
| page.find_by_css(selector) | element by CSS selector |
|
|
17
|
+
| page.find_by_xpath(xpath) | element by XPath expression |
|
|
18
|
+
| page.aria_snapshot() | accessibility-tree page state |
|
|
19
|
+
| page.screenshot() | full-page PNG bytes |
|
|
20
|
+
| page.url | current URL |
|
|
21
|
+
| page.scroll_to_element(element) | bring an element into the viewport (works inside scrollable ancestors) |
|
|
22
|
+
| page.scroll_down(pixels) | scroll the page down by an amount |
|
|
23
|
+
| page.scroll_up(pixels) | scroll the page up by an amount |
|
|
24
|
+
| page.scroll_to_bottom() | scroll to the end of the page |
|
|
25
|
+
| page.scroll_to_top() | scroll to the start of the page |
|
|
26
|
+
| page.scroll_into_view(element, container) | bring an element into view inside a specific scrollable container |
|
|
27
|
+
| page.scroll_container_down(container, pixels) | scroll a scrollable container down by an amount |
|
|
28
|
+
| page.scroll_container_up(container, pixels) | scroll a scrollable container up by an amount |
|
|
29
|
+
| element.click() | click with auto-wait |
|
|
30
|
+
| element.fill(value) | set input text |
|
|
31
|
+
| element.select_option(value) | choose an option |
|
|
32
|
+
| element.expect_visible() | assert visible |
|
|
33
|
+
| element.expect_text(text) | assert text |
|
|
34
|
+
| element.expect_enabled() | assert enabled |
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
page.open("https://example.com/login")
|
|
40
|
+
page.find_by_label("Username").fill("user")
|
|
41
|
+
page.find_by_label("Password").fill("secret")
|
|
42
|
+
page.find_by_role("button", name="Sign in").click()
|
|
43
|
+
page.find_by_text("Welcome back").expect_visible()
|
|
44
|
+
|
|
45
|
+
# locating by data attributes, CSS and XPath
|
|
46
|
+
page.find_by_attribute("data-test-id", "submit-button").click()
|
|
47
|
+
page.find_by_css("form > button.primary").expect_enabled()
|
|
48
|
+
page.find_by_xpath("//button[@type='submit']").expect_visible()
|
|
49
|
+
|
|
50
|
+
# scroll scenarios
|
|
51
|
+
page.scroll_down(600)
|
|
52
|
+
page.find_by_text("Footer").expect_visible()
|
|
53
|
+
|
|
54
|
+
cards = page.find_by_role("list", name="Recommendations")
|
|
55
|
+
page.scroll_container_down(cards, 400)
|
|
56
|
+
page.find_by_text("Fifth card").expect_visible()
|
|
57
|
+
|
|
58
|
+
snapshot = page.aria_snapshot()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Rules
|
|
62
|
+
|
|
63
|
+
- One browser process per test: each test owns its browser through its runtime; contexts stay isolated
|
|
64
|
+
- Every call executes in the library's driver thread and returns when done: driving is strictly sequential, and the calling thread never adopts the Playwright event loop — hand-written step code stays safe in interactive hosts (IPython, Jupyter)
|
|
65
|
+
- Auto-wait everywhere: no time.sleep, no fixed delays in step code — including around scrolls: the scrolled state is awaited through locators and expectations
|
|
66
|
+
- Never put secrets into step actions — step texts and code land in the repository cache
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Imports:
|
|
2
|
+
- Types:
|
|
3
|
+
- Config
|
|
4
|
+
From: prettyplay/config
|
|
5
|
+
|
|
6
|
+
Usages:
|
|
7
|
+
conventions: .goga/usages/conventions.md
|
|
8
|
+
playwright: .goga/usages/cooks/playwright.md
|
|
9
|
+
|
|
10
|
+
Annotations: |
|
|
11
|
+
Use `conventions` for code writing rules and testing.
|
|
12
|
+
Use `playwright` for the sync API lifecycle, locators, auto-wait, the accessibility snapshot, browser channels, remote connects and the scroll primitives.
|
|
13
|
+
|
|
14
|
+
The driver is Playwright sync-only: the async API is out of scope.
|
|
15
|
+
The whole Playwright session — start, browser, contexts, pages — lives in one dedicated driver thread owned by the library: the sync API parks its private event loop on its starting thread, so the thread executing the steps never holds a running asyncio loop (interactive hosts such as IPython and Jupyter keep working between steps).
|
|
16
|
+
Driver-thread calls are strictly sequential: one facade call runs at a time; concurrent driving is out of scope.
|
|
17
|
+
One browser process per test: the session is owned by the test's runtime — no state is shared between tests through the library.
|
|
18
|
+
The start mode branches on the browser_endpoint setting of `Config`: empty — local launch with headless and the channel for chrome/msedge; set — connect over the Playwright ws endpoint: headless is ignored, channels do not apply, the browser setting selects the engine (see `playwright`).
|
|
19
|
+
All waits go through locators and expectations; fixed delays (time.sleep and similar) are forbidden.
|
|
20
|
+
The facade surface is a backward-compatibility contract: generated step code works only through `PageFacade` and LocatorFacade, so the existing method set must not break across library releases — extend, never rename or remove.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
"DriverSession(config: Config)":
|
|
25
|
+
location: session.py
|
|
26
|
+
annotations: |
|
|
27
|
+
Lifecycle owner of the Playwright sync driver and the browser process of one test.
|
|
28
|
+
|
|
29
|
+
`config`: project settings; the browser setting selects the browser of the {chromium, firefox, webkit, chrome, msedge} set — chrome and msedge launch the locally installed browser through the channel mechanism; headless controls the window visibility of a local launch; browser_endpoint switches the start to a remote connect (see `playwright`).
|
|
30
|
+
|
|
31
|
+
Requirements:
|
|
32
|
+
- The Playwright session lives in a dedicated driver thread owned by the session: every Playwright-touching operation of this type runs there, and the calling thread never holds a running asyncio loop after any call
|
|
33
|
+
methods:
|
|
34
|
+
"open_context() -> page: PageFacade": |
|
|
35
|
+
Open a fresh isolated context with one page of this test's browser.
|
|
36
|
+
|
|
37
|
+
Algorithm:
|
|
38
|
+
1. Start lazily on the first call: constructing the session starts nothing — start the dedicated driver thread, then start Playwright inside it; an empty browser_endpoint — launch the selected engine locally with headless from the project settings and the channel for the chrome/msedge values; a set browser_endpoint — connect over the Playwright ws endpoint of the selected engine: headless is ignored and channels do not apply; a failed launch or connect stops the started driver and closes the thread, so a retry begins from a clean state
|
|
39
|
+
2. Create a fresh isolated browser context and its page inside the driver thread (see `playwright`)
|
|
40
|
+
3. Wrap the page into `PageFacade` bound to the driver thread and return it
|
|
41
|
+
|
|
42
|
+
Requirements:
|
|
43
|
+
- Each result is isolated from every other context
|
|
44
|
+
- A channel launch without the installed browser fails loudly with an actionable message naming the missing browser
|
|
45
|
+
- A failed connect fails loudly with an actionable message naming the endpoint
|
|
46
|
+
"close()": |
|
|
47
|
+
Stop the browser, the Playwright driver and the driver thread; safe to call when nothing was started.
|
|
48
|
+
|
|
49
|
+
"PageFacade(page: Page, context: BrowserContext)":
|
|
50
|
+
location: page.py
|
|
51
|
+
annotations: |
|
|
52
|
+
The narrow, stable facade of a single test page — the only page API the generated step code may use.
|
|
53
|
+
Wraps one isolated browser context created by `DriverSession`.
|
|
54
|
+
|
|
55
|
+
`page`: the wrapped Playwright page object; never exposed through the facade.
|
|
56
|
+
`context`: the isolated browser context owning the page; the boundary the close method closes.
|
|
57
|
+
|
|
58
|
+
Requirements:
|
|
59
|
+
- Every Playwright call runs in the driver thread of the owning session: the call blocks until it finishes, strictly one at a time, and the calling thread never adopts the Playwright event loop
|
|
60
|
+
- Locating methods never sleep: waiting is the locator's own auto-wait behavior
|
|
61
|
+
- Scroll methods never sleep: the scrolled state is awaited through locators and expectations
|
|
62
|
+
- aria_snapshot and screenshot reflect the state at call time
|
|
63
|
+
|
|
64
|
+
Constraints:
|
|
65
|
+
- No method exposes raw Playwright objects: the facade is the boundary generated code works against
|
|
66
|
+
properties:
|
|
67
|
+
"url -> str": |
|
|
68
|
+
The current page URL.
|
|
69
|
+
methods:
|
|
70
|
+
"open(url: str)": |
|
|
71
|
+
Navigate to `url` and wait for the load state (see `playwright`).
|
|
72
|
+
"find_by_role(role: str, name: str) -> element: LocatorFacade": |
|
|
73
|
+
Locate one element by its aria role and accessible name; returns a `LocatorFacade`.
|
|
74
|
+
"find_by_label(label: str) -> element: LocatorFacade": |
|
|
75
|
+
Locate one element by its associated label; returns a `LocatorFacade`.
|
|
76
|
+
"find_by_text(text: str) -> element: LocatorFacade": |
|
|
77
|
+
Locate one element by its visible text; returns a `LocatorFacade`.
|
|
78
|
+
"find_by_attribute(name: str, value: str) -> element: LocatorFacade": |
|
|
79
|
+
Locate one element by the value of the attribute `name` — the intended use is data-* attributes (data-test-id, data-qa and any other data attribute). Auto-waits exactly like the other locating methods.
|
|
80
|
+
|
|
81
|
+
`name`: the full attribute name, e.g. data-test-id.
|
|
82
|
+
`value`: the attribute value to match.
|
|
83
|
+
"find_by_css(selector: str) -> element: LocatorFacade": |
|
|
84
|
+
Locate one element by a CSS selector.
|
|
85
|
+
|
|
86
|
+
`selector`: a valid CSS selector expression, e.g. form > button.primary.
|
|
87
|
+
"find_by_xpath(xpath: str) -> element: LocatorFacade": |
|
|
88
|
+
Locate one element by an XPath expression.
|
|
89
|
+
|
|
90
|
+
`xpath`: a valid XPath expression, e.g. //button[@type='submit'].
|
|
91
|
+
"aria_snapshot() -> snapshot: str": |
|
|
92
|
+
The structured accessibility-tree representation of the page — the primary machine-readable page state (see `playwright`).
|
|
93
|
+
"screenshot() -> image: bytes": |
|
|
94
|
+
A full-page PNG image of the current state.
|
|
95
|
+
"scroll_to_element(element: LocatorFacade)": |
|
|
96
|
+
Scroll the page so `element` enters the viewport — inside its nearest scrollable ancestor when the element lives in a scrollable container (see `playwright`).
|
|
97
|
+
|
|
98
|
+
`element`: the located element to bring into view.
|
|
99
|
+
"scroll_down(pixels: int)": |
|
|
100
|
+
Scroll the page down by `pixels`.
|
|
101
|
+
|
|
102
|
+
`pixels`: a positive scroll amount in CSS pixels.
|
|
103
|
+
"scroll_up(pixels: int)": |
|
|
104
|
+
Scroll the page up by `pixels`.
|
|
105
|
+
|
|
106
|
+
`pixels`: a positive scroll amount in CSS pixels.
|
|
107
|
+
"scroll_to_bottom()": |
|
|
108
|
+
Scroll the page to its end.
|
|
109
|
+
"scroll_to_top()": |
|
|
110
|
+
Scroll the page to its start.
|
|
111
|
+
"scroll_into_view(element: LocatorFacade, container: LocatorFacade)": |
|
|
112
|
+
Bring `element` into the visible area of the specific scrollable `container` — for nested scrollables where the nearest-ancestor behavior of scroll_to_element is not enough (see `playwright`).
|
|
113
|
+
|
|
114
|
+
`element`: the located element to bring into view.
|
|
115
|
+
`container`: the located scrollable container, e.g. a carousel.
|
|
116
|
+
"scroll_container_down(container: LocatorFacade, pixels: int)": |
|
|
117
|
+
Scroll the scrollable `container` down by `pixels`.
|
|
118
|
+
|
|
119
|
+
`container`: the located scrollable container.
|
|
120
|
+
`pixels`: a positive scroll amount in CSS pixels.
|
|
121
|
+
"scroll_container_up(container: LocatorFacade, pixels: int)": |
|
|
122
|
+
Scroll the scrollable `container` up by `pixels`.
|
|
123
|
+
|
|
124
|
+
`container`: the located scrollable container.
|
|
125
|
+
`pixels`: a positive scroll amount in CSS pixels.
|
|
126
|
+
"close()": |
|
|
127
|
+
Close the isolated context of this page; the browser process keeps running.
|
|
128
|
+
|
|
129
|
+
"LocatorFacade(locator: Locator)":
|
|
130
|
+
location: page.py
|
|
131
|
+
annotations: |
|
|
132
|
+
An auto-waiting handle of one located element — the only element API the generated step code may use.
|
|
133
|
+
|
|
134
|
+
`locator`: the wrapped Playwright locator object; never exposed through the facade.
|
|
135
|
+
|
|
136
|
+
Requirements:
|
|
137
|
+
- Every action and expectation runs in the driver thread inherited from the page facade that created the handle
|
|
138
|
+
- Every action and expectation auto-waits for actionability (see `playwright`)
|
|
139
|
+
- Failed expectations raise assertion-style errors destined for failure classification
|
|
140
|
+
|
|
141
|
+
Constraints:
|
|
142
|
+
- No fixed delays; no raw Playwright objects exposed
|
|
143
|
+
methods:
|
|
144
|
+
"click()": |
|
|
145
|
+
Click the element, waiting for actionability.
|
|
146
|
+
"fill(value: str)": |
|
|
147
|
+
Set the text input value of the element to `value`.
|
|
148
|
+
"select_option(value: str)": |
|
|
149
|
+
Select the option with `value` in a list or combo box.
|
|
150
|
+
"expect_visible()": |
|
|
151
|
+
Assert the element is visible.
|
|
152
|
+
"expect_text(text: str)": |
|
|
153
|
+
Assert the element text equals or contains `text`.
|
|
154
|
+
"expect_enabled()": |
|
|
155
|
+
Assert the element is enabled.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
Author: Goga
|
|
160
|
+
CreatedAt: 07/09/26
|
|
161
|
+
Description: |
|
|
162
|
+
The Playwright sync driver of prettyplay: the per-test session with local launches and remote ws connects, and the narrow backward-compatible page facade with universal locators and scroll abilities for generated step code.
|