blob: 19f03af5d54154317ebe2c2574f8f8d9323a0c6f [file] [log] [blame]
// 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));
final TextField textField = tester.widget<TextField>(find.byType(TextField));
expect(hintText.style?.color, delegate.searchFieldStyle?.color);
expect(hintText.style?.fontSize, delegate.searchFieldStyle?.fontSize);
expect(textField.style?.color, delegate.searchFieldStyle?.color);
expect(textField.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.bodyLarge!.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.bodyLarge!.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);
});
testWidgets('Query text field shows toolbar initially', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/95588
final _TestSearchDelegate delegate = _TestSearchDelegate();
final List<String> selectedResults = <String>[];
await tester.pumpWidget(TestHomePage(
delegate: delegate,
results: selectedResults,
));
// Open search.
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
final Finder textFieldFinder = find.byType(TextField);
final TextField textField = tester.widget<TextField>(textFieldFinder);
expect(textField.controller!.text.length, 0);
mockClipboard.handleMethodCall(const MethodCall(
'Clipboard.setData',
<String, dynamic>{
'text': 'pasteablestring',
},
));
// Long press shows toolbar.
await tester.longPress(textFieldFinder);
await tester.pump();
expect(find.text('Paste'), findsOneWidget);
await tester.tap(find.text('Paste'));
await tester.pump();
expect(textField.controller!.text.length, 15);
}, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web.
}
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++;
}
}