recon_gen.common.browser
Playwright-driven browser helpers + typed screenshot harness.
Promoted out of tests/e2e/ in M.1.10 (per spike finding F7) so
production CLI code (M.6 deploy, M.7 docs render, M.8 training
render) can import the screenshot pipeline without reaching into
tests/.
The module pair:
recon_gen.common.browser.helpers— Playwright page-driving primitives (URL gen, page setup, sheet-tab navigation, table/control probing, waits). Also re-exported here for convenience.recon_gen.common.browser.screenshot— typedScreenshotHarnesswalker over anApptree.
Production callers typically only need ScreenshotHarness +
generate_dashboard_embed_url + webkit_page. The full
probe / assertion surface (count_table_rows,
read_kpi_value, etc.) is for e2e test code; it lives in the
same module today because splitting it cleanly will be more
obvious once M.6/M.7/M.8 surface what production really needs.
- class recon_gen.common.browser.ScreenshotHarness(app, page, output_dir, embed_url=None, timeout_ms=30000)[source]
Bases:
objectWalk an App + Page; produce a directory of named screenshots.
- Parameters:
app (App)
page (Any)
output_dir (Path)
embed_url (str | None)
timeout_ms (int)
- capture_all_sheets()[source]
One full-page screenshot per sheet on the App’s analysis.
Returns
dict[Sheet, Path]keyed by the Sheet object ref. Filenames remain{sheet_id}.pngso re-running overwrites the same on-disk file; only the in-memory key shape changed (M.1.10 / F8).- Return type:
dict[Sheet,Path]
- capture_per_visual(sheet)[source]
One screenshot per visual on
sheet, element-cropped.Returns
dict[VisualLike, Path]keyed by the Visual object ref. Skips visuals without a resolvedvisual_id— the auto-ID walker hasn’t run, or the visual is a factory wrapper without an explicit id. Caller shouldapp.emit_analysis()once before to resolve auto-IDs (the validator + this harness usually share a session-scoped fixture that already does that).- Return type:
dict[VisualLike,Path]- Parameters:
sheet (Sheet)
- capture_with_state(*, parameter_values, suffix='state')[source]
Re-load the dashboard with parameter values applied via URL hash (
#p.<name>=<value>), then capture every sheet.Returns
dict[Sheet, Path]keyed by Sheet object ref. Filenames suffix-tagged so multiple states don’t overwrite each other:{sheet_id}-{suffix}.png. Pass distinctsuffixvalues per state.Per the project memory
project_qs_url_parameter_no_control_sync, the on-screen control widget may not reflect the URL value even when the data is filtered. The screenshot captures what the analyst SEES, which is the rendered visual state — that’s the right semantics for handbook screenshots.Requires
embed_urlset on construction.- Return type:
dict[Sheet,Path]- Parameters:
parameter_values (dict[ParameterDeclLike, Any])
suffix (str)
- embed_url: str | None = None
- output_dir: Path
- page: Any
- timeout_ms: int = 30000
- recon_gen.common.browser.generate_dashboard_embed_url(*, aws_account_id, aws_region, dashboard_id, user_arn=None, session_lifetime_minutes=60)[source]
Generate a pre-authenticated embed URL for a dashboard.
Builds a boto3 QuickSight client in
aws_region(the dashboard’s region) and signs the URL with it. Embed URLs MUST be signed by a client whose region matches the dashboard’s region — using the identity region (us-east-1) for a dashboard deployed elsewhere returns a URL QuickSight rejects with “We can’t open that dashboard, another Quick account or it was deleted” — a confusing error that suggests permission/account/deletion when the actual cause is region mismatch. The M.4.1.i first AWS-side dry-run burned an hour on this when the harness called this helper with the identity-region client.Earlier the signature took a pre-built client which made it possible to pass the wrong region’s client. This version requires callers to pass
aws_regionand constructs the client itself, making the bug class unrepresentable.All args keyword-only — protects against positional-arg drift if the parameter list ever changes again.
- Return type:
str- Parameters:
aws_account_id (str)
aws_region (str)
dashboard_id (str)
user_arn (str | None)
session_lifetime_minutes (int)
- recon_gen.common.browser.get_user_arn()[source]
Return the QuickSight user ARN to embed dashboards for.
Reads
RECON_E2E_USER_ARN. RaisesRuntimeErrorwhen unset — the previous silent fallback to a hardcoded account-specific ARN string masked CI misconfiguration (Phase W’sci-botuser has a different ARN than the local-dev default; the fallback produced an embed URL the bot couldn’t view) and burned a project AWS account ID into the source. Fail-loud is the contract.- Return type:
str
- recon_gen.common.browser.webkit_page(headless=True, viewport=(1600, 1000))[source]
Yield a Playwright WebKit page; tears down browser on exit.
On exception inside the
withbody, captures six diagnostics per failing test:screenshot.png(or<test_id>.pngin legacy mode) — full-page screenshot of the failure statedom.html— serialized DOM of the top-level frame at failure moment (page.content()). Pairs with the screenshot: the pixels show what’s visually there, the DOM shows what the test’s selectors were actually looking at. Critical for “click target not found” / “control didn’t mount” failures.console.txt— every JS console message + uncaughtpageerroraccumulated since page creation (M.4.4.11 pattern, lifted from_harness_browser._attach_console_capture)qs_errors.txt— text content of any QS error overlays visible on the page (the “Failed to load visual” / SQL error tooltips that classic-QS shows for failed dataset queries)network.txt— HTTP status + URL for every non-2xx response the page made. The X.1.b investigation revealed multiple404 Not Foundresponses paired with theSample values not foundJS errors — the URL pattern should disambiguate which QS-side resource is missing.ws_frames.txt(AA.A.qs-triage.1) — QS-only: every text WebSocket frame the page sent (QS’s data-layer protocol —START_VIScarries the actual parameter substitution QS made for the visual’s CustomSql). Sink lives onQsEmbedDriver._ws_framesand is attached to the page aspage._qs_gen_ws_frames_sink; App2-only tests leave the sink empty and land an empty file (signal that the test didn’t open a QS embed).trace.zip(Y.2.gate.c.11) — Playwright trace bundle: full action timeline, DOM snapshots per action, screenshots, network, and console. Open withplaywright show-trace trace.zip. Plain-text artifacts (dom.html) cover the grep-able path; trace.zip is for full-UI replay.
Output destination depends on
RECON_GEN_RUN_DIR:Set (running under the test layer chain runner):
$RECON_GEN_RUN_DIR/browser/<test_id>/{screenshot.png,dom.html, console.txt,qs_errors.txt,network.txt,ws_frames.txt,trace.zip}— per-test directory so artifacts cluster cleanly.Unset (legacy
./run_e2e.sh/ directpytestinvocation):tests/e2e/screenshots/_failures/<test_id>.pngetc., flat directory with per-file<test_id>_prefix to disambiguate. Trace.zip is NOT written in legacy mode (no run-dir to put it in).
The test_id is snapshotted at
webkit_pageentry (when pytest’sPYTEST_CURRENT_TESTenv var is reliably set inside the test body) rather than re-read inside theexcepthandler — that handler can run during fixture teardown after pytest has cleared the var, which would silently demote captures tounknown/.Trace capture policy: - On exception → trace always written (under the run-dir mode). - On clean exit → trace written iff
RECON_GEN_TRACE_ALL=1is set(operator opt-in for “I want the full trace even on green tests”; flag plumbed by
Y.2.gate.c.7).Capture is best-effort: each capture function catches its own exceptions and emits a
[CAPTURE FAILURE] <artifact>: <type>: <msg>line to stderr (loud-fail). The original assertion still bubbles up unmasked — but a regression in the capture path is visible in the layer’sstderr.loginstead of being invisible until the next forensic session.- Return type:
Generator[Page, None, None]
- Parameters:
headless (bool)
viewport (tuple[int, int])
Modules
Helpers for driving QuickSight dashboards in a Playwright browser. |
|
ScreenshotHarness: walk a deployed App's tree and capture screenshots systematically. |