Skip to content

feat(resolver): Phase 1 — RNTL-style discovery resolver (byRole/byText/byPlaceholder + selector bundle)#362

Merged
Lykhoyda merged 23 commits into
mainfrom
feat/rnt-selector-resolver-projector
Jun 19, 2026
Merged

feat(resolver): Phase 1 — RNTL-style discovery resolver (byRole/byText/byPlaceholder + selector bundle)#362
Lykhoyda merged 23 commits into
mainfrom
feat/rnt-selector-resolver-projector

Conversation

@Lykhoyda

Copy link
Copy Markdown
Owner

Phase 1: Discovery resolver (precision half of the selector-resolver design)

Adds an RNTL-style discovery ladder to the in-app fiber matcher, ported against a fiber→host adapter, emitting a fail-closed selector bundle.

  • Spec: docs/superpowers/specs/2026-06-19-rnt-selector-resolver-projector-design.md
  • Plan: docs/superpowers/plans/2026-06-19-rnt-selector-resolver-phase1.md

What's added (all ES5, inside the INJECTED_HELPERS IIFE)

  • 5 leaf helpers ported from React Native Testing Library v14 (MIT): __match (matcher + normalizer), __hostKind (fiber host classifier), __role (getRole), __accessibleName (computeAccessibleName; labelledBy resolved via text-content, cycle-safe), __hidden (isHiddenFromAccessibility).
  • Fail-closed truncation in interact()'s findFiber — the old silent 8000-node cap could silent-pick or mis-report "not found".
  • resolveLadder — composes the helpers into byRole(+name) / byText / byPlaceholder, with fail-closed multiplicity (Ambiguous), hidden-exclusion by default, and a selector bundle.
  • interact() routing through the ladder — provably gated so it cannot alter the testID / accessibilityLabel / truncation paths.
  • Anchor capturebundle.anchors ancestor walk for durable re-selection.
  • A vm-based test harness (test/unit/helpers/inject-harness.js).

