Skip to content

chore(test): add e2e tests for overrideScrollViewContentInsetAdjustmentBehavior (iOS)#3960

Open
LKuchno wants to merge 10 commits intomainfrom
@lkuchno/e2e-test-tabs-override-scroll-view-content-inset-ios
Open

chore(test): add e2e tests for overrideScrollViewContentInsetAdjustmentBehavior (iOS)#3960
LKuchno wants to merge 10 commits intomainfrom
@lkuchno/e2e-test-tabs-override-scroll-view-content-inset-ios

Conversation

@LKuchno
Copy link
Copy Markdown
Collaborator

@LKuchno LKuchno commented Apr 30, 2026

Description

This PR adds Detox e2e tests for the overrideScrollViewContentInsetAdjustmentBehavior prop on TabsScreen (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 required testID and tabBarItemAccessibilityLabel attributes; this PR completes that work by adding the automated tests.

Closes: https://github.com/software-mansion/react-native-screens-labs/issues/1232

Changes

  • FabricExample/e2e: Added test-tabs-override-scroll-view-content-inset-ios.e2e.ts with four Detox describe blocks covering the false, true, default, and cross-tab comparison cases; frame-position helpers are used to assert inset vs. non-inset behavior without pixel-hardcoding
  • Test/Tabs: Updated scenario.md e2e section to reflect that automated tests are now in place

@LKuchno LKuchno added area:tabs Issue related to bottom tabs type:e2e Actions related to e2e maintenance and development, especially automation. platform:ios Issue related to iOS part of the library labels Apr 30, 2026
@LKuchno LKuchno requested a review from Copilot April 30, 2026 14:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/tabBarItemAccessibilityLabel to 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.

Comment on lines +26 to +34
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;
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

describe('True tab (overrideScrollViewContentInsetAdjustmentBehavior: true)', () => {
beforeAll(async () => {
await element(by.label('override-inset-tab-true')).tap();
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
await element(by.label('override-inset-tab-true')).tap();
await forceTapByLabeliOS('override-inset-tab-true');

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +122
beforeAll(async () => {
await element(by.label('override-inset-tab-default')).tap();
});
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: use forceTapByLabeliOS (or a shared forceSelectTabByLabel wrapper) for tab selection on iOS to avoid flakiness from obstructed tab bar items.

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +172
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(
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +193
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(
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +87
const informationFrame = await getElementFrame(
'overrideScrollViewContentInsetAdjustmentBehavior: false',
);
jestExpect(informationFrame.y).toEqual(16);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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,
);

Copilot uses AI. Check for mistakes.
jestExpect(isAboveTabBar(lastItemFrame, tabFrame)).toBe(false);
});

it('should hide the information text behind the inset ', async () => {
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
it('should hide the information text behind the inset ', async () => {
it('should render the information text clipped behind the navigation bar', async () => {

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +201
const informationFrame = await getElementFrame(
'overrideScrollViewContentInsetAdjustmentBehavior: false',
);
jestExpect(informationFrame.y).toEqual(16);

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +180
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(
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +16
async function getElementAttributes(
testLabel: string,
): Promise<IosElementAttributes> {
const attrs = await element(by.label(testLabel)).getAttributes();
return attrs as IosElementAttributes;
}

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
async function getElementAttributes(
testLabel: string,
): Promise<IosElementAttributes> {
const attrs = await element(by.label(testLabel)).getAttributes();
return attrs as IosElementAttributes;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:tabs Issue related to bottom tabs platform:ios Issue related to iOS part of the library type:e2e Actions related to e2e maintenance and development, especially automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants