callstack / callstack/react-native-pager-view
Synthetic touches (XCUITest/Detox) silently ignored on iPhone in portrait (horizontalSizeClass=compact), but work fine on iPad (regular)
- Dominant language
- TypeScript
- Stars
- 3.4k
- Forks
- 476
- Avg merge
- 10d 21h
- Merged PRs (30d)
- 2
Description
# 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.
Contributor guide
Assessment
This issue has not been assessed yet.