feat(mobile): name screens by an app-declared marker with screenIdPrefix

SP
16h ago
+5544-1644

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 from testID; 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.
  • screenMarker is private. screenName requires the resolved viewport from surface.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-context and 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.ts captured the home list, pushed modal-flow, the sheet twice, the Actions tab, and layered-markers.
  • The sheet captures expose one marker each. The layered-markers capture has two visible markers on each platform. screenName picks Cover, 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 to y=841.
capturescreenName with e2e-screen:
ios modal-flowModal Flow (the bar label is still the back button, Benchmark Examples)
ios modal-flow-sheet-a and -bBenchmark modal, both times, not Modal Flow
ios bottom-tabs / bottom-tabs-actionsHome tab / Actions tab
android, the same sixthe same names
ios and android layered-markersCover, 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/**/*.mdx page for the behavior, and skills/e2e/ if the skill describes it.
  • Contract change: emitted .d.ts reviewed, tests/types/sdk-types.ts updated; wire change edits the schema and both fixtures.
  • Engine contract change (e2e/engine): changesets for e2e, @e2e-dev/web, and @e2e-dev/mobile.