From 4c3e912513e50b4e859e6bf41857a02528449656 Mon Sep 17 00:00:00 2001 From: lkuchno Date: Wed, 29 Apr 2026 13:05:22 +0200 Subject: [PATCH 1/8] refactor(test): override-scroll-view-content-inset --- ...rride-scroll-view-content-inset-ios.e2e.ts | 124 ++++++++++++++++ .../tests/single-feature-tests/tabs/index.ts | 4 +- .../index.tsx} | 36 ++++- .../scenario.md | 134 ++++++++++++++++++ 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts rename apps/src/tests/single-feature-tests/tabs/{override-scroll-view-content-inset.tsx => test-tabs-override-scroll-view-content-inset-ios/index.tsx} (79%) create mode 100644 apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md diff --git a/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts new file mode 100644 index 0000000000..dd70e32558 --- /dev/null +++ b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts @@ -0,0 +1,124 @@ +import { expect as jestExpect } from '@jest/globals'; +import { device, expect, element, by } from 'detox'; +import { IosElementAttributes } from 'detox/detox'; +import { describeIfiOS, selectSingleFeatureTestsScreen } from '../../e2e-utils'; + +async function getElementAttributes( + testLabel: string, +): Promise { + const attrs = await element(by.label(testLabel)).getAttributes(); + return attrs as IosElementAttributes; +} + +async function getTabBarFrame(): Promise<{ + x: number; + y: number; + width: number; + height: number; +}> { + const attrs = await element(by.type('UITabBar')).getAttributes(); + return (attrs as IosElementAttributes).frame; +} + +function isAboveTabBar( + itemFrame: { y: number; height: number }, + tabBarFrame: { y: number }, +): boolean { + return itemFrame.y + itemFrame.height <= tabBarFrame.y; +} + +async function resetScrollToTop(scrollViewId: string) { + await element(by.id(scrollViewId)).scroll(5000, 'up', NaN, 0.9); +} + +describeIfiOS('Override Scroll View Content Inset (iOS)', () => { + beforeEach(async () => { + await device.reloadReactNative(); + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-override-scroll-view-content-inset-ios', + ); + }); + + describe('False tab (overrideScrollViewContentInsetAdjustmentBehavior: false)', () => { + it('should display the false tab scrollview with the tab bar visible', async () => { + await element(by.id('override-inset-false-scrollview')).scroll( + 600, + 'down', + Number.NaN, + 0.85, + ); + + const tabFalse = await getElementAttributes('override-inset-tab-false'); + console.log(tabFalse.frame.x, tabFalse.frame.y); + await device.tap({ x: tabFalse.frame.x, y: tabFalse.frame.y }); + await element(by.id('override-inset-false-scrollview')).scroll( + 600, + 'down', + Number.NaN, + 0.85, + ); + // await expect(element(by.id('override-inset-false-scrollview'))).toBeVisible(); + // await expect(element(by.type('UITabBar'))).toBeVisible(); + // const tabBarFrame = getTabBarFrame(); + // console.log(tabBarFrame) + }); + + // it('should hide the header label behind the inset (not visible)', async () => { + // await expect( + // element(by.label('overrideScrollViewContentInsetAdjustmentBehavior: false')), + // ).not.toBeVisible(); + // }); + + // it('should render the last item overlapping or behind the tab bar (not fully above it)', async () => { + // await element(by.id('override-inset-false-scrollview')).scroll( + // 5000, + // 'down', + // NaN, + // 0.1, + // ); + + // const lastItem = element(by.text('Item 30')); + // await expect(lastItem).toBeVisible(); + + // const lastItemFrame = await getFrame(lastItem); + // const tabBarFrame = await getTabBarFrame(); + + // jestExpect(isAboveTabBar(lastItemFrame, tabBarFrame)).toBe(false); + // }); + }); + + // describe('True tab (overrideScrollViewContentInsetAdjustmentBehavior: true)', () => { + // beforeEach(async () => { + // await element(by.id('override-inset-tab-true')).tap(); + // }); + + // it('should display the true tab scrollview with the tab bar visible', async () => { + // await expect(element(by.id('override-inset-true-scrollview'))).toBeVisible(); + // await expect(element(by.type('UITabBar'))).toBeVisible(); + // }); + + // it('should show the header label visible (not hidden behind inset)', async () => { + // await expect( + // element(by.label('overrideScrollViewContentInsetAdjustmentBehavior: true')), + // ).toBeVisible(); + // }); + + // it('should render the last item fully above the tab bar (proper inset applied)', async () => { + // await element(by.id('override-inset-true-scrollview')).scroll( + // 5000, + // 'down', + // NaN, + // 0.1, + // ); + + // const lastItem = element(by.text('Item 30')); + // await expect(lastItem).toBeVisible(); + + // const lastItemFrame = await getFrame(lastItem); + // const tabBarFrame = await getTabBarFrame(); + + // jestExpect(isAboveTabBar(lastItemFrame, tabBarFrame)).toBe(true); + // }); + // }); +}); diff --git a/apps/src/tests/single-feature-tests/tabs/index.ts b/apps/src/tests/single-feature-tests/tabs/index.ts index 4703ab3fb9..b9c01f8792 100644 --- a/apps/src/tests/single-feature-tests/tabs/index.ts +++ b/apps/src/tests/single-feature-tests/tabs/index.ts @@ -1,7 +1,7 @@ import type { ScenarioGroup } from '@apps/tests/shared/helpers'; import BottomAccessoryScenario from './bottom-accessory-layout'; -import OverrideScrollViewContentInsetScenario from './override-scroll-view-content-inset'; +import TestTabsOverrideScrollViewContentInset from './test-tabs-override-scroll-view-content-inset-ios'; import TestTabsTabBarHidden from './test-tabs-tab-bar-hidden'; import TabsScreenOrientationScenario from './tabs-screen-orientation'; import TabBarAppearanceDefinedBySelectedTabScenario from './test-tabs-appearance-defined-by-selected-tab'; @@ -17,7 +17,7 @@ import TestTabsTabBarControllerMode from './test-tabs-tab-bar-controller-mode-io const scenarios = { BottomAccessoryScenario, - OverrideScrollViewContentInsetScenario, + TestTabsOverrideScrollViewContentInset, TabBarAppearanceDefinedBySelectedTabScenario, TestTabsTabBarHidden, TabsScreenOrientationScenario, diff --git a/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx similarity index 79% rename from apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx rename to apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx index a1218a3161..50796c8db4 100644 --- a/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx @@ -10,7 +10,7 @@ import { createScenario } from '@apps/tests/shared/helpers'; const scenarioDescription: ScenarioDescription = { name: 'Override ScrollView Content Inset', - key: 'override-scroll-view-content-inset', + key: 'test-tabs-override-scroll-view-content-inset-ios', details: 'Tests overrideScrollViewContentInsetAdjustmentBehavior with different static values per tab. ' + 'False: content scrolls behind bars. True/Default: content is inset from bars.', @@ -19,9 +19,15 @@ const scenarioDescription: ScenarioDescription = { const ITEM_COUNT = 30; -function ScrollContent({ label }: { label: string }) { +function ScrollContent({ + label, + testID, +}: { + label: string; + testID: string; +}) { return ( - + overrideScrollViewContentInsetAdjustmentBehavior: {label} @@ -37,15 +43,30 @@ function ScrollContent({ label }: { label: string }) { } function FalseTab() { - return ; + return ( + + ); } function TrueTab() { - return ; + return ( + + ); } function DefaultTab() { - return ; + return ( + + ); } export function App() { @@ -59,6 +80,7 @@ export function App() { Component: FalseTab, options: { title: 'False', + tabBarItemAccessibilityLabel: 'override-inset-tab-false', ios: { overrideScrollViewContentInsetAdjustmentBehavior: false, icon: { type: 'sfSymbol', name: 'xmark.circle' }, @@ -70,6 +92,7 @@ export function App() { Component: TrueTab, options: { title: 'True', + tabBarItemTestID: 'override-inset-tab-true', ios: { overrideScrollViewContentInsetAdjustmentBehavior: true, icon: { type: 'sfSymbol', name: 'checkmark.circle' }, @@ -81,6 +104,7 @@ export function App() { Component: DefaultTab, options: { title: 'Default', + tabBarItemTestID: 'override-inset-tab-default', ios: { icon: { type: 'sfSymbol', name: 'circle.dashed' } }, }, }, diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md new file mode 100644 index 0000000000..5a282da4b0 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md @@ -0,0 +1,134 @@ +# Test Scenario: overrideScrollViewContentInsetAdjustmentBehavior + +## Details + +**Description:** Validates the +`overrideScrollViewContentInsetAdjustmentBehavior` prop on `TabsScreen` +(iOS only). By default, React Native sets ScrollView's +`contentInsetAdjustmentBehavior` to `never`, which prevents the scroll +view from respecting navigation bar and tab bar insets. When this prop +is `true` (the default), the behavior is overridden back to UIKit's +`automatic`, so content is inset from the bars. When set to `false`, +the content scrolls behind the bars without insets. +The test verifies that the three tabs — **False**, **True**, and +**Default** — each exhibit the expected inset behavior and that the +**Default** tab (prop omitted) matches the **True** tab. + +**OS test creation version:** iOS 18.6 and 26.2 + +## E2E test + +No: The distinction between content scrolling behind bars versus being +inset from them is a visual/pixel-level difference that cannot be +reliably expressed as a pass/fail assertion in the current E2E +infrastructure without snapshot or frame-comparison tooling. + +## Prerequisites + +- iOS device or simulator (iPhone) + +## Note + +- This prop is iOS-only and Fabric-only; skip on Android. +- "Content scrolls behind bars" means list items are visible underneath + the navigation bar and/or tab bar when scrolled to the top or bottom + of the list. +- "Content inset from bars" means the first and last list items are + fully visible and never hidden behind a bar, even when the scroll + view is at its extreme positions. +- The navigation bar at the top and the tab bar at the bottom are both + relevant reference points for inset verification. + +## Steps + +### Baseline + +1. Launch the app and navigate to the + **Override Scroll View Content Inset** screen. + +- [ ] Expected: Three tabs are displayed in the tab bar: **False**, + **True**, and **Default**. The **False** tab is selected and shows + a scrollable list of 30 items. + +--- + +### `false` — content scrolls behind bars + +2. Confirm the **False** tab is active and scroll the list to the top. + +- [ ] Expected: The text label + `overrideScrollViewContentInsetAdjustmentBehavior: false` at + the top of the scroll content is partially or fully obscured + behind the navigation bar, because + `overrideScrollViewContentInsetAdjustmentBehavior` is `false` + and the scroll view uses + `contentInsetAdjustmentBehavior: never`. + +3. Scroll the list to the bottom. + +- [ ] Expected: The last item in the list is partially or fully + obscured behind the tab bar, confirming that no bottom inset is + applied. + +--- + +### `true` — content inset from bars + +4. Tap the **True** tab. + +- [ ] Expected: The **True** tab becomes active and shows a + scrollable list of 30 items. + +5. Scroll the list to the top. + +- [ ] Expected: The text label + `overrideScrollViewContentInsetAdjustmentBehavior: true` + at the top of the scroll content is fully visible below the + navigation bar and not obscured behind it. The scroll view + respects the top inset + (`contentInsetAdjustmentBehavior: automatic`). + +6. Scroll the list to the bottom. + +- [ ] Expected: The last item is fully visible and is not obscured by + the tab bar. The scroll view respects the bottom inset. + +--- + +### Default (prop omitted) — same as `true` + +7. Tap the **Default** tab. + +- [ ] Expected: The **Default** tab becomes active and shows a + scrollable list of 30 items. + +8. Scroll the list to the top. + +- [ ] Expected: The text label + `overrideScrollViewContentInsetAdjustmentBehavior: + (not set, defaults to true)` at the top of the scroll content + is fully visible below the navigation bar and not obscured + behind it — identical behavior to the **True** tab. + +9. Scroll the list to the bottom. + +- [ ] Expected: The last item is fully visible and not obscured by + the tab bar — identical behavior to the **True** tab. + +--- + +### Cross-tab comparison + +10. Switch between the **True** tab and the **Default** tab several + times while keeping each list scrolled to the top. + +- [ ] Expected: Both tabs show the first item fully visible below + the navigation bar with identical inset. No layout jump or + visual difference between the two tabs. + +11. Switch to the **False** tab and scroll to the top, then + immediately switch to the **True** tab. + +- [ ] Expected: The **True** tab correctly shows the first item + inset from the navigation bar. No crash or blank screen occurs + during the switch. From 4736878c3b08919bf91e8157722391116a4cbd7d Mon Sep 17 00:00:00 2001 From: lkuchno Date: Wed, 29 Apr 2026 13:05:22 +0200 Subject: [PATCH 2/8] refactor(test): override-scroll-view-content-inset --- ...rride-scroll-view-content-inset-ios.e2e.ts | 124 ++++++++++++++++ .../tests/single-feature-tests/tabs/index.ts | 4 +- .../index.tsx} | 36 ++++- .../scenario.md | 134 ++++++++++++++++++ 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts rename apps/src/tests/single-feature-tests/tabs/{override-scroll-view-content-inset.tsx => test-tabs-override-scroll-view-content-inset-ios/index.tsx} (79%) create mode 100644 apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md diff --git a/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts new file mode 100644 index 0000000000..dd70e32558 --- /dev/null +++ b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts @@ -0,0 +1,124 @@ +import { expect as jestExpect } from '@jest/globals'; +import { device, expect, element, by } from 'detox'; +import { IosElementAttributes } from 'detox/detox'; +import { describeIfiOS, selectSingleFeatureTestsScreen } from '../../e2e-utils'; + +async function getElementAttributes( + testLabel: string, +): Promise { + const attrs = await element(by.label(testLabel)).getAttributes(); + return attrs as IosElementAttributes; +} + +async function getTabBarFrame(): Promise<{ + x: number; + y: number; + width: number; + height: number; +}> { + const attrs = await element(by.type('UITabBar')).getAttributes(); + return (attrs as IosElementAttributes).frame; +} + +function isAboveTabBar( + itemFrame: { y: number; height: number }, + tabBarFrame: { y: number }, +): boolean { + return itemFrame.y + itemFrame.height <= tabBarFrame.y; +} + +async function resetScrollToTop(scrollViewId: string) { + await element(by.id(scrollViewId)).scroll(5000, 'up', NaN, 0.9); +} + +describeIfiOS('Override Scroll View Content Inset (iOS)', () => { + beforeEach(async () => { + await device.reloadReactNative(); + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-override-scroll-view-content-inset-ios', + ); + }); + + describe('False tab (overrideScrollViewContentInsetAdjustmentBehavior: false)', () => { + it('should display the false tab scrollview with the tab bar visible', async () => { + await element(by.id('override-inset-false-scrollview')).scroll( + 600, + 'down', + Number.NaN, + 0.85, + ); + + const tabFalse = await getElementAttributes('override-inset-tab-false'); + console.log(tabFalse.frame.x, tabFalse.frame.y); + await device.tap({ x: tabFalse.frame.x, y: tabFalse.frame.y }); + await element(by.id('override-inset-false-scrollview')).scroll( + 600, + 'down', + Number.NaN, + 0.85, + ); + // await expect(element(by.id('override-inset-false-scrollview'))).toBeVisible(); + // await expect(element(by.type('UITabBar'))).toBeVisible(); + // const tabBarFrame = getTabBarFrame(); + // console.log(tabBarFrame) + }); + + // it('should hide the header label behind the inset (not visible)', async () => { + // await expect( + // element(by.label('overrideScrollViewContentInsetAdjustmentBehavior: false')), + // ).not.toBeVisible(); + // }); + + // it('should render the last item overlapping or behind the tab bar (not fully above it)', async () => { + // await element(by.id('override-inset-false-scrollview')).scroll( + // 5000, + // 'down', + // NaN, + // 0.1, + // ); + + // const lastItem = element(by.text('Item 30')); + // await expect(lastItem).toBeVisible(); + + // const lastItemFrame = await getFrame(lastItem); + // const tabBarFrame = await getTabBarFrame(); + + // jestExpect(isAboveTabBar(lastItemFrame, tabBarFrame)).toBe(false); + // }); + }); + + // describe('True tab (overrideScrollViewContentInsetAdjustmentBehavior: true)', () => { + // beforeEach(async () => { + // await element(by.id('override-inset-tab-true')).tap(); + // }); + + // it('should display the true tab scrollview with the tab bar visible', async () => { + // await expect(element(by.id('override-inset-true-scrollview'))).toBeVisible(); + // await expect(element(by.type('UITabBar'))).toBeVisible(); + // }); + + // it('should show the header label visible (not hidden behind inset)', async () => { + // await expect( + // element(by.label('overrideScrollViewContentInsetAdjustmentBehavior: true')), + // ).toBeVisible(); + // }); + + // it('should render the last item fully above the tab bar (proper inset applied)', async () => { + // await element(by.id('override-inset-true-scrollview')).scroll( + // 5000, + // 'down', + // NaN, + // 0.1, + // ); + + // const lastItem = element(by.text('Item 30')); + // await expect(lastItem).toBeVisible(); + + // const lastItemFrame = await getFrame(lastItem); + // const tabBarFrame = await getTabBarFrame(); + + // jestExpect(isAboveTabBar(lastItemFrame, tabBarFrame)).toBe(true); + // }); + // }); +}); diff --git a/apps/src/tests/single-feature-tests/tabs/index.ts b/apps/src/tests/single-feature-tests/tabs/index.ts index 8e9cc62cbe..93aacd6b6b 100644 --- a/apps/src/tests/single-feature-tests/tabs/index.ts +++ b/apps/src/tests/single-feature-tests/tabs/index.ts @@ -1,7 +1,7 @@ import type { ScenarioGroup } from '@apps/tests/shared/helpers'; import BottomAccessoryScenario from './bottom-accessory-layout'; -import OverrideScrollViewContentInsetScenario from './override-scroll-view-content-inset'; +import TestTabsOverrideScrollViewContentInset from './test-tabs-override-scroll-view-content-inset-ios'; import TestTabsTabBarHidden from './test-tabs-tab-bar-hidden'; import TabsScreenOrientationScenario from './tabs-screen-orientation'; import TabBarAppearanceDefinedBySelectedTabScenario from './test-tabs-appearance-defined-by-selected-tab'; @@ -18,7 +18,7 @@ import TestTabsSpecialEffectsScrollToTop from './test-tabs-special-effects-scrol const scenarios = { BottomAccessoryScenario, - OverrideScrollViewContentInsetScenario, + TestTabsOverrideScrollViewContentInset, TabBarAppearanceDefinedBySelectedTabScenario, TestTabsTabBarHidden, TabsScreenOrientationScenario, diff --git a/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx similarity index 79% rename from apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx rename to apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx index a1218a3161..50796c8db4 100644 --- a/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx @@ -10,7 +10,7 @@ import { createScenario } from '@apps/tests/shared/helpers'; const scenarioDescription: ScenarioDescription = { name: 'Override ScrollView Content Inset', - key: 'override-scroll-view-content-inset', + key: 'test-tabs-override-scroll-view-content-inset-ios', details: 'Tests overrideScrollViewContentInsetAdjustmentBehavior with different static values per tab. ' + 'False: content scrolls behind bars. True/Default: content is inset from bars.', @@ -19,9 +19,15 @@ const scenarioDescription: ScenarioDescription = { const ITEM_COUNT = 30; -function ScrollContent({ label }: { label: string }) { +function ScrollContent({ + label, + testID, +}: { + label: string; + testID: string; +}) { return ( - + overrideScrollViewContentInsetAdjustmentBehavior: {label} @@ -37,15 +43,30 @@ function ScrollContent({ label }: { label: string }) { } function FalseTab() { - return ; + return ( + + ); } function TrueTab() { - return ; + return ( + + ); } function DefaultTab() { - return ; + return ( + + ); } export function App() { @@ -59,6 +80,7 @@ export function App() { Component: FalseTab, options: { title: 'False', + tabBarItemAccessibilityLabel: 'override-inset-tab-false', ios: { overrideScrollViewContentInsetAdjustmentBehavior: false, icon: { type: 'sfSymbol', name: 'xmark.circle' }, @@ -70,6 +92,7 @@ export function App() { Component: TrueTab, options: { title: 'True', + tabBarItemTestID: 'override-inset-tab-true', ios: { overrideScrollViewContentInsetAdjustmentBehavior: true, icon: { type: 'sfSymbol', name: 'checkmark.circle' }, @@ -81,6 +104,7 @@ export function App() { Component: DefaultTab, options: { title: 'Default', + tabBarItemTestID: 'override-inset-tab-default', ios: { icon: { type: 'sfSymbol', name: 'circle.dashed' } }, }, }, diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md new file mode 100644 index 0000000000..5a282da4b0 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md @@ -0,0 +1,134 @@ +# Test Scenario: overrideScrollViewContentInsetAdjustmentBehavior + +## Details + +**Description:** Validates the +`overrideScrollViewContentInsetAdjustmentBehavior` prop on `TabsScreen` +(iOS only). By default, React Native sets ScrollView's +`contentInsetAdjustmentBehavior` to `never`, which prevents the scroll +view from respecting navigation bar and tab bar insets. When this prop +is `true` (the default), the behavior is overridden back to UIKit's +`automatic`, so content is inset from the bars. When set to `false`, +the content scrolls behind the bars without insets. +The test verifies that the three tabs — **False**, **True**, and +**Default** — each exhibit the expected inset behavior and that the +**Default** tab (prop omitted) matches the **True** tab. + +**OS test creation version:** iOS 18.6 and 26.2 + +## E2E test + +No: The distinction between content scrolling behind bars versus being +inset from them is a visual/pixel-level difference that cannot be +reliably expressed as a pass/fail assertion in the current E2E +infrastructure without snapshot or frame-comparison tooling. + +## Prerequisites + +- iOS device or simulator (iPhone) + +## Note + +- This prop is iOS-only and Fabric-only; skip on Android. +- "Content scrolls behind bars" means list items are visible underneath + the navigation bar and/or tab bar when scrolled to the top or bottom + of the list. +- "Content inset from bars" means the first and last list items are + fully visible and never hidden behind a bar, even when the scroll + view is at its extreme positions. +- The navigation bar at the top and the tab bar at the bottom are both + relevant reference points for inset verification. + +## Steps + +### Baseline + +1. Launch the app and navigate to the + **Override Scroll View Content Inset** screen. + +- [ ] Expected: Three tabs are displayed in the tab bar: **False**, + **True**, and **Default**. The **False** tab is selected and shows + a scrollable list of 30 items. + +--- + +### `false` — content scrolls behind bars + +2. Confirm the **False** tab is active and scroll the list to the top. + +- [ ] Expected: The text label + `overrideScrollViewContentInsetAdjustmentBehavior: false` at + the top of the scroll content is partially or fully obscured + behind the navigation bar, because + `overrideScrollViewContentInsetAdjustmentBehavior` is `false` + and the scroll view uses + `contentInsetAdjustmentBehavior: never`. + +3. Scroll the list to the bottom. + +- [ ] Expected: The last item in the list is partially or fully + obscured behind the tab bar, confirming that no bottom inset is + applied. + +--- + +### `true` — content inset from bars + +4. Tap the **True** tab. + +- [ ] Expected: The **True** tab becomes active and shows a + scrollable list of 30 items. + +5. Scroll the list to the top. + +- [ ] Expected: The text label + `overrideScrollViewContentInsetAdjustmentBehavior: true` + at the top of the scroll content is fully visible below the + navigation bar and not obscured behind it. The scroll view + respects the top inset + (`contentInsetAdjustmentBehavior: automatic`). + +6. Scroll the list to the bottom. + +- [ ] Expected: The last item is fully visible and is not obscured by + the tab bar. The scroll view respects the bottom inset. + +--- + +### Default (prop omitted) — same as `true` + +7. Tap the **Default** tab. + +- [ ] Expected: The **Default** tab becomes active and shows a + scrollable list of 30 items. + +8. Scroll the list to the top. + +- [ ] Expected: The text label + `overrideScrollViewContentInsetAdjustmentBehavior: + (not set, defaults to true)` at the top of the scroll content + is fully visible below the navigation bar and not obscured + behind it — identical behavior to the **True** tab. + +9. Scroll the list to the bottom. + +- [ ] Expected: The last item is fully visible and not obscured by + the tab bar — identical behavior to the **True** tab. + +--- + +### Cross-tab comparison + +10. Switch between the **True** tab and the **Default** tab several + times while keeping each list scrolled to the top. + +- [ ] Expected: Both tabs show the first item fully visible below + the navigation bar with identical inset. No layout jump or + visual difference between the two tabs. + +11. Switch to the **False** tab and scroll to the top, then + immediately switch to the **True** tab. + +- [ ] Expected: The **True** tab correctly shows the first item + inset from the navigation bar. No crash or blank screen occurs + during the switch. From 96fb36dd2b33376860eb1ea3ff5652595411d96e Mon Sep 17 00:00:00 2001 From: lkuchno Date: Thu, 30 Apr 2026 10:23:20 +0200 Subject: [PATCH 3/8] remove e2e test from this branch --- ...rride-scroll-view-content-inset-ios.e2e.ts | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts diff --git a/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts deleted file mode 100644 index dd70e32558..0000000000 --- a/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { expect as jestExpect } from '@jest/globals'; -import { device, expect, element, by } from 'detox'; -import { IosElementAttributes } from 'detox/detox'; -import { describeIfiOS, selectSingleFeatureTestsScreen } from '../../e2e-utils'; - -async function getElementAttributes( - testLabel: string, -): Promise { - const attrs = await element(by.label(testLabel)).getAttributes(); - return attrs as IosElementAttributes; -} - -async function getTabBarFrame(): Promise<{ - x: number; - y: number; - width: number; - height: number; -}> { - const attrs = await element(by.type('UITabBar')).getAttributes(); - return (attrs as IosElementAttributes).frame; -} - -function isAboveTabBar( - itemFrame: { y: number; height: number }, - tabBarFrame: { y: number }, -): boolean { - return itemFrame.y + itemFrame.height <= tabBarFrame.y; -} - -async function resetScrollToTop(scrollViewId: string) { - await element(by.id(scrollViewId)).scroll(5000, 'up', NaN, 0.9); -} - -describeIfiOS('Override Scroll View Content Inset (iOS)', () => { - beforeEach(async () => { - await device.reloadReactNative(); - await selectSingleFeatureTestsScreen( - 'Tabs', - 'test-tabs-override-scroll-view-content-inset-ios', - ); - }); - - describe('False tab (overrideScrollViewContentInsetAdjustmentBehavior: false)', () => { - it('should display the false tab scrollview with the tab bar visible', async () => { - await element(by.id('override-inset-false-scrollview')).scroll( - 600, - 'down', - Number.NaN, - 0.85, - ); - - const tabFalse = await getElementAttributes('override-inset-tab-false'); - console.log(tabFalse.frame.x, tabFalse.frame.y); - await device.tap({ x: tabFalse.frame.x, y: tabFalse.frame.y }); - await element(by.id('override-inset-false-scrollview')).scroll( - 600, - 'down', - Number.NaN, - 0.85, - ); - // await expect(element(by.id('override-inset-false-scrollview'))).toBeVisible(); - // await expect(element(by.type('UITabBar'))).toBeVisible(); - // const tabBarFrame = getTabBarFrame(); - // console.log(tabBarFrame) - }); - - // it('should hide the header label behind the inset (not visible)', async () => { - // await expect( - // element(by.label('overrideScrollViewContentInsetAdjustmentBehavior: false')), - // ).not.toBeVisible(); - // }); - - // it('should render the last item overlapping or behind the tab bar (not fully above it)', async () => { - // await element(by.id('override-inset-false-scrollview')).scroll( - // 5000, - // 'down', - // NaN, - // 0.1, - // ); - - // const lastItem = element(by.text('Item 30')); - // await expect(lastItem).toBeVisible(); - - // const lastItemFrame = await getFrame(lastItem); - // const tabBarFrame = await getTabBarFrame(); - - // jestExpect(isAboveTabBar(lastItemFrame, tabBarFrame)).toBe(false); - // }); - }); - - // describe('True tab (overrideScrollViewContentInsetAdjustmentBehavior: true)', () => { - // beforeEach(async () => { - // await element(by.id('override-inset-tab-true')).tap(); - // }); - - // it('should display the true tab scrollview with the tab bar visible', async () => { - // await expect(element(by.id('override-inset-true-scrollview'))).toBeVisible(); - // await expect(element(by.type('UITabBar'))).toBeVisible(); - // }); - - // it('should show the header label visible (not hidden behind inset)', async () => { - // await expect( - // element(by.label('overrideScrollViewContentInsetAdjustmentBehavior: true')), - // ).toBeVisible(); - // }); - - // it('should render the last item fully above the tab bar (proper inset applied)', async () => { - // await element(by.id('override-inset-true-scrollview')).scroll( - // 5000, - // 'down', - // NaN, - // 0.1, - // ); - - // const lastItem = element(by.text('Item 30')); - // await expect(lastItem).toBeVisible(); - - // const lastItemFrame = await getFrame(lastItem); - // const tabBarFrame = await getTabBarFrame(); - - // jestExpect(isAboveTabBar(lastItemFrame, tabBarFrame)).toBe(true); - // }); - // }); -}); From 8df7d768a520455520266805d97b18b2ac46bbd9 Mon Sep 17 00:00:00 2001 From: lkuchno Date: Thu, 30 Apr 2026 10:25:29 +0200 Subject: [PATCH 4/8] e2e section update --- .../scenario.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md index 5a282da4b0..f8c1be726e 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md @@ -18,10 +18,7 @@ The test verifies that the three tabs — **False**, **True**, and ## E2E test -No: The distinction between content scrolling behind bars versus being -inset from them is a visual/pixel-level difference that cannot be -reliably expressed as a pass/fail assertion in the current E2E -infrastructure without snapshot or frame-comparison tooling. +No: Ongoing research. ## Prerequisites From 8f6aa6b64f60a52a680bea1aeadb9bbc8ed13a86 Mon Sep 17 00:00:00 2001 From: lkuchno Date: Thu, 30 Apr 2026 10:42:58 +0200 Subject: [PATCH 5/8] change scroll order first scroll down then to top - make more sense, added missing ":" --- .../scenario.md | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md index f8c1be726e..9e6d943012 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md @@ -14,7 +14,7 @@ The test verifies that the three tabs — **False**, **True**, and **Default** — each exhibit the expected inset behavior and that the **Default** tab (prop omitted) matches the **True** tab. -**OS test creation version:** iOS 18.6 and 26.2 +**OS test creation version:** iOS: 18.6 and 26.2 ## E2E test @@ -51,7 +51,13 @@ No: Ongoing research. ### `false` — content scrolls behind bars -2. Confirm the **False** tab is active and scroll the list to the top. +2. Confirm the **False** tab is active and scroll the list to the bottom. + +- [ ] Expected: The last item in the list is partially or fully + obscured behind the tab bar, confirming that no bottom inset is + applied. + +3. Scroll the list to the top. - [ ] Expected: The text label `overrideScrollViewContentInsetAdjustmentBehavior: false` at @@ -61,12 +67,6 @@ No: Ongoing research. and the scroll view uses `contentInsetAdjustmentBehavior: never`. -3. Scroll the list to the bottom. - -- [ ] Expected: The last item in the list is partially or fully - obscured behind the tab bar, confirming that no bottom inset is - applied. - --- ### `true` — content inset from bars @@ -76,7 +76,12 @@ No: Ongoing research. - [ ] Expected: The **True** tab becomes active and shows a scrollable list of 30 items. -5. Scroll the list to the top. +5. Scroll the list to the bottom. + +- [ ] Expected: The last item is fully visible and is not obscured by + the tab bar. The scroll view respects the bottom inset. + +6. Scroll the list to the top. - [ ] Expected: The text label `overrideScrollViewContentInsetAdjustmentBehavior: true` @@ -85,11 +90,6 @@ No: Ongoing research. respects the top inset (`contentInsetAdjustmentBehavior: automatic`). -6. Scroll the list to the bottom. - -- [ ] Expected: The last item is fully visible and is not obscured by - the tab bar. The scroll view respects the bottom inset. - --- ### Default (prop omitted) — same as `true` @@ -99,7 +99,12 @@ No: Ongoing research. - [ ] Expected: The **Default** tab becomes active and shows a scrollable list of 30 items. -8. Scroll the list to the top. +8. Scroll the list to the bottom. + +- [ ] Expected: The last item is fully visible and not obscured by + the tab bar — identical behavior to the **True** tab. + +9. Scroll the list to the top. - [ ] Expected: The text label `overrideScrollViewContentInsetAdjustmentBehavior: @@ -107,11 +112,6 @@ No: Ongoing research. is fully visible below the navigation bar and not obscured behind it — identical behavior to the **True** tab. -9. Scroll the list to the bottom. - -- [ ] Expected: The last item is fully visible and not obscured by - the tab bar — identical behavior to the **True** tab. - --- ### Cross-tab comparison From 7b5bd7a37ab9101f9e3e4e794ecc237fc4966f44 Mon Sep 17 00:00:00 2001 From: lkuchno Date: Thu, 30 Apr 2026 15:31:41 +0200 Subject: [PATCH 6/8] chore(test) test-tabs-override-scroll-view-content-inset-ios --- ...rride-scroll-view-content-inset-ios.e2e.ts | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts diff --git a/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts new file mode 100644 index 0000000000..54e654b8c7 --- /dev/null +++ b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios.e2e.ts @@ -0,0 +1,214 @@ +import { expect as jestExpect } from '@jest/globals'; +import { device, expect, element, by } from 'detox'; +import { IosElementAttributes } from 'detox/detox'; +import { + describeIfiOS, + forceTapByLabeliOS, + selectSingleFeatureTestsScreen, +} from '../../e2e-utils'; + +async function getElementAttributes( + testLabel: string, +): Promise { + const attrs = await element(by.label(testLabel)).getAttributes(); + return attrs as IosElementAttributes; +} + +async function getTabBarFrame(): Promise<{ + x: number; + y: number; + width: number; + height: number; +}> { + const attrs = await element(by.type('UITabBar')).getAttributes(); + return (attrs as IosElementAttributes).frame; +} +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; +} + +function isAboveTabBar( + itemFrame: { y: number; height: number }, + tabBarFrame: { y: number }, +): boolean { + return itemFrame.y + itemFrame.height < tabBarFrame.y; +} + +async function scrollToMaxBottom(scrollViewId: string) { + await element(by.id(scrollViewId)).scrollTo('bottom', NaN, 0.5); +} +async function scrollToMaxTop(scrollViewId: string) { + await element(by.id(scrollViewId)).scrollTo('top', NaN, 0.5); +} + +describeIfiOS('Override Scroll View Content Inset (iOS)', () => { + beforeAll(async () => { + await device.reloadReactNative(); + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-override-scroll-view-content-inset-ios', + ); + }); + + describe('False tab (overrideScrollViewContentInsetAdjustmentBehavior: false)', () => { + beforeEach(async () => { + await forceTapByLabeliOS('override-inset-tab-false'); + }); + + it('should display the false tab scrollview with the tab bar visible', async () => { + await expect( + element(by.id('override-inset-false-scrollview')), + ).toBeVisible(); + await expect(element(by.type('UITabBar'))).toBeVisible(); + }); + + it('should render the last item overlapping or behind the tab bar (not fully above it)', async () => { + await scrollToMaxBottom('override-inset-false-scrollview'); + const lastItemFrame = await getElementFrame('Item 30'); + const tabFrame = await getTabBarFrame(); + jestExpect(isAboveTabBar(lastItemFrame, tabFrame)).toBe(false); + }); + + it('should hide the information text behind the inset ', async () => { + await expect( + element(by.id('override-inset-false-scrollview')), + ).toBeVisible(); + await scrollToMaxTop('override-inset-false-scrollview'); + + const informationFrame = await getElementFrame( + 'overrideScrollViewContentInsetAdjustmentBehavior: false', + ); + jestExpect(informationFrame.y).toEqual(16); + }); + }); + + describe('True tab (overrideScrollViewContentInsetAdjustmentBehavior: true)', () => { + beforeAll(async () => { + await element(by.label('override-inset-tab-true')).tap(); + }); + + it('should display the true tab scrollview with the tab bar visible', async () => { + await expect( + element(by.id('override-inset-true-scrollview')), + ).toBeVisible(); + await expect(element(by.type('UITabBar'))).toBeVisible(); + }); + + it('should render the last item fully above the tab bar (proper inset applied)', async () => { + await scrollToMaxBottom('override-inset-true-scrollview'); + const lastItemFrame = await getElementFrame('Item 30'); + const tabFrame = await getTabBarFrame(); + jestExpect(isAboveTabBar(lastItemFrame, tabFrame)).toBe(true); + }); + + it('should show the header label visible (not hidden behind inset)', async () => { + await scrollToMaxTop('override-inset-true-scrollview'); + await expect( + element( + by.label('overrideScrollViewContentInsetAdjustmentBehavior: true'), + ), + ).toBeVisible(); + }); + }); + describe('Default tab (prop omitted)', () => { + beforeAll(async () => { + await element(by.label('override-inset-tab-default')).tap(); + }); + + it('should display the default tab scrollview with the tab bar visible', async () => { + await expect( + element(by.id('override-inset-default-scrollview')), + ).toBeVisible(); + await expect(element(by.type('UITabBar'))).toBeVisible(); + }); + + it('should render the last item fully above the tab bar (proper inset applied)', async () => { + await scrollToMaxBottom('override-inset-default-scrollview'); + const lastItemFrame = await getElementFrame('Item 30'); + const tabFrame = await getTabBarFrame(); + jestExpect(isAboveTabBar(lastItemFrame, tabFrame)).toBe(true); + }); + + it('should show the header label visible (not hidden behind inset)', async () => { + await scrollToMaxTop('override-inset-default-scrollview'); + await expect( + element( + by.label( + 'overrideScrollViewContentInsetAdjustmentBehavior: (not set, defaults to true)', + ), + ), + ).toBeVisible(); + }); + }); + + describe('Cross-tab comparison', () => { + beforeAll(async () => { + await forceTapByLabeliOS('override-inset-tab-default'); + }); + + 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( + element(by.id('override-inset-false-scrollview')), + ).toBeVisible(); + await expect(element(by.label('Item 1'))).toBeVisible(); + const informationFrame = await getElementFrame( + 'overrideScrollViewContentInsetAdjustmentBehavior: false', + ); + jestExpect(informationFrame.y).toEqual(16); + + 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(); + }); + }); +}); From 09b8e90a4dbfe1ee9769ecbb33f173bcc288690c Mon Sep 17 00:00:00 2001 From: lkuchno Date: Thu, 30 Apr 2026 15:32:29 +0200 Subject: [PATCH 7/8] test changes in index --- .../index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx index 50796c8db4..cf91c49642 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/index.tsx @@ -92,7 +92,7 @@ export function App() { Component: TrueTab, options: { title: 'True', - tabBarItemTestID: 'override-inset-tab-true', + tabBarItemAccessibilityLabel: 'override-inset-tab-true', ios: { overrideScrollViewContentInsetAdjustmentBehavior: true, icon: { type: 'sfSymbol', name: 'checkmark.circle' }, @@ -104,7 +104,7 @@ export function App() { Component: DefaultTab, options: { title: 'Default', - tabBarItemTestID: 'override-inset-tab-default', + tabBarItemAccessibilityLabel: 'override-inset-tab-default', ios: { icon: { type: 'sfSymbol', name: 'circle.dashed' } }, }, }, From 2a098f820af99258d1688f6dd3cb765bfa7b1035 Mon Sep 17 00:00:00 2001 From: lkuchno Date: Thu, 30 Apr 2026 16:12:12 +0200 Subject: [PATCH 8/8] update e2e section in scenario --- .../scenario.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md index 9e6d943012..c22e01c0a4 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-override-scroll-view-content-inset-ios/scenario.md @@ -18,7 +18,7 @@ The test verifies that the three tabs — **False**, **True**, and ## E2E test -No: Ongoing research. +Yes: Covers all manual scenario steps. ## Prerequisites