fix(mobile): read an iOS screen title from the navigation bar identifier

Merged
SP
4d ago
+1351-557

What and why

Split out of #1105, for #842. This does not close #842: an untitled sheet still takes the navigation bar of the screen behind it.

  • screenTitle returns an iOS navigation bar's identifier only when a text inside the bar shows that same string. UIKit and SwiftUI put the title in both. The bar's label is the back button on a pushed screen, and an untitled screen's identifier is a class name (_Tt…, Module.ClassName, or a bare ObjC name) with no matching text, so neither is a title.
  • An Android toolbar's resource id is not a title. The title is the toolbar's label, else the first text inside it.
  • iOS recordings that start on a pushed screen were re-recorded on an iPhone 17 Pro running the iOS 26.5 runtime. --strict-cache fails with REPLAY_STALE until that re-record.

Verified

iPhone 17 Pro, iOS 26.5 (23F73), the benchmark Release build.

  • pnpm run typecheck and packages/mobile unit tests (308).
  • Committed iOS captures assert screenTitle (modal-flow → Modal Flow, home → Benchmark Examples, bottom-tabs, stripe-paymentsheet, and the untitled SwiftUI screen → none).
  • SwiftUI fixture app (tests-capture/swiftui-bar, NavigationStack plus a custom toolbar), raw snapshots committed: Inbox / Message / Compose take the navigationTitle; the untitled screen's identifier is _TtGC7SwiftUI32NavigationStackHosting and screenTitle is undefined.
  • Agent cache re-recorded on that simulator. Login Form, Modal Flow, Apple Pay, and Permission Prompt replay under --strict-cache at dev.e2e.benchmark / <screen title>. Bottom Tabs (no bar) replays at dev.e2e.benchmark.
  • WebView Accessibility, Debounced Search, and Choice Controls are main's recordings with only startPath and endPath changed to the screen title. WebView replays under --strict-cache with Coupon applied in its end check.
  • Permission Prompt resets the microphone on the device under test (device.setPermission('microphone', 'reset'), iOS only) before the step, so the recorded Allow tap replays on a reused simulator. Replayed under --strict-cache.
  • Deleted: Apple Pay Billing Address (the sheet keyboard covers the remaining fields, AUTOMATION_UNSUPPORTED; CI already skips it) and Stripe PaymentSheet (the recording taps New card, dismisses the keyboard, and taps Pay without entering a card, so it cannot replay to the end).
  • Main's Debounced Search and Choice Controls do not finish under --strict-cache on iOS 26.5 either, with or without this change: the recorded tap on the Benchmark Target row has no touch point of its own (action-failed), and the ✓, Mushrooms checkbox is not on screen (target-not-found). iOS CI does not run --strict-cache, so both hand off to the model there as they do on main.

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.