diff --git a/apps/src/tests/single-feature-tests/tabs/index.ts b/apps/src/tests/single-feature-tests/tabs/index.ts index 223193ef06..367e3371c3 100644 --- a/apps/src/tests/single-feature-tests/tabs/index.ts +++ b/apps/src/tests/single-feature-tests/tabs/index.ts @@ -18,6 +18,7 @@ import TestTabsSpecialEffectsScrollToTop from './test-tabs-special-effects-scrol import TestTabsTabBarExperimentalUserInterfaceStyle from './test-tabs-tab-bar-experimental-user-interface-style-ios'; import TestTabsLifecycleEvents from './test-tabs-lifecycle-events'; import TestTabsItemTitle from './test-tabs-item-title'; +import TestTabsItemIcon from './test-tabs-item-icon'; const scenarios = { TestTabBottomAccessory, @@ -38,6 +39,7 @@ const scenarios = { TestTabsTabBarExperimentalUserInterfaceStyle, TestTabsLifecycleEvents, TestTabsItemTitle, + TestTabsItemIcon, }; const TabsScenarioGroup: ScenarioGroup = { diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/index.tsx new file mode 100644 index 0000000000..95c80cc4c7 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/index.tsx @@ -0,0 +1,330 @@ +import React from 'react'; +import { Platform, StyleSheet, Text, View } from 'react-native'; +import { scenarioDescription } from './scenario-description'; +import { createScenario } from '@apps/tests/shared/helpers'; +import { + TabsContainerWithHostConfigContext, + type TabRouteConfig, + DEFAULT_TAB_ROUTE_OPTIONS, +} from '@apps/shared/gamma/containers/tabs'; +import { Colors } from '@apps/shared/styling'; + +function TintTab() { + return ( + + + Template Source (Host Tint) + + + + Host `tabBarTintColor`:{' '} + GreenDark100 + {'\n'} + `icon`: templateSource icon.png{'\n'} + `selectedIcon`: templateSource icon_fill.png{'\n'} + `tabBarItemIconColor` is NOT set.{'\n'} + {'\n'} + Selected: filled template image, tinted{' '} + GREEN.{'\n'} + Unselected: Titles and icons render in the system theme color. For the last tab, the icon + retains the black color from its source image. + + + + ); +} + +function OverrideTab() { + return ( + + + SF Symbol (Tint Color Override) + + + Host `tabBarTintColor`:{' '} + GreenDark100 + {'\n'} + `selected.tabBarItemIconColor`:{' '} + RedLight100 + {'\n'} + `icon`: SF Symbol "star"{'\n'} + `selectedIcon`: SF Symbol "star.fill"{'\n'} + {'\n'} + Selected: filled star, tinted{' '} + RED{'\n'} title on iOS18:{' '} + GREEN on iOS26:{' '} + RED + {'\n'} + Unselected: Titles and icons render in the system theme color. For the last tab, the icon + retains the black color from its source image. + + + ); +} + +function XcassetDrawableResourceTab() { + return ( + + {Platform.OS === 'ios' ? ( + <> + Xcasset (Host Tint) + + Host `tabBarTintColor`:{' '} + GreenDark100 + {'\n'} + `icon`: Xcasset custom-icon-fill{'\n'} + `tabBarItemIconColor` is NOT set.{'\n'} + {'\n'} + Selected: filled template image, tinted{' '} + GREEN.{'\n'} + Unselected: Titles and icons render in the system theme color. For the last tab, the icon + retains the black color from its source image. + + + ) : ( + <> + Drawable Resource + + + `icon`: drawableResource sym_call_missed{'\n'} + `selectedIcon`: drawableResource sym_call_incoming{'\n'} + `tabBarItemIconColor` is NOT set.{'\n'} + {'\n'} + Both icons (for selected and unselected tabs) displayed in the system default color. + + + )} + + ); +} + +function ImageTab() { + return ( + + + {Platform.OS === 'ios' ? 'Image Source (Non-Tintable)' : 'Image Source'} + + + {Platform.OS === 'ios' ? ( + <> + Host `tabBarTintColor`:{' '} + GreenDark100 + {'\n'} + `normal.tabBarItemIconColor`:{' '} + BlueDark100 + {'\n'} + `icon`: imageSource icon.png{'\n'} + `selectedIcon`: imageSource icon_fill.png{'\n'} + {'\n'} + `imageSource` icons render in their original colors and are NOT affected by `tabBarTintColor` + or `tabBarItemIconColor`.{'\n'} + {'\n'} + Selected: filled image in its black color (the host{' '} + GREEN tint is ignored). + {'\n'} + Unselected iOS18: outline icons in BLUE{' '} + color.{'\n'} + Unselected iOS26: icons in system theme color. + + ) : ( + <> + `selected.tabBarItemIconColor`:{' '} + RedDark100 + {'\n'} + `normal.tabBarItemIconColor`:{' '} + GreenDark100 + {'\n'} + `focused.tabBarItemIconColor`:{' '} + NavyLight100 + {'\n'} + `icon`: imageSource icon.png{'\n'} + `selectedIcon`: imageSource icon_fill.png{'\n'} + {'\n'} + Selected: filled image in {' '} + RED color. + {'\n'} + Unselected: outline icons in {' '} + GREEN color. + {'\n'} + Focused (via keyboard navigation Tab key):{'\n'} + icon should appear{' '} + DARK BLUE. + + )} + + + ); +} + +const IOS_ROUTES: TabRouteConfig[] = [ + { + name: 'Tint', + Component: TintTab, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Tint', + ios: { + icon: { + type: 'templateSource', + templateSource: require('@assets/variableIcons/icon.png'), + }, + selectedIcon: { + type: 'templateSource', + templateSource: require('@assets/variableIcons/icon_fill.png'), + }, + }, + }, + }, + { + name: 'Override', + Component: OverrideTab, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Override', + ios: { + icon: { + type: 'sfSymbol', + name: 'star', + }, + selectedIcon: { + type: 'sfSymbol', + name: 'star.fill', + }, + standardAppearance: { + stacked: { + selected: { + tabBarItemIconColor: Colors.RedLight100, + }, + }, + }, + }, + }, + }, + { + name: 'XcassetIcon', + Component: XcassetDrawableResourceTab, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Xcasset', + ios: { + icon: { + type: 'xcasset', + name: 'custom-icon-fill', + }, + }, + }, + }, + { + name: 'Image', + Component: ImageTab, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Image', + ios: { + icon: { + type: 'imageSource', + imageSource: require('@assets/variableIcons/icon.png'), + }, + selectedIcon: { + type: 'imageSource', + imageSource: require('@assets/variableIcons/icon_fill.png'), + }, + standardAppearance: { + stacked: { + normal: { + tabBarItemIconColor: Colors.BlueDark100, + }, + }, + }, + }, + }, + }, +]; + +const ANDROID_ROUTES: TabRouteConfig[] = [ + { + name: 'DrawableResource', + Component: XcassetDrawableResourceTab, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'DrawableResource', + android: { + icon: { + type: 'drawableResource', + name: 'sym_call_missed', + }, + selectedIcon: { + type: 'drawableResource', + name: 'sym_call_incoming', + }, + }, + }, + }, + { + name: 'Image', + Component: ImageTab, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Image', + android: { + icon: { + type: 'imageSource', + imageSource: require('@assets/variableIcons/icon.png'), + }, + selectedIcon: { + type: 'imageSource', + imageSource: require('@assets/variableIcons/icon_fill.png'), + }, + standardAppearance: { + selected: { + tabBarItemIconColor: Colors.RedDark100, + }, + normal: { + tabBarItemIconColor: Colors.GreenDark100, + }, + focused: { + tabBarItemIconColor: Colors.NavyLight100, + }, + }, + }, + }, + }, +]; + +export const ROUTE_CONFIGS = Platform.select({ + ios: IOS_ROUTES, + android: ANDROID_ROUTES, + default: IOS_ROUTES, // fallback +}); + +export function App() { + return ( + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 12, + }, + label: { + fontSize: 17, + fontWeight: '600', + textAlign: 'center', + }, + hint: { + fontSize: 13, + color: Colors.LightOffNavy, + textAlign: 'center', + lineHeight: 20, + }, +}); + +export default createScenario(App, scenarioDescription); diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/scenario-description.ts b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/scenario-description.ts new file mode 100644 index 0000000000..d6876c5349 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/scenario-description.ts @@ -0,0 +1,13 @@ +import type { ScenarioDescription } from '@apps/tests/shared/helpers'; + +export const scenarioDescription: ScenarioDescription = { + name: 'Tab Bar Item Icon', + key: 'test-tabs-item-icon', + details: + 'Exercises tab bar item icon props: iOS icon types (templateSource, sfSymbol,' + + ' xcasset, imageSource) with tabBarItemIconColor overrides; Android' + + ' tabBarItemIconColor in normal, selected and focused states.', + platforms: ['ios', 'android'], + e2eCoverage: 'incomplete', + smokeTest: false, +}; diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/scenario.md new file mode 100644 index 0000000000..79fd2e6339 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-icon/scenario.md @@ -0,0 +1,156 @@ +# Test Scenario: Tab Bar Item Icon + +## Details + +**Description:** Validates tab bar item icon properties (icon, selectedIcon) and +tinting behaviors for iOS and Android. Covers cross-platform icon types including +templateSource, sfSymbol, xcasset, drawableResource, and imageSource. +Verifies that per-tab appearance color configurations (selected, normal, +Android only: focused) correctly override host-level color tints. + +**OS test creation version:** iOS 18.6 and iOS 26.5, Android: API Level 36. + +## E2E test + +Incomplete: Not automated. All observable outcomes are purely visual (icon color, +selected vs. unselected glyph). Detox does not expose tint color or rendered image +attributes of native tab bar items, so automated assertion is not feasible. + +## Prerequisites + +- Android device or emulator. +- iOS device or simulator running iOS 18 or later. +- The device/simulator/emulator portrait orientation is the primary verification surface (stacked layout). + +## Note + +- Scenario steps are divided by platform, as test screens vary between iOS and Android. + +iOS specific notes: +- **Normal (unselected) state ([iOS26 KI](https://github.com/software-mansion/react-native-screens-labs/discussions/395)):** + On iOS 18 and lower, any per-tab `normal.tabBarItemIconColor` apply to unselected tab icons. + On iOS 26, only the selected tab is tinted by `tabBarItemIconColor`, unselected tabs adopt the system theme appearance. +- On iOS26 setting tabBarItemIconColor overrides tabBarTintColor also for +tabBarItemTitleFontColor - it's reported native bug. +- `tabBarTintColor` is applied only to selected tab bar item icon and title. +- **`imageSource` icons are non-tintable:** they render in their original + colors regardless of `tabBarTintColor` or `tabBarItemIconColor`. + `templateSource`, `xcasset` and `sfSymbol` icons are tintable. + +## Steps - iOS + +### Host `tabBarTintColor` applies to a tintable selected icon + +1. Launch the app and navigate to the **Tab Bar Item Icon** screen. + +- [ ] Four tabs are visible in the tab bar: **Tint**, + **Override**, **Xcasset**, and **Image**. +- [ ] The **Tint** tab is selected by default. Its icon is the filled template image + tinted **green** by the host `tabBarTintColor`. +- [ ] The unselected **Override** and **Xcasset** tabs render their icons and + titles in the system theme color. +- [ ] The unselected **Image** tab title renders in the system theme color, + but its icon keeps its original source colors. + +--- + +### `icon` vs `selectedIcon` swap + +2. Tap the **Override** tab. + +- [ ] The **Override** tab's icon swaps from the outline + star to the filled star. +- [ ] The previously selected **Tint** tab swaps from the filled template + image back to the outline template image. + +--- + +### `tabBarItemIconColor` overrides `tabBarTintColor` + +3. With **Override** still selected, observe the selected icon color. + +- [ ] The filled star is **red**, NOT green. +- [ ] On iOS 18 the selected title is + green (host tint). +- [ ] On iOS 26 the selected title is red (override - it's native + bug KI linked in Notes section). + +4. Tap the **Tint** tab, then tap **Override** again. + +- [ ] On re-selection the red filled star reappears immediately with no visual glitch. +- [ ] The **Tint** tab shows the system-theme outline template image. + +--- + +### `xcasset` icon uses host tint, no `selectedIcon` + +5. Tap the **Xcasset** tab. + +- [ ] The **Xcasset** tab's icon shows the `custom-icon-fill` xcasset image + tinted **green**. Because no `selectedIcon` is configured for this + tab, the same icon asset is used in both selected and unselected states. +- [ ] The previously selected **Override** tab reverts to the + outline star in system theme color. + +--- + +### `imageSource` icons are non-tintable + +6. Tap the **Image** tab. + +- [ ] The icon swaps from the outline image to the filled image. Both renders use the original PNG + colors - the host `tabBarTintColor` (green) have NO effect on the selected icon. +- [ ] On iOS 18 the unselected icon renders in **blue**. +- [ ] On iOS 26 the unselected icon renders in the system theme color. + +--- + +### Stability check + +7. Cycle through all four tabs in order + (Tint -> Override -> Xcasset -> Image), then in reverse. + +- [ ] Each tab swaps between its `icon` and `selectedIcon` + (where configured) consistently on selection. The correct tint + behavior is applied each time: green host tint for **Tint** and + **Xcasset**, red override for **Override**'s selected state, and + no tint effect for **Image**. +- [ ] No crash, layout freeze, or visual + artifact occurs during rapid cycling. + +## Steps - Android + +### Default color settings + +1. Launch the app and navigate to the **Tab Bar Item Icon** screen. + +- [ ] Two tabs are visible in the tab bar: **DrawableResource** + and **Image**. The **DrawableResource** tab is + selected by default. Its icon is the sym_call_incoming icon. Both + tabs render their icons and titles in the system theme color. + +--- + +### Color settings for different states + +2. Tap the **Image** tab. + +- [ ] The icon swaps from the outline image to + the filled image. Unselected tab icon changes to sym_call_missed. + Selected tab icon is **red** and unselected icon renders in **green**. + +3. While **Image** tab is selected, use the Tab key on keyboard to +switch focus to the **DrawableResource** tab. + +- [ ] Focused tab title is dark blue while selected tab title +remains red. + +--- + +### Stability check + +4. Switch between two tabs few times. + +- [ ] Each tab swaps between its `icon` and `selectedIcon` consistently on selection. +- [ ] The correct colors are applied each time: red for **Image**'s selected state and green for + **DrawableResource**'s unselected state.