From 9822daf96da4cf73647c1c67ae8a4e175a6000c5 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Thu, 9 Jul 2026 12:34:57 +0100 Subject: [PATCH 1/3] fix android tab test ids --- .changeset/android-tab-test-ids.md | 6 +++++ apps/example/e2e/native-tabs-test-id.yaml | 22 ++++++++++++++++ apps/example/package.json | 1 + .../example/src/Examples/NativeBottomTabs.tsx | 3 +++ .../main/java/com/rcttabview/RCTTabView.kt | 26 ++++++++++++++----- 5 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 .changeset/android-tab-test-ids.md create mode 100644 apps/example/e2e/native-tabs-test-id.yaml diff --git a/.changeset/android-tab-test-ids.md b/.changeset/android-tab-test-ids.md new file mode 100644 index 00000000..d6ff04db --- /dev/null +++ b/.changeset/android-tab-test-ids.md @@ -0,0 +1,6 @@ +--- +"react-native-bottom-tabs": patch +"@bottom-tabs/react-navigation": patch +--- + +Expose Android tab button test IDs to UIAutomator-based E2E tools. diff --git a/apps/example/e2e/native-tabs-test-id.yaml b/apps/example/e2e/native-tabs-test-id.yaml new file mode 100644 index 00000000..115651c2 --- /dev/null +++ b/apps/example/e2e/native-tabs-test-id.yaml @@ -0,0 +1,22 @@ +appId: bottomtabs.example +name: Android native tabs testID smoke +tags: + - smoke +--- +- launchApp +- scrollUntilVisible: + element: + text: ^Native Bottom Tabs$ + direction: DOWN + timeout: 15000 +- tapOn: + text: ^Native Bottom Tabs$ +- assertVisible: Press me +- tapOn: + id: articleTestID +- assertVisible: What is Lorem Ipsum? +- tapOn: + id: albumsTestID +- tapOn: + id: chatTestID +- assertVisible: Press me diff --git a/apps/example/package.json b/apps/example/package.json index e1c5d607..e1342abf 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -7,6 +7,7 @@ "build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist && react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a -PnewArchEnabled=true\"", "build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.ios.jsbundle --assets-dest dist", "ios": "react-native run-ios", + "e2e:android": "maestro test --platform=android e2e", "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", "start": "react-native start" }, diff --git a/apps/example/src/Examples/NativeBottomTabs.tsx b/apps/example/src/Examples/NativeBottomTabs.tsx index f4ec24e2..47d19329 100644 --- a/apps/example/src/Examples/NativeBottomTabs.tsx +++ b/apps/example/src/Examples/NativeBottomTabs.tsx @@ -60,6 +60,7 @@ function NativeBottomTabs() { name="Albums" component={Albums} options={{ + tabBarButtonTestID: 'albumsTestID', tabBarIcon: () => require('../../assets/icons/grid_dark.png'), }} /> @@ -72,6 +73,7 @@ function NativeBottomTabs() { }, }} options={{ + tabBarButtonTestID: 'contactsTestID', tabBarIcon: () => require('../../assets/icons/person_dark.png'), tabBarActiveTintColor: 'yellow', preventsDefault: true, @@ -86,6 +88,7 @@ function NativeBottomTabs() { }, }} options={{ + tabBarButtonTestID: 'chatTestID', tabBarIcon: () => require('../../assets/icons/chat_dark.png'), tabBarActiveTintColor: 'white', }} diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 911d2789..8745b023 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -20,7 +20,10 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.AccessibilityDelegateCompat import androidx.core.view.MenuItemCompat +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.forEachIndexed import coil3.ImageLoader import coil3.asDrawable @@ -281,12 +284,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { onTabSelected(menuItem) } - item.testID?.let { testId -> - view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container) - ?.apply { - tag = testId - } - } + view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container) + ?.setTabTestID(item.testID) } } } @@ -301,6 +300,21 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title) } + private fun View.setTabTestID(testId: String?) { + tag = testId + ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + testId?.let { + info.viewIdResourceName = it + } + } + }) + } + private fun updateIconTintMode(menuItem: MenuItem, item: TabInfo) { MenuItemCompat.setIconTintMode( menuItem, From 5300be9638ad518185c3e32518c504a780da6013 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Thu, 9 Jul 2026 14:59:36 +0100 Subject: [PATCH 2/3] chore: account for null testIds --- .../android/src/main/java/com/rcttabview/RCTTabView.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 8745b023..3c3d4bdb 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -302,15 +302,18 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { private fun View.setTabTestID(testId: String?) { tag = testId + if (testId == null) { + ViewCompat.setAccessibilityDelegate(this, null) + return + } + ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() { override fun onInitializeAccessibilityNodeInfo( host: View, info: AccessibilityNodeInfoCompat ) { super.onInitializeAccessibilityNodeInfo(host, info) - testId?.let { - info.viewIdResourceName = it - } + info.viewIdResourceName = testId } }) } From b57a4f617d2e89e31fece0df845acf2abe417ee4 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Thu, 9 Jul 2026 15:03:15 +0100 Subject: [PATCH 3/3] chore: cleanup --- apps/example/e2e/native-tabs-test-id.yaml | 4 +--- apps/example/package.json | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/example/e2e/native-tabs-test-id.yaml b/apps/example/e2e/native-tabs-test-id.yaml index 115651c2..2f34fc3e 100644 --- a/apps/example/e2e/native-tabs-test-id.yaml +++ b/apps/example/e2e/native-tabs-test-id.yaml @@ -1,7 +1,5 @@ appId: bottomtabs.example -name: Android native tabs testID smoke -tags: - - smoke +name: Navigate tabs via testIDs --- - launchApp - scrollUntilVisible: diff --git a/apps/example/package.json b/apps/example/package.json index e1342abf..565eab4c 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -7,6 +7,7 @@ "build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist && react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a -PnewArchEnabled=true\"", "build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.ios.jsbundle --assets-dest dist", "ios": "react-native run-ios", + "e2e:ios": "maestro test --platform=ios e2e", "e2e:android": "maestro test --platform=android e2e", "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", "start": "react-native start"