// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/semantics_tester.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final MockClipboard mockClipboard = MockClipboard(); setUp(() async { // Fill the clipboard so that the Paste option is available in the text // selection menu. TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); }); tearDown(() { TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); }); testWidgets('Changing query moves cursor to the end of query', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage(delegate: delegate)); await tester.tap(find.byTooltip('Search')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); delegate.query = 'Foo'; final TextField textField = tester.widget<TextField>(find.byType(TextField)); expect( textField.controller!.selection, TextSelection( baseOffset: delegate.query.length, extentOffset: delegate.query.length, ), ); }); testWidgets('Can open and close search', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); final List<String> selectedResults = <String>[]; await tester.pumpWidget(TestHomePage( delegate: delegate, results: selectedResults, )); // We are on the homepage expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); // Open search await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('HomeTitle'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); expect(selectedResults, hasLength(0)); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.focusNode!.hasFocus, isTrue); // Close search await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); expect(selectedResults, <String>['Result']); }); testWidgets('Can close search with system back button to return null', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/18145 final _TestSearchDelegate delegate = _TestSearchDelegate(); final List<String?> selectedResults = <String?>[]; await tester.pumpWidget(TestHomePage( delegate: delegate, results: selectedResults, )); // We are on the homepage expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); // Open search await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('HomeTitle'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Bottom'), findsOneWidget); // Simulate system back button final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(selectedResults, <String?>[null]); // We are on the homepage again expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); // Open search again await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('HomeTitle'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); }); testWidgets('Hint text color overridden', (WidgetTester tester) async { const String searchHintText = 'Enter search terms'; final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHintText); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); final Text hintText = tester.widget(find.text(searchHintText)); expect(hintText.style!.color, _TestSearchDelegate.hintTextColor); }); testWidgets('Requests suggestions', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(delegate.query, ''); expect(delegate.queriesForSuggestions.last, ''); expect(delegate.queriesForResults, hasLength(0)); // Type W o w into search field delegate.queriesForSuggestions.clear(); await tester.enterText(find.byType(TextField), 'W'); await tester.pumpAndSettle(); expect(delegate.query, 'W'); await tester.enterText(find.byType(TextField), 'Wo'); await tester.pumpAndSettle(); expect(delegate.query, 'Wo'); await tester.enterText(find.byType(TextField), 'Wow'); await tester.pumpAndSettle(); expect(delegate.query, 'Wow'); expect(delegate.queriesForSuggestions, <String>['W', 'Wo', 'Wow']); expect(delegate.queriesForResults, hasLength(0)); }); testWidgets('Shows Results and closes search', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); final List<String> selectedResults = <String>[]; await tester.pumpWidget(TestHomePage( delegate: delegate, results: selectedResults, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'Wow'); await tester.pumpAndSettle(); await tester.tap(find.text('Suggestions')); await tester.pumpAndSettle(); // We are on the results page for Wow expect(find.text('HomeBody'), findsNothing); expect(find.text('HomeTitle'), findsNothing); expect(find.text('Suggestions'), findsNothing); expect(find.text('Results'), findsOneWidget); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.focusNode!.hasFocus, isFalse); expect(delegate.queriesForResults, <String>['Wow']); // Close search await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); expect(find.text('Results'), findsNothing); expect(selectedResults, <String>['Result']); }); testWidgets('Can switch between results and suggestions', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); // Showing suggestions expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Results'), findsNothing); // Typing query Wow delegate.queriesForSuggestions.clear(); await tester.enterText(find.byType(TextField), 'Wow'); await tester.pumpAndSettle(); expect(delegate.query, 'Wow'); expect(delegate.queriesForSuggestions, <String>['Wow']); expect(delegate.queriesForResults, hasLength(0)); await tester.tap(find.text('Suggestions')); await tester.pumpAndSettle(); // Showing Results expect(find.text('Suggestions'), findsNothing); expect(find.text('Results'), findsOneWidget); expect(delegate.query, 'Wow'); expect(delegate.queriesForSuggestions, <String>['Wow']); expect(delegate.queriesForResults, <String>['Wow']); TextField textField = tester.widget(find.byType(TextField)); expect(textField.focusNode!.hasFocus, isFalse); // Tapping search field to go back to suggestions await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); textField = tester.widget(find.byType(TextField)); expect(textField.focusNode!.hasFocus, isTrue); expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Results'), findsNothing); expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow']); expect(delegate.queriesForResults, <String>['Wow']); await tester.enterText(find.byType(TextField), 'Foo'); await tester.pumpAndSettle(); expect(delegate.query, 'Foo'); expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']); expect(delegate.queriesForResults, <String>['Wow']); // Go to results again await tester.tap(find.text('Suggestions')); await tester.pumpAndSettle(); expect(find.text('Suggestions'), findsNothing); expect(find.text('Results'), findsOneWidget); expect(delegate.query, 'Foo'); expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']); expect(delegate.queriesForResults, <String>['Wow', 'Foo']); textField = tester.widget(find.byType(TextField)); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('Fresh search always starts with empty query', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(delegate.query, ''); delegate.query = 'Foo'; await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(delegate.query, ''); }); testWidgets('Initial queries are honored', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); expect(delegate.query, ''); await tester.pumpWidget(TestHomePage( delegate: delegate, passInInitialQuery: true, initialQuery: 'Foo', )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(delegate.query, 'Foo'); }); testWidgets('Initial query null re-used previous query', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); delegate.query = 'Foo'; await tester.pumpWidget(TestHomePage( delegate: delegate, passInInitialQuery: true, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(delegate.query, 'Foo'); }); testWidgets('Changing query shows up in search field', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, passInInitialQuery: true, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); delegate.query = 'Foo'; expect(find.text('Foo'), findsOneWidget); expect(find.text('Bar'), findsNothing); delegate.query = 'Bar'; expect(find.text('Foo'), findsNothing); expect(find.text('Bar'), findsOneWidget); }); testWidgets('transitionAnimation runs while search fades in/out', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, passInInitialQuery: true, )); // runs while search fades in expect(delegate.transitionAnimation.status, AnimationStatus.dismissed); await tester.tap(find.byTooltip('Search')); expect(delegate.transitionAnimation.status, AnimationStatus.forward); await tester.pumpAndSettle(); expect(delegate.transitionAnimation.status, AnimationStatus.completed); // does not run while switching to results await tester.tap(find.text('Suggestions')); expect(delegate.transitionAnimation.status, AnimationStatus.completed); await tester.pumpAndSettle(); expect(delegate.transitionAnimation.status, AnimationStatus.completed); // runs while search fades out await tester.tap(find.byTooltip('Back')); expect(delegate.transitionAnimation.status, AnimationStatus.reverse); await tester.pumpAndSettle(); expect(delegate.transitionAnimation.status, AnimationStatus.dismissed); }); testWidgets('Closing nested search returns to search', (WidgetTester tester) async { final List<String?> nestedSearchResults = <String?>[]; final _TestSearchDelegate nestedSearchDelegate = _TestSearchDelegate( suggestions: 'Nested Suggestions', result: 'Nested Result', ); final List<String> selectedResults = <String>[]; final _TestSearchDelegate delegate = _TestSearchDelegate( actions: <Widget>[ Builder( builder: (BuildContext context) { return IconButton( tooltip: 'Nested Search', icon: const Icon(Icons.search), onPressed: () async { final String? result = await showSearch( context: context, delegate: nestedSearchDelegate, ); nestedSearchResults.add(result); }, ); }, ), ], ); await tester.pumpWidget(TestHomePage( delegate: delegate, results: selectedResults, )); expect(find.text('HomeBody'), findsOneWidget); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Nested Suggestions'), findsNothing); await tester.tap(find.byTooltip('Nested Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('Suggestions'), findsNothing); expect(find.text('Nested Suggestions'), findsOneWidget); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); expect(nestedSearchResults, <String>['Nested Result']); expect(find.text('HomeBody'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Nested Suggestions'), findsNothing); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); expect(find.text('Nested Suggestions'), findsNothing); expect(selectedResults, <String>['Result']); }); testWidgets('Closing search with nested search shown goes back to underlying route', (WidgetTester tester) async { late _TestSearchDelegate delegate; final List<String?> nestedSearchResults = <String?>[]; final _TestSearchDelegate nestedSearchDelegate = _TestSearchDelegate( suggestions: 'Nested Suggestions', result: 'Nested Result', actions: <Widget>[ Builder( builder: (BuildContext context) { return IconButton( tooltip: 'Close Search', icon: const Icon(Icons.close), onPressed: () async { delegate.close(context, 'Result Foo'); }, ); }, ), ], ); final List<String> selectedResults = <String>[]; delegate = _TestSearchDelegate( actions: <Widget>[ Builder( builder: (BuildContext context) { return IconButton( tooltip: 'Nested Search', icon: const Icon(Icons.search), onPressed: () async { final String? result = await showSearch( context: context, delegate: nestedSearchDelegate, ); nestedSearchResults.add(result); }, ); }, ), ], ); await tester.pumpWidget(TestHomePage( delegate: delegate, results: selectedResults, )); expect(find.text('HomeBody'), findsOneWidget); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Nested Suggestions'), findsNothing); await tester.tap(find.byTooltip('Nested Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('Suggestions'), findsNothing); expect(find.text('Nested Suggestions'), findsOneWidget); await tester.tap(find.byTooltip('Close Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); expect(find.text('Nested Suggestions'), findsNothing); expect(nestedSearchResults, <String?>[null]); expect(selectedResults, <String>['Result Foo']); }); testWidgets('Custom searchFieldLabel value', (WidgetTester tester) async { const String searchHint = 'custom search hint'; final String defaultSearchHint = const DefaultMaterialLocalizations().searchFieldLabel; final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHint); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text(searchHint), findsOneWidget); expect(find.text(defaultSearchHint), findsNothing); }); testWidgets('Default searchFieldLabel is used when it is set to null', (WidgetTester tester) async { final String searchHint = const DefaultMaterialLocalizations().searchFieldLabel; final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text(searchHint), findsOneWidget); }); testWidgets('Custom searchFieldStyle value', (WidgetTester tester) async { const String searchHintText = 'Enter search terms'; const TextStyle searchFieldStyle = TextStyle(color: Colors.red, fontSize: 3); final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHintText, searchFieldStyle: searchFieldStyle); await tester.pumpWidget(TestHomePage(delegate: delegate)); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); final Text hintText = tester.widget(find.text(searchHintText)); expect(hintText.style?.color, delegate.searchFieldStyle?.color); expect(hintText.style?.fontSize, delegate.searchFieldStyle?.fontSize); }); testWidgets('keyboard show search button by default', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); await tester.showKeyboard(find.byType(TextField)); expect(tester.testTextInput.setClientArgs!['inputAction'], TextInputAction.search.toString()); }); testWidgets('Custom textInputAction results in keyboard with corresponding button', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(textInputAction: TextInputAction.done); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); await tester.showKeyboard(find.byType(TextField)); expect(tester.testTextInput.setClientArgs!['inputAction'], TextInputAction.done.toString()); }); group('contributes semantics', () { TestSemantics buildExpected({ required String routeName }) { return TestSemantics.root( children: <TestSemantics>[ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: <TestSemantics>[ TestSemantics( id: 2, children: <TestSemantics>[ TestSemantics( id: 7, flags: <SemanticsFlag>[ SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute, ], label: routeName, textDirection: TextDirection.ltr, children: <TestSemantics>[ TestSemantics( id: 9, children: <TestSemantics>[ TestSemantics( id: 10, flags: <SemanticsFlag>[ SemanticsFlag.hasEnabledState, SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, ], actions: <SemanticsAction>[SemanticsAction.tap], tooltip: 'Back', textDirection: TextDirection.ltr, ), TestSemantics( id: 11, flags: <SemanticsFlag>[ SemanticsFlag.isTextField, SemanticsFlag.isFocused, SemanticsFlag.isHeader, if (debugDefaultTargetPlatformOverride != TargetPlatform.iOS && debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, ], actions: <SemanticsAction>[ if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS || debugDefaultTargetPlatformOverride == TargetPlatform.windows) SemanticsAction.didGainAccessibilityFocus, SemanticsAction.tap, SemanticsAction.setSelection, SemanticsAction.setText, SemanticsAction.paste, ], label: 'Search', textDirection: TextDirection.ltr, textSelection: const TextSelection(baseOffset: 0, extentOffset: 0), ), TestSemantics( id: 14, label: 'Bottom', textDirection: TextDirection.ltr, ), ], ), TestSemantics( id: 8, flags: <SemanticsFlag>[ SemanticsFlag.hasEnabledState, SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, ], actions: <SemanticsAction>[SemanticsAction.tap], label: 'Suggestions', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ); } testWidgets('includes routeName on Android', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(semantics, hasSemantics( buildExpected(routeName: 'Search'), ignoreId: true, ignoreRect: true, ignoreTransform: true, )); semantics.dispose(); }); testWidgets('does not include routeName', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final _TestSearchDelegate delegate = _TestSearchDelegate(); await tester.pumpWidget(TestHomePage( delegate: delegate, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(semantics, hasSemantics( buildExpected(routeName: ''), ignoreId: true, ignoreRect: true, ignoreTransform: true, )); semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }); testWidgets('Custom searchFieldDecorationTheme value', (WidgetTester tester) async { const InputDecorationTheme searchFieldDecorationTheme = InputDecorationTheme( hintStyle: TextStyle(color: _TestSearchDelegate.hintTextColor), ); final _TestSearchDelegate delegate = _TestSearchDelegate( searchFieldDecorationTheme: searchFieldDecorationTheme, ); await tester.pumpWidget(TestHomePage(delegate: delegate)); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); final ThemeData textFieldTheme = Theme.of(tester.element(find.byType(TextField))); expect(textFieldTheme.inputDecorationTheme, searchFieldDecorationTheme); }); // Regression test for: https://github.com/flutter/flutter/issues/66781 testWidgets('text in search bar contrasts background (light mode)', (WidgetTester tester) async { final ThemeData themeData = ThemeData.light(); final _TestSearchDelegate delegate = _TestSearchDelegate( defaultAppBarTheme: true, ); const String query = 'search query'; await tester.pumpWidget(TestHomePage( delegate: delegate, passInInitialQuery: true, initialQuery: query, themeData: themeData, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); final Material appBarBackground = tester.widget<Material>(find.descendant( of: find.byType(AppBar), matching: find.byType(Material), )); expect(appBarBackground.color, Colors.white); final TextField textField = tester.widget<TextField>(find.byType(TextField)); expect(textField.style!.color, themeData.textTheme.bodyText1!.color); expect(textField.style!.color, isNot(equals(Colors.white))); }); // Regression test for: https://github.com/flutter/flutter/issues/66781 testWidgets('text in search bar contrasts background (dark mode)', (WidgetTester tester) async { final ThemeData themeData = ThemeData.dark(); final _TestSearchDelegate delegate = _TestSearchDelegate( defaultAppBarTheme: true, ); const String query = 'search query'; await tester.pumpWidget(TestHomePage( delegate: delegate, passInInitialQuery: true, initialQuery: query, themeData: themeData, )); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); final Material appBarBackground = tester.widget<Material>(find.descendant( of: find.byType(AppBar), matching: find.byType(Material), )); expect(appBarBackground.color, themeData.primaryColor); final TextField textField = tester.widget<TextField>(find.byType(TextField)); expect(textField.style!.color, themeData.textTheme.bodyText1!.color); expect(textField.style!.color, isNot(equals(themeData.primaryColor))); }); // Regression test for: https://github.com/flutter/flutter/issues/78144 testWidgets('`Leading` and `Actions` nullable test', (WidgetTester tester) async { // The search delegate page is displayed with no issues // even with a null return values for [buildLeading] and [buildActions]. final _TestEmptySearchDelegate delegate = _TestEmptySearchDelegate(); final List<String> selectedResults = <String>[]; await tester.pumpWidget(TestHomePage( delegate: delegate, results: selectedResults, )); // We are on the homepage. expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); // Open the search page. await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsNothing); expect(find.text('HomeTitle'), findsNothing); expect(find.text('Suggestions'), findsOneWidget); expect(selectedResults, hasLength(0)); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.focusNode!.hasFocus, isTrue); // Close the search page. await tester.tap(find.byTooltip('Close')); await tester.pumpAndSettle(); expect(find.text('HomeBody'), findsOneWidget); expect(find.text('HomeTitle'), findsOneWidget); expect(find.text('Suggestions'), findsNothing); expect(selectedResults, <String>['Result']); }); testWidgets('showSearch with useRootNavigator', (WidgetTester tester) async { final _MyNavigatorObserver rootObserver = _MyNavigatorObserver(); final _MyNavigatorObserver localObserver = _MyNavigatorObserver(); final _TestEmptySearchDelegate delegate = _TestEmptySearchDelegate(); await tester.pumpWidget(MaterialApp( navigatorObservers: <NavigatorObserver>[rootObserver], home: Navigator( observers: <NavigatorObserver>[localObserver], onGenerateRoute: (RouteSettings settings) { if (settings.name == 'nested') { return MaterialPageRoute<dynamic>( builder: (BuildContext context) => Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextButton( onPressed: () async { await showSearch(context: context, delegate: delegate, useRootNavigator: true); }, child: const Text('showSearchRootNavigator')), TextButton( onPressed: () async { await showSearch(context: context, delegate: delegate); }, child: const Text('showSearchLocalNavigator')), ], ), settings: settings, ); } throw UnimplementedError(); }, initialRoute: 'nested', ), )); expect(rootObserver.pushCount, 0); expect(localObserver.pushCount, 0); // showSearch normal and back await tester.tap(find.text('showSearchLocalNavigator')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Close')); await tester.pumpAndSettle(); expect(rootObserver.pushCount, 0); expect(localObserver.pushCount, 1); // showSearch with rootNavigator await tester.tap(find.text('showSearchRootNavigator')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Close')); await tester.pumpAndSettle(); expect(rootObserver.pushCount, 1); expect(localObserver.pushCount, 1); }); } class TestHomePage extends StatelessWidget { const TestHomePage({ super.key, this.results, required this.delegate, this.passInInitialQuery = false, this.initialQuery, this.themeData, }); final List<String?>? results; final SearchDelegate<String> delegate; final bool passInInitialQuery; final ThemeData? themeData; final String? initialQuery; @override Widget build(BuildContext context) { return MaterialApp( theme: themeData, home: Builder(builder: (BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('HomeTitle'), actions: <Widget>[ IconButton( tooltip: 'Search', icon: const Icon(Icons.search), onPressed: () async { String? selectedResult; if (passInInitialQuery) { selectedResult = await showSearch<String>( context: context, delegate: delegate, query: initialQuery, ); } else { selectedResult = await showSearch<String>( context: context, delegate: delegate, ); } results?.add(selectedResult); }, ), ], ), body: const Text('HomeBody'), ); }), ); } } class _TestSearchDelegate extends SearchDelegate<String> { _TestSearchDelegate({ this.suggestions = 'Suggestions', this.result = 'Result', this.actions = const <Widget>[], this.defaultAppBarTheme = false, super.searchFieldDecorationTheme, super.searchFieldStyle, String? searchHint, super.textInputAction, }) : super( searchFieldLabel: searchHint, ); final bool defaultAppBarTheme; final String suggestions; final String result; final List<Widget> actions; static const Color hintTextColor = Colors.green; @override ThemeData appBarTheme(BuildContext context) { if (defaultAppBarTheme) { return super.appBarTheme(context); } final ThemeData theme = Theme.of(context); return theme.copyWith( inputDecorationTheme: searchFieldDecorationTheme ?? InputDecorationTheme( hintStyle: searchFieldStyle ?? const TextStyle( color: hintTextColor, ), ), ); } @override Widget buildLeading(BuildContext context) { return IconButton( tooltip: 'Back', icon: const Icon(Icons.arrow_back), onPressed: () { close(context, result); }, ); } final List<String> queriesForSuggestions = <String>[]; final List<String> queriesForResults = <String>[]; @override Widget buildSuggestions(BuildContext context) { queriesForSuggestions.add(query); return MaterialButton( onPressed: () { showResults(context); }, child: Text(suggestions), ); } @override Widget buildResults(BuildContext context) { queriesForResults.add(query); return const Text('Results'); } @override List<Widget> buildActions(BuildContext context) { return actions; } @override PreferredSizeWidget buildBottom(BuildContext context) { return const PreferredSize( preferredSize: Size.fromHeight(56.0), child: Text('Bottom'), ); } } class _TestEmptySearchDelegate extends SearchDelegate<String> { @override Widget? buildLeading(BuildContext context) => null; @override List<Widget>? buildActions(BuildContext context) => null; @override Widget buildSuggestions(BuildContext context) { return MaterialButton( onPressed: () { showResults(context); }, child: const Text('Suggestions'), ); } @override Widget buildResults(BuildContext context) { return const Text('Results'); } @override PreferredSizeWidget buildBottom(BuildContext context) { return PreferredSize( preferredSize: const Size.fromHeight(56.0), child: IconButton( tooltip: 'Close', icon: const Icon(Icons.arrow_back), onPressed: () { close(context, 'Result'); }, ), ); } } class _MyNavigatorObserver extends NavigatorObserver { int pushCount = 0; @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { // don't count the root route if (<String>['nested', '/'].contains(route.settings.name)) { return; } pushCount++; } }