feat(mobile): name screens by an app-declared marker with screenIdPrefix
SP
16h ago+5544+5544+0123456789012345678901234567890123456789-1644-1644-0123456789012345678901234567890123456789
What and why
screenTitle()reads the screen only from an iOS navigation bar or an Android toolbar. React Native apps commonly hide that header (headerShown: false, custom headers), so every screen's location collapses to<bundle id>and the replay cache's start-screen check (wrong-context) cannot tell screens apart.- New opt-in
mobile({ screenIdPrefix }): the value after the prefix in an on-screen element's accessibility identifier (iOS) or resource id (Android) names the screen. React Native sets both fromtestID; a native Android id's<package>:id/is dropped before matching. - When several elements match, hidden nodes and nodes wholly outside the viewport are skipped. Of the rest, the last in document order wins. The layered fixture proves that order on real iOS and Android trees; it does not establish ordering between separate navigator screens. iOS has no
visibleToUser, so viewport overlap matters for screens kept mounted underneath. screenMarkeris private.screenNamerequires the resolved viewport fromsurface.ts.- A marker outranks the title bar. The option is opt-in, so this changes nothing for existing users. With the option unset, locations stay as they are. A screen that gains a marker gets a new location, so its recorded steps miss once with
wrong-contextand record again. The cache key is unchanged. - The iOS navigation-bar identifier fix is #1183, not this PR. Related to #842, which stays open: an untitled sheet still takes the bar of the screen behind it.
// e2e.config.ts
const iphone = mobile({ platform: 'ios', screenIdPrefix: 'e2e-screen:' });
// a React Native screen with headerShown: false
<View testID="e2e-screen:Offers" style={{ flex: 1 }}>{/* ... */}</View>
// location: "<bundle id> / Offers" (was "<bundle id>")
Verified
Ran it locally: yes
- iPhone 17 Pro on iOS 26.5 and Pixel 10 on Android 37, benchmark Release builds.
tests-capture/snapshots.e2e.tscaptured the home list, pushedmodal-flow, the sheet twice, the Actions tab, andlayered-markers. - The sheet captures expose one marker each. The
layered-markerscapture has two visible markers on each platform.screenNamepicksCover, the last one in document order. - The iOS home list's horizontal scroll bar is back at
y=807, matching the baseline before the wrapper view moved it toy=841.
| capture | screenName with e2e-screen: |
|---|---|
ios modal-flow | Modal Flow (the bar label is still the back button, Benchmark Examples) |
ios modal-flow-sheet-a and -b | Benchmark modal, both times, not Modal Flow |
ios bottom-tabs / bottom-tabs-actions | Home tab / Actions tab |
| android, the same six | the same names |
ios and android layered-markers | Cover, with Underlay also visible |
packages/mobile unit tests: 360. screenMarker is not exported.
Checklist
- Changeset added (
pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only. - Breaking change: title carries
!, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md). - Docs updated in the same PR: the
docs/**/*.mdxpage for the behavior, andskills/e2e/if the skill describes it. - Contract change: emitted
.d.tsreviewed,tests/types/sdk-types.tsupdated; wire change edits the schema and both fixtures. - Engine contract change (
e2e/engine): changesets fore2e,@e2e-dev/web, and@e2e-dev/mobile.