Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# mobile/android: RN a11y-label container nodes are LEAVES — group card content by geometric containment

Symptom: an Appium page-source parse shows a PR-review thread card's label node
(`Discussion thread ...`) with its full card bounds but ZERO children; the snippet,
comments, and pills look "outside" the card when checked by XML parentage.

Cause: on Android (uiautomator2), React Native hoists a labeled container's accessible
children to siblings in the flattened a11y tree; the label node keeps the card's rect
but stays a leaf.

Fix: assign content to cards GEOMETRICALLY — a node belongs to the card whose
label-node rect contains it (`inside(cardRect, nodeRect)` with a 2px tolerance). Works
with the viewport-clamp learning (clamped bands still nest). Two companion traps:
`scrollUntilVisible` stops the instant the label peeks past the clamp edge — the card's
header controls are not necessarily rendered yet, so swipe in a bounded loop until the
card's own control is INSIDE its rect; and after a settle swipe, re-query rects (the
card moved) — a stale rect in a swipe loop reads as missing content. Expansion state is
component state and survives Appium sessions: remount the tab (tap sibling tab, tap
back) at flow start when a flow depends on default expansion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# mobile/android PR-review E2E: toast a11y-invisibility, deep-link dedupe, disabled-Pressable attribute (pr-review-ux-7f22 r2, 2026-07-30)

Non-machine-specific techniques confirmed on the pixel9 API35 emulator:

1. **sonner-native toasts are NOT in the uiautomator tree at all.** A
`toast.error('Clipboard is empty')` rendered visually for ~4-8s while 10
consecutive UiSelector polls (text + content-desc, full-string) returned
zero matches. Detection must be pixel-based: screenshot ~1.2s after the
triggering tap, crop the top band (~y17-237 on 1080x2424), diff against a
baseline frame; auto-dismiss shows up as the band returning to baseline.
Extends `android-picker-tap-races-and-toast-capture` (which assumed the
toast is catchable by timing; on this build it is never in the tree).

2. **RN disabled Pressable on Android exports `enabled="false"` but keeps
`clickable="true"`** (handler artifact). Assert `enabled === 'false'`, and
for a read-only control also do a functional no-op probe (elementClick,
then assert state + backend request log unchanged).

3. **Same-route `mobile: deepLink` dedupes**: the router keeps the mounted
screen, so scroll offset, expansion state, and uncontrolled-field text all
survive across verifier runs. Scroll-to-top loops must not stop at the
first a11y sliver of a card (clamped band) — scroll until the needed child
(e.g. the first hunk gutter line) intersects the card rect. For a truly
fresh mount: `stopApp` + `launchApp`, then wait for a shell element
(`Home, tab, 1 of 4`) BEFORE the deep link — a link fired during boot is
dropped.

4. **CSS `uppercase` transforms the a11y text**: the Resolved badge matches
`RESOLVED`, not `Resolved`. Check the source for text-transform classes
before pinning selectors.

5. **DiffLine a11y labels (`Added line 9: ...`) are not exported to
uiautomator**; the visible per-line signal is the gutter glyph text
(`+ 9`, `- 8`, `· 15`) plus the merged code text node. Scope glyph/text
assertions to the card rect (sibling cards render the same strings).

6. **Android uncontrolled-field cleanup**: the entry screen's clear button
calls `TextInput.clear()`, which on Android unmounts the button (state
says empty) but leaves the native text. Use driver-level
`eraseText()` (elementClear) — a real edit that fires onChangeText and
clears dependent helpers.

7. **adb wedge recovery variant**: guest wedged under host load (3 slots,
load ~30-60) presented as adbd timeouts -> `adb devices` shows the device
OFFLINE with qemu at 0% CPU. `adb root` (then `unroot`) restarted adbd and
the guest came back; bounded polls of `getprop sys.boot_completed`,
`pidof system_server`, and `pm list packages` confirm health before
rerunning login.sh. No emulator relaunch needed. Same family as
`android-emulator-systemserver-restart-systemui-anr-under-load`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# mobile e2e: helper patterns are full-string REGEXES — escape parens/dots/plus in literal labels

Symptom: a flow's `waitVisible('Discussion thread src/alpha.ts L10 (RIGHT)')` times out
for 20s while the exact card is on screen; `scrollUntilVisible('Conversation comment at T+2')`
overscrolls to the list bottom; an invariant that tolerates empty `findRects` results then
passes VACUOUSLY.

Cause: `e2e/wdio/helpers.js` `findAll` feeds the pattern to Android UiSelector
`textMatches`/`descriptionMatches` (whole-string Java regex) or the iOS `MATCHES` predicate.
Literal labels containing regex specials silently never match: `(RIGHT)` reads as a group
(missing the literal parens), `.` matches anything, and `T+2` reads as "one or more T then 2".
Worse, `(await findRects(bad))` returns `[]`, so "no X intersects Y" invariants pass with the
target never located.

