chore(test): add e2e tests for overrideScrollViewContentInsetAdjustmentBehavior (iOS)#3960
chore(test): add e2e tests for overrideScrollViewContentInsetAdjustmentBehavior (iOS)#3960
Conversation
…tware-mansion/react-native-screens into @lkuchno/test-tab-screen-options-ios
…added missing ":"
…ns into @lkuchno/e2e-test-tabs-override-scroll-view-content-inset-ios
There was a problem hiding this comment.
Pull request overview
Adds iOS-only Detox coverage for the overrideScrollViewContentInsetAdjustmentBehavior prop on the Tabs single-feature test scenario, validating inset vs. behind-bars behavior across false, true, and default (omitted) configurations, plus cross-tab switching regression checks.
Changes:
- Added a new iOS-only Detox e2e spec exercising inset behavior on False/True/Default tabs and cross-tab switching.
- Updated the Tabs single-feature tests index to import the renamed scenario module.
- Added/updated scenario documentation and wired
testID/tabBarItemAccessibilityLabelto support e2e selection.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
| apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md | Documents manual expectations and notes that e2e coverage exists. |
| apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx | Updates scenario key and adds testID/tabBarItemAccessibilityLabel used by Detox. |
| apps/src/tests/single-feature-tests/tabs/index.ts | Switches import/registration to the renamed scenario module. |
| FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts | New Detox e2e tests asserting inset vs. non-inset behavior and cross-tab stability. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function getElementFrame(label: string): Promise<{ | ||
| x: number; | ||
| y: number; | ||
| width: number; | ||
| height: number; | ||
| }> { | ||
| const attrs = await element(by.label(label)).getAttributes(); | ||
| return (attrs as IosElementAttributes).frame; | ||
| } |
There was a problem hiding this comment.
getElementFrame assumes by.label(label) matches a single element and blindly casts getAttributes() to IosElementAttributes. In this scenario each tab renders identical labels (e.g. "Item 30"), and TabsContainer mounts all tab screens, so Detox can return a multi-match elements payload and this will crash or read the wrong frame. Scope queries to the active tab (e.g., withAncestor(by.id(scrollViewId))) and/or reuse getElementAttributes from e2e-utils which throws on multiple matches.
|
|
||
| describe('True tab (overrideScrollViewContentInsetAdjustmentBehavior: true)', () => { | ||
| beforeAll(async () => { | ||
| await element(by.label('override-inset-tab-true')).tap(); |
There was a problem hiding this comment.
Tab selection here uses element(...).tap() instead of the iOS coordinate-based forceTapByLabeliOS. Other tab e2e tests in this repo use forced taps on iOS to avoid tab bar items being obstructed (e.g. by the iOS 26 “Liquid Glass lens”), so these taps are likely to be flaky on iOS. Use the same forced-tap helper for consistency and stability.
| await element(by.label('override-inset-tab-true')).tap(); | |
| await forceTapByLabeliOS('override-inset-tab-true'); |
| beforeAll(async () => { | ||
| await element(by.label('override-inset-tab-default')).tap(); | ||
| }); |
There was a problem hiding this comment.
Same as above: use forceTapByLabeliOS (or a shared forceSelectTabByLabel wrapper) for tab selection on iOS to avoid flakiness from obstructed tab bar items.
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( | ||
| element( | ||
| by.label('overrideScrollViewContentInsetAdjustmentBehavior: true'), | ||
| ), | ||
| ).toBeVisible(); | ||
|
|
||
| await element(by.label('override-inset-tab-default')).tap(); | ||
| await expect( | ||
| element(by.id('override-inset-default-scrollview')), | ||
| ).toBeVisible(); | ||
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( |
There was a problem hiding this comment.
These expect(element(by.label('Item 1'))) assertions are vulnerable to ambiguous matches because every tab renders the same item labels (and inactive tabs remain mounted). Scope the matcher to the active scroll view (e.g. withAncestor(by.id('override-inset-true-scrollview'))) or give the items stable testIDs to ensure Detox targets the correct element.
| it('should show the information text visible (not hidden behind inset)', async () => { | ||
| await element(by.label('override-inset-tab-true')).tap(); | ||
| await expect( | ||
| element(by.id('override-inset-true-scrollview')), | ||
| ).toBeVisible(); | ||
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( | ||
| element( | ||
| by.label('overrideScrollViewContentInsetAdjustmentBehavior: true'), | ||
| ), | ||
| ).toBeVisible(); | ||
|
|
||
| await element(by.label('override-inset-tab-default')).tap(); | ||
| await expect( | ||
| element(by.id('override-inset-default-scrollview')), | ||
| ).toBeVisible(); | ||
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( | ||
| element( | ||
| by.label( | ||
| 'overrideScrollViewContentInsetAdjustmentBehavior: (not set, defaults to true)', | ||
| ), | ||
| ), | ||
| ).toBeVisible(); | ||
| await element(by.label('override-inset-tab-true')).tap(); | ||
| await expect( | ||
| element(by.id('override-inset-true-scrollview')), | ||
| ).toBeVisible(); | ||
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( | ||
| element( | ||
| by.label('overrideScrollViewContentInsetAdjustmentBehavior: true'), | ||
| ), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| it('should show the information text visible (not hidden behind inset)', async () => { | ||
| await element(by.label('override-inset-tab-false')).tap(); | ||
| await expect( |
There was a problem hiding this comment.
These two it(...) blocks in the same describe have identical titles, which makes it harder to identify which one failed in CI output. Rename one (e.g., distinguish True/Default comparison vs False→True switching).
| const informationFrame = await getElementFrame( | ||
| 'overrideScrollViewContentInsetAdjustmentBehavior: false', | ||
| ); | ||
| jestExpect(informationFrame.y).toEqual(16); |
There was a problem hiding this comment.
The assertion informationFrame.y === 16 hard-codes an absolute pixel position, which is very likely to vary across devices, orientations, Dynamic Type, and different navigation bar configurations. Instead, assert relative positioning (e.g., compare the header frame against the navigation bar/tab bar frames, or compare False vs True/Default header positions).
| const informationFrame = await getElementFrame( | |
| 'overrideScrollViewContentInsetAdjustmentBehavior: false', | |
| ); | |
| jestExpect(informationFrame.y).toEqual(16); | |
| const falseInformationFrame = await getElementFrame( | |
| 'overrideScrollViewContentInsetAdjustmentBehavior: false', | |
| ); | |
| await forceTapByLabeliOS('override-inset-tab-true'); | |
| await expect( | |
| element(by.id('override-inset-true-scrollview')), | |
| ).toBeVisible(); | |
| await scrollToMaxTop('override-inset-true-scrollview'); | |
| const trueInformationFrame = await getElementFrame( | |
| 'overrideScrollViewContentInsetAdjustmentBehavior: true', | |
| ); | |
| jestExpect(falseInformationFrame.y).toBeLessThan( | |
| trueInformationFrame.y, | |
| ); |
| jestExpect(isAboveTabBar(lastItemFrame, tabFrame)).toBe(false); | ||
| }); | ||
|
|
||
| it('should hide the information text behind the inset ', async () => { |
There was a problem hiding this comment.
This test name has a trailing space and refers to "behind the inset", but the scenario is about being clipped behind the navigation bar/tab bar. Tightening the wording (and removing the trailing space) will make failures easier to interpret.
| it('should hide the information text behind the inset ', async () => { | |
| it('should render the information text clipped behind the navigation bar', async () => { |
| const informationFrame = await getElementFrame( | ||
| 'overrideScrollViewContentInsetAdjustmentBehavior: false', | ||
| ); | ||
| jestExpect(informationFrame.y).toEqual(16); | ||
|
|
There was a problem hiding this comment.
This second informationFrame.y === 16 assertion has the same hard-coded pixel fragility as the earlier one and can easily break on different devices/safe-area insets. Prefer a relative assertion (e.g., compare the False header frame to the True header frame after scrolling both to top, or compare against the navigation bar frame).
| await element(by.label('override-inset-tab-true')).tap(); | ||
| await expect( | ||
| element(by.id('override-inset-true-scrollview')), | ||
| ).toBeVisible(); | ||
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( | ||
| element( | ||
| by.label('overrideScrollViewContentInsetAdjustmentBehavior: true'), | ||
| ), | ||
| ).toBeVisible(); | ||
|
|
||
| await element(by.label('override-inset-tab-default')).tap(); | ||
| await expect( | ||
| element(by.id('override-inset-default-scrollview')), | ||
| ).toBeVisible(); | ||
| await expect(element(by.label('Item 1'))).toBeVisible(); | ||
| await expect( | ||
| element( | ||
| by.label( | ||
| 'overrideScrollViewContentInsetAdjustmentBehavior: (not set, defaults to true)', | ||
| ), | ||
| ), | ||
| ).toBeVisible(); | ||
| await element(by.label('override-inset-tab-true')).tap(); | ||
| await expect( |
There was a problem hiding this comment.
Tab selection in this cross-tab flow uses tap() on iOS. For consistency with other tab e2e tests (and to reduce flakiness when tab bar items are partially obstructed), use forceTapByLabeliOS for these tab item interactions as well.
| async function getElementAttributes( | ||
| testLabel: string, | ||
| ): Promise<IosElementAttributes> { | ||
| const attrs = await element(by.label(testLabel)).getAttributes(); | ||
| return attrs as IosElementAttributes; | ||
| } | ||
|
|
There was a problem hiding this comment.
getElementAttributes is defined but never used, and it duplicates the shared getElementAttributes helper in e2e-utils (which also guards against multiple matches). Remove this function or switch call sites to the shared helper to avoid dead code and inconsistent attribute handling.
| async function getElementAttributes( | |
| testLabel: string, | |
| ): Promise<IosElementAttributes> { | |
| const attrs = await element(by.label(testLabel)).getAttributes(); | |
| return attrs as IosElementAttributes; | |
| } |
Description
This PR adds Detox e2e tests for the
overrideScrollViewContentInsetAdjustmentBehaviorprop onTabsScreen(iOS). The tests verify the three distinct tab configurations -false,true, and default (prop omitted) - asserting that scroll-view content is either clipped behind the tab bar or correctly inset from it, depending on the prop value. A cross-tab comparison suite is also included to guard against regressions when switching between tabs with different inset behaviors.The scenario screen (
test-tabs-override-scroll-view-content-inset-ios) was already refactored in a predecessor branch to add the requiredtestIDandtabBarItemAccessibilityLabelattributes; this PR completes that work by adding the automated tests.Closes: https://github.com/software-mansion/react-native-screens-labs/issues/1232
Changes
test-tabs-override-scroll-view-content-inset-ios.e2e.tswith four Detoxdescribeblocks covering thefalse,true, default, and cross-tab comparison cases; frame-position helpers are used to assert inset vs. non-inset behavior without pixel-hardcodingscenario.mde2e section to reflect that automated tests are now in place