callstack / callstack/react-native-pager-view
Synthetic touches (XCUITest/Detox) silently ignored on iPhone in portrait (horizontalSizeClass=compact), but work fine on iPad (regular)
- Vorherrschende Sprache
- TypeScript
- Sterne
- 3.4k
- Forks
- 476
- Ø Merge
- 10 T. 21 Std.
- Gemergte PRs (30 T.)
- 2
Beschreibung
# Synthetic touches (XCUITest/Detox) silently ignored on iPhone in portrait (horizontalSizeClass=compact), but work fine on iPad (regular)
## Environment
- `react-native-pager-view`: 8.0.0
- `detox`: 20.50.4
- React Native / Expo Dev Client
- iOS 26.5, tested on multiple simulators: iPhone 14, iPhone 17, iPhone 17 Pro Max (all portrait), and iPad (landscape)
- New Architecture (SwiftUI-based iOS implementation, confirmed via view hierarchy dump: `react_native_pager_view.PagerViewProvider` → `UIKitPagingView` → `PagingCollectionView`, all wrapped through nested `UIHostingView`/`SwiftUIIntrospect` layers)
## Summary
A `TouchableOpacity` rendered on the first page of a `PagerView` never receives `onPressIn`/`onPressOut`/`onPress` when tapped via a synthetic touch (XCUITest through Detox), **but works perfectly with a real click on the same simulator**. The failure is 100% reproducible on every iPhone model tested (regardless of screen size) while portrait, and never reproduces on iPad in landscape, using the exact same JS/JSX code path.
## Reproduction
```jsx
console.log('onPress fired')}
onPressIn={() => console.log('onPressIn fired')}
onPressOut={() => console.log('onPressOut fired')}
>
Tap me
{/* ...other pages */}
```
Detox test:
```js
await element(by.id('my-button')).tap();
```
## Expected behavior
`onPressIn` → `onPressOut` → `onPress` fire in order, same as a real tap.
## Actual behavior
- On **iPad, landscape** (`horizontalSizeClass == .regular`): all three handlers fire correctly with `.tap()`.
- On **any iPhone, portrait** (`horizontalSizeClass == .compact`): **none** of the three handlers fire. No error is thrown by Detox — the tap action itself completes without exception, and Detox reports the element as `hittable: true`, `visible: true`, with a correct `frame`/`activationPoint` (confirmed via `getAttributes()`), yet nothing happens in the app.
- A **real mouse click** on the same simulator, same button, same session, fires all three handlers correctly and immediately. This rules out any issue in the button's own code, layout, or `onPress` logic.
## What we ruled out
- **Geometry / hit-testing target**: `getAttributes()` before the tap shows a correct `frame`, `elementFrame`, `activationPoint` (centered within the visible colored box), `hittable: true`, `visible: true`, `enabled: true` — identical shape of data on both iPhone (fails) and iPad (passes).
- **Occlusion**: full view hierarchy dump (`--loglevel verbose`) at the moment of failure shows no other view on top of the button; it is the last element in its local z-order.
- **Gesture variant**: tried `.tap()`, `.tap({x, y})` with explicit coordinates, `.longPress(150)`, `.longPress(600)`, `.multiTap(2)` — all fail identically (no handler fires).
- **Leaf vs. container element**: moved the `testID` from the outer `View` to the inner `Text` (leaf node) — same failure.
- **Screen size**: tested iPhone 14, iPhone 17, and iPhone 17 Pro Max (largest available) in portrait — all fail identically. This rules out screen size / safe area as the variable; the only difference between "works" and "fails" that we could isolate is **`horizontalSizeClass` (`compact` for any iPhone in portrait, `regular` for iPad)**, not device model or physical screen size.
- **Simulator instance / degradation**: reproduced identically on two freshly created simulator instances (`simctl create` from scratch), ruling out a corrupted/stale simulator as the cause.
- **Detox synchronization**: adding `device.disableSynchronization()` around the tap made no difference.
## Additional data point
Delaying the tap by ~1.5s after the screen is visible changes the *failure mode* (from a Detox-level `"View is not hittable at its visible point"` hit-test error, seemingly caused by hitting an animating full-screen `UIView` mid-transition, to a silent no-op with no handler firing at all) but does not fix the underlying issue.
## Hypothesis (unconfirmed)
We suspect this may be related to how the SwiftUI-based paging container's own gesture recognizer (for horizontal swipe) claims/arbitrates touches on `compact` width, in a way that differs from `regular` width and that a synthetic (XCUITest-generated) touch loses in this arbitration while a real touch does not. We have **not** been able to confirm this at the native Swift level — this is a JS-side diagnosis only, using `console.log` in touch handlers and Detox's `getAttributes()`/view-hierarchy dump.
We found a related, unresolved Apple Developer Forums report describing SwiftUI `Button` actions being ignored inside a horizontal `ScrollView` near the top of a view (iOS/iPadOS 27 beta), which suggests this may be a broader category of SwiftUI gesture-arbitration issue rather than something specific to this library — but we have no confirmation either way.
## Question for maintainers
- Is this a known issue with the SwiftUI-based (New Architecture) iOS implementation introduced in 8.x?
- Is there a recommended workaround (e.g. a prop to adjust gesture recognizer behavior, or a way to bypass the paging container's gesture claim for children) for consumers who need reliable XCUITest/Detox automation on iPhone in portrait?
- Was this present in the pre-8.x (`UIPageViewController`-based) implementation? We have not tested a downgrade yet.
Happy to provide the full view hierarchy dumps, `getAttributes()` output, or a minimal reproduction repo if useful.
Beitragsleitfaden
Rechercherichtung
Beginne mit dem bereitgestellten PagerView JSX und der Reproduktion des Detox-Taps und vergleiche dabei iPhone im Hochformat (compact) mit iPad im Querformat (regular). Untersuche die SwiftUI-basierte iOS-Paging-Implementierung und ihre Gesten-Arbitrierung und ermittle anschließend, ob synthetische Berührungen von Child-Elementen verloren gehen. Als abgeschlossen gilt die Untersuchung, wenn eine bestätigte Ursache in der Library identifiziert und ein Workaround dokumentiert wurde oder festgestellt wurde, dass das Problem außerhalb der Library liegt.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- ios, react-native, swift
- Bereich
- mobile-dev, testing
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Muss geklärt werden
- Anfängerfreundlichkeit
- 45/100