Repository: zulip/zulip-flutter (language: dart)

Issue:
Label icons on main nav bar
In the bottom nav bar on the home screens of the app, we want to show a text label along with the icon for each button.

This should also resolve the lack of such labels for screen readers, which was one of the points noted at https://github.com/zulip/zulip-flutter/issues/535#issuecomment-2973276995 .

### References
* Chat thread with user request: [#mobile > Flutter feedback - Icons in app](https://chat.zulip.org/#narrow/channel/48-mobile/topic/Flutter.20feedback.20-.20Icons.20in.20app/with/2223128)
* Design: [#mobile > Flutter feedback - Icons in app @ 💬](https://chat.zulip.org/#narrow/channel/48-mobile/topic/Flutter.20feedback.20-.20Icons.20in.20app/near/2219685)


Semantic labels on main nav bar
This was found in https://github.com/zulip/zulip-flutter/issues/535#issuecomment-2973276995 when investigating the app's behavior with a screen reader:
> The home page bottom tabs do not have labels. The caption for each of them is just: "button, double tap to activate".

We should fix this.

This might get fixed as part of the same change as:
* #1857




Discussion hints:

@MritunjayTiwari14, would you check whether your #1879 resolves this issue, and if so then mark it as doing so?

Yes @gnprice , it can also fix it. In most app i checked out like Google and Whatsapp using TalkBack On. I noticed that both of them uses `<navbar_item_name> Tab` as a convention to announce the different bottom navbar items. So i am implementing the same in Zulip-flutter.

Pull request: home: Add icons label on main navigation bar.
**Key Changes:** (Screenshots Outdated) 

| Dark Theme | Light Theme |
|---------|---------|
| ![1000051916](https://github.com/user-attachments/assets/a85fada6-7038-45a0-a595-13bcc386197e) | ![1000051917](https://github.com/user-attachments/assets/8e1cd84f-4641-42c1-b5d9-63ff21a84ebb) |




I have kept the height of the Navigation to be fixed because a fully dynamic navigation bar height that adjusts based on label content across languages is not feasibly after having done some feasibility study. The UI jitter when the bar changes its size suddenly when the user changes the app language is extremely unlikely to be solved and can negatively impact user experience hence even in day to day app these types of dynamic navigation bar are not used.

integration of the page bodies and semantics has been done

Discussion : [#mobile > Flutter feedback - Icons in app @ 💬](https://chat.zulip.org/#narrow/channel/48-mobile/topic/Flutter.20feedback.20-.20Icons.20in.20app/near/2219685)
    

- Language Consistency for all language for the added labels will be fixed after receiving feedback from the Moderators about the Implementation of the solution.

Thank you

[Figma Design Link](https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=11677-62351&t=z0Pq39n8h1rzALdc-4)

Fixes #1857
Fixes #1960

Test command: flutter test test/widgets/home_test.dart test/widgets/recent_dm_conversations_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 45c7e6ca94..38b49c2edd 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -1261,6 +1261,10 @@
   "@wildcardMentionTopicDescription": {
     "description": "Description for \"@topic\" wildcard-mention autocomplete options when writing a channel message."
   },
+  "navBarMenuLabel": "Menu",
+  "@navBarMenuLabel": {
+    "description": "Label for the Menu button on the bottom navigation bar."
+  },
   "messageIsEditedLabel": "EDITED",
   "@messageIsEditedLabel": {
     "description": "Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)"
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index ce46ae6e7d..fe79921c07 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -1833,6 +1833,12 @@ abstract class ZulipLocalizations {
   /// **'Notify topic'**
   String get wildcardMentionTopicDescription;
 
+  /// Label for the Menu button on the bottom navigation bar.
+  ///
+  /// In en, this message translates to:
+  /// **'Menu'**
+  String get navBarMenuLabel;
+
   /// Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)
   ///
   /// In en, this message translates to:
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index d4c35968bf..5e69a181da 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'إخطار الموضوع';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart
index 0a43ac50b5..241e886e55 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -1073,6 +1073,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Thema benachrichtigen';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'BEARBEITET';
 
diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart
index c7725a64e2..ce72dc77bf 100644
--- a/lib/generated/l10n/zulip_localizations_el.dart
+++ b/lib/generated/l10n/zulip_localizations_el.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 334df16239..6d10362555 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart
index f45d3db383..80098d557f 100644
--- a/lib/generated/l10n/zulip_localizations_es.dart
+++ b/lib/generated/l10n/zulip_localizations_es.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart
index 0d64c3b679..adb41177b2 100644
--- a/lib/generated/l10n/zulip_localizations_fr.dart
+++ b/lib/generated/l10n/zulip_localizations_fr.dart
@@ -1070,6 +1070,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart
index 5e1609ccba..e59309be3e 100644
--- a/lib/generated/l10n/zulip_localizations_he.dart
+++ b/lib/generated/l10n/zulip_localizations_he.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart
index aacb5d9d8d..6a3c11465c 100644
--- a/lib/generated/l10n/zulip_localizations_hu.dart
+++ b/lib/generated/l10n/zulip_localizations_hu.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart
index 4fc1ba6e42..cd27a1daa7 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -1067,6 +1067,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notifica argomento';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'MODIFICATO';
 
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index e779208dcf..d11aedf19f 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -1029,6 +1029,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'トピック参加者に通知';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => '編集済み';
 
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 3d9b7990e7..162ca9e677 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index ea8d3b4fd8..40682171b2 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -1070,6 +1070,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Powiadom w wątku';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'ZMIENIONO';
 
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index 17f82a0d26..04f8903b91 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -1083,6 +1083,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Оповестить тему';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'ИЗМЕНЕНО';
 
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 092070d0f3..e2dc542fc5 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -1056,6 +1056,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'UPRAVENÉ';
 
diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart
index f18f440748..6f9d8343ca 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -1091,6 +1091,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Obvesti udeležence teme';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'UREJENO';
 
diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart
index 3810960b63..84df26a42e 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -1071,6 +1071,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Повідомити канал';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'РЕДАГОВАНО';
 
diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart
index 5db806ac0e..b08c25004e 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -1054,6 +1054,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
   @override
   String get wildcardMentionTopicDescription => 'Notify topic';
 
+  @override
+  String get navBarMenuLabel => 'Menu';
+
   @override
   String get messageIsEditedLabel => 'EDITED';
 
diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart
index 62c09c0857..c74da27a86 100644
--- a/lib/widgets/home.dart
+++ b/lib/widgets/home.dart
@@ -1,6 +1,7 @@
 import 'dart:async';
 
 import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
 
 import '../generated/l10n/zulip_localizations.dart';
 import '../model/narrow.dart';
@@ -47,6 +48,9 @@ class HomePage extends StatefulWidget {
       HomePage.buildRoute(accountId: accountId)));
   }
 
+  static String contentSemanticsIdentifier = 'home-page-content';
+  static String titleSemanticsIdentifier = 'home-page-title';
+
   @override
   State<HomePage> createState() => _HomePageState();
 }
@@ -93,58 +97,97 @@ class _HomePageState extends State<HomePage> {
       (_HomePageTab.directMessages, RecentDmConversationsPageBody()),
     ];
 
-    _NavigationBarButton button(_HomePageTab tab, IconData icon) {
-      return _NavigationBarButton(icon: icon,
-        selected: _tab.value == tab,
-        onPressed: () {
-          _tab.value = tab;
-        });
-    }
+    return Scaffold(
+      appBar: ZulipAppBar(titleSpacing: 16,
+        title: Semantics(
+          identifier: HomePage.titleSemanticsIdentifier,
+          namesRoute: true,
+          child: Text(_currentTabTitle))),
+      body: Semantics(
+        role: SemanticsRole.tabPanel,
+        identifier: HomePage.contentSemanticsIdentifier,
+        container: true,
+        explicitChildNodes: true,
+        child: Stack(
+          children: [
+            for (final (tab, body) in pageBodies)
+              Offstage(offstage: tab != _tab.value, child: body),
+          ])),
+      bottomNavigationBar: _BottomNavBar(tabNotifier: _tab));
+  }
+}
+
+class _BottomNavBar extends StatelessWidget {
+  const _BottomNavBar({required this.tabNotifier});
+
+  final ValueNotifier<_HomePageTab> tabNotifier;
+
+  _NavigationBarButton _button({
+    required _HomePageTab tab,
+    required IconData icon,
+    required String label,
+  }) {
+    return _NavigationBarButton(icon: icon,
+      label: label,
+      selected: tabNotifier.value == tab,
+      onPressed: () {
+        tabNotifier.value = tab;
+      });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final designVariables = DesignVariables.of(context);
+    final zulipLocalizations = ZulipLocalizations.of(context);
 
     // TODO(a11y): add tooltips for these buttons
     final navigationBarButtons = [
-      button(_HomePageTab.inbox,          ZulipIcons.inbox),
-      _NavigationBarButton(         icon: ZulipIcons.message_feed,
+      _button(tab: _HomePageTab.inbox,
+        icon: ZulipIcons.inbox,
+        label: zulipLocalizations.inboxPageTitle),
+      _NavigationBarButton(icon: ZulipIcons.message_feed,
+        label: zulipLocalizations.combinedFeedPageTitle,
         selected: false,
         onPressed: () => Navigator.push(context,
           MessageListPage.buildRoute(context: context,
             narrow: const CombinedFeedNarrow()))),
-      button(_HomePageTab.channels,       ZulipIcons.hash_italic),
+      _button(tab: _HomePageTab.channels,
+        icon: ZulipIcons.hash_italic,
+        label: zulipLocalizations.channelsPageTitle),
       // TODO(#1094): Users
-      button(_HomePageTab.directMessages, ZulipIcons.two_person),
-      _NavigationBarButton(         icon: ZulipIcons.menu,
+      _button(tab: _HomePageTab.directMessages,
+        icon: ZulipIcons.two_person,
+        label: zulipLocalizations.recentDmConversationsPageTitle),
+      _NavigationBarButton(icon: ZulipIcons.menu,
+        label: zulipLocalizations.navBarMenuLabel,
         selected: false,
-        onPressed: () => _showMainMenu(context, tabNotifier: _tab)),
+        onPressed: () => _showMainMenu(context, tabNotifier: tabNotifier)),
     ];
 
-    final designVariables = DesignVariables.of(context);
-    return Scaffold(
-      appBar: ZulipAppBar(titleSpacing: 16,
-        title: Text(_currentTabTitle)),
-      body: Stack(
-        children: [
-          for (final (tab, body) in pageBodies)
-            // TODO(#535): Decide if we find it helpful to use something like
-            //   [SemanticsProperties.namesRoute] to structure this UI better
-            //   for screen-reader software.
-            Offstage(offstage: tab != _tab.value, child: body),
-        ]),
-      bottomNavigationBar: DecoratedBox(
-        decoration: BoxDecoration(
-          border: Border(top: BorderSide(color: designVariables.borderBar)),
-          color: designVariables.bgBotBar),
-        child: SafeArea(
-          child: SizedBox(height: 48,
-            child: Center(
-              child: ConstrainedBox(
-                // TODO(design): determine a suitable max width for bottom nav bar
-                constraints: const BoxConstraints(maxWidth: 600),
-                child: Row(
-                  crossAxisAlignment: CrossAxisAlignment.stretch,
-                  children: [
-                    for (final navigationBarButton in navigationBarButtons)
-                      Expanded(child: navigationBarButton),
-                  ])))))));
+    Widget result = DecoratedBox(
+      decoration: BoxDecoration(
+        border: Border(top: BorderSide(color: designVariables.borderBar)),
+        color: designVariables.bgBotBar),
+      child: SafeArea(
+        child: Center(
+          heightFactor: 1,
+          child: ConstrainedBox(
+            // TODO(design): determine a suitable max width for bottom nav bar
+            constraints: const BoxConstraints(maxWidth: 600, minHeight: 48),
+            child: Row(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                for (final navigationBarButton in navigationBarButtons)
+                  Expanded(child: navigationBarButton),
+              ])))));
+
+    result = Semantics(
+      container: true,
+      explicitChildNodes: true,
+      role: SemanticsRole.tabBar,
+      child: result);
+
+    return result;
   }
 }
 
@@ -229,37 +272,62 @@ class _LoadingPlaceholderPageState extends State<_LoadingPlaceholderPage> {
 class _NavigationBarButton extends StatelessWidget {
   const _NavigationBarButton({
     required this.icon,
+    required this.label,
     required this.selected,
     required this.onPressed,
   });
 
   final IconData icon;
+  final String label;
   final bool selected;
   final void Function() onPressed;
 
   @override
   Widget build(BuildContext context) {
     final designVariables = DesignVariables.of(context);
+    final color = selected ? designVariables.iconSelected : designVariables.icon;
 
-    final iconColor = WidgetStateColor.fromMap({
-      WidgetState.pressed:  designVariables.iconSelected,
-      ~WidgetState.pressed: selected ? designVariables.iconSelected
-                                     : designVariables.icon,
-    });
-
-    return AnimatedScaleOnTap(
+    Widget result = AnimatedScaleOnTap(
       scaleEnd: 0.875,
       duration: const Duration(milliseconds: 100),
-      child: IconButton(
-        icon: Icon(icon, size: 24),
-        onPressed: onPressed,
-        style: IconButton.styleFrom(
+      child: Material(
+        type: MaterialType.transparency,
+        child: InkWell(
+          borderRadius: BorderRadius.all(Radius.circular(4)),
           // TODO(#417): Disable splash effects for all buttons globally.
           splashFactory: NoSplash.splashFactory,
           highlightColor: designVariables.navigationButtonBg,
-          shape: const RoundedRectangleBorder(
-            borderRadius: BorderRadius.all(Radius.circular(4))),
-        ).copyWith(foregroundColor: iconColor)));
+          onTap: onPressed,
+          child: Padding(
+            // (Added 3px horizontal padding not present in Figma, to make the
+            // text wrap before getting too close to the button's edge, which is
+            // visible on tap-down.)
+            padding: const EdgeInsets.fromLTRB(3, 6, 3, 3),
+            child: Column(
+              spacing: 3,
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                Icon(icon, size: 24, color: color),
+                Flexible(
+                  child: Text(
+                    label,
+                    style: TextStyle(fontSize: 12, color: color, height: 12 / 12),
+                    textAlign: TextAlign.center,
+                    textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))),
+              ])))));
+
+    result = MergeSemantics(
+      child: Semantics(
+        role: SemanticsRole.tab,
+        controlsNodes: {
+          HomePage.contentSemanticsIdentifier,
+          HomePage.titleSemanticsIdentifier,
+        },
+        selected: selected,
+        onTap: onPressed,
+        child: result));
+
+    return result;
   }
 }
 