Fix: in flow files, wrap every literal label in an escaper and assert presence before geometry:

```js
const rx = s => new RegExp('^' + s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$');
```

A `^...$` anchor is harmless (the driver matches whole strings anyway). When a wait/scroll
times out on something visibly on screen, suspect the pattern before suspecting the app.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# mobile: scrollUntilVisible throws "scrolled N times without finding" but lands on the target

Symptom: `scrollUntilVisible('<full label>')` throws after exhausting maxScrolls, yet a
hierarchy dump taken immediately after shows the target fully visible and correctly
positioned (observed twice on iOS PR-review Discussion tab, pr-review-ux-7f22).

Cause: the helper checks visibility between flicks; the final flick moves the list
past the last check point (momentum + FlashList re-layout), so the loop exhausts while
the end state is fine. It is the same flick-overshoot family as
`mobile-e2e-top-clip-positioning-flashlist-clamp` — not a new defect.

Fix: after a failed scrollUntilVisible, dump the hierarchy before retrying or
classifying — the target is often already on screen. For flows that must not throw,
wrap in `.catch(() => {})` and follow with an explicit `assertVisible` probe.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# mobile iOS: PR-link entry screen — Open button shifts +7pt when the field gains content (clear-X)

Symptom: verifying "invalid link renders INLINE in the reserved slot, Open button bounds
unchanged" by recording Open bounds on the empty entry screen, pasting `not-a-url`, and
re-measuring FAILS: Open moves y 236→243 and grows h 39→40 even though the helper slot is
documented as layout-stable.

Cause: the in-field clear-X (`Clear pull request link`, h-13/w-13 ≈ 46pt) appears whenever the
field has content; it is taller than the 39pt input, so the input ROW grows 39→46 and pushes the
helper slot and Open button down ~7pt. This is field-CONTENT state, not helper-message state —
the clear-X predates the pr-review-ux-7f22 changes (present at r1 head `dd659d4a9`).

Fix (decisive experiment for the slot invariant): hold field state constant across the helper
transition — paste `not-a-url` (helper visible, clear-X present), tap the clear-X: the shipped
clear handler does NOT clear `helperMessage`, so the invalid helper stays mounted with an empty
field, and Open returns EXACTLY to the initial rect ({x:21,y:236,w:360,h:39} on iPhone 17).
That byte-identical comparison is the assertion the AC intends; the +7pt with a filled field is
pre-existing approved behavior, not a regression.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# mobile iOS: XCUI tree — a11y-labeled parent Views are leaves; assert by geometry, not XML ancestry

Symptom: a flow parses `driver.getPageSource()` to assert "snippet inside thread card" via XML
parent/child containment and finds the card node has ZERO descendants — while pills, snippets and
comment texts all render on screen. Plain-text labels also appear exactly TWICE in the tree, and
`visible="true"` matches nothing, so visibility filtering empties the probe.

Cause (iOS 26.5 sim, RN 0.86, PR-review Discussion tab, verified 2026-07-30):
- A RN View with `accessibilityLabel` (thread cards, header Pressables) becomes ONE accessibility
element; its children are NOT XML descendants — they appear as geometric SIBLINGS elsewhere in
the tree. Containment must be rect-in-rect on the page-source coordinates (`x/y/width/height`).
- Plain RN Texts are mirrored across two windows at identical rects — dedupe by
(label,x,y,width,height) before counting, and never assert `count === 1` on a Text label
(transiently or consistently 2). Interactive elements (Pressables with labels) appear once.
- Badge Texts using NativeWind `uppercase` expose the TRANSFORMED string (`RESOLVED`, `OUTDATED`,
`FILE`) — match the uppercase form.
- Off-screen FlashList rows stay mounted in the tree; the `visible` attribute is useless (never
`true`). Before coordinate-tapping a node, require `0 <= y && y+h <= screenHeight`; swipe
(clamped, harmless) until the node is in the viewport.
- Diff-line gutters (line numbers, +/- markers) are `accessibilityElementsHidden`; the code
container's `buildDiffLineAccessibilityLabel` (`Deleted line 8: ...`) is NOT what reaches the
tree — the inner selectable RNText's raw code string is. Assert snippet content via code texts
(`// stub change`, `return 1;`), and rely on screenshots for line-number evidence.
- Emoji labels (`👍 reaction, 2 reactions`) match fine through the helpers' predicate path.

Fix: parse page source into a rect tree (fast-xml-parser from the repo's `.pnpm` store requires
fine from scratch flows), keep `flat()` (dedupe) + `insideRect()` + `inViewport()` helpers, and
tap by node-rect centre via W3C actions instead of global `tapOn` when the same label exists in
several cards (e.g. `Resolve thread`).
Loading
Loading