diff --git a/FabricExample/e2e/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction.e2e.ts b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction.e2e.ts new file mode 100644 index 0000000000..7c6fde9e5f --- /dev/null +++ b/FabricExample/e2e/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction.e2e.ts @@ -0,0 +1,210 @@ +import { expect as jestExpect } from '@jest/globals'; +import { device, expect, element, by } from 'detox'; +import { AndroidElementAttributes, IosElementAttributes } from 'detox/detox'; +import { selectSingleFeatureTestsScreen } from '../../e2e-utils'; + +type ElementAttributes = IosElementAttributes | AndroidElementAttributes; + +async function getElementAttributes( + testLabel: string, +): Promise { + const attrs = await element(by.label(testLabel)).getAttributes(); + return attrs as ElementAttributes; +} + +async function scrollTo(selector: { id: string } | { text: string }) { + const el = + 'text' in selector + ? element(by.text(selector.text)) + : element(by.id(selector.id)); + + await waitFor(el) + .toBeVisible() + .whileElement(by.id('tab-bar-layout-direction-scrollview')) + .scroll(100, 'down'); +} + +async function selectDirection(direction: 'inherit' | 'rtl' | 'ltr') { + await scrollTo({ id: 'tab-bar-layout-direction-picker' }); + await element(by.id('tab-bar-layout-direction-picker')).tap(); + await scrollTo({ text: direction }); + await element(by.text(direction)).tap(); + await element(by.id('tab-bar-layout-direction-picker')).tap(); + await expect(element(by.id('tab-bar-layout-direction-picker'))).toHaveLabel( + `direction: ${direction}`, + ); +} +const expectTab1ToBeLeftOfTab2 = async (shouldBeLeft: boolean) => { + const t1 = await getElementAttributes('tab-bar-item-1-label'); + const t2 = await getElementAttributes('tab-bar-item-2-label'); + if (shouldBeLeft) { + jestExpect(t2.frame.x).toBeGreaterThan(t1.frame.x); + } else { + jestExpect(t1.frame.x).toBeGreaterThan(t2.frame.x); + } +}; + +describe('Tab Bar Layout Direction - system settings: LTR', () => { + beforeEach(async () => { + await device.reloadReactNative(); + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-tab-bar-layout-direction', + ); + }); + + it('displays default options and renders Tab1 at the visually leftmost position (LTR)', async () => { + await expect( + element(by.id('tab-bar-layout-direction-scrollview')), + ).toBeVisible(); + await expect(element(by.id('react-force-rtl-picker'))).toHaveLabel( + 'forceRTL: false', + ); + await expect(element(by.id('react-allow-rtl-picker'))).toHaveLabel( + 'allowRTL: true', + ); + await scrollTo({ id: 'tab-bar-layout-direction-picker' }); + await expect(element(by.id('tab-bar-layout-direction-picker'))).toHaveLabel( + 'direction: inherit', + ); + await expect(element(by.id('is-rtl-information'))).toHaveText( + 'I18nManager.isRTL == false', + ); + await expectTab1ToBeLeftOfTab2(true); + }); + + it('follows system LTR settings when direction is set to inherit', async () => { + await selectDirection('inherit'); + await expectTab1ToBeLeftOfTab2(true); + }); + + it('overrides system LTR settings and renders the tab bar in RTL order', async () => { + await selectDirection('rtl'); + await expectTab1ToBeLeftOfTab2(false); + }); + + it('remains in LTR order when direction is explicitly set to ltr', async () => { + await selectDirection('ltr'); + await expectTab1ToBeLeftOfTab2(true); + }); + + it('cycle through inherit → rtl → ltr → rtl → inherit renders the tab bar in correct order', async () => { + await selectDirection('inherit'); + await expectTab1ToBeLeftOfTab2(true); + + await selectDirection('rtl'); + await expectTab1ToBeLeftOfTab2(false); + + await selectDirection('ltr'); + await expectTab1ToBeLeftOfTab2(true); + + await selectDirection('rtl'); + await expectTab1ToBeLeftOfTab2(false); + + await selectDirection('inherit'); + await expectTab1ToBeLeftOfTab2(true); + }); +}); + +describe('Tab Bar Layout Direction - system settings: RTL', () => { + beforeAll(async () => { + if (device.getPlatform() === 'ios') { + await device.launchApp({ + newInstance: true, + launchArgs: { + AppleTextDirection: 'YES', + NSForceRightToLeftWritingDirection: 'YES', + I18NIsRTL: 'YES', + }, + }); + } else { + await device.launchApp({ newInstance: true }); + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-tab-bar-layout-direction', + ); + await element(by.id('react-force-rtl-picker')).tap(); + await device.reloadReactNative(); + } + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-tab-bar-layout-direction', + ); + }); + + afterAll(async () => { + if (device.getPlatform() === 'ios') { + await device.launchApp({ + newInstance: true, + launchArgs: { + AppleTextDirection: 'NO', + NSForceRightToLeftWritingDirection: 'NO', + I18NIsRTL: 'NO', + }, + }); + } else { + await device.launchApp({ newInstance: true }); + await selectSingleFeatureTestsScreen( + 'Tabs', + 'test-tabs-tab-bar-layout-direction', + ); + await element(by.id('react-force-rtl-picker')).multiTap(2); + await expect(element(by.id('react-force-rtl-picker'))).toHaveLabel( + 'forceRTL: false', + ); + await device.reloadReactNative(); + } + }); + + it('displays default options and renders Tab2 at the visually leftmost position (RTL)', async () => { + await expect( + element(by.id('tab-bar-layout-direction-scrollview')), + ).toBeVisible(); + await expect(element(by.id('react-force-rtl-picker'))).toHaveLabel( + 'forceRTL: false', + ); + await expect(element(by.id('react-allow-rtl-picker'))).toHaveLabel( + 'allowRTL: true', + ); + await scrollTo({ id: 'tab-bar-layout-direction-picker' }); + await expect(element(by.id('tab-bar-layout-direction-picker'))).toHaveLabel( + 'direction: inherit', + ); + await expect(element(by.id('is-rtl-information'))).toHaveText( + 'I18nManager.isRTL == true', + ); + await expectTab1ToBeLeftOfTab2(false); + }); + + it('follows system RTL settings when direction is set to inherit', async () => { + await selectDirection('inherit'); + await expectTab1ToBeLeftOfTab2(false); + }); + + it('remains in RTL order when direction is explicitly set to rtl', async () => { + await selectDirection('rtl'); + await expectTab1ToBeLeftOfTab2(false); + }); + + it('overrides system RTL settings and renders the tab bar in LTR order', async () => { + await selectDirection('ltr'); + await expectTab1ToBeLeftOfTab2(true); + }); + + it('cycle through inherit → ltr → rtl → ltr → inherit renders the tab bar in correct order', async () => { + await selectDirection('inherit'); + await expectTab1ToBeLeftOfTab2(false); + + await selectDirection('ltr'); + await expectTab1ToBeLeftOfTab2(true); + + await selectDirection('rtl'); + await expectTab1ToBeLeftOfTab2(false); + + await selectDirection('ltr'); + await expectTab1ToBeLeftOfTab2(true); + + await selectDirection('inherit'); + await expectTab1ToBeLeftOfTab2(false); + }); +}); diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/index.tsx index b77d15141a..62ba1a1eef 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/index.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/index.tsx @@ -41,7 +41,8 @@ function ConfigScreen() { }, [reactAllowRtl]); return ( - + There are 3 sources of layout direction: system, React Native and our @@ -62,7 +63,7 @@ function ConfigScreen() { React Native's isRTL - + {'I18nManager.isRTL == ' + (I18nManager.isRTL ? 'true' : 'false')} @@ -79,6 +80,7 @@ function ConfigScreen() { onValueChange={function (value: boolean): void { setReactForceRtl(value); }} + testID='react-force-rtl-picker' /> @@ -94,6 +96,7 @@ function ConfigScreen() { onValueChange={function (value: boolean): void { setReactAllowRtl(value); }} + testID='react-allow-rtl-picker' /> @@ -104,6 +107,7 @@ function ConfigScreen() { value={hostConfig.direction ?? 'inherit'} onValueChange={value => updateHostConfig({ direction: value })} items={['inherit', 'ltr', 'rtl']} + testID='tab-bar-layout-direction-picker' /> @@ -112,11 +116,12 @@ function ConfigScreen() { const ROUTE_CONFIGS: TabRouteConfig[] = [ { - name: 'Config', + name: 'Tab1', Component: ConfigScreen, options: { ...DEFAULT_TAB_ROUTE_OPTIONS, - title: 'Config', + title: 'Tab1', + tabBarItemAccessibilityLabel: 'tab-bar-item-1-label', safeAreaConfiguration: { edges: { bottom: true, @@ -130,6 +135,7 @@ const ROUTE_CONFIGS: TabRouteConfig[] = [ options: { ...DEFAULT_TAB_ROUTE_OPTIONS, title: 'Tab2', + tabBarItemAccessibilityLabel: 'tab-bar-item-2-label', }, }, ]; diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/scenario.md index 759bc7c144..ea3ed76746 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/scenario.md +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-layout-direction/scenario.md @@ -1,83 +1,105 @@ # Test Scenario: direction -**E2E test:** ongoing research +## Details -## Prerequisites - -- iOS device or simulator -with at least one RTL language localization configured in Xcode (e.g. empty ar.lproj/InfoPlist.strings), or system language set to Arabic/Hebrew -- Android emulator -Android emulator with supportRtl enabled in app manifest +**Description:** This test scenario validates the layout directionality of +the TabBar component across iOS and Android. It specifically verifies that +the component correctly handles Right-to-Left (RTL) and Left-to-Right (LTR) +rendering by testing the precedence between system-level settings, React +Native's I18nManager, and the explicit direction prop from +react-native-screens. -Note: +**OS test creation version:** iOS: 18.6 and 26.2, Android: 16.0 (Baklava) -- App restart is required after changing forceRTL / allowRTL -- Assumption: system and RN settings are working correctly. Here only react-native-screens prop is tested. +## E2E test -## Steps +Yes: Covers all manual scenario steps for LTR/RTL configured via React Native. -### Baseline +Implementation Details: -1. Launch the app and navigate to the scenario +- iOS: The system RTL direction is set by configuring I18NIsRTL to YES + during the app launch sequence. +- Android: RTL direction must be triggered using the forceRTL toggle located + within the Layout Direction screen. -- [ ] Expected: Config and Tab2 are shown in LTR order (Config on left, Tab2 to its right). All controls default to forceRTL=false, allowRTL=true, TabsHost direction = inherit +Scenarios where RTL is enabled at the device level by setting a system-wide +RTL language are NOT covered by e2e tests. ---- +## Prerequisites -### TabsHost inherit — follows RN/system +- iOS device or simulator with at least one RTL language localization + configured in Xcode (e.g. empty ar.lproj/InfoPlist.strings), or system + language set to Arabic/Hebrew, +- Android emulator with supportRtl enabled in app manifest. -2. Ensure system/RN is LTR (I18nManager.isRTL == false), set TabsHost direction = inherit +## Note -- [ ] Expected: Tab bar displays in LTR order (Config on left, Tab2 to its right) +- Assumption: System and RN settings are working correctly. Here only + react-native-screens prop is tested. +- Each of the below steps must be executed twice: once with a system-wide RTL + language enabled at the device level and once with the RTL direction set + via React Native. The device-level test is particularly critical, as the + React Native configuration is already covered by E2E tests. -3. Set system/RN to RTL (I18nManager.isRTL == true), keep TabsHost direction = inherit +## Steps -- [ ] Expected: Tab bar displays in RTL order — (Config on right, Tab2 to its left) +### Baseline ---- +1. Launch the app and navigate to the scenario. -### TabsHost ltr +- [ ] Expected: Tab1 and Tab2 are shown in LTR order. Tab1 displayed as the + leftmost item and Tab2 as second. All controls default to + forceRTL=false, allowRTL=true, TabsHost direction = inherit. -4. Set system/RN to RTL, set TabsHost direction = ltr +--- -- [ ] Expected: Tab bar displays in LTR order — TabsHost overrides RTL from RN/system +### TabsHost inherit — follows RN/system -5. Set system/RN to LTR, keep TabsHost direction = ltr +2. Ensure system/RN is LTR (I18nManager.isRTL == false), set TabsHost + direction = inherit. -- [ ] Expected: Tab bar stays LTR +- [ ] Expected: Tab bar displays in LTR order. Tab1 is displayed as the + leftmost item and Tab2 as second. -6. Cycle through inherit → rtl → ltr → rtl → inherit +3. Set system/RN to RTL (I18nManager.isRTL == true), keep TabsHost + direction = inherit. -- [ ] Expected: Tab bar direction updates immediately with each change, no crash or layout freeze +- [ ] Expected: Tab bar displays in RTL order. Tab2 displayed as the + leftmost item and Tab1 as second. --- -### TabsHost rtl +### TabsHost ltr -7. Set system/RN to LTR, set TabsHost direction = rtl +4. Set system/RN to RTL, set TabsHost direction = ltr. -- [ ] Expected: Tab bar displays in RTL order — TabsHost overrides LTR from RN/system +- [ ] Expected: Tab bar displays in LTR order — TabsHost overrides RTL from + RN/system. Tab1 is displayed as the leftmost item. -8. Set system/RN to RTL, keep TabsHost direction = rtl +1. Set system/RN to LTR, keep TabsHost direction = ltr. -- [ ] Expected: Tab bar stays RTL +- [ ] Expected: Tab bar remains in LTR order. Tab1 is displayed as the the + leftmost item. -9. Cycle through inherit → ltr → rtl → ltr → inherit +6. Cycle through inherit → rtl → ltr → rtl → inherit. -- [ ] Expected: Tab bar direction updates immediately with each change, no crash or layout freeze +- [ ] Expected: Tab bar direction updates immediately with each change; no + crashes or layout freezes occur. --- -### Precedence chain verification +### TabsHost rtl -10. System = RTL, forceRTL=false, allowRTL=false, TabsHost = inherit +7. Set system/RN to LTR, set TabsHost direction = rtl. -- [ ] Expected: Tab bar is LTR (allowRTL=false blocks system RTL) +- [ ] Expected: Tab bar displays in RTL order — TabsHost overrides LTR from + RN/system. Tab2 displayed as the leftmost item. -11. System = LTR, forceRTL=true (restart), TabsHost = inherit +8. Set system/RN to RTL, keep TabsHost direction = rtl. -- [ ] Expected: Tab bar is RTL (forceRTL overrides system) +- [ ] Expected: Tab bar remains RTL. Tab2 displayed as the leftmost item. -12. System = LTR, forceRTL=true (restart), TabsHost = ltr +9. Cycle through inherit → ltr → rtl → ltr → inherit. -- [ ] Expected: Tab bar is LTR (TabsHost wins over forceRTL) +- [ ] Expected: Tab bar direction updates immediately with each change, no + crashes or layout freezes occur.