Test patch:
diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart
index 1ee0a0ae8e..239449c990 100644
--- a/test/widgets/home_test.dart
+++ b/test/widgets/home_test.dart
@@ -1,3 +1,5 @@
+import 'dart:ui';
+
 import 'package:checks/checks.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_checks/flutter_checks.dart';
@@ -82,6 +84,15 @@ void main () {
   }
 
   group('bottom nav navigation', () {
+    final findBottomNavSemantics = find.byWidgetPredicate((widget) {
+      if (widget is! Semantics) return false;
+      return widget.properties.role == SemanticsRole.tab;
+    });
+
+    // Finds a widget within the bottom navbar's semantics box subtree.
+    Finder findInBottomNav(Finder finder) =>
+      find.descendant(of: findBottomNavSemantics, matching: finder);
+
     testWidgets('preserve states when switching between views', (tester) async {
       await prepare(tester);
       await store.addUser(eg.otherUser);
@@ -131,6 +142,26 @@ void main () {
         matching: find.text('Direct messages'))).findsOne();
     });
 
+    testWidgets("view switches when labels are tapped", (tester) async {
+      await prepare(tester);
+
+      check(find.descendant(
+        of: find.byType(ZulipAppBar),
+        matching: find.text('Inbox'))).findsOne();
+
+      await tester.tap(findInBottomNav(find.text('Channels')));
+      await tester.pump();
+      check(find.descendant(
+        of: find.byType(ZulipAppBar),
+        matching: find.text('Channels'))).findsOne();
+
+      await tester.tap(findInBottomNav(find.text('Direct messages')));
+      await tester.pump();
+      check(find.descendant(
+        of: find.byType(ZulipAppBar),
+        matching: find.text('Direct messages'))).findsOne();
+    });
+
     testWidgets('combined feed', (tester) async {
       await prepare(tester);
       pushedRoutes.clear();
diff --git a/test/widgets/recent_dm_conversations_test.dart b/test/widgets/recent_dm_conversations_test.dart
index a558734119..b5fdd10217 100644
--- a/test/widgets/recent_dm_conversations_test.dart
+++ b/test/widgets/recent_dm_conversations_test.dart
@@ -59,7 +59,7 @@ Future<void> setupPage(WidgetTester tester, {
 
   // Switch to direct messages tab.
   await tester.tap(find.descendant(
-    of: find.byType(Center),
+    of: find.byType(DecoratedBox),
     matching: find.byIcon(ZulipIcons.two_person)));
   await tester.pump();
 }