steppy-dev 0.3.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.
- steppy/CHANGELOG.md +862 -0
- steppy/__init__.py +9 -0
- steppy/__main__.py +13 -0
- steppy/_cli_common.py +183 -0
- steppy/_docker/Dockerfile.installed +53 -0
- steppy/_parallel/__init__.py +7 -0
- steppy/_parallel/compose.py +349 -0
- steppy/_parallel/event_bus.py +418 -0
- steppy/_parallel/failures.py +267 -0
- steppy/_parallel/fingerprint.py +141 -0
- steppy/_parallel/image.py +380 -0
- steppy/_parallel/lastfailed.py +125 -0
- steppy/_parallel/lifecycle.py +90 -0
- steppy/_parallel/preauth.py +109 -0
- steppy/_parallel/progress.py +101 -0
- steppy/_parallel/queue.py +385 -0
- steppy/_parallel/renderer.py +489 -0
- steppy/_parallel/retry.py +316 -0
- steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
- steppy/_player_bundle/__init__.py +100 -0
- steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
- steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
- steppy/_player_bundle/steppy-player.js +5419 -0
- steppy/_player_bundle/steppy-player.js.map +1 -0
- steppy/_player_bundle/steppy-video.js +472 -0
- steppy/_player_bundle/steppy-video.js.map +1 -0
- steppy/_templating.py +117 -0
- steppy/assertions.py +251 -0
- steppy/auth.py +493 -0
- steppy/build.py +2798 -0
- steppy/build_models.py +669 -0
- steppy/buildid.py +134 -0
- steppy/bundle/__init__.py +47 -0
- steppy/bundle/archive.py +239 -0
- steppy/bundle/classify.py +329 -0
- steppy/bundle/create.py +490 -0
- steppy/bundle/hashing.py +28 -0
- steppy/bundle/manifest.py +334 -0
- steppy/bundle/results.py +546 -0
- steppy/bundle_cli.py +455 -0
- steppy/captures.py +207 -0
- steppy/cli.py +2489 -0
- steppy/cloud/__init__.py +58 -0
- steppy/cloud/client.py +267 -0
- steppy/cloud/upload.py +497 -0
- steppy/compat.py +109 -0
- steppy/config.py +231 -0
- steppy/config_schema.py +87 -0
- steppy/contracts/__init__.py +54 -0
- steppy/contracts/bundle.py +215 -0
- steppy/contracts/report.py +346 -0
- steppy/contracts/tours.py +505 -0
- steppy/coordinator.py +1352 -0
- steppy/coverage/__init__.py +9 -0
- steppy/coverage/gaps.py +190 -0
- steppy/coverage/manifest.py +193 -0
- steppy/coverage/merge.py +363 -0
- steppy/coverage/normalize.py +174 -0
- steppy/coverage/report.py +178 -0
- steppy/coverage/sourcemap.py +219 -0
- steppy/coverage/store.py +473 -0
- steppy/coverage/triage.py +151 -0
- steppy/coverage_cli.py +265 -0
- steppy/durations.py +48 -0
- steppy/exclusive.py +178 -0
- steppy/flaky_cli.py +169 -0
- steppy/generator.py +633 -0
- steppy/guide.py +1467 -0
- steppy/history.py +352 -0
- steppy/init_cli.py +330 -0
- steppy/locales.py +282 -0
- steppy/migrate/__init__.py +35 -0
- steppy/migrate/apply.py +194 -0
- steppy/migrate/plan.py +199 -0
- steppy/migrate/rules.py +465 -0
- steppy/migrate/verify.py +163 -0
- steppy/migrate_cli.py +152 -0
- steppy/models.py +2539 -0
- steppy/naming.py +37 -0
- steppy/parser.py +475 -0
- steppy/pdf.py +193 -0
- steppy/presets.py +284 -0
- steppy/record_cli.py +796 -0
- steppy/report/__init__.py +37 -0
- steppy/report/assets.py +197 -0
- steppy/report/build.py +261 -0
- steppy/report/coverage_source.py +277 -0
- steppy/report/model.py +423 -0
- steppy/report/render.py +303 -0
- steppy/report/source.py +238 -0
- steppy/report_cli.py +173 -0
- steppy/runmeta.py +114 -0
- steppy/selection.py +183 -0
- steppy/sitemap.py +205 -0
- steppy/skills/steppy-app-tours/SKILL.md +448 -0
- steppy/skills/steppy-ci-report/SKILL.md +567 -0
- steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
- steppy/skills/steppy-template/SKILL.md +676 -0
- steppy/templates/_steppy_events.py +291 -0
- steppy/templates/_steppy_tours.py +404 -0
- steppy/templates/angular/component-interactive.ts.j2 +109 -0
- steppy/templates/angular/component-video.ts.j2 +112 -0
- steppy/templates/angular/component.ts.j2 +229 -0
- steppy/templates/angular/content.ts.j2 +31 -0
- steppy/templates/angular/gitignore.j2 +3 -0
- steppy/templates/angular/group.component.ts.j2 +39 -0
- steppy/templates/angular/onboarding.component.ts.j2 +43 -0
- steppy/templates/angular/routes.ts.j2 +34 -0
- steppy/templates/ci/github-actions.yml +284 -0
- steppy/templates/ci/gitlab-ci.yml +178 -0
- steppy/templates/conftest.py.j2 +2405 -0
- steppy/templates/guide.book.html.j2 +145 -0
- steppy/templates/guide.fragment.html.j2 +257 -0
- steppy/templates/guide.html.j2 +301 -0
- steppy/templates/guide.md.j2 +18 -0
- steppy/templates/init/config.yaml +124 -0
- steppy/templates/init/credentials.yaml.example +30 -0
- steppy/templates/init/scenarios/example--first-flow.md +53 -0
- steppy/templates/report/_head.html.j2 +14 -0
- steppy/templates/report/_mark.html.j2 +3 -0
- steppy/templates/report/coverage.html.j2 +124 -0
- steppy/templates/report/index.html.j2 +203 -0
- steppy/templates/report/report.css +401 -0
- steppy/templates/report/scenario.html.j2 +137 -0
- steppy/templates/static/_headers.j2 +30 -0
- steppy/templates/static/_mode_links.html.j2 +22 -0
- steppy/templates/static/_seo_head.html.j2 +37 -0
- steppy/templates/static/cursor.js.j2 +10 -0
- steppy/templates/static/group.html.j2 +78 -0
- steppy/templates/static/guides.css +678 -0
- steppy/templates/static/index.html.j2 +79 -0
- steppy/templates/static/interactive.html.j2 +100 -0
- steppy/templates/static/onboarding.html.j2 +64 -0
- steppy/templates/static/page.html.j2 +145 -0
- steppy/templates/static/robots.txt.j2 +7 -0
- steppy/templates/static/video.html.j2 +135 -0
- steppy/templates/test_scenario.py.j2 +767 -0
- steppy/testids.py +166 -0
- steppy/totp.py +26 -0
- steppy/tour_spec.py +163 -0
- steppy/tours/__init__.py +22 -0
- steppy/tours/actions.py +402 -0
- steppy/tours/compiler.py +144 -0
- steppy/tours/diagnostics.py +137 -0
- steppy/tours/emit.py +239 -0
- steppy/tours/ids.py +40 -0
- steppy/tours/inference.py +399 -0
- steppy/tours/model.py +120 -0
- steppy/tours/paths.py +126 -0
- steppy/tours/pipeline.py +277 -0
- steppy/tours/publish.py +124 -0
- steppy/tours/replay.py +234 -0
- steppy/tours/routes.py +175 -0
- steppy/tours/settings.py +164 -0
- steppy/tours/text.py +237 -0
- steppy/tours_cli.py +219 -0
- steppy/tts/__init__.py +280 -0
- steppy/tts/cache.py +185 -0
- steppy/tts/presets.py +125 -0
- steppy/tts/providers/__init__.py +6 -0
- steppy/tts/providers/http.py +184 -0
- steppy/tts/providers/local.py +207 -0
- steppy/tts/providers/null.py +28 -0
- steppy/tts/registry.py +201 -0
- steppy/tts/track.py +158 -0
- steppy/tts/types.py +94 -0
- steppy/update_cli.py +460 -0
- steppy/video.py +730 -0
- steppy/video_cache.py +239 -0
- steppy/video_cli.py +271 -0
- steppy/worker.py +301 -0
- steppy_dev-0.3.0.dist-info/METADATA +1335 -0
- steppy_dev-0.3.0.dist-info/RECORD +177 -0
- steppy_dev-0.3.0.dist-info/WHEEL +4 -0
- steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
- steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
- steppy_dev-0.3.0.dist-info/licenses/NOTICE +13 -0
steppy/pdf.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""PDF export — headless-Chromium print-to-PDF over already-generated guide HTML.
|
|
2
|
+
|
|
3
|
+
Steppy never lays out a PDF itself. The guide templates already carry
|
|
4
|
+
``@media print`` rules (hide the animated cursor, keep a step on one page, cap
|
|
5
|
+
image height so a tall capture is scaled instead of clipped), so a PDF is just
|
|
6
|
+
"print the page we already render". That keeps one source of truth for the
|
|
7
|
+
layout and means a themed project's PDFs inherit its theme for free.
|
|
8
|
+
|
|
9
|
+
A **leaf module**: stdlib + ``models`` only, and ``playwright`` is imported
|
|
10
|
+
inside :func:`render_pdfs` so importing ``steppy.cli`` stays cheap
|
|
11
|
+
(guard: ``tests/test_no_playwright_import_on_default_path.py``).
|
|
12
|
+
|
|
13
|
+
Two invariants worth keeping if you touch this:
|
|
14
|
+
|
|
15
|
+
* **Pages are loaded over ``file://``.** Guide HTML references its screenshots
|
|
16
|
+
with *relative* URLs; ``page.set_content`` would resolve them against
|
|
17
|
+
``about:blank`` and every image would be blank.
|
|
18
|
+
* **Lazy images are forced eager and awaited.** Both guide templates emit
|
|
19
|
+
``loading="lazy"``; images below the fold are the classic reason a
|
|
20
|
+
print-to-PDF comes out with empty stages.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import html as _html
|
|
26
|
+
import sys
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Callable, Optional, Sequence
|
|
30
|
+
|
|
31
|
+
from .models import PdfConfig
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PdfRenderError(Exception):
|
|
35
|
+
"""Fatal PDF-export failure — playwright missing, or Chromium unlaunchable.
|
|
36
|
+
|
|
37
|
+
Per-page failures are *not* this: they land in a :class:`PdfResult` so one
|
|
38
|
+
broken guide can't cost the other 149.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class PdfJob:
|
|
44
|
+
"""One page to print. ``title`` is only used for the footer."""
|
|
45
|
+
|
|
46
|
+
html: Path
|
|
47
|
+
pdf: Path
|
|
48
|
+
title: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class PdfResult:
|
|
53
|
+
job: PdfJob
|
|
54
|
+
ok: bool
|
|
55
|
+
error: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Chromium renders header/footer templates in their own document: external
|
|
59
|
+
# styles do not apply and an unset font-size renders at zero. Only the special
|
|
60
|
+
# classes (pageNumber / totalPages / title / date / url) are substituted.
|
|
61
|
+
_EMPTY_HEADER = "<span></span>"
|
|
62
|
+
|
|
63
|
+
_FOOTER_STYLE = (
|
|
64
|
+
"width:100%;font-size:9px;color:#666;padding:0 12mm;"
|
|
65
|
+
"font-family:-apple-system,'Segoe UI',Roboto,sans-serif;"
|
|
66
|
+
"display:flex;justify-content:space-between;"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Flip lazy images to eager so Chromium starts loading the ones below the fold.
|
|
70
|
+
_EAGER_IMAGES_JS = """() => {
|
|
71
|
+
for (const img of document.images) {
|
|
72
|
+
if (img.loading === 'lazy') { img.loading = 'eager'; }
|
|
73
|
+
}
|
|
74
|
+
}"""
|
|
75
|
+
|
|
76
|
+
_IMAGES_COMPLETE_JS = "() => Array.from(document.images).every(img => img.complete)"
|
|
77
|
+
|
|
78
|
+
_BROWSER_HINT = "Chromium is required for PDF export. Install it with:\n python -m playwright install chromium"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _footer_template(title: str) -> str:
|
|
82
|
+
"""Footer with the guide title on the left and ``N / M`` on the right."""
|
|
83
|
+
return (
|
|
84
|
+
f'<div style="{_FOOTER_STYLE}">'
|
|
85
|
+
f"<span>{_html.escape(title)}</span>"
|
|
86
|
+
'<span><span class="pageNumber"></span> / <span class="totalPages"></span></span>'
|
|
87
|
+
"</div>"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def pdf_options(options: PdfConfig, title: str = "") -> dict:
|
|
92
|
+
"""Map a :class:`~steppy.models.PdfConfig` onto ``page.pdf()`` kwargs.
|
|
93
|
+
|
|
94
|
+
Split out from :func:`render_pdfs` so the mapping is testable without a
|
|
95
|
+
browser. ``path`` is added by the caller.
|
|
96
|
+
"""
|
|
97
|
+
kwargs: dict = {
|
|
98
|
+
"format": options.format,
|
|
99
|
+
"landscape": options.landscape,
|
|
100
|
+
"print_background": options.print_background,
|
|
101
|
+
"scale": options.scale,
|
|
102
|
+
"margin": {
|
|
103
|
+
"top": options.margin.top,
|
|
104
|
+
"right": options.margin.right,
|
|
105
|
+
"bottom": options.margin.bottom,
|
|
106
|
+
"left": options.margin.left,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
if options.page_numbers:
|
|
110
|
+
kwargs["display_header_footer"] = True
|
|
111
|
+
# Without an explicit header template Chromium prints its own
|
|
112
|
+
# (date + document title), which is not what "page numbers" asked for.
|
|
113
|
+
kwargs["header_template"] = _EMPTY_HEADER
|
|
114
|
+
kwargs["footer_template"] = _footer_template(title)
|
|
115
|
+
return kwargs
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _default_warn(message: str) -> None:
|
|
119
|
+
print(f"WARNING: {message}", file=sys.stderr)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def render_pdfs(
|
|
123
|
+
jobs: Sequence[PdfJob],
|
|
124
|
+
options: Optional[PdfConfig] = None,
|
|
125
|
+
*,
|
|
126
|
+
on_done: Optional[Callable[[PdfResult], None]] = None,
|
|
127
|
+
on_warning: Optional[Callable[[str], None]] = None,
|
|
128
|
+
) -> list[PdfResult]:
|
|
129
|
+
"""Print every job to PDF, reusing one browser and one page for the batch.
|
|
130
|
+
|
|
131
|
+
A guide project is a cartesian of scenarios × locales × layouts, so the
|
|
132
|
+
batch is routinely in the hundreds — a browser launch per file would
|
|
133
|
+
dominate the runtime.
|
|
134
|
+
|
|
135
|
+
``on_done`` is invoked after each job so callers can stream progress.
|
|
136
|
+
Returns one :class:`PdfResult` per job, in order. Raises
|
|
137
|
+
:class:`PdfRenderError` only when the browser itself can't start.
|
|
138
|
+
"""
|
|
139
|
+
if not jobs:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
options = options or PdfConfig()
|
|
143
|
+
warn = on_warning or _default_warn
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
from playwright.sync_api import sync_playwright
|
|
147
|
+
except ImportError as exc: # pragma: no cover - playwright is a hard dep
|
|
148
|
+
raise PdfRenderError(f"playwright is not installed: {exc}") from exc
|
|
149
|
+
|
|
150
|
+
results: list[PdfResult] = []
|
|
151
|
+
try:
|
|
152
|
+
with sync_playwright() as p:
|
|
153
|
+
try:
|
|
154
|
+
browser = p.chromium.launch()
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
raise PdfRenderError(f"Could not launch Chromium: {exc}\n{_BROWSER_HINT}") from exc
|
|
157
|
+
try:
|
|
158
|
+
page = browser.new_page()
|
|
159
|
+
for job in jobs:
|
|
160
|
+
result = _render_one(page, job, options, warn)
|
|
161
|
+
results.append(result)
|
|
162
|
+
if on_done:
|
|
163
|
+
on_done(result)
|
|
164
|
+
finally:
|
|
165
|
+
browser.close()
|
|
166
|
+
except PdfRenderError:
|
|
167
|
+
raise
|
|
168
|
+
except Exception as exc: # driver startup / teardown failures
|
|
169
|
+
raise PdfRenderError(f"PDF export failed: {exc}\n{_BROWSER_HINT}") from exc
|
|
170
|
+
|
|
171
|
+
return results
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _render_one(page, job: PdfJob, options: PdfConfig, warn: Callable[[str], None]) -> PdfResult:
|
|
175
|
+
"""Print a single job. Never raises — failures come back as a result."""
|
|
176
|
+
try:
|
|
177
|
+
if not job.html.is_file():
|
|
178
|
+
return PdfResult(job=job, ok=False, error=f"source HTML not found: {job.html}")
|
|
179
|
+
|
|
180
|
+
page.goto(job.html.resolve().as_uri(), wait_until="load", timeout=options.timeout)
|
|
181
|
+
page.evaluate(_EAGER_IMAGES_JS)
|
|
182
|
+
try:
|
|
183
|
+
page.wait_for_function(_IMAGES_COMPLETE_JS, timeout=options.timeout)
|
|
184
|
+
except Exception:
|
|
185
|
+
# A PDF with one missing screenshot beats no PDF at all — say so
|
|
186
|
+
# and print anyway.
|
|
187
|
+
warn(f"{job.html.name}: some screenshots did not finish loading within {options.timeout}ms")
|
|
188
|
+
|
|
189
|
+
job.pdf.parent.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
page.pdf(path=str(job.pdf), **pdf_options(options, job.title))
|
|
191
|
+
return PdfResult(job=job, ok=True)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
return PdfResult(job=job, ok=False, error=str(exc))
|
steppy/presets.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""UI-kit selector presets for the component-driver config blocks (FLAWS §4.3).
|
|
2
|
+
|
|
3
|
+
The ``select-option`` / ``confirm`` / ``toggle`` / ``pick-date`` / ``pick-time`` /
|
|
4
|
+
``expand`` actions drive framework components (dropdowns, dialogs, switches,
|
|
5
|
+
datepickers, fieldsets) whose internal markup has no stable ``data-testid``.
|
|
6
|
+
Their selectors therefore come from a config preset. Historically the only
|
|
7
|
+
preset was PrimeNG, baked in as the field defaults.
|
|
8
|
+
|
|
9
|
+
A config block may now name a ``preset:`` and get a whole kit's selectors at
|
|
10
|
+
once — e.g. ``dropdown: {preset: material}``. Explicitly set selector fields
|
|
11
|
+
always win over the preset; omitting ``preset:`` keeps the PrimeNG defaults, so
|
|
12
|
+
existing configs are byte-identical.
|
|
13
|
+
|
|
14
|
+
``primeng`` is authoritative (it mirrors the long-standing defaults, verified
|
|
15
|
+
against real PrimeNG apps). Every other preset — ``material`` (Angular Material
|
|
16
|
+
MDC), ``primevue`` (PrimeVue), ``mui`` (Material UI / React), ``vuetify``
|
|
17
|
+
(Vuetify 3 / Vue), and ``shadcn`` (shadcn/ui + Radix / React) — is a
|
|
18
|
+
**best-effort starter set**: the markup varies by framework version and by
|
|
19
|
+
add-ons, so treat these selectors as a starting point and override any that
|
|
20
|
+
does not match your app. They are foundational and will be hardened against
|
|
21
|
+
real markup as Steppy is exercised on cross-framework showcase projects; until
|
|
22
|
+
then, `steppy validate` + a quick run against your app is the way to confirm
|
|
23
|
+
them.
|
|
24
|
+
|
|
25
|
+
Each entry maps a config key (``dropdown``/``dialog``/``toggle``/
|
|
26
|
+
``datepicker``/``fieldset``) to the selector fields of the matching model.
|
|
27
|
+
Only selector fields appear here — non-selector fields (e.g. dropdown
|
|
28
|
+
``settle``) are never touched by a preset.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
PRESETS: dict[str, dict[str, dict[str, str]]] = {
|
|
32
|
+
# ── PrimeNG (default) ────────────────────────────────────────────────────
|
|
33
|
+
# Mirrors the field defaults on the models below. Keep in sync if a default
|
|
34
|
+
# changes, so `preset: primeng` stays equivalent to omitting `preset`.
|
|
35
|
+
"primeng": {
|
|
36
|
+
"dropdown": {
|
|
37
|
+
"trigger": "p-select",
|
|
38
|
+
"overlay": ".p-select-overlay",
|
|
39
|
+
"option": ".p-select-option, .p-select-list-container li",
|
|
40
|
+
"filter": ".p-select-filter-container input, .p-select-filter",
|
|
41
|
+
},
|
|
42
|
+
"dialog": {
|
|
43
|
+
"overlay": ".p-confirmdialog, p-confirmdialog",
|
|
44
|
+
"accept": "button.p-confirmdialog-accept-button, button.p-confirm-dialog-accept",
|
|
45
|
+
"reject": "button.p-confirmdialog-reject-button, button.p-confirm-dialog-reject",
|
|
46
|
+
"danger": "button.p-button-danger, button.p-confirmdialog-accept-button",
|
|
47
|
+
},
|
|
48
|
+
"toggle": {
|
|
49
|
+
"switch": 'input[role="switch"]',
|
|
50
|
+
"segment": ".p-togglebutton",
|
|
51
|
+
},
|
|
52
|
+
"datepicker": {
|
|
53
|
+
"panel": ".p-datepicker-panel",
|
|
54
|
+
"next_button": ".p-datepicker-next-button",
|
|
55
|
+
"prev_button": ".p-datepicker-prev-button",
|
|
56
|
+
"day_cell": ".p-datepicker-day-cell:not(.p-datepicker-other-month)",
|
|
57
|
+
"trigger": "button.p-datepicker-dropdown, button.p-datepicker-trigger",
|
|
58
|
+
"hour_picker": ".p-datepicker-hour-picker",
|
|
59
|
+
"minute_picker": ".p-datepicker-minute-picker",
|
|
60
|
+
},
|
|
61
|
+
"fieldset": {
|
|
62
|
+
"toggle": ".p-fieldset-toggle-button, .p-fieldset-legend",
|
|
63
|
+
"content": ".p-toggleable-content, .p-fieldset-content-container",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
# ── Angular Material (MDC) — starter set, verify against your app ─────────
|
|
67
|
+
"material": {
|
|
68
|
+
"dropdown": {
|
|
69
|
+
"trigger": "mat-select",
|
|
70
|
+
"overlay": ".cdk-overlay-pane .mat-mdc-select-panel",
|
|
71
|
+
"option": "mat-option, .mat-mdc-option",
|
|
72
|
+
# mat-select has no native filter; ngx-mat-select-search adds one.
|
|
73
|
+
"filter": ".mat-select-search-input, input.mat-mdc-input-element",
|
|
74
|
+
},
|
|
75
|
+
"dialog": {
|
|
76
|
+
"overlay": "mat-dialog-container, .mat-mdc-dialog-container",
|
|
77
|
+
"accept": '.mat-mdc-dialog-actions button[color="primary"], .mat-mdc-dialog-actions button:last-child',
|
|
78
|
+
"reject": '.mat-mdc-dialog-actions button:not([color="primary"]), .mat-mdc-dialog-actions button:first-child',
|
|
79
|
+
"danger": '.mat-mdc-dialog-actions button[color="warn"]',
|
|
80
|
+
},
|
|
81
|
+
"toggle": {
|
|
82
|
+
"switch": 'mat-slide-toggle button[role="switch"], .mat-mdc-slide-toggle button',
|
|
83
|
+
"segment": ".mat-button-toggle",
|
|
84
|
+
},
|
|
85
|
+
"datepicker": {
|
|
86
|
+
"panel": ".mat-datepicker-content, .mat-calendar",
|
|
87
|
+
"next_button": ".mat-calendar-next-button",
|
|
88
|
+
"prev_button": ".mat-calendar-previous-button",
|
|
89
|
+
"day_cell": ".mat-calendar-body-cell:not(.mat-calendar-body-disabled)",
|
|
90
|
+
"trigger": ".mat-datepicker-toggle button",
|
|
91
|
+
# Material core has no time picker; ngx-mat-timepicker markup shown.
|
|
92
|
+
"hour_picker": ".ngx-mat-timepicker .mat-mdc-form-field:nth-child(1) input",
|
|
93
|
+
"minute_picker": ".ngx-mat-timepicker .mat-mdc-form-field:nth-child(2) input",
|
|
94
|
+
},
|
|
95
|
+
"fieldset": {
|
|
96
|
+
"toggle": "mat-expansion-panel-header, .mat-expansion-panel-header",
|
|
97
|
+
"content": ".mat-expansion-panel-content, .mat-expansion-panel-body",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
# ── PrimeVue — starter set, verify against your app ───────────────────────
|
|
101
|
+
# PrimeVue shares PrimeNG's ``.p-*`` DNA, so the cheapest cross-framework
|
|
102
|
+
# win is to reuse the PrimeNG selectors almost verbatim. Kept as its own
|
|
103
|
+
# entry (rather than aliasing) so it can diverge as PrimeVue-specific markup
|
|
104
|
+
# is discovered on real apps.
|
|
105
|
+
"primevue": {
|
|
106
|
+
"dropdown": {
|
|
107
|
+
"trigger": ".p-select, .p-dropdown",
|
|
108
|
+
"overlay": ".p-select-overlay, .p-dropdown-panel",
|
|
109
|
+
"option": ".p-select-option, .p-dropdown-item, .p-select-list-container li",
|
|
110
|
+
"filter": ".p-select-filter, .p-dropdown-filter, .p-select-filter-container input",
|
|
111
|
+
},
|
|
112
|
+
"dialog": {
|
|
113
|
+
"overlay": ".p-confirmdialog, .p-confirm-dialog",
|
|
114
|
+
"accept": "button.p-confirmdialog-accept-button, button.p-confirm-dialog-accept",
|
|
115
|
+
"reject": "button.p-confirmdialog-reject-button, button.p-confirm-dialog-reject",
|
|
116
|
+
"danger": "button.p-button-danger, button.p-confirmdialog-accept-button",
|
|
117
|
+
},
|
|
118
|
+
"toggle": {
|
|
119
|
+
"switch": 'input[role="switch"], .p-toggleswitch-input',
|
|
120
|
+
"segment": ".p-togglebutton",
|
|
121
|
+
},
|
|
122
|
+
"datepicker": {
|
|
123
|
+
"panel": ".p-datepicker-panel",
|
|
124
|
+
"next_button": ".p-datepicker-next-button",
|
|
125
|
+
"prev_button": ".p-datepicker-prev-button",
|
|
126
|
+
"day_cell": ".p-datepicker-day-cell:not(.p-datepicker-other-month)",
|
|
127
|
+
"trigger": "button.p-datepicker-dropdown, button.p-datepicker-trigger",
|
|
128
|
+
"hour_picker": ".p-datepicker-hour-picker",
|
|
129
|
+
"minute_picker": ".p-datepicker-minute-picker",
|
|
130
|
+
},
|
|
131
|
+
"fieldset": {
|
|
132
|
+
"toggle": ".p-fieldset-toggle-button, .p-fieldset-legend",
|
|
133
|
+
"content": ".p-toggleable-content, .p-fieldset-content-container",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
# ── Material UI (React / MUI v5–v6) — starter set, verify against your app ─
|
|
137
|
+
# MUI ships stable ``.Mui*`` class names; the date picker is the ``@mui/x``
|
|
138
|
+
# add-on (``.MuiPickers*`` / ``.MuiClock*``). Override if you use a custom
|
|
139
|
+
# theme that strips the default classes.
|
|
140
|
+
"mui": {
|
|
141
|
+
"dropdown": {
|
|
142
|
+
"trigger": ".MuiSelect-select, .MuiAutocomplete-root .MuiInputBase-root",
|
|
143
|
+
"overlay": ".MuiMenu-paper, .MuiAutocomplete-popper .MuiPaper-root",
|
|
144
|
+
"option": ".MuiMenuItem-root, .MuiAutocomplete-option",
|
|
145
|
+
"filter": ".MuiAutocomplete-input, .MuiInputBase-input",
|
|
146
|
+
},
|
|
147
|
+
"dialog": {
|
|
148
|
+
"overlay": ".MuiDialog-paper",
|
|
149
|
+
"accept": ".MuiDialogActions-root button:last-child",
|
|
150
|
+
"reject": ".MuiDialogActions-root button:first-child",
|
|
151
|
+
"danger": ".MuiDialogActions-root button.MuiButton-colorError, .MuiDialogActions-root button.MuiButton-containedError",
|
|
152
|
+
},
|
|
153
|
+
"toggle": {
|
|
154
|
+
"switch": ".MuiSwitch-input",
|
|
155
|
+
"segment": ".MuiToggleButton-root",
|
|
156
|
+
},
|
|
157
|
+
"datepicker": {
|
|
158
|
+
"panel": ".MuiPickersLayout-root, .MuiDateCalendar-root",
|
|
159
|
+
"next_button": '.MuiPickersArrowSwitcher-button[title="Next month"], .MuiPickersArrowSwitcher-button:last-child',
|
|
160
|
+
"prev_button": '.MuiPickersArrowSwitcher-button[title="Previous month"], .MuiPickersArrowSwitcher-button:first-child',
|
|
161
|
+
"day_cell": ".MuiPickersDay-root:not(.Mui-disabled):not(.MuiPickersDay-dayOutsideMonth)",
|
|
162
|
+
"trigger": ".MuiInputAdornment-root button, button.MuiIconButton-root",
|
|
163
|
+
# @mui/x digital clock: sections of selectable minute/hour options.
|
|
164
|
+
"hour_picker": ".MuiMultiSectionDigitalClockSection-root:nth-child(1)",
|
|
165
|
+
"minute_picker": ".MuiMultiSectionDigitalClockSection-root:nth-child(2)",
|
|
166
|
+
},
|
|
167
|
+
"fieldset": {
|
|
168
|
+
"toggle": ".MuiAccordionSummary-root",
|
|
169
|
+
"content": ".MuiAccordionDetails-root, .MuiCollapse-root",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
# ── Vuetify 3 (Vue) — starter set, verify against your app ────────────────
|
|
173
|
+
# Vuetify renders overlays into a ``.v-overlay-container``; components use
|
|
174
|
+
# stable ``.v-*`` BEM-ish classes. The date picker is ``.v-date-picker``.
|
|
175
|
+
"vuetify": {
|
|
176
|
+
"dropdown": {
|
|
177
|
+
"trigger": ".v-select, .v-autocomplete",
|
|
178
|
+
"overlay": ".v-overlay .v-list, .v-menu > .v-overlay__content",
|
|
179
|
+
"option": ".v-list-item",
|
|
180
|
+
"filter": ".v-autocomplete input, .v-field__input",
|
|
181
|
+
},
|
|
182
|
+
"dialog": {
|
|
183
|
+
"overlay": ".v-dialog .v-card, .v-overlay--active .v-card",
|
|
184
|
+
"accept": ".v-card-actions button:last-child",
|
|
185
|
+
"reject": ".v-card-actions button:first-child",
|
|
186
|
+
"danger": ".v-card-actions button.text-error, .v-card-actions button.bg-error",
|
|
187
|
+
},
|
|
188
|
+
"toggle": {
|
|
189
|
+
"switch": ".v-switch input",
|
|
190
|
+
"segment": ".v-btn-toggle .v-btn",
|
|
191
|
+
},
|
|
192
|
+
"datepicker": {
|
|
193
|
+
"panel": ".v-date-picker, .v-date-picker-month",
|
|
194
|
+
"next_button": ".v-date-picker-controls button:last-child, .v-date-picker-month__header button:last-child",
|
|
195
|
+
"prev_button": ".v-date-picker-controls button:first-child, .v-date-picker-month__header button:first-child",
|
|
196
|
+
"day_cell": ".v-date-picker-month__day:not(.v-date-picker-month__day--hide-adjacent) .v-btn:not(.v-btn--disabled)",
|
|
197
|
+
"trigger": ".v-field__append-inner button, .v-input__append button",
|
|
198
|
+
# Vuetify core has no time picker; ``.v-time-picker`` is the add-on.
|
|
199
|
+
"hour_picker": ".v-time-picker-clock__container, .v-time-picker__controls .v-time-picker__hour",
|
|
200
|
+
"minute_picker": ".v-time-picker-clock__container, .v-time-picker__controls .v-time-picker__minute",
|
|
201
|
+
},
|
|
202
|
+
"fieldset": {
|
|
203
|
+
"toggle": ".v-expansion-panel-title",
|
|
204
|
+
"content": ".v-expansion-panel-text",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
# ── shadcn/ui (Radix primitives, React) — starter set, verify per app ─────
|
|
208
|
+
# shadcn/ui composes Radix primitives, which expose ARIA roles and
|
|
209
|
+
# ``[data-state]`` / ``[data-radix-*]`` attributes rather than stable class
|
|
210
|
+
# names — so these selectors lean on roles/data-attributes. Class names come
|
|
211
|
+
# from your own Tailwind copy of each component, so override where you
|
|
212
|
+
# renamed things.
|
|
213
|
+
"shadcn": {
|
|
214
|
+
"dropdown": {
|
|
215
|
+
# Radix Select trigger + Command combobox both expose role.
|
|
216
|
+
"trigger": '[role="combobox"]',
|
|
217
|
+
"overlay": '[role="listbox"], [data-radix-popper-content-wrapper]',
|
|
218
|
+
"option": '[role="option"]',
|
|
219
|
+
"filter": '[role="combobox"] input, [cmdk-input]',
|
|
220
|
+
},
|
|
221
|
+
"dialog": {
|
|
222
|
+
"overlay": '[role="alertdialog"], [role="dialog"]',
|
|
223
|
+
# shadcn AlertDialog: Cancel then Action, in DOM order.
|
|
224
|
+
"accept": '[role="alertdialog"] button:last-of-type, [role="dialog"] button:last-of-type',
|
|
225
|
+
"reject": '[role="alertdialog"] button:first-of-type, [role="dialog"] button:first-of-type',
|
|
226
|
+
"danger": '[role="alertdialog"] button.bg-destructive, [role="dialog"] button.bg-destructive',
|
|
227
|
+
},
|
|
228
|
+
"toggle": {
|
|
229
|
+
"switch": '[role="switch"]',
|
|
230
|
+
"segment": '[role="radiogroup"] button, [role="group"] [data-state]',
|
|
231
|
+
},
|
|
232
|
+
"datepicker": {
|
|
233
|
+
# shadcn date picker = a Popover wrapping react-day-picker (rdp).
|
|
234
|
+
"panel": ".rdp, [data-radix-popper-content-wrapper] table",
|
|
235
|
+
"next_button": 'button[name="next-month"], .rdp-button_next',
|
|
236
|
+
"prev_button": 'button[name="previous-month"], .rdp-button_previous',
|
|
237
|
+
"day_cell": ".rdp-day:not([disabled]):not(.rdp-day_outside)",
|
|
238
|
+
"trigger": '[data-state] > button, button[aria-haspopup="dialog"]',
|
|
239
|
+
# react-day-picker has no time picker; leave the PrimeNG-style
|
|
240
|
+
# placeholders empty-ish so pick-time is clearly unsupported until a
|
|
241
|
+
# real app supplies a widget. Override these if you add one.
|
|
242
|
+
"hour_picker": '[data-time="hour"]',
|
|
243
|
+
"minute_picker": '[data-time="minute"]',
|
|
244
|
+
},
|
|
245
|
+
"fieldset": {
|
|
246
|
+
# shadcn Accordion (Radix): trigger + region content.
|
|
247
|
+
"toggle": "[data-radix-collection-item] button, [data-state] > h3 button",
|
|
248
|
+
"content": '[role="region"], [data-state="open"] > [data-radix-accordion-content]',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def available_presets() -> list[str]:
|
|
255
|
+
"""Sorted list of known preset names (for error messages)."""
|
|
256
|
+
return sorted(PRESETS)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ── IdP login-form presets (FLAWS §6.2) ──────────────────────────────────────
|
|
260
|
+
# The generic hosted-login form-fill (auth ``mode: form``, formerly ``auth0``)
|
|
261
|
+
# needs three selectors: username field, password field, submit button. Most
|
|
262
|
+
# IdPs differ only in these — so a named ``preset:`` fills them at once, exactly
|
|
263
|
+
# like the UI-kit presets above but on a separate registry so the two name
|
|
264
|
+
# spaces don't collide. ``auth0`` mirrors the historical FormAuthConfig defaults.
|
|
265
|
+
IDP_PRESETS: dict[str, dict[str, dict[str, str]]] = {
|
|
266
|
+
"auth0": {
|
|
267
|
+
"form": {
|
|
268
|
+
"username_selector": "input[name='username']",
|
|
269
|
+
"password_selector": "input[name='password']",
|
|
270
|
+
"submit_selector": "button[data-action-button-primary='true']",
|
|
271
|
+
# Auth0 Guardian OTP prompt — best-effort starter, verify per tenant.
|
|
272
|
+
"totp_selector": "input[name='code']",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
"keycloak": {
|
|
276
|
+
"form": {
|
|
277
|
+
"username_selector": "#username",
|
|
278
|
+
"password_selector": "#password",
|
|
279
|
+
"submit_selector": "#kc-login",
|
|
280
|
+
# Keycloak OTP form — best-effort starter, verify per realm/theme.
|
|
281
|
+
"totp_selector": "#otp",
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|