diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 62c09c0857..bca19170ac 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -84,6 +84,23 @@ class _HomePageState extends State { } } + List? get _currentTabAppBarActions { + switch(_tab.value) { + case _HomePageTab.inbox: + return [ + IconButton( + icon: const Icon(ZulipIcons.search), + tooltip: ZulipLocalizations.of(context).searchMessagesPageTitle, + onPressed: () => Navigator.of(context).push(MessageListPage.buildRoute( + context: context, narrow: KeywordSearchNarrow(''))), + ), + ]; + case _HomePageTab.channels: + case _HomePageTab.directMessages: + return null; + } + } + @override Widget build(BuildContext context) { const pageBodies = [ @@ -120,7 +137,9 @@ class _HomePageState extends State { final designVariables = DesignVariables.of(context); return Scaffold( appBar: ZulipAppBar(titleSpacing: 16, - title: Text(_currentTabTitle)), + title: Text(_currentTabTitle), + actions: _currentTabAppBarActions + ), body: Stack( children: [ for (final (tab, body) in pageBodies) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 77e2e8cb24..15cc761948 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -416,6 +416,12 @@ abstract class _MessageListAppBar { List actions = []; switch (narrow) { case CombinedFeedNarrow(): + actions.add(IconButton( + icon: const Icon(ZulipIcons.search), + tooltip: zulipLocalizations.searchMessagesPageTitle, + onPressed: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: KeywordSearchNarrow(''))))); case MentionsNarrow(): case StarredMessagesNarrow(): case KeywordSearchNarrow(): diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 1ee0a0ae8e..04c2a22f85 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -111,24 +111,40 @@ void main () { check(find.byIcon(ZulipIcons.arrow_right)).findsExactly(2); }); - testWidgets('update app bar title when switching between views', (tester) async { + testWidgets('update app bar title and actions when switching between views', (tester) async { await prepare(tester); + // Helper to check if search button is present + bool hasSearchButton() { + final appBar = tester.widget(find.byType(ZulipAppBar)); + final actions = appBar.actions ?? []; + return actions.any((widget) => + widget is IconButton && + widget.icon is Icon && + (widget.icon as Icon).icon == ZulipIcons.search); + } + + // Inbox tab: should have title "Inbox" and search button check(find.descendant( of: find.byType(ZulipAppBar), matching: find.text('Inbox'))).findsOne(); + check(hasSearchButton()).isTrue(); + // Channels tab: should have title "Channels" and no search button await tester.tap(find.byIcon(ZulipIcons.hash_italic)); await tester.pump(); check(find.descendant( of: find.byType(ZulipAppBar), matching: find.text('Channels'))).findsOne(); + check(hasSearchButton()).isFalse(); + // Direct messages tab: should have title "Direct messages" and no search button await tester.tap(find.byIcon(ZulipIcons.two_person)); await tester.pump(); check(find.descendant( of: find.byType(ZulipAppBar), matching: find.text('Direct messages'))).findsOne(); + check(hasSearchButton()).isFalse(); }); testWidgets('combined feed', (tester) async { diff --git a/test/widgets/inbox_test.dart b/test/widgets/inbox_test.dart index fe96b427ab..95cde7e3c7 100644 --- a/test/widgets/inbox_test.dart +++ b/test/widgets/inbox_test.dart @@ -4,17 +4,23 @@ import 'package:flutter_checks/flutter_checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/api/model/events.dart'; import 'package:zulip/api/model/model.dart'; +import 'package:zulip/model/narrow.dart'; import 'package:zulip/model/store.dart'; +import 'package:zulip/widgets/app_bar.dart'; import 'package:zulip/widgets/color.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/icons.dart'; import 'package:zulip/widgets/channel_colors.dart'; +import 'package:zulip/widgets/message_list.dart'; +import 'package:zulip/widgets/page.dart'; import 'package:zulip/widgets/unread_count_badge.dart'; import '../example_data.dart' as eg; import '../flutter_checks.dart'; import '../model/binding.dart'; import '../model/test_store.dart'; +import '../test_navigation.dart'; +import 'checks.dart'; import 'test_app.dart'; /// Repeatedly drags `view` by `moveStep` until `finder` is invisible. @@ -649,5 +655,32 @@ void main() { // reappear because you unmuted a conversation.) }); }); + + testWidgets('tapping search button navigates to search page', (tester) async { + // Set up navigation tracking + final pushedRoutes = >[]; + final testNavObserver = TestNavigatorObserver() + ..onPushed = (route, prevRoute) => pushedRoutes.add(route); + + // Use existing setupPage function with navigation observer + await setupPage(tester, + unreadMessages: [], + navigatorObserver: testNavObserver); + + // Clear the initial route + assert(pushedRoutes.length == 1); + pushedRoutes.clear(); + + // Tap the search button in the app bar + await tester.tap(find.descendant( + of: find.byType(ZulipAppBar), + matching: find.byIcon(ZulipIcons.search))); + await tester.pump(); + + // Verify that we navigated to the search page (MessageListPage with KeywordSearchNarrow) + check(pushedRoutes).single.isA().page + .isA() + .initNarrow.equals(KeywordSearchNarrow('')); + }); }); } diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index acdde3fd2c..9d18ab8982 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -363,6 +363,42 @@ void main() { check(find.text('DMs with Muted user, User 2, Muted user')).findsOne(); }); + + testWidgets('search button on combined feed navigates to search page', (tester) async { + // Set up navigation tracking + final pushedRoutes = >[]; + final testNavObserver = TestNavigatorObserver() + ..onPushed = (route, prevRoute) => pushedRoutes.add(route); + + // Set up the combined feed view with navigation observer + await setupMessageListPage(tester, + narrow: const CombinedFeedNarrow(), + messages: [], + navObservers: [testNavObserver]); + + // Verify that the search button is present in the app bar + final searchButtonFinder = find.descendant( + of: find.byType(ZulipAppBar), + matching: find.byIcon(ZulipIcons.search)); + check(searchButtonFinder).findsOne(); + + // Clear any initial navigation that happened during setup + pushedRoutes.clear(); + + // Mock the API call for the search page + connection.prepare(json: eg.newestGetMessagesResult( + foundOldest: true, messages: []).toJson()); + + // Tap the search button in the app bar + await tester.tap(searchButtonFinder); + await tester.pump(); + + // Verify that we navigated to the search page with an empty search query + check(pushedRoutes).single.isA().page + .isA() + .initNarrow.equals(KeywordSearchNarrow('')); + await tester.pump(Duration.zero); // Allow message list fetch to complete + }); }); group('no-messages placeholder', () {