Quality

  • TDD throughout (RED→GREEN per step); 11 new test files; full unit suite 2491/2491 green.
  • Per-task reviews (Tasks 0–8) + a final whole-branch review — all clean after fixing one Critical (a stale committed dist/ that omitted Task 8's anchors; rebuilt in fix(build)).
  • ES5-pure inside the injected IIFE; the labelledBy cycle is proven bounded.

Scope / follow-ups (Phase 2/3 — not in this PR)

  • The projector (bundle → Maestro YAML, CONTAINS-aware, TS-platform-gated) and bundle persistence + self-heal are separate phases per the design spec.
  • Deferred Minors (Phase 2): extract the shared resolveLadder/__resolveLadderFiber predicate; document matches cap-at-10 vs true count; __refTextContent join '' fidelity fix.
  • Recommended CI guard: git diff --exit-code -- scripts/cdp-bridge/dist/ after build — the committed dist is the shipped artifact (no build hook), and this class of stale-mirror bug already bit once.

🤖 Generated with Claude Code

Lykhoyda and others added 15 commits June 19, 2026 22:19
…(design)

Two-phase design: an RNTL-style discovery ladder (byRole/byText/byPlaceholder)
on the live fiber tree emitting a selector bundle, projected to Maestro YAML via
a fail-closed, CONTAINS-aware, TS-owned-gated fallback ladder, with bundle-aware
self-heal.

Grounded by a Codex adversarial debate (approach selection) + a 6-agent
hardening pass across rn-dev-agent / RNTL / maestro-runner. Captures the verified
CONTAINS-not-EXACT native-match verdict and two Section-1 corrections: the
maestro-runner selector gate is advisory-only (so the hard gate is re-homed into
the TS projector), and the RNTL helpers must be ported against a fiber->host
adapter rather than copied verbatim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9 bite-sized TDD tasks: Task 0 vm test harness, then __match / __hostKind /
__role / __accessibleName / __hidden ports, fail-closed truncation,
resolveLadder + interact() routing, and anchor capture — implementing the
precision half of the selector-resolver design spec.

Each task is failing-test (vm + buildFiber) -> ES5 port inside the
INJECTED_HELPERS IIFE -> passing test -> commit, with source-drift guards.
Drafted via an 8-agent grounding + drafting workflow against real RNTL v14
and injected-helpers source; task-number cross-refs and the bounds-null
caveat reconciled in self-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-matcher form ({value,exact?} | {regexSource,regexFlags?}); trim+collapse
normalizer that does NOT lowercase (case-insensitivity lives in the non-exact
compare). Bumps HELPERS_VERSION 26->27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port RNTL host-component-names (isHostText/TextInput/Image/Switch/
ScrollView/Modal) into a single hostKind(fiber) that maps a host name
(string fiber.type or fiber.type.displayName/name) to one of
text|textinput|image|switch|scrollview|modal|null. Name lists widened
to native view names (RCTSinglelineTextInputView, RCTImageView,
RCTModalHostView, ...) since live fibers expose the platform view name.
Placed at IIFE top level (not inside getTree); returns null for Views,
user components, text nodes, and null types. HELPERS_VERSION already
at 27 from Task 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 bumps HELPERS_VERSION past 26; the two #321 guards pinned it to
exactly 26 and broke on the first bump. Assert the >=26 baseline instead so
feature branches bump freely without false regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add normalizeRole + __role (explicit role → accessibilityRole image→img →
host Text → none), reusing __hostKind from Task 2. Does not reuse the digest
inferRole (which defaults Pressable to button); divergence pinned by test.
Bump HELPERS_VERSION 26→27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port RNTL computeAccessibleName + computeAriaLabel + getAriaLabelledByIds +
joinAccessibleNameParts to ES5 on __RN_AGENT. labelledBy nativeID refs are
resolved to the referenced node's plain TEXT CONTENT (port of RNTL
getTextContent via __refTextContent), NOT by recursively computing its
accessible name — this matches RNTL's computeAriaLabel and makes a malformed
labelledBy cycle (A->B->A) safe (no stack overflow). The normal child-name
recursion in computeAccessibleName is retained. Inline host-text parts join
with '' (so 'Sign'+'In' -> 'SignIn'), otherwise ' '. TextInput placeholder is
the name only at root. HELPERS_VERSION unchanged (27).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Climbs fiber.return, flattens memoizedProps.style arrays manually (no
StyleSheet.flatten in-page), and treats aria-hidden /
accessibilityElementsHidden / importantForAccessibility=no-hide-descendants
/ display:none / aria-modal host siblings as hidden. opacity:0 is not
hidden. HELPERS_VERSION stays at 27 (already bumped on this branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the silent `if (findCount > 8000) return;` cap with a
rootsSeeded-scaled node budget (Math.min(40000, 8000*roots)) plus a 3s
wall-clock guard, mirroring the salient-digest budget. On trip, set a
findTruncated flag and short-circuit interact() to return
{error:"Resolution truncated", truncated:true, scanned:<n>, hint:...}
BEFORE any tier[0] pick, "Component not found" branch, or onPress fire —
so a partial scan can never trigger a false action. HELPERS_VERSION stays
at 27 (single branch bump).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add __RN_AGENT.resolveLadder(specJson) composing __match/__role/
__accessibleName/__hidden/hostKind into byRole/byText/byPlaceholder
predicates. Collect-all across renderers: 0 -> Component not found, >1 ->
Ambiguous component match (count + descriptors), 1 -> bundle. Hidden
excluded unless includeHidden; bundle.bounds is null in Phase 1.
interact() routes role/name/text/placeholder specs (no testID/
accessibilityLabel) through the resolver and presses the found fiber or
its nearest onPress ancestor, via the fiber-returning twin
__resolveLadderFiber. Legacy testID/accessibilityLabel paths and Task 6
fail-closed truncation untouched. HELPERS_VERSION stays 27 (no bump).

Also widen the find-active-renderer-migration B145 guard window
(3000 -> 8000 chars): a pre-existing brittle source-position assertion
that Task 6 already pushed out of range; the guarded findFiber-in-
forEachRootFiber behavior is intact, only its byte offset moved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ixes; HELPERS_VERSION single-branch-bump policy)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add __collectAnchors — a bounded (depth 8) fiber.return ancestor walk
mirroring the setFieldValue ancestor walk — recording nearest-first
{testID, text, relation: childOf, depth, provenance} entries for any
ancestor with a testID/nativeID or explicit accessibility label
(__ariaLabel only, no recursive child traversal so bare host Text
nodes are skipped). provenance is authored-testID for testID/nativeID
ancestors, else text. Populate bundle.anchors in resolveLadder.
Expose __collectAnchors on the __RN_AGENT public surface.

Full suite: 2437/2437 pass, 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fix)

Task 8 changed src/injected-helpers.ts but did not rebuild the tracked
dist/injected-helpers.js. dist is the shipped artifact (imported by
cdp/setup.ts; no build hook), so the committed resolver silently omitted
bundle.anchors; CI masked it by rebuilding before tests. Regenerated dist
so the committed artifact matches src.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f3e4ebf3e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cdp-bridge/src/injected-helpers.ts
Comment thread scripts/cdp-bridge/src/injected-helpers.ts Outdated
… (live-device bug)

Live-simulator testing revealed every RN text/input element matched TWICE in
resolveLadder — once as the composite fiber (Text/TextInput) and once as its
host child (RCTText/RCTSinglelineTextInputView), both passing hostKind — so
byText/byPlaceholder always fail-closed as Ambiguous on a real device. The vm
tests missed it because buildFiber made one node per element.

Add __deepestOnly (RNTL matchDeepestOnly parity): drop any match that is an
ancestor of another match, keeping the deepest; distinct siblings stay
ambiguous. Applied in both resolveLadder and __resolveLadderFiber. Bump
HELPERS_VERSION 27->28.

Verified live on the running test app: byText "Go to Dashboard" and
byPlaceholder "Add a task..." now resolve uniquely with correct anchors.
Full suite 2495/2495.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Lykhoyda

Copy link
Copy Markdown
Owner Author

Validated live on the simulator (real device fibers)

Injected the built INJECTED_HELPERS over CDP into the running test app (iPhone 17, com.rndevagent.testapp) and ran resolveLadder against real fibers — the validation vm unit tests can't give (the spec's residual-risk #2: "unit tests on synthetic fibers can pass while device host-type detection is wrong").

Found + fixed a device-only bug (e53471e0): every RN text/input element matched twice — the composite Text/TextInput fiber and its host child (RCTText/RCTSinglelineTextInputView), both passing hostKind — so byText/byPlaceholder always fail-closed as Ambiguous on-device. Added matchDeepestOnly (RNTL parity): drop a match that is an ancestor of another, keep the deepest; distinct siblings stay ambiguous.

After the fix (live results):

  • byText "Go to Dashboard" → ✓ resolves uniquely, anchors: ["go-to-dashboard", …]
  • byPlaceholder "Add a task..." → ✓ task-input; "Search tasks..." → ✓ task-search-input
  • Correctly fail-closed: byRole('button') (38 matches) and absent text → no silent pick
  • __role correctly does not infer 'button' for unmarked Pressables (RNTL parity)

Full suite 2495/2495.

Note: resolveLadder doesn't accept testID as a matcher (that's the legacy interact fast-path) — minor, by design. Anchor lists repeat the same authored testID across ancestor depths — a Phase-2 dedup polish.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e53471e0e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cdp-bridge/src/injected-helpers.ts Outdated
Comment thread scripts/cdp-bridge/src/injected-helpers.ts
Lykhoyda and others added 2 commits June 19, 2026 23:12
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion)

The 'Require changeset' gate (per the #361/#363 post-mortem) requires shippable
src changes to bump the plugin manifest too, else the change reaches main but
not marketplace installs via /plugin update.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 557c4527b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cdp-bridge/src/injected-helpers.ts Outdated
Comment thread scripts/cdp-bridge/src/injected-helpers.ts Outdated
…ation, byText content, non-press guard

PR #362 review (chatgpt-codex-connector) — 4 P2 findings, all verified + fixed:

- #1 cdp_interact now accepts the ladder selectors (role / text / placeholder /
  exact / includeHidden): relax the host guard, forward the fields, add them to
  the zod schema. The resolver is reachable via the supported tool, not only via
  raw eval.
