Skip to content

focus_on: check who has the focus after a key that changes nothing visible - #1579

Draft
abrichr wants to merge 1 commit into
mainfrom
feat/focus-on-postcondition
Draft

abrichr wants to merge 1 commit into
mainfrom
feat/focus-on-postcondition

Conversation

@abrichr

@abrichr abrichr commented Oct 25, 2026

Copy link
Copy Markdown
Member

Bug 22 from the second-installation work (#1548).

Problem

Medplum's form has controls with nothing readable beside them, so #1548 teaches some of its fields by keyboard. Two of those keys change nothing the compiler can see:

  • a Tab onto a small icon button;
  • the ArrowDown that highlights the gender option.

The compiler gave those steps no postcondition, and clinical-write refused the program (prohibit_vacuous_postconditions, max_unverified_steps).

Change

A structural postcondition, focus_on, alongside url_changed and title_changed. It is mined only for key steps that have no visual postcondition.

  • Recorder: around each key or hotkey event, it keeps the focused element's identity (focus_before, focus_after). PlaywrightBackend.focus_identity() reads:
    • the data-testid and the id, each with the count of elements sharing it in the element's document;
    • the role and accessible name from Playwright's accessibility snapshot, with the count of elements sharing that role and name on the page;
    • whether an option is active: aria-activedescendant names an element, or the listbox it controls has an aria-selected option.
  • Compiler: it adds focus_on only when the key moved the focus or made an option active, and the element has a specific identity no other element shared at teach time. That is a testid, then an id, then a role with a non-empty name. A tag alone, a role without a name, or a shared identity gets no postcondition, and the step stays vacuous.
  • Replay fails closed. The focused element must have the recorded identity, and be the only element with it. With focus_active_option, an option must also be active. An unreadable focus, nothing focused, a different element, a second element with the same identity, or a postcondition with no specific identity all fail.
  • Policy: is_vacuous counts a focus_on as asserting something only when it names a specific element (focus_identity_is_specific). A degenerate one is still refused by prohibit_vacuous_postconditions.
  • Other:
    • lint_param_leakage checks the focus name and value for demonstrated parameter values.
    • Durable resume leaves focus_on out of revalidation, as it does local_edit: it is relative to the moment the key was pressed.
    • The new fields are dropped when unset, so existing postconditions and bundle digests serialize exactly as before.

Tests

tests/test_focus_on_postcondition.py (18 tests):

  • Mining: a unique testid, or a unique role with a name, becomes focus_on. A tag only, a role with no name, a shared role and name, a shared testid, or a key that moved nothing gets no postcondition. An option highlighted on the same control is marked focus_active_option.
  • Policy: a specific focus_on makes a key step non-vacuous. One without a specific identity stays vacuous.
  • In real Chromium:
    • passes on the recorded focus;
    • fails on another button with the same role;
    • fails when the name is shared;
    • fails without a specific identity;
    • checks the active option;
    • the recorder keeps the focus around a key.

Local runs:

  • ruff, ruff format, mypy and strict mypy are clean.
  • The postcondition, policy, compile, recorder, local-edit, certification and golden selection gave 739 passed and 2 failed. Both failures (test_record_compile_replay_emit_roundtrip and test_installer_starts_with_an_empty_allow_list_and_default_reboot_policy) fail the same way on a branch without this change: a subprocess can't import the package in this environment.

Not yet run against Medplum. That comes at the next fixture lease, on #1548.

🤖 Generated with Claude Code

…sible

A Tab onto an icon button, or an ArrowDown that highlights an option, gave its step no postcondition, and a strict policy refused it as vacuous. The recorder now keeps the focused element's identity around each key, and the compiler adds a focus_on postcondition when that element has a specific identity no other element shares at teach time: a data-testid, an id, or a role with a non-empty accessible name. At replay the focused element must have that identity and be the only one with it, and, for an option highlight, an option must be active (aria-activedescendant or aria-selected). Anything weaker asserts nothing, never passes, and still counts as vacuous.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abrichr
abrichr marked this pull request as draft October 25, 2026 14:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant