recon_gen.common.browser.helpers
Helpers for driving QuickSight dashboards in a Playwright browser.
Used by both the e2e test suite (tests/e2e/test_*.py) and
production CLI code (the screenshot pipeline that renders handbook
images against a deployed dashboard). Promoted out of
tests/e2e/ in M.1.10 so production no longer has to import
from tests/.
The QuickSight identity region (us-east-1) is where embed URL generation and user operations live, even when the dashboard itself is deployed in another region.
Functions
|
Click the visual's page-size dropdown → 10000, no scrolling or counting. |
|
Reset a FilterControl to its "all values" default. |
|
Click an entry in QuickSight's data-point context menu by visible text. |
|
Click the first data cell (row 0, col 0) of the named visual. |
|
Activate a sheet tab by its visible name and wait for the switch. |
|
Count distinct categorical entries (bars / slices) in a chart. |
|
Count distinct table rows in the visual whose title matches. |
|
Return the full (post-filter) row count of a QS table visual via the scroll-accumulate dance. |
|
AA.A.l2ft-rails-inverse.2.e — return the first row whose visible cells subset-match |
|
Generate a pre-authenticated embed URL for a dashboard. |
|
Return the visible sheet tab labels in order. |
Return the QuickSight user ARN to embed dashboards for. |
|
|
Return the title text of every visual currently on the page. |
|
Return the ordered category labels (bar names / slice names) of a chart visual, parsed from QS's screen-reader aria-label. |
|
Return the data-value option labels in a FilterControl dropdown. |
|
Return the displayed big-number text of a KPI visual. |
|
Read the DOM-visible rows of a QuickSight Table visual as a list of dicts keyed by column-header text, in display order. |
|
De-virtualized table read — scroll through the table and accumulate every row that mounts. |
|
Append one SQL-query entry to |
|
Right-click the first data cell of the named visual. |
|
Save a screenshot under tests/e2e/screenshots/[subdir/]. |
|
Scroll the visual with the given title to the viewport center. |
|
Fill the two date-range pickers and commit each with Enter. |
|
Pick a single value from a SINGLE_SELECT FilterControl by title. |
|
Pick one or more values from a MULTI_SELECT FilterControl by title. |
|
Fill a single |
|
Set a single-value |
|
Return the visible titles of filter controls on the active sheet. |
|
Cheap DOM probe — does this visual have a QS pagination control? |
|
Public-API capture trigger for the pytest-fixture path. |
|
AA.A.8 — cheap DOM probe — does this visual show a QS error overlay? |
|
Cheap DOM probe — does this visual show QS's empty-state overlay? |
|
Wait for the QuickSight dashboard chrome (sheet tabs) to appear. |
|
Poll |
|
Poll a table visual's row count until it differs from |
|
Block until every title in |
|
Wait until at least min_count visual containers are rendered. |
|
Yield a Playwright WebKit page; tears down browser on exit. |
- recon_gen.common.browser.helpers.bump_table_page_size_to_10000(page, visual_title, timeout_ms)[source]
Click the visual’s page-size dropdown → 10000, no scrolling or counting. Returns True if the click sequence completed; False if the pagination dropdown never appeared (table isn’t actually paginated — use
table_is_paginatedto pre-check and skip the call entirely).Does not wait for the post-bump re-fetch. QS fires a fresh data query when page size changes; caller must settle that re-fetch separately (via
QsEmbedDriver._settle_after_param_change, which keys off the WebSocket START/STOP frames — not a fixedwait_for_timeout). The original implementation’swait_for_timeout(500)here was the AA.H.11 race trigger: 500 ms wasn’t enough for the re-fetch to land on cold visuals, and the follow-up scroll-accumulate dance read the empty container.The focus-click on the visual title (required to reveal the pagination control) is the same JS-dispatched
.click()pattern used elsewhere — bypasses Playwright’s actionability checks that race against QS’s re-render churn.- Return type:
bool
- Parameters:
page (Page)
visual_title (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.clear_dropdown(page, control_title, timeout_ms)[source]
Reset a FilterControl to its “all values” default.
Opens the dropdown and clicks the “Select all” / “All” entry. Works for both SINGLE_SELECT and MULTI_SELECT controls — QuickSight uses the same listbox markup for both.
- Return type:
None
- Parameters:
page (Page)
control_title (str)
timeout_ms (int)
Click an entry in QuickSight’s data-point context menu by visible text.
QS’s right-click menu mounts as a portal with each entry as a
[role="menuitem"]. The drill action’s Name parameter from the Python builder appears verbatim as the menu item’s text.- Return type:
None
- Parameters:
page (Page)
item_text (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.click_first_row_of_visual(page, visual_title, timeout_ms)[source]
Click the first data cell (row 0, col 0) of the named visual.
Tags the cell with a unique
data-e2e-targetattribute first so the click selector is unambiguous even when multiple tables share the same globalsn-table-cell-0-0. Clears the marker after so subsequent calls don’t pick up a stale target.- Return type:
None
- Parameters:
page (Page)
visual_title (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.click_sheet_tab(page, name, timeout_ms)[source]
Activate a sheet tab by its visible name and wait for the switch.
QuickSight tears down the prior sheet’s visuals on switch. We snapshot the current visual titles before the click, then wait for them to be replaced — otherwise a wait that just checks “≥ N visuals present” can be satisfied by the prior sheet.
- Return type:
None
- Parameters:
page (Page)
name (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.count_chart_categories(page, visual_title)[source]
Count distinct categorical entries (bars / slices) in a chart.
QS renders charts to
<canvas>, so there are no DOM bars/slices to count directly. Two signals we can read:Chart aria-label: QS publishes a screen-reader description like “This is a chart with type Bar chart … the data for X is Y, the data for Z is W, …”. Counting
the data foroccurrences yields the category count reliably (works for bar + line + pie).Legend rows (
data-automation-id="visual_legend_item_value"): present on pie/donut charts and any chart with a legend.
Returns the max of the two signals, or
-1if the visual isn’t found. Use to assert change, not exact value (chart may hide low-freq series).- Return type:
int
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.count_table_rows(page, visual_title)[source]
Count distinct table rows in the visual whose title matches.
Returns -1 if no visual with that title is on the page. Returns 0 if the visual is present but empty. Caller is responsible for ensuring the visual is hydrated (use
scroll_visual_into_viewfor below-the-fold tables).- Return type:
int
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.count_table_total_rows(page, visual_title, timeout_ms)[source]
Return the full (post-filter) row count of a QS table visual via the scroll-accumulate dance.
QS tables virtualize —
count_table_rowsonly sees the ~10 rows currently mounted in the DOM. For filter-narrowing assertions where both pre and post totals exceed the viewport, DOM counts stay flat and the assertion silently passes. This helper:Scrolls the inner
.grid-containerto the bottom, tracking the highestsn-table-cell-N-*index seen.
- Return type:
int
- Returns:
-1if the visual isn’t on the page.-2if the visual is present but has no.grid-container(e.g. a one-row table that QS renders without a scroll container). The post-bump count otherwise (max + 1, or0if no cells).- Parameters:
page (Page)
visual_title (str)
timeout_ms (int)
Caller must page-size-bump first for tables exceeding the QS default page size (~10–15 rows) — this helper only scrolls, no longer bumps (the bump was extracted to
bump_table_page_size_to_10000). For small tables (no pagination control),count_table_rowsis the exact total — pre-check viatable_is_paginatedand skip the bump + scroll entirely.Pre-AA.H.11 this helper bundled the focus-click + page-size-bump + scroll into one call with a fixed
wait_for_timeout(500)after the bump. The 500 ms wasn’t enough for the post-bump re-fetch to land, so the scroll read an empty container and returned 0 — causing the audit-agreement test to reportqs_count=0on cold sheets where the table actually had 2+ rows (verified via screenshot + DOM capture). The fix split the orchestration so callers can:Cheap-path-skip via
table_is_paginated(no clicks, no risk).Bump + WS-settle deterministically when overflow is real.
Read via the scroll-accumulate dance below.
- recon_gen.common.browser.helpers.find_row_in_table_via_scroll(page, visual_title, predicate)[source]
AA.A.l2ft-rails-inverse.2.e — return the first row whose visible cells subset-match
predicate(header → value), walking the scroll-accumulate dance so virtualized rows below the fold get mounted before they’re checked. ReturnsNoneif no row matches after scrolling to the bottom (or 500 stable steps).Use case: the inverse-picker test asks “is there ANY row in the post-pick result that still has the anchor’s value for the toggled column?”. Early-exits the scroll loop on first match — typical “yes, broken filter” returns in <500 ms; “no, anchor excluded” walks to the bottom (~1-3s on a 100-row table at 120ms/step).
The membership check IS the contract — count-based assertions can’t tell apart “filter narrowed” from “filter returned different rows for unrelated reasons”, but “is the anchor’s row excluded?” is the actual question the picker test wants to answer.
Returns
Noneif the visual isn’t on the page or has no.grid-container. The predicate’s keys must match column- header display labels (usevisual_column_labelto resolve SQL column names → display labels).- Return type:
dict[str, str] | None
- Parameters:
page (Page)
visual_title (str)
predicate (dict[str, str])
- recon_gen.common.browser.helpers.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.helpers.get_sheet_tab_names(page)[source]
Return the visible sheet tab labels in order.
- Return type:
list[str]
- Parameters:
page (Page)
- recon_gen.common.browser.helpers.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.helpers.get_visual_titles(page)[source]
Return the title text of every visual currently on the page.
- Return type:
list[str]
- Parameters:
page (Page)
- recon_gen.common.browser.helpers.read_chart_categories(page, visual_title)[source]
Return the ordered category labels (bar names / slice names) of a chart visual, parsed from QS’s screen-reader aria-label.
QS aria-labels a chart container with “…the data for <CAT> is <N>, the data for <CAT> is <N>, …”. Parse that into an ordered list. Returns [] if the visual isn’t found or has no aria description.
- Return type:
list[str]
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.read_dropdown_options(page, control_title, timeout_ms)[source]
Return the data-value option labels in a FilterControl dropdown.
Opens the dropdown for
control_title, reads every[role="option"]label, dismisses the popover, and returns the list with sentinel entries filtered out ("Select all","All", blanks). Used by data-agnostic e2e tests that need to pick a valid value from the dropdown without hardcoding what the values are — e.g., “pick the first selectable value to narrow the table.”- Return type:
list[str]
- Parameters:
page (Page)
control_title (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.read_kpi_value(page, visual_title, *, timeout_ms=8000)[source]
Return the displayed big-number text of a KPI visual.
QS renders the value inside
.visual-x-center(the actual text node). Thekpi-display-valueautomation-id wraps the container but its innerText sometimes includes the comparison label — prefer the center node and fall back to the automation-id if unavailable.Stability poll: returns only when 2 consecutive reads (200ms apart) match. Handles both:
First-paint race (BO.1 test_bg3_drift_sheet_kpis_match_matview_counts fix): caller’s wait_loaded(…) may settle one visual while sibling KPIs on the same sheet are still rendering; poll past the None window until the value appears stable.
Post-param-change stale-read (BO.1 test_bg2_daily_statement_kpis_match_summary_matview fix): after a date pick, QS’s WS settle may return before the KPI cell repaints — DOM transiently still shows the previous day’s value. Stability poll waits for the new value to land.
Raises
AssertionErrorif the value never reaches a stable state within the timeout.- Return type:
str
- Parameters:
page (Page)
visual_title (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.read_table_rows_dom(page, visual_title)[source]
Read the DOM-visible rows of a QuickSight Table visual as a list of dicts keyed by column-header text, in display order.
QS virtualizes — only ~10 rows are in the DOM at once — so this returns that window, not necessarily the whole table. Caller is responsible for getting the table on screen (use
scroll_visual_into_viewfirst; bump the page size if the full table is needed). Returns[]if the visual isn’t found or has no body cells (empty table / still loading).Column headers come from the
[data-automation-id="sn-table-column-N"]divs (their.titlespan — the visible header text); body cells fromsn-table-cell-{row}-{col}. Headers and cells are zipped by position — the Nth header (left-to-right in the DOM) pairs with the Nth cell (smallestcolfirst) in each row — so it’s robust to QS’s internal column-index numbering (the header’ssn-table-column-Nand the body’ssn-table-cell-r-cuse differentN/corigins).- Return type:
list[dict[str, str]]
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.read_table_rows_via_scroll(page, visual_title)[source]
De-virtualized table read — scroll through the table and accumulate every row that mounts. Returns a list of dicts keyed by column-header text, in row-index order across the full dataset.
QS virtualizes ~10-37 rows in the DOM at a time.
read_table_rows_domcaptures only what’s currently mounted; for tables with N > window, callers need this scroll-collect shape to get every row. Used by l1_invariant_rows_seen post-BO.1 fix: row-identity checks against a 119-row overdraft table were comparing a 37-row DOM window to the page-size-bumped count of 119 and failing the assertion.Walks the same scroll-accumulate dance as find_row_in_table_via_scroll but returns the ACCUMULATED row map instead of early-exiting on the first predicate match. Dedupes by the absolute row index in sn-table-cell-{row}-{col} (QS uses an absolute index across the whole dataset, not the window-relative index, so newly-mounted rows have row indices we haven’t seen yet).
Returns
[]if the visual isn’t found or has no body cells. Bounded by 500 scroll steps + the table’s natural scroll-bottom.- Return type:
list[dict[str, str]]
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.record_sql_trace(label, sql, summary)[source]
Append one SQL-query entry to
<capture_dir>/sql_trace.txt.Browser-test helpers (
fetch_anchor_row, future picker-universe probes) call this after executing a query so the failure-capture bundle records what the test asked the DB for alongside the DOM + db_counts artifacts. When a dropdown later showsMuiAutocomplete-noOptionsfor a value the test typed, the triage answers “did the DB return that value” by readingsql_trace.txtinstead of re-running the queries by hand.Format per entry (append-mode):
## <label> – ran at <iso-timestamp> <sql> — <summary>
Test ID resolves from
PYTEST_CURRENT_TEST— runs outside pytest no-op the write. Sidecar contract: swallow exceptions; capture failures must never mask the original test failure.- Return type:
None- Parameters:
label (str)
sql (str)
summary (str)
- recon_gen.common.browser.helpers.right_click_first_row_of_visual(page, visual_title, timeout_ms)[source]
Right-click the first data cell of the named visual.
Mirror of
click_first_row_of_visualbut dispatches a contextmenu event so QuickSight opens the visual’s DATA_POINT_MENU drill list. Tags the cell withdata-e2e-targetfirst so the click target is unambiguous when multiple tables share the same global cell selectors.- Return type:
None
- Parameters:
page (Page)
visual_title (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.screenshot(page, name, subdir=None)[source]
Save a screenshot under tests/e2e/screenshots/[subdir/].
Pass
subdirto namespace outputs per-app (e.g."payment_recon"or"account_recon") so the two apps’ screenshots don’t overwrite each other when they happen to share a test name.- Return type:
Path
- Parameters:
page (Page)
name (str)
subdir (str | None)
- recon_gen.common.browser.helpers.scroll_visual_into_view(page, visual_title, timeout_ms, *, wait_for_cells=True)[source]
Scroll the visual with the given title to the viewport center.
QuickSight virtualizes below-the-fold visuals — table cells are absent from the DOM until the visual is on screen. Browser tests that click into such a table must call this first, or the click-target selector will return nothing.
Pass
wait_for_cells=Falsefor chart visuals (bar / pie / line), which don’t rendersn-table-cell-*markers and would otherwise time out.- Return type:
None
- Parameters:
page (Page)
visual_title (str)
timeout_ms (int)
wait_for_cells (bool)
- recon_gen.common.browser.helpers.set_date_range(page, start, end, timeout_ms, picker_indices=(0, 1))[source]
Fill the two date-range pickers and commit each with Enter.
start/enduse QuickSight’s accepted text format (YYYY/MM/DD).picker_indicesdefaults to (0, 1) — the first date-range control on the active sheet. Override when a sheet has multiple ranges.For parameter-driven date pickers (
ParameterDateTimePicker, notFilterDateTimePicker.type=DATE_RANGE), useset_parameter_datetime_value()instead — those render as separate sheet controls each with their own scoped DOM, not a shared 0-and-1-indexed range widget.- Return type:
None
- Parameters:
page (Page)
start (str)
end (str)
timeout_ms (int)
picker_indices (tuple[int, int])
- recon_gen.common.browser.helpers.set_dropdown_value(page, control_title, value, timeout_ms)[source]
Pick a single value from a SINGLE_SELECT FilterControl by title.
Opens the dropdown for
control_titleand clicks the option whose text equalsvalue. Useclear_dropdownto reset to “All”. Dismisses the popover with Escape so subsequent visual interactions aren’t blocked by the listbox overlay.Handles both
_open_control_dropdownvariants transparently:Simple variant — listbox is fully rendered on open;
has_textfinds the option directly.Search-enabled variant (MUI Autocomplete) — options are virtualized and
valuemay live outside the rendered window. Typevalueinto the autocomplete search input first so the Autocomplete narrows to the matching subset, then click. This is the operator’s actual flow for an 8000-option dropdown (you can’t scroll to your target — you type to find it). Per AA.H.8 the driver carries this dance so everypick_filter/set_dropdown_valuecaller works against both variants without renderer-specific code in the test.
- Return type:
None
- Parameters:
page (Page)
control_title (str)
value (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.set_multi_select_values(page, control_title, values, timeout_ms)[source]
Pick one or more values from a MULTI_SELECT FilterControl by title.
Deselects any currently-checked options first (via the option’s aria-selected state), then ticks only the requested values. Commits by pressing Escape to dismiss the popover.
- Return type:
None
- Parameters:
page (Page)
control_title (str)
values (list[str])
timeout_ms (int)
- recon_gen.common.browser.helpers.set_parameter_datetime_value(page, control_title, value, timeout_ms)[source]
Fill a single
ParameterDateTimePickercontrol by its title.Each ParameterDateTimePicker on a sheet renders as its own
sheet_controlcard scoped bydata-automation-contextto the control’s title. The date input lives atdata-automation-id="date_picker_0"within that card. Targeting by title avoids the cross-control collision (each card has its own locally-indexed picker).valueuses QuickSight’s accepted text format (YYYY/MM/DD).- Return type:
None
- Parameters:
page (Page)
control_title (str)
value (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.set_parameter_slider_value(page, control_title, value, timeout_ms)[source]
Set a single-value
ParameterSliderControlby its title.QS renders each ParameterSliderControl as its own
sheet_controlcard scoped bydata-automation-contextto the control title. The card carries an MUI slider (a draggable thumb — fragile to drive pixel-accurately in Playwright) AND a typable text box that commits the value when it loses focus (typing alone doesn’t take effect). So the reliable path: find the one non-hidden<input>in the card (the MUI slider’s own<input type="hidden">value carrier is the other one), fill it, blur it.valueis the numeric slider position — an int for the typical step-1 control; rendered without a trailing.0.- Return type:
None
- Parameters:
page (Page)
control_title (str)
value (float)
timeout_ms (int)
- recon_gen.common.browser.helpers.sheet_control_titles(page)[source]
Return the visible titles of filter controls on the active sheet.
- Return type:
list[str]
- Parameters:
page (Page)
- recon_gen.common.browser.helpers.table_is_paginated(page, visual_title)[source]
Cheap DOM probe — does this visual have a QS pagination control?
Returns True iff a
simplePagedDisplayNav_dropdown_pageSizeelement exists in the DOM, scoped to the visual whose title matches. Small tables (≤ QS default page size, typically ~10–15 rows) render without pagination — the DOM holds every row, socount_table_rowsis the exact total and no bump is needed.This is a pure read — no clicks, no timeouts, no re-render risk. Critical for callers that previously triggered a spurious re-fetch by clicking the page-size dropdown on small tables that didn’t need it (the AA.H.11 root cause: the click → re-fetch →
getMaxRowread the empty container mid-refetch → returned 0 even though cells were about to mount).- Return type:
bool
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.trigger_failure_capture(page, *, test_id=None, cfg=None)[source]
Public-API capture trigger for the pytest-fixture path.
Pytest’s yield-fixture semantics don’t re-throw test-body exceptions back into the fixture’s generator — so
webkit_page’sexcept BaseException:handler never fires for a typical e2e test failure. The fixture’s teardown (post-yield code, after consultingrequest.node.rep_call.failedvia the standardpytest_runtest_makereporthook) calls this function instead to drop the same artifacts.Reads the sinks
webkit_pageattached to the page (_qs_gen_console_sink,_qs_gen_network_sink,_qs_gen_test_id). Falls back to_test_id_from_pytest_env()when no test_id is passed and no page-attached default exists.cfgis the Config dataclass; when present, also dumpsdb_counts.txt(per-table row counts for every relation matchingcfg.db_table_prefix_*). PassNoneto skip — the QS-side artifacts still land. Conftest’smaybe_capture_on_failureresolves cfg from thecfgfixture and forwards it.Sets
page._qs_gen_capture_triggered = Truesowebkit_page’sfinallyblock knows to save the trace.zip (otherwise the trace would only land when the exception bubbled through theexcept).Idempotent — calling twice with the same test_id just overwrites the artifacts (last call wins).
- Return type:
None
- Parameters:
page (Page)
test_id (str | None)
cfg (object | None)
- recon_gen.common.browser.helpers.visual_error_text(page, visual_title)[source]
AA.A.8 — cheap DOM probe — does this visual show a QS error overlay?
QuickSight surfaces per-visual rendering failures via an error overlay scoped inside the
[data-automation-id="analysis_visual"]container — e.g. the “your tabular report contains duplicate columns. To proceed, remove all duplicates.” message that surfaces on Pending Aging’s Stuck Pending Detail today (the AA.A.8.bug case). Returns the overlay’s text content (concatenated across matched nodes) when an error is present;Nonewhen the visual is mounted without errors OR when the visual isn’t found.Use this in
DashboardDriver.wait_loadedto HARD-FAIL on error overlays — pre-AA.A.8 a broken visual would silently time out the cell/empty-overlay predicate and the caller had to wait the full visual timeout before getting a generic “didn’t render” failure (with no error text). After AA.A.8: the operator gets the actual QS error string in the test failure message, fast.Selector union mirrors
_capture_failure_qs_errors(the post- failure forensics path) but scopes to the named visual container — that’s the meaningful axis for “did THIS visual fail to render”.- Return type:
str | None
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.visual_is_empty(page, visual_title)[source]
Cheap DOM probe — does this visual show QS’s empty-state overlay?
QS mounts a
[data-automation-id="visual-overlay-title"]element withdata-automation-context="No data"inside any visual whose backing dataset returned zero rows (table / Sankey / chart / KPI). The overlay mounts at the same time the visual’s frame renders — typically within a few hundred ms of the sheet load — so a positive return from this probe is high-signal: the visual is mounted AND confirmed empty, no row-fetch race to wait out.Use this BEFORE
scroll_visual_into_viewto short-circuit empty cases.scroll_visual_into_view’swait_for_cells=Truemode waits forsn-table-cell-0-0to appear, which empty tables never mount — without this probe the helper times out attimeout_mson every empty visual. Returns False if the visual isn’t found (let downstream selectors raise the “no visual” error with their own context).- Return type:
bool
- Parameters:
page (Page)
visual_title (str)
- recon_gen.common.browser.helpers.wait_for_dashboard_loaded(page, timeout_ms)[source]
Wait for the QuickSight dashboard chrome (sheet tabs) to appear.
Polls for
[role="tab"]directly. Don’t wait fornetworkidlefirst — QS holds open WebSocket / long-polling connections that keep the network busy indefinitely, so networkidle never fires and a 120s timeout burns waiting for an event that won’t happen. The sheet-tab strip attaching to the DOM is the authoritative “chrome is up” signal; in practice it appears within ~1s of the embed URL load completing.- Return type:
None
- Parameters:
page (Page)
timeout_ms (int)
- recon_gen.common.browser.helpers.wait_for_dropdown_options_present(page, dropdown_title, timeout_ms)[source]
Poll
read_dropdown_optionsuntil it returns at least one option (or timeout).Replaces fixed-sleep waits in cascade-dropdown tests where one parameter pick (e.g., a Metadata Key) repopulates a downstream dropdown’s options (e.g., Metadata Value). The dropdown is closed between reads, so this is a Python-side retry loop rather than a
page.wait_for_functionJS poll. Fail-fasts on a real “options never populate” regression; tight on the happy path.- Return type:
list[str]
- Parameters:
page (Page)
dropdown_title (str)
timeout_ms (int)
- recon_gen.common.browser.helpers.wait_for_table_rows_to_change(page, visual_title, before, timeout_ms)[source]
Poll a table visual’s row count until it differs from
before.Returns the new row count. Raises a Playwright timeout if the count never changes. Use this after triggering a filter / drill action so the test doesn’t sleep blindly.
- Return type:
int
- Parameters:
page (Page)
visual_title (str)
before (int)
timeout_ms (int)
- recon_gen.common.browser.helpers.wait_for_visual_titles_present(page, expected_titles, timeout_ms)[source]
Block until every title in
expected_titlesis rendered as ananalysis_visual_title_label. Visual containers attach before their title labels hydrate, so a simple container count isn’t enough when the test asserts on specific titles.- Return type:
None
- Parameters:
page (Page)
expected_titles (list[str])
timeout_ms (int)
- recon_gen.common.browser.helpers.wait_for_visuals_present(page, min_count, timeout_ms)[source]
Wait until at least min_count visual containers are rendered.
Returns the actual count observed.
- Return type:
int
- Parameters:
page (Page)
min_count (int)
timeout_ms (int)
- recon_gen.common.browser.helpers.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])