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:

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: object

Walk an App + Page; produce a directory of named screenshots.

Parameters:
  • app (App)

  • page (Any)

  • output_dir (Path)

  • embed_url (str | None)

  • timeout_ms (int)

app: App
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}.png so 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 resolved visual_id — the auto-ID walker hasn’t run, or the visual is a factory wrapper without an explicit id. Caller should app.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 distinct suffix values 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_url set on construction.

Return type:

dict[Sheet, Path]

Parameters:
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_region and 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. Raises RuntimeError when unset — the previous silent fallback to a hardcoded account-specific ARN string masked CI misconfiguration (Phase W’s ci-bot user 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 with body, captures six diagnostics per failing test:

  • screenshot.png (or <test_id>.png in legacy mode) — full-page screenshot of the failure state

  • dom.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 + uncaught pageerror accumulated 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 multiple 404 Not Found responses paired with the Sample values not found JS 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_VIS carries the actual parameter substitution QS made for the visual’s CustomSql). Sink lives on QsEmbedDriver._ws_frames and is attached to the page as page._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 with playwright 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 / direct pytest invocation): tests/e2e/screenshots/_failures/<test_id>.png etc., 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_page entry (when pytest’s PYTEST_CURRENT_TEST env var is reliably set inside the test body) rather than re-read inside the except handler — that handler can run during fixture teardown after pytest has cleared the var, which would silently demote captures to unknown/.

Trace capture policy: - On exception → trace always written (under the run-dir mode). - On clean exit → trace written iff RECON_GEN_TRACE_ALL=1 is 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’s stderr.log instead of being invisible until the next forensic session.

Return type:

Generator[Page, None, None]

Parameters:
  • headless (bool)

  • viewport (tuple[int, int])

Modules

helpers

Helpers for driving QuickSight dashboards in a Playwright browser.

screenshot

ScreenshotHarness: walk a deployed App's tree and capture screenshots systematically.