diff --git a/apps/src/tests/single-feature-tests/tabs/index.ts b/apps/src/tests/single-feature-tests/tabs/index.ts index 223193ef06..3c53025edf 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 TestTabsItemBadge from './test-tabs-item-badge'; const scenarios = { TestTabBottomAccessory, @@ -38,6 +39,7 @@ const scenarios = { TestTabsTabBarExperimentalUserInterfaceStyle, TestTabsLifecycleEvents, TestTabsItemTitle, + TestTabsItemBadge, }; const TabsScenarioGroup: ScenarioGroup = { diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/index.tsx new file mode 100644 index 0000000000..d7f56567a2 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/index.tsx @@ -0,0 +1,259 @@ +import React from 'react'; +import { Platform, PlatformColor, ScrollView, 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 Tab1Screen() { + return ( + + Default badge appearance + {Platform.OS === 'ios' ? ( + + `badgeValue`: "1"{'\n'}{'\n'} + `standardAppearance` and `scrollEdgeAppearance` are not defined.{'\n'}{'\n'} + Badges render with the default iOS appearance: + `tabBarItemBadgeBackgroundColor`{' '} + RED with white text. + + ) : ( + + `badgeValue`: ""{'\n'} + An empty string badge value renders as a "small dot" using the color defined in `tabBarItemBadgeBackgroundColor`, or the system default if not set.{'\n'}{'\n'} + Badge appearance is not defined.{'\n'}{'\n'} + Badges render with the default system appearance: + background{' '} + DARK RED with white text. + + )} + + ); +} + +function Tab2Screen() { + return ( + + + Long Badge Value + {Platform.OS === 'ios' ? ( + <> + + `badgeValue`: "1234567890"{'\n'}{'\n'} + `standardAppearance`{'\n'} + `tabBarItemBadgeBackgroundColor`:{' '} + BLUE + {'\n'}{'\n'} + `scrollEdgeAppearance`{'\n'} + `tabBarItemBadgeBackgroundColor`:{' '} + YELLOW + {'\n'}{'\n'} + + + + Scroll all the way down so the list edge meets the tab bar to apply + `scrollEdgeAppearance`. Scroll back up to restore `standardAppearance`. + + + + ) : ( + + `badgeValue`: "1234567890" displayed as "999+"{'\n'}{'\n'} + `tabBarItemBadgeBackgroundColor`:{' '} + BLUE + {'\n'} + `tabBarItemBadgeTextColor`:{' '} + YELLOW + + )} + + + ); +} + +function Tab3Screen() { + return ( + + String Badge Value + {Platform.OS === 'ios' ? ( + + `badgeValue`: "NEW!"{'\n'}{'\n'} + selected: `tabBarItemBadgeBackgroundColor`:{' '} + BLUE + {'\n'}{'\n'} + normal: `tabBarItemBadgeBackgroundColor`:{' '} + PURPLE + {'\n'}{'\n'} + + ) : ( + + `badgeValue`: "NEW!"{'\n'}{'\n'} + `tabBarItemBadgeBackgroundColor`:{' '} + PURPLE + {'\n'} + `tabBarItemBadgeTextColor`:{' '} + NAVY + + )} + + ); +} + +function Tab4Screen() { + return ( + + Transparent badge background + {Platform.OS === 'ios' ? ( + + `badgeValue`: "⚠️"{'\n'}{'\n'} + Badge appearance is defined only for selected tab: setting background to `transparent` value.{'\n'}{'\n'} + Unselected badges render with the default system appearance: + badge background{' '} + RED with white text. + + ) : ( + + `badgeValue`: "⚠️"{'\n'}{'\n'} + `tabBarItemBadgeBackgroundColor`: `transparent` + {'\n'} + `tabBarItemBadgeTextColor`:{' '} + RED + + )} + + ); +} + +const ROUTE_CONFIGS: TabRouteConfig[] = [ + { + name: 'Tab1', + Component: Tab1Screen, + options: { + title: 'Tab1', + badgeValue: Platform.OS === 'ios' ? '1' : '', + ios: { + ...DEFAULT_TAB_ROUTE_OPTIONS.ios, + }, + android: { + ...DEFAULT_TAB_ROUTE_OPTIONS.android, + standardAppearance: { + tabBarItemLabelVisibilityMode: 'labeled', + }, + }, + }, + }, + { + name: 'Tab2', + Component: Tab2Screen, + options: { + title: 'Tab2', + badgeValue: '1234567890', + ios: { + ...DEFAULT_TAB_ROUTE_OPTIONS.ios, + standardAppearance: { + stacked: { + normal: { tabBarItemBadgeBackgroundColor: Colors.BlueDark100 }, + }, + }, + scrollEdgeAppearance: { + stacked: { + normal: { tabBarItemBadgeBackgroundColor: Colors.YellowDark100 }, + }, + }, + }, + android: { + ...DEFAULT_TAB_ROUTE_OPTIONS.android, + standardAppearance: { + tabBarItemLabelVisibilityMode: 'labeled', + tabBarItemBadgeBackgroundColor: Colors.BlueDark100, + tabBarItemBadgeTextColor: Colors.YellowDark100, + }, + }, + }, + }, + { + name: 'Tab3', + Component: Tab3Screen, + options: { + title: 'Tab3', + badgeValue: 'NEW!', + ios: { + ...DEFAULT_TAB_ROUTE_OPTIONS.ios, + standardAppearance: { + stacked: { + normal: { tabBarItemBadgeBackgroundColor: Colors.PurpleDark100 }, + selected: { tabBarItemBadgeBackgroundColor: Colors.BlueDark100 }, + }, + }, + }, + android: { + ...DEFAULT_TAB_ROUTE_OPTIONS.android, + standardAppearance: { + tabBarItemLabelVisibilityMode: 'labeled', + tabBarItemBadgeBackgroundColor: Colors.PurpleDark100, + tabBarItemBadgeTextColor: Colors.NavyLight100, + }, + }, + }, + }, + { + name: 'Tab4', + Component: Tab4Screen, + options: { + title: 'Tab4', + badgeValue: '⚠️', + ios: { + ...DEFAULT_TAB_ROUTE_OPTIONS.ios, + standardAppearance: { + stacked: { + selected: { tabBarItemBadgeBackgroundColor: 'transparent' }, + } + }, + }, + android: { + ...DEFAULT_TAB_ROUTE_OPTIONS.android, + standardAppearance: { + tabBarItemLabelVisibilityMode: 'labeled', + tabBarItemBadgeBackgroundColor: 'transparent', + tabBarItemBadgeTextColor: Colors.RedLight100, + }, + }, + }, + }, +]; + +export function App() { + return ; +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + margin: 24, + padding: 24, + gap: 12, + + }, + spacer: { + height: 220, + }, + label: { + fontSize: 17, + fontWeight: '600', + marginBottom: 12, + marginTop: 24, + alignSelf: 'center', + }, + hint: { + fontSize: 13, + color: Colors.LightOffNavy, + lineHeight: 20, + textAlign: 'center', + }, +}); + +export default createScenario(App, scenarioDescription); diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/scenario-description.ts b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/scenario-description.ts new file mode 100644 index 0000000000..4709bf84bb --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/scenario-description.ts @@ -0,0 +1,11 @@ +import type { ScenarioDescription } from '@apps/tests/shared/helpers'; + +export const scenarioDescription: ScenarioDescription = { + name: 'Tab Bar Item Badge', + key: 'test-tabs-item-badge', + details: + 'Exercises tab bar item badge props: badgeValue, tabBarItemBadgeBackgroundColor, and tabBarItemBadgeTextColor (Android only).', + platforms: ['ios', 'android'], + e2eCoverage: 'incomplete', + smokeTest: false, +}; diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/scenario.md b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/scenario.md new file mode 100644 index 0000000000..560dfd2c7b --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-item-badge/scenario.md @@ -0,0 +1,196 @@ +# Test Scenario: Tab Bar Item Badge + +## Details + +**Description:** Validates the `badgeValue` prop on `TabsScreen` and the +badge appearance props (`tabBarItemBadgeBackgroundColor` and Android only: `tabBarItemBadgeTextColor`). The scenario covers +four badge cases: a badge with default system appearance, a +long numeric string that overflows the max display width, a custom string +badge, and an emoji badge. For iOS, it also exercises +`scrollEdgeAppearance` badge color. On Android, all badge color +customisation is driven by `standardAppearance`. + +**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 (badge color, text color, displayed string). +Detox does not expose color or text-content attributes of native tab bar +badge items, so automated assertion requires a screenshot-diff approach +not yet in place. + +## Prerequisites + +- iOS device or simulator. +- Android emulator. + +## Note + +`badgeValue`: + +- On Android the maximum badge string length rendered verbatim is 4 + characters; longer numeric strings are capped and shown as `999+`. + When `badgeValue` is set to an empty string it renders as "small dot" badge if + `tabBarItemBadgeBackgroundColor` is not `transparent`. +- On iOS 18: badges render the full `badgeValue` string without truncation; +because the badge text color is white, any characters overflowing the badge's colored +background will blend into a white screen background and become invisible. +- On iOS 26: long badges render the `badgeValue` string with truncation, ending with "...". + +`tabBarItemBadgeBackgroundColor`: + +- Setting a transparent `tabBarItemBadgeBackgroundColor` removes the badge's pill +background. On iOS, only the selected tab's badgeValue floats without a colored +background, while the background color of unselected badges remains visible. +On Android, this property makes the background fully transparent for all tabs, +effectively making Tab1 appear badgeless. +- On iOS 26 ([KI 1072](https://github.com/software-mansion/react-native-screens-labs/issues/1072)) +When `tabBarItemBadgeBackgroundColor` is defined with different colors for the +normal and selected states, the badge color of the selected tab breaks and +partially uses the normal state's background color. Additionally, if a tab on +the left side has a long badge value, it can be affected as well, causing its +color to partially break and incorrectly inherit the selected state's background color. + +--- + +## Steps (iOS) + +### Baseline + +1. Launch the app and navigate to the **Tab Bar Item Badge** screen. + +- [ ] Four tabs are visible - **Tab1**, **Tab2**, **Tab3**, **Tab4**. +- [ ] Tab1 is active. Each tab shows a badge in the tab bar. +- [ ] Tab1: badge reads **1**. +- [ ] Tab2: on iOS 26 badge reads **12345...**; on iOS 18 **23456789** is visible. +- [ ] Tab3's badge reads **NEW!**. +- [ ] Tab4's badge reads **⚠️**. + +--- + +### Tab1 - default badge appearance + +2. Confirm Tab1 is active. Observe the Tab1 badge in the tab bar. + +- [ ] Badge shows **1**. +- [ ] The badge renders with the iOS system default: red background with white text. + +--- + +### Tab2 - long badge value and scrollEdgeAppearance + +3. Tap **Tab2** in the tab bar. + +- [ ] Tab2 is selected. The badge in the tab bar reads: + - on iOS 26 badge reads **12345...**; + - on iOS 18 **23456789** is visible. +- [ ] The badge pill is blue. + +4. iOS 26 only: Scroll the Tab2 content all the way to the bottom until the scroll + edge meets the tab bar. + +- [ ] The badge pill color transitions from blue to yellow for all tabs. + +5. iOS 26 only: Scroll back up so the content edge no longer touches the tab bar. + +- [ ] The badge pill returns to blue. + +--- + +### Tab3 - string badge value and normal/selected state colors + +6. Tap **Tab3** in the tab bar. + +- [ ] Tab3 is selected. The badge in the tab bar reads **NEW!**. +- [ ] The selected tab badge pill is blue, other tabs badges pills are purple. +- [ ] On iOS 26 selected badge pill color is divided into selected (blue) and +normal (purple), also Tab2 badge is affected and its badge color is partially blue +(see KI from Note section) + +--- + +### Tab4 - emoji badge and transparent selected state + +7. Tap **Tab4** in the tab bar. + +- [ ] Tab4 is selected. The badge in the tab bar shows **⚠️**. +- [ ] The selected state has `tabBarItemBadgeBackgroundColor: transparent`, + so the badge background is invisible - the emoji floats without a + colored pill. +- [ ] The badge background for unselected tabs is default red. + +--- + +### Stability check + +8. Cycle through all four tabs in order (Tab1 → Tab2 → Tab3 → Tab4), + then in reverse (Tab4 → Tab3 → Tab2 → Tab1). + +- [ ] Each badge updates correctly for the selected vs. unselected state on every tab switch. + +--- + +## Steps (Android) + +### Baseline + +1. Launch the app and navigate to the **Tab Bar Item Badge** screen. + +- [ ] Four tabs are visible - **Tab1**, **Tab2**, **Tab3**, **Tab4**. +- [ ] Tab1 is active. Each tab shows a badge in the tab bar. +- [ ] Tab1: small dot is visible as badge value is set to an empty string. +- [ ] Tab2's badge is capped at **999+** (the string "1234567890" exceeds the +maximum and is truncated by the system). +- [ ] Tab3's badge reads **NEW!**. +- [ ] Tab4's badge reads **⚠️**. + +--- + +### Tab1 - default badge appearance + +2. Confirm Tab1 is active. Observe the Tab1 badge in the tab bar. + +- [ ] Badge shows as a small dot. +- [ ] The badge renders with the Android system default: red background with white text. + +--- + +### Tab2 - long badge value and custom colors + +3. Tap **Tab2** in the tab bar. + +- [ ] Tab2 is selected. The badge in the tab bar is shown + as **999+**. +- [ ] The badge background is blue and the badge text is + yellow for all tabs. + +--- + +### Tab3 - string badge value and custom colors + +4. Tap **Tab3** in the tab bar. + +- [ ] Tab3 is selected. The badge in the tab bar reads **NEW!**. +- [ ] The badge background is purple and the badge text is navy for all tabs. + +--- + +### Tab4 - emoji badge and transparent background + +5. Tap **Tab4** in the tab bar. + +- [ ] Tab4 is selected. The badge in the tab bar shows emoji **⚠️**. +- [ ] The badge background is transparent for all tabs, so the emoji and text +floats without a colored pill. +- [ ] The badge text color is red. + +--- + +### Stability check + +6. Cycle through all four tabs in order (Tab1 → Tab2 → Tab3 → Tab4), + then in reverse (Tab4 → Tab3 → Tab2 → Tab1). + +- [ ]Each badge is displayed correctly on every tab switch. The colors, text +values, and transparent states remain stable.