Assigned browser tool unavailable: its launch failed because `/opt/google/chrome/chrome` is missing. Supplemental Playwright Chromium rendered checks succeeded; findings below explicitly distinguish those observations from static inference.

# Wallet terminal QA and accessibility review

Reviewed 2026-10-05, source revision `731734e794b3c04a1dfa3e9542212601f0e53050`. **Verdict: changes needed**, including a mobile pane-selection blocker. This is a review only; no application fixes were made. Required assigned-tool inspection and some accessibility checks remain incomplete. No WCAG conformance claim is made.

## Scope, method and evidence

Reviewed the production Vite export of `web/`, served at `/preview/`, using `web/tests/fixture.mjs` for RPC, Blockscout, ENS and wallet responses. Supplemental browser: headless Chromium **153.0.8010.12**. Screenshots were inspected visually, alongside DOM measurements and keyboard actions. These are actual rendered observations, but **not observations from the assigned browser tool**, and not screen-reader sessions. All transactions were mocked; nothing was broadcast.

The pinned Better Interface inputs (workflow and six core domains, plus licenses) and Ethereum frontend UX inputs were read as review criteria. Consequential assumptions: vertical scrolling inside a mobile pane is acceptable; normal desktop panes must fit without scrolling; permissionless governance execution is distinct from governor-only proposal controls. Severity reflects task impact, not a formal WCAG scoring system.

Application files and dependencies were copied to `/tmp/imd-qa`; installs, builds and browser execution ran there. Evidence was written here under `artifacts/`. See [README](README.md) for commands and replay harnesses. Initial attempts and repaired checks are retained rather than reported as clean first runs:

- Assigned browser navigation failed before loading a page: [exact error](assigned-browser-error.txt).
- Initial `npm ci` failed because the default npm cache was read-only; retry with `/tmp/imd-npm-cache` succeeded: [install log](install.log).
- Initial build passed TypeScript and Vite but failed manifest verification because the temporary copy had no Git history: [build log](build.log). Rebuilding with `GIT_DIR` pointing read-only to the original repository succeeded, verifying four pinned ABIs and 20 assets: [successful build](build-retry.log).
- `npm test`: **11/11 passed** ([output](unit-tests.log)). `npm run test:browser`: **40 assertion groups passed** ([output](baseline-browser.log), [measured results](baseline-results.json)). These are baseline assertions, not independent proof of their broad labels.
- Additional fixture-driven checks: [widget/layout results](extra-browser.jsonl), [dialog/mobile/search results](deep-browser.jsonl), [transaction confirmations](transactions.jsonl), [feed and Keeper states](states-retry.jsonl). An initial additional state test used an incorrect deposit-form selector and timed out; [failed attempt](states.jsonl) was corrected in the replay harness.

## Findings

Unless stated otherwise, start from `fixture()` defaults, connect account `0x0000000000000000000000000000000000000a11`, and switch the mock wallet to Sepolia. Borrower `candidate` is `0x0000000000000000000000000000000000000b22`. Evidence paths are relative to this report. No finding below relies on an axe pass.

| ID | Severity | WCAG 2.2 SC | Area / exact reproduction | Expected versus actual | Evidence and status | Suggested fix / source |
| --- | --- | --- | --- | --- | --- | --- |
| QA-01 | blocker | 1.4.10 Reflow; related 2.4.11 for obscured controls | Mobile pane picker. At 1440×900 choose monitor **Backing**, then resize to **320×740** and choose **Redeem** in View pane. No keys required. Repeat with Loan book or Oracle as the desktop monitor; dark 320×740 and 390×844 also show multiple panes. | Expected: only the chosen pane occupies the workspace, with its controls available through its own scroll area. Actual: the retained desktop monitor stays visible and consumes a full pane height; Redeem is laid out beneath it, outside the clipped workspace. The picker says Redeem while Backing is on screen. | **Observed rendered.** [mobile-from-backing.png](mobile-from-backing.png), [mobile-from-loans.png](mobile-from-loans.png), [dark mobile](layout-320-740.png), [390px](layout-390-844.png). `mobile-visible-backing` in [deep results](deep-browser.jsonl): Backing y=146.39–626; Redeem y=626–1105.61, viewport height 740. | In `web/src/style.css:1249`, make mobile hiding override the higher-specificity desktop `[data-monitor]` / `[data-desk]` display selectors, or scope those selectors to desktop. Test that exactly one panel is displayed and its controls intersect the workspace, rather than merely using `isVisible()`. |
| QA-02 | major | 1.4.13 Content on Hover or Focus | Info popover, 1440×900, light. Open Backing; hover **About Backing ratio**; move the pointer from the icon to the middle of the displayed tooltip. No keys. Separately focus the icon and press Escape. | Expected: pointer can enter the explanation and it remains visible; Escape dismisses it. Actual: tooltip disappears as soon as the pointer leaves its icon. Escape works. This prevents inspecting hover content under a pointer-following magnifier. | **Observed rendered.** `tooltip-before` shows a visible 280×120.28 tooltip; `tooltip-after-pointer-move=false` in [extra results](extra-browser.jsonl). Source corroborates trigger `onMouseLeave={hide}` and tooltip `pointer-events:none`. | `web/src/actions.tsx:376` and `web/src/style.css:1325`: retain hover state across the trigger and popup, allow pointer entry, and preserve Escape/blur dismissal. |
| QA-03 | major | 4.1.2 Name, Role, Value | Transaction review, 1440×900, light. Redeem COMP=**10**, default reserve=100 IMD, slippage=50 bps; Quote redemption, then Review redemption. | Expected: the dialog has the accessible name “Review transaction.” Actual: its heading is visible, but the dialog itself has no accessible name. A heading inside a dialog is not automatically its label. | **Observed accessibility tree/DOM; spoken output unverified.** `dialog-aria` begins `- dialog:` with no name; both `aria-label` and `aria-labelledby` are null. [Deep results](deep-browser.jsonl), [extra results](extra-browser.jsonl). | `web/src/App.tsx:588`: give the heading an id and bind the dialog with `aria-labelledby`. Verify its announced name with assistive technology. |
| QA-04 | major | 2.4.3 Focus Order | Transaction review, same quote as QA-03, 1440×900 light. Click Review redemption; Tab eight times; click Cancel. Then explicitly focus Review redemption, Enter, and Escape. | Expected: closing returns focus to Review redemption. Actual: both cancellation paths leave `document.activeElement` as BODY in this sequence. The keyboard user loses their place. The existing suite passes a different quote-to-review sequence, so this is path-dependent. | **Observed keyboard/DOM.** `dialog-cancel-return` and `keyboard-dialog-return` in [deep results](deep-browser.jsonl) show BODY. Native modal traversal cycles through its controls and a browser/body boundary; that boundary alone is **not** counted as an application focus-trap defect. | `web/src/App.tsx:164` and `:273`: retain the initiating element before simulation temporarily disables controls, and restore it after cancellation/close when enabled and visible. Test both pointer and keyboard opening. |
| QA-05 | major | 4.1.3 Status Messages | Async results. 1440×900 light: Keeper → Inspect; paste `candidate`, wait for automatic inspection (default candidateCR=180, candidate marked). Separately Loan book → search **definitelyabsent**. No keys required beyond typing/paste. | Expected: concise successful inspection status and changed search-result count are programmatically announceable without moving focus. Actual: Keeper's only status region stays empty on success, while summary/next-step changes outside it. Search shows “No positions match” outside any live region. | **Observed DOM; actual screen-reader silence is unverified.** `keeper-success-live`: `statuses=[""]`, `summaryLive=false`; `search-empty`: `live=[]`, feed=`No positions match`. [Extra results](extra-browser.jsonl), [deep results](deep-browser.jsonl). | `web/src/Panes.tsx:680` and `web/src/Charts.tsx:337`: update stable polite status regions with an inspection-complete summary and search count. Avoid announcing every numeric animation or every keystroke. |
| QA-06 | major | 1.4.11 Non-text Contrast; related 2.4.7 Focus Visible | Select active option. 1440×900, **both themes**. Loan book → focus Filter by zone; ArrowDown opens, End moves active descendant to **Safe** while All zones remains selected. | Expected: visually distinguish the keyboard-active option from its neighbors, independently of the selected value. Actual: the list has no focus outline; active Safe is distinguished only by a nearly identical background. Text itself passes contrast. | **Observed rendered and measured.** [select-light.png](select-light.png), [select-dark.png](select-dark.png); active id=`loan-zone-option-3` in [extra results](extra-browser.jsonl). Active/background contrast is **1.072:1 light** (#f7f5ef/#fffdf8), **1.111:1 dark** (#202020/#161616). [Ratios](contrast.json). | `web/src/style.css:1700` / `.select-list li.is-active`: add a sufficiently contrasting active-option outline, marker or highlight. Keep the selected-value marker distinct and retain `aria-activedescendant`. |

WCAG interpretation uses the [W3C WCAG 2.2 Recommendation](https://www.w3.org/TR/WCAG22/), especially [hover content](https://www.w3.org/TR/WCAG22/#content-on-hover-or-focus), [non-text contrast](https://www.w3.org/TR/WCAG22/#non-text-contrast), [status messages](https://www.w3.org/TR/WCAG22/#status-messages) and [reflow](https://www.w3.org/TR/WCAG22/#reflow). Keyboard expectations were cross-checked with the [W3C APG listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/). Criterion mappings are this reviewer's assessments; screen-reader impact remains unverified where noted.

## Tested and passed, within these limits

- **Connect / approve / execute:** baseline covers absent wallet, rejection recovery, chain switching/addition, exact approval, receipt wait and refreshed allowance. Additional checks confirmed simulated deposit, borrow, repay, withdraw, work mint, liquidation, clear mark, apply pending, reserve sync, spread proposal and feed report; each closed review after confirmation and displayed “Confirmed. Refreshing balances and state…”. Mark execution was additionally confirmed in the deep check; redemption and approval execution in baseline. The fixture does not enforce all contract guards, so successful mock execution proves frontend wiring, not economic correctness.
- **Blocking states:** with `stale=true`, or `stale=false, spotMultiplier=2`, borrowing, indebted withdrawal, redemption, work mint, marking, beneficiary marking and liquidation disable. Deposit, repayment and clear mark remain available. Baseline confirms RPC failure closes transaction gates and refresh recovers; missing code fails closed; ratio-guard errors translate; backing=0.8 COMP-dollar backing with pinned feeds caps the quote. Governor proposal controls disappear when `governor` differs from account, while permissionless apply remains.
- **Keeper labels:** candidateCR=180 marked → View actions; candidateCR=140 unmarked → Mark; candidateCR=140 marked with `s.now=wallClock+1000s` → Grace; normal timestamp → Liquidate; `s.now=wallClock−5000s` → Mark again. The fixture generates mark time as `s.now−22000s`, grace=21600s and execution window=3600s. All five labels rendered correctly ([states](states-retry.jsonl)); synthetic future block time was used only to exercise Grace, not to validate chain timestamps.
- **Keyboard / semantics:** monitor ArrowRight moves focus and selection; source has roving tabindex in both tablists (static; full traversal unverified). Select opens, End/Home moves active descendant, Enter selects in baseline, Escape returns focus, and Tab closes and advances to Sort positions. Info opens on focus and Escape dismisses. Choice uses named groups and pressed buttons (static; spoken behavior unverified). Theme, refresh and info have descriptive names. Review uses native `showModal`; no background application control received focus during sampled traversal. Full traversal and spoken announcements are not certified.
- **Layout:** default populated desktop panels fit at 1440×900, 1280×800 and 1280×720, including a quoted Redeem and both Keeper modes ([extra results](extra-browser.jsonl)). Mobile pane bodies generally have internal vertical scrolling and no measured horizontal body overflow at 390×844/320×740; QA-01 overrides any claim of mobile usability. Existing suite's “all mobile panes reachable” label is too broad for its visibility-only assertion.
- **Color / typography / UI:** sampled text, chart and status pairs in both themes passed. Main/muted text on surface: light 16.137/5.901:1; dark 15.597/7.610:1. Healthy chart marks on risk bands: light 5.018:1; dark 5.082:1. Danger token on surface: light 8.061:1, dark 8.006:1. Surface/raised token calculations are in [contrast.json](contrast.json); raised status pairs not present in the inspected state are calculations, **not observations**. Selected list marker uses main text color; keyboard-active highlight fails separately. [Visible input focus](baseline-keyboard-focus.png) was inspected. Sampled pane buttons were at least 24×24 CSS px. Baseline emulated reduced motion and verified disabled chart transitions/entrances. Text-spacing override (line-height 1.5, letter-spacing .12em, word-spacing .16em, paragraph margin-bottom 2em) added internal vertical scrolling in Redeem/Work/Keeper at 1280×720 without measured horizontal overflow; [screenshot](text-spacing.png). This is a sampled check, not exhaustive spacing coverage.
- **Loan book / ENS:** empty RPC logs use Blockscout fallback; empty fallback or partial owner-read failure displays unknown/unreadable, not “no positions.” Search matches ENS, deterministic names and addresses. Aborted ENS routes after reload leave the connected wallet displayed as `0x0000…0a11`; expected network errors were logged for deliberately aborted requests. Slow ENS is only statically reviewed: `Who` renders the address immediately and replaces it after resolution.

## Unverified and unanswered

No real screen reader, physical touch device, native browser **400% zoom**, forced-colors interaction, or OS text-enlargement session was available. A 320px viewport is only a reflow proxy. The assigned browser tool never launched, despite a successful supplemental browser install; its requested evidence path remains unmet. All source-only claims above are unverified behavior, and no automated axe pass establishes WCAG conformance.

Not exhaustively tested: every text/non-text pair on every hover/error/selected background; all visible focus states across both themes; every target's spacing exception; tap-only Info dismissal; Shift+Tab from each Select; full cross-pane Tab order; loan-search spoken counts; slow ENS timing; Keeper overlapping-request races; all governance operation variants and beneficiary-mark confirmation; all translated refusal errors with their blocking quantities; action-specific stale feeds; real-wallet gas estimation or actual receipts. Remaining required checks make the review coverage **incomplete**, though the report and evidence are delivered. Findings require fixes and regression checks before a clean accessibility/QA verdict.