- #2 resolveLadder / __resolveLadderFiber had their own silent 8000-fiber cap;
  a duplicate past the cap could leave matched.length===1 and press the wrong
  element. Now fail-closed (truncated:true) with a rootsSeeded-scaled budget +
  wall-clock guard, matching the legacy findFiber path.
- #3 byText matched __accessibleName (accessibilityLabel precedence) instead of
  the visible text content — now uses __refTextContent (getTextContent port);
  accessible names stay for byRole/name; bundle.text is text content too.
- #4 the ladder interact() branch pressed regardless of the requested action —
  now fails closed for any action other than press.

Verified live on the running test app; full suite 2502/2502.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Lykhoyda

Copy link
Copy Markdown
Owner Author

Addressed all four findings in ab9f8936 (new unit tests + verified live on the running test app; full suite 2502/2502):

  • Allow ladder selectors through cdp_interact — relaxed the host guard, forwarded role/text/placeholder/exact/includeHidden, and added them to the cdp_interact zod schema. The resolver is now reachable via the supported tool, not only raw eval.
  • Report truncation before returning partial matchesresolveLadder/__resolveLadderFiber now fail closed (truncated:true) with a rootsSeeded-scaled budget + wall-clock guard, matching the legacy findFiber path. (Test: a duplicate past the cap returns truncated, not found:1.)
  • Match byText against visible text content — switched byText + bundle.text from __accessibleName to __refTextContent (the getTextContent port); accessible names stay for byRole/name. (Test: <Text accessibilityLabel="AX">Visible</Text> matches Visible, not AX.)
  • Reject non-press ladder interactions — the ladder branch now fails closed for any action other than press (verified live: longPress returns an error instead of pressing).

Thanks — all four were accurate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab9f8936cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cdp-bridge/src/injected-helpers.ts
… testID matcher

PR #362 (chatgpt-codex-connector) — 2 P2 findings:
- #5 __hostKind now recognizes Android's `AndroidTextInput` host name; without it
  byPlaceholder/byText returned null for every TextInput in Android sessions.
- #6 resolveLadder / __resolveLadderFiber now match a `testID` spec — the spec
  already accepted testID but had no matcher, so resolveLadder({testID:'x'})
  returned Component not found for mounted ids.

Bump HELPERS_VERSION 28->29 (the injected surface changed across the review
fixes; a same-version re-inject is skipped by the __v freshness guard).
Verified live (testID + byText resolve on the running app) + full suite 2507/2507.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Lykhoyda

Copy link
Copy Markdown
Owner Author

Round 2 addressed in the latest commit (full suite 2507/2507, testID resolution verified live):

  • Add AndroidTextInput to textinput hosts — added AndroidTextInput to the __hostKind textinput set; byPlaceholder/byText now resolve TextInputs in Android sessions.
  • Resolve testID specs in the ladderresolveLadder/__resolveLadderFiber now match a testID spec (exact testID/nativeID), so resolveLadder({testID}) resolves mounted elements; fail-closed Component-not-found for absent ids.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9b84b4028

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cdp-bridge/src/injected-helpers.ts
Comment thread scripts/cdp-bridge/src/injected-helpers.ts Outdated
…element duplicates

#7: __deepestOnly blanket-dropped every ancestor match, so two DISTINCT nested
components both matching a selector (e.g. an outer card button + an inner button
both named "Settings") collapsed to the inner one and got silently pressed
instead of failing closed as Ambiguous.

Now drop a match's nearest matching ancestor only when they are the SAME element:
(1) composite+host pair (the composite wrapper of a host match), or (2) the same
testID/nativeID (one element whose id propagates across nested fibers, e.g. a tab
button). Distinct nested matches stay Ambiguous.

The same-testID arm also keeps resolveLadder({testID}) at found:1 (a tab's id
spans ~7 fibers on-device). Bump HELPERS_VERSION 29->30.
Verified live (testID / byText / byPlaceholder resolve; nested stays ambiguous);
full suite 2509/2509.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Lykhoyda

Copy link
Copy Markdown
Owner Author

Round 3 (#7 Keep nested role matches ambiguous) addressed in the latest commit (full suite 2509/2509; verified live):

__deepestOnly no longer blanket-drops every ancestor. It now collapses a match's nearest matching ancestor only when they are the same element — either a composite+host pair, or fibers sharing the same testID/nativeID (one element whose id propagates across nested fibers). Two distinct nested components that both match (outer card button + inner button) stay Ambiguous. The same-testID arm also keeps resolveLadder({testID}) resolving to one element (a tab id spans ~7 fibers on-device).

…opt-out + role normalization)

#8: byRole now respects the explicit accessible={false} opt-out (RNTL
isAccessibilityElement) — an opted-out duplicate (e.g. an offscreen Pressable
kept mounted) no longer causes false ambiguity or gets pressed.

#9: the REQUESTED role is now normalized (normalizeRole(spec.role)) like the
element side, so byRole({role:'image'}) matches an element whose accessibilityRole
'image' normalizes to 'img' — callers pass the RN prop value, not 'img'.

Bump HELPERS_VERSION 30->31. Full suite 2513/2513; byRole verified live
(buttons still resolve, no accessible-exclusion regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Lykhoyda

Copy link
Copy Markdown
Owner Author

Round 4 addressed in the latest commit (full suite 2513/2513; byRole verified live):

  • Respect accessible={false} in role matchesbyRole now requires an accessibility element and skips fibers with an explicit accessible={false} (RNTL isAccessibilityElement). An opted-out duplicate no longer causes false ambiguity or gets pressed.
  • Normalize requested image roles — the requested role is now normalized via normalizeRole(spec.role) just like the element side, so resolveLadder({role:"image"}) / cdp_interact role:"image" match a React Native image (accessibilityRole:"image"img); callers pass the RN prop value, not img.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d689212a91

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cdp-bridge/src/injected-helpers.ts Outdated
…cessibility-element predicate

The earlier accessible={false} exclusion still let a plain
<View accessibilityRole="button"> with accessible undefined match byRole, even
though RNTL / screen readers would not expose it. byRole now requires the full
isAccessibilityElement predicate (__isA11yElement): Text / TextInput / Switch and
Image+alt are accessibility elements by default; everything else must opt in
with accessible={true}. Applied in both isCandidate and __resolveLadderFiber.

Verified live: real RN buttons expose accessible:true in their fiber
memoizedProps (40/48 role=button fibers — host RCTView + inner Pressable layers),
so byRole({role:'button'}) still resolves them (count unchanged) — only plain-View
role props are now excluded. Updated synthetic byRole tests to model real a11y
elements (accessible:true). Bump HELPERS_VERSION 31->32. Full suite 2518/2518.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Lykhoyda

Copy link
Copy Markdown
Owner Author

Round 5 addressed in the latest commit (full suite 2518/2518; verified live on the running app):

byRole now gates on the full RNTL isAccessibilityElement predicate (__isA11yElement), not just the accessible={false} exclusion — a plain <View accessibilityRole="button"> with accessible undefined is no longer matched. Text / TextInput / Switch (and Image+alt) remain accessibility elements by default; everything else must opt in with accessible={true}. Applied in both isCandidate and the mirrored __resolveLadderFiber role branch.

I probed the live fiber tree before committing: real RN buttons expose accessible:true in their memoizedProps (40/48 role=button fibers — the host RCTView + inner Pressable layers), so byRole({role:"button"}) still resolves them (count unchanged). Only plain-View role props are excluded.

@Lykhoyda Lykhoyda merged commit 98d3fb7 into main Jun 19, 2026
11 checks passed
@Lykhoyda Lykhoyda deleted the feat/rnt-selector-resolver-projector branch June 19, 2026 22:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17c5b8847f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

var target = matched[0];
var tprops = target.memoizedProps || {};
var bundle = {
testID: tprops.testID,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve nativeID-backed IDs in selector bundles

When the resolver matches an element by nativeID (the predicate above accepts tprops.testID === spec.testID || tprops.nativeID === spec.testID, and placeholder/text matches can also land on a nativeID-only element), this bundle drops the durable id because it only copies tprops.testID. In that scenario resolveLadder({testID:'email-native'}) reports found:true but returns no bundle.testID, so the projector/self-heal path loses the strongest selector and falls back to weaker text/name fields; mirror the matcher/anchor logic here with tprops.testID || tprops.nativeID.

Useful? React with 👍 / 👎.

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