// 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 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Returns the RenderEditable at the given index, or the first if not given. RenderEditable findRenderEditable(WidgetTester tester, {int index = 0}) { final RenderObject root = tester.renderObject(find.byType(EditableText).at(index)); expect(root, isNotNull); late RenderEditable renderEditable; void recursiveFinder(RenderObject child) { if (child is RenderEditable) { renderEditable = child; return; } child.visitChildren(recursiveFinder); } root.visitChildren(recursiveFinder); expect(renderEditable, isNotNull); return renderEditable; } List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) { return points.map<TextSelectionPoint>((TextSelectionPoint point) { return TextSelectionPoint( box.localToGlobal(point.point), point.direction, ); }).toList(); } Offset textOffsetToPosition(WidgetTester tester, int offset, {int index = 0}) { final RenderEditable renderEditable = findRenderEditable(tester, index: index); final List<TextSelectionPoint> endpoints = globalize( renderEditable.getEndpointsForSelection( TextSelection.collapsed(offset: offset), ), renderEditable, ); expect(endpoints.length, 1); return endpoints[0].point + const Offset(kIsWeb? 1.0 : 0.0, -2.0); } testWidgetsWithLeakTracking('SearchBar defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colorScheme = theme.colorScheme; await tester.pumpWidget( MaterialApp( theme: theme, home: const Material( child: SearchBar( hintText: 'hint text', ) ), ), ); final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); final Material material = tester.widget<Material>(searchBarMaterial); checkSearchBarDefaults(tester, colorScheme, material); }); testWidgetsWithLeakTracking('SearchBar respects controller property', (WidgetTester tester) async { const String defaultText = 'default text'; final TextEditingController controller = TextEditingController(text: defaultText); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( controller: controller, ), ), ), ); expect(controller.value.text, defaultText); expect(find.text(defaultText), findsOneWidget); const String updatedText = 'updated text'; await tester.enterText(find.byType(SearchBar), updatedText); expect(controller.value.text, updatedText); expect(find.text(defaultText), findsNothing); expect(find.text(updatedText), findsOneWidget); }); testWidgetsWithLeakTracking('SearchBar respects focusNode property', (WidgetTester tester) async { final FocusNode node = FocusNode(); addTearDown(node.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( focusNode: node, ), ), ), ); expect(node.hasFocus, false); node.requestFocus(); await tester.pump(); expect(node.hasFocus, true); node.unfocus(); await tester.pump(); expect(node.hasFocus, false); }); testWidgetsWithLeakTracking('SearchBar focusNode is hot swappable', (WidgetTester tester) async { final FocusNode node1 = FocusNode(); addTearDown(node1.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( focusNode: node1, ), ), ), ); expect(node1.hasFocus, isFalse); node1.requestFocus(); await tester.pump(); expect(node1.hasFocus, isTrue); node1.unfocus(); await tester.pump(); expect(node1.hasFocus, isFalse); final FocusNode node2 = FocusNode(); addTearDown(node2.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( focusNode: node2, ), ), ), ); expect(node1.hasFocus, isFalse); expect(node2.hasFocus, isFalse); node2.requestFocus(); await tester.pump(); expect(node1.hasFocus, isFalse); expect(node2.hasFocus, isTrue); node2.unfocus(); await tester.pump(); expect(node1.hasFocus, isFalse); expect(node2.hasFocus, isFalse); await tester.pumpWidget( const MaterialApp( home: Material( child: SearchBar(), ), ), ); expect(node1.hasFocus, isFalse); expect(node2.hasFocus, isFalse); await tester.tap(find.byType(SearchBar)); await tester.pump(); expect(node1.hasFocus, isFalse); expect(node2.hasFocus, isFalse); }); testWidgetsWithLeakTracking('SearchBar has correct default layout and padding LTR', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: SearchBar( leading: IconButton( icon: const Icon(Icons.search), onPressed: () {}, ), trailing: <Widget>[ IconButton( icon: const Icon(Icons.menu), onPressed: () {}, ) ], ), ), ), ); final Rect barRect = tester.getRect(find.byType(SearchBar)); expect(barRect.size, const Size(800.0, 56.0)); expect(barRect, equals(const Rect.fromLTRB(0.0, 272.0, 800.0, 328.0))); final Rect leadingIcon = tester.getRect(find.widgetWithIcon(IconButton, Icons.search)); // Default left padding is 8.0, and icon button has 8.0 padding, so in total the padding between // the edge of the bar and the icon of the button is 16.0, which matches the spec. expect(leadingIcon.left, equals(barRect.left + 8.0)); final Rect textField = tester.getRect(find.byType(TextField)); expect(textField.left, equals(leadingIcon.right + 8.0)); final Rect trailingIcon = tester.getRect(find.widgetWithIcon(IconButton, Icons.menu)); expect(trailingIcon.left, equals(textField.right + 8.0)); expect(trailingIcon.right, equals(barRect.right - 8.0)); }); testWidgetsWithLeakTracking('SearchBar has correct default layout and padding - RTL', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.rtl, child: Center( child: SearchBar( leading: IconButton( icon: const Icon(Icons.search), onPressed: () {}, ), trailing: <Widget>[ IconButton( icon: const Icon(Icons.menu), onPressed: () {}, ) ], ), ), ), ), ); final Rect barRect = tester.getRect(find.byType(SearchBar)); expect(barRect.size, const Size(800.0, 56.0)); expect(barRect, equals(const Rect.fromLTRB(0.0, 272.0, 800.0, 328.0))); // The default padding is set to 8.0 so the distance between the icon of the button // and the edge of the bar is 16.0, which matches the spec. final Rect leadingIcon = tester.getRect(find.widgetWithIcon(IconButton, Icons.search)); expect(leadingIcon.right, equals(barRect.right - 8.0)); final Rect textField = tester.getRect(find.byType(TextField)); expect(textField.right, equals(leadingIcon.left - 8.0)); final Rect trailingIcon = tester.getRect(find.widgetWithIcon(IconButton, Icons.menu)); expect(trailingIcon.right, equals(textField.left - 8.0)); expect(trailingIcon.left, equals(barRect.left + 8.0)); }); testWidgetsWithLeakTracking('SearchBar respects hintText property', (WidgetTester tester) async { const String hintText = 'hint text'; await tester.pumpWidget( const MaterialApp( home: Material( child: SearchBar( hintText: hintText, ), ), ), ); expect(find.text(hintText), findsOneWidget); }); testWidgetsWithLeakTracking('SearchBar respects leading property', (WidgetTester tester) async { final ThemeData theme = ThemeData(); final ColorScheme colorScheme = theme.colorScheme; await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( leading: IconButton( icon: const Icon(Icons.search), onPressed: () {}, ), ), ), ), ); expect(find.widgetWithIcon(IconButton, Icons.search), findsOneWidget); final Color? iconColor = _iconStyle(tester, Icons.search)?.color; expect(iconColor, colorScheme.onSurface); // Default icon color. }); testWidgetsWithLeakTracking('SearchBar respects trailing property', (WidgetTester tester) async { final ThemeData theme = ThemeData(); final ColorScheme colorScheme = theme.colorScheme; await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( trailing: <Widget>[ IconButton( icon: const Icon(Icons.menu), onPressed: () {}, ), ], ), ), ), ); expect(find.widgetWithIcon(IconButton, Icons.menu), findsOneWidget); final Color? iconColor = _iconStyle(tester, Icons.menu)?.color; expect(iconColor, colorScheme.onSurfaceVariant); // Default icon color. }); testWidgetsWithLeakTracking('SearchBar respects onTap property', (WidgetTester tester) async { int tapCount = 0; await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: SearchBar( onTap: () { setState(() { tapCount++; }); } ), ); } ), ), ); expect(tapCount, 0); await tester.tap(find.byType(SearchBar)); expect(tapCount, 1); await tester.tap(find.byType(SearchBar)); expect(tapCount, 2); }); testWidgetsWithLeakTracking('SearchBar respects onChanged property', (WidgetTester tester) async { int changeCount = 0; await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: SearchBar( onChanged: (_) { setState(() { changeCount++; }); } ), ); } ), ), ); expect(changeCount, 0); await tester.enterText(find.byType(SearchBar), 'a'); expect(changeCount, 1); await tester.enterText(find.byType(SearchBar), 'b'); expect(changeCount, 2); }); testWidgetsWithLeakTracking('SearchBar respects onSubmitted property', (WidgetTester tester) async { String submittedQuery = ''; await tester.pumpWidget( MaterialApp( home: Material( child: SearchBar( onSubmitted: (String text) { submittedQuery = text; }, ), ), ), ); await tester.enterText(find.byType(SearchBar), 'query'); await tester.testTextInput.receiveAction(TextInputAction.done); expect(submittedQuery, equals('query')); }); testWidgetsWithLeakTracking('SearchBar respects constraints property', (WidgetTester tester) async { const BoxConstraints constraints = BoxConstraints(maxWidth: 350.0, minHeight: 80); await tester.pumpWidget( const MaterialApp( home: Center( child: Material( child: SearchBar( constraints: constraints, ), ), ), ), ); final Rect barRect = tester.getRect(find.byType(SearchBar)); expect(barRect.size, const Size(350.0, 80.0)); }); testWidgetsWithLeakTracking('SearchBar respects elevation property', (WidgetTester tester) async { const double pressedElevation = 0.0; const double hoveredElevation = 1.0; const double focusedElevation = 2.0; const double defaultElevation = 3.0; double getElevation(Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedElevation; } if (states.contains(MaterialState.hovered)) { return hoveredElevation; } if (states.contains(MaterialState.focused)) { return focusedElevation; } return defaultElevation; } await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( elevation: MaterialStateProperty.resolveWith<double>(getElevation), ), ), ), ), ); final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); Material material = tester.widget<Material>(searchBarMaterial); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.elevation, hoveredElevation); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); material = tester.widget<Material>(searchBarMaterial); expect(material.elevation, pressedElevation); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.elevation, focusedElevation); }); testWidgetsWithLeakTracking('SearchBar respects backgroundColor property', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( backgroundColor: MaterialStateProperty.resolveWith<Color>(_getColor), ), ), ), ), ); final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); Material material = tester.widget<Material>(searchBarMaterial); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.color, hoveredColor); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); material = tester.widget<Material>(searchBarMaterial); expect(material.color, pressedColor); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.color, focusedColor); }); testWidgetsWithLeakTracking('SearchBar respects shadowColor property', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( shadowColor: MaterialStateProperty.resolveWith<Color>(_getColor), ), ), ), ), ); final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); Material material = tester.widget<Material>(searchBarMaterial); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.shadowColor, hoveredColor); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); material = tester.widget<Material>(searchBarMaterial); expect(material.shadowColor, pressedColor); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.shadowColor, focusedColor); }); testWidgetsWithLeakTracking('SearchBar respects surfaceTintColor property', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( surfaceTintColor: MaterialStateProperty.resolveWith<Color>(_getColor), ), ), ), ), ); final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); Material material = tester.widget<Material>(searchBarMaterial); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.surfaceTintColor, hoveredColor); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); material = tester.widget<Material>(searchBarMaterial); expect(material.surfaceTintColor, pressedColor); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.surfaceTintColor, focusedColor); }); testWidgetsWithLeakTracking('SearchBar respects overlayColor property', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( focusNode: focusNode, overlayColor: MaterialStateProperty.resolveWith<Color>(_getColor), ), ), ), ), ); RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pumpAndSettle(); expect(inkFeatures, paints..rect(color: hoveredColor.withOpacity(1.0))); // On pressed. await tester.pumpAndSettle(); await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect()..rect(color: pressedColor.withOpacity(1.0))); // On focused. await tester.pumpAndSettle(); await gesture.up(); await tester.pumpAndSettle(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect()..rect(color: focusedColor.withOpacity(1.0))); }); testWidgetsWithLeakTracking('SearchBar respects side and shape properties', (WidgetTester tester) async { const BorderSide pressedSide = BorderSide(width: 2.0); const BorderSide hoveredSide = BorderSide(width: 3.0); const BorderSide focusedSide = BorderSide(width: 4.0); const BorderSide defaultSide = BorderSide(width: 5.0); const OutlinedBorder pressedShape = RoundedRectangleBorder(); const OutlinedBorder hoveredShape = ContinuousRectangleBorder(); const OutlinedBorder focusedShape = CircleBorder(); const OutlinedBorder defaultShape = StadiumBorder(); BorderSide getSide(Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedSide; } if (states.contains(MaterialState.hovered)) { return hoveredSide; } if (states.contains(MaterialState.focused)) { return focusedSide; } return defaultSide; } OutlinedBorder getShape(Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedShape; } if (states.contains(MaterialState.hovered)) { return hoveredShape; } if (states.contains(MaterialState.focused)) { return focusedShape; } return defaultShape; } await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( side: MaterialStateProperty.resolveWith<BorderSide>(getSide), shape: MaterialStateProperty.resolveWith<OutlinedBorder>(getShape), ), ), ), ), ); final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); Material material = tester.widget<Material>(searchBarMaterial); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.shape, hoveredShape.copyWith(side: hoveredSide)); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); material = tester.widget<Material>(searchBarMaterial); expect(material.shape, pressedShape.copyWith(side: pressedSide)); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); material = tester.widget<Material>(searchBarMaterial); expect(material.shape, focusedShape.copyWith(side: focusedSide)); }); testWidgetsWithLeakTracking('SearchBar respects padding property', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center( child: Material( child: SearchBar( leading: Icon(Icons.search), padding: MaterialStatePropertyAll<EdgeInsets>(EdgeInsets.all(16.0)), trailing: <Widget>[ Icon(Icons.menu), ] ), ), ), ), ); final Rect barRect = tester.getRect(find.byType(SearchBar)); final Rect leadingRect = tester.getRect(find.byIcon(Icons.search)); final Rect textFieldRect = tester.getRect(find.byType(TextField)); final Rect trailingRect = tester.getRect(find.byIcon(Icons.menu)); expect(barRect.left, leadingRect.left - 16.0); expect(leadingRect.right, textFieldRect.left - 16.0); expect(textFieldRect.right, trailingRect.left - 16.0); expect(trailingRect.right, barRect.right - 16.0); }); testWidgetsWithLeakTracking('SearchBar respects hintStyle property', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( hintText: 'hint text', hintStyle: MaterialStateProperty.resolveWith<TextStyle?>(_getTextStyle), ), ), ), ), ); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); Text helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, hoveredColor); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, pressedColor); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, focusedColor); }); testWidgetsWithLeakTracking('SearchBar respects textStyle property', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'input text'); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( controller: controller, textStyle: MaterialStateProperty.resolveWith<TextStyle?>(_getTextStyle), ), ), ), ), ); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); EditableText inputText = tester.widget(find.text('input text')); expect(inputText.style.color, hoveredColor); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); inputText = tester.widget(find.text('input text')); expect(inputText.style.color, pressedColor); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); inputText = tester.widget(find.text('input text')); expect(inputText.style.color, focusedColor); }); testWidgetsWithLeakTracking('SearchBar respects textCapitalization property', (WidgetTester tester) async { Widget buildSearchBar(TextCapitalization textCapitalization) { return MaterialApp( home: Center( child: Material( child: SearchBar( textCapitalization: textCapitalization, ), ), ), ); } await tester.pumpWidget(buildSearchBar(TextCapitalization.characters)); await tester.pump(); TextField textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.characters); await tester.pumpWidget(buildSearchBar(TextCapitalization.sentences)); await tester.pump(); textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.sentences); await tester.pumpWidget(buildSearchBar(TextCapitalization.words)); await tester.pump(); textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.words); await tester.pumpWidget(buildSearchBar(TextCapitalization.none)); await tester.pump(); textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.none); }); testWidgetsWithLeakTracking('SearchAnchor respects textCapitalization property', (WidgetTester tester) async { Widget buildSearchAnchor(TextCapitalization textCapitalization) { return MaterialApp( home: Center( child: Material( child: SearchAnchor( textCapitalization: textCapitalization, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.ac_unit), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildSearchAnchor(TextCapitalization.characters)); await tester.pump(); await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); TextField textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.characters); await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); await tester.pump(); await tester.pumpWidget(buildSearchAnchor(TextCapitalization.none)); await tester.pump(); await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.none); }); testWidgetsWithLeakTracking('SearchAnchor respects viewOnChanged and viewOnSubmitted properties', (WidgetTester tester) async { final SearchController controller = SearchController(); addTearDown(controller.dispose); int onChangedCalled = 0; int onSubmittedCalled = 0; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Center( child: Material( child: SearchAnchor( searchController: controller, viewOnChanged: (String value) { setState(() { onChangedCalled = onChangedCalled + 1; }); }, viewOnSubmitted: (String value) { setState(() { onSubmittedCalled = onSubmittedCalled + 1; }); controller.closeView(value); }, builder: (BuildContext context, SearchController controller) { return SearchBar( onTap: () { if (!controller.isOpen) { controller.openView(); } }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ); } ), )); await tester.tap(find.byType(SearchBar)); // Open search view. await tester.pumpAndSettle(); expect(controller.isOpen, true); final Finder barOnView = find.descendant( of: findViewContent(), matching: find.byType(TextField) ); await tester.enterText(barOnView, 'a'); expect(onChangedCalled, 1); await tester.enterText(barOnView, 'abc'); expect(onChangedCalled, 2); await tester.testTextInput.receiveAction(TextInputAction.done); expect(onSubmittedCalled, 1); expect(controller.isOpen, false); }); testWidgetsWithLeakTracking('SearchAnchor.bar respects textCapitalization property', (WidgetTester tester) async { Widget buildSearchAnchor(TextCapitalization textCapitalization) { return MaterialApp( home: Center( child: Material( child: SearchAnchor.bar( textCapitalization: textCapitalization, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildSearchAnchor(TextCapitalization.characters)); await tester.pump(); await tester.tap(find.byType(SearchBar)); // Open search view. await tester.pumpAndSettle(); final Finder textFieldFinder = find.descendant(of: findViewContent(), matching: find.byType(TextField)); final TextField textFieldInView = tester.widget<TextField>(textFieldFinder); expect(textFieldInView.textCapitalization, TextCapitalization.characters); // Close search view. await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); await tester.pumpAndSettle(); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.textCapitalization, TextCapitalization.characters); }); testWidgetsWithLeakTracking('SearchAnchor.bar respects onChanged and onSubmitted properties', (WidgetTester tester) async { final SearchController controller = SearchController(); addTearDown(controller.dispose); int onChangedCalled = 0; int onSubmittedCalled = 0; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Center( child: Material( child: SearchAnchor.bar( searchController: controller, onSubmitted: (String value) { setState(() { onSubmittedCalled = onSubmittedCalled + 1; }); controller.closeView(value); }, onChanged: (String value) { setState(() { onChangedCalled = onChangedCalled + 1; }); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ); } ), )); await tester.tap(find.byType(SearchBar)); // Open search view. await tester.pumpAndSettle(); expect(controller.isOpen, true); final Finder barOnView = find.descendant( of: findViewContent(), matching: find.byType(TextField) ); await tester.enterText(barOnView, 'a'); expect(onChangedCalled, 1); await tester.enterText(barOnView, 'abc'); expect(onChangedCalled, 2); await tester.testTextInput.receiveAction(TextInputAction.done); expect(onSubmittedCalled, 1); expect(controller.isOpen, false); await tester.testTextInput.receiveAction(TextInputAction.done); expect(onSubmittedCalled, 2); }); testWidgetsWithLeakTracking('hintStyle can override textStyle for hintText', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: Material( child: SearchBar( hintText: 'hint text', hintStyle: MaterialStateProperty.resolveWith<TextStyle?>(_getTextStyle), textStyle: const MaterialStatePropertyAll<TextStyle>(TextStyle(color: Colors.pink)), ), ), ), ), ); // On hovered. final TestGesture gesture = await _pointGestureToSearchBar(tester); await tester.pump(); Text helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, hoveredColor); // On pressed. await gesture.down(tester.getCenter(find.byType(SearchBar))); await tester.pumpAndSettle(); helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, pressedColor); // On focused. await gesture.up(); await tester.pump(); // Remove the pointer so we are no longer hovering. await gesture.removePointer(); await tester.pump(); helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, focusedColor); }); // Regression test for https://github.com/flutter/flutter/issues/127092. testWidgetsWithLeakTracking('The text is still centered when SearchBar text field is smaller than 48', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), home: const Center( child: Material( child: SearchBar( constraints: BoxConstraints.tightFor(height: 35.0), ), ), ), ), ); await tester.enterText(find.byType(TextField), 'input text'); final Finder textContent = find.text('input text'); final double textCenterY = tester.getCenter(textContent).dy; final Finder searchBar = find.byType(SearchBar); final double searchBarCenterY = tester.getCenter(searchBar).dy; expect(textCenterY, searchBarCenterY); }); testWidgetsWithLeakTracking('The search view defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colorScheme = theme.colorScheme; await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Material( child: Align( alignment: Alignment.topLeft, child: SearchAnchor( viewHintText: 'hint text', builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ), ); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Material material = getSearchViewMaterial(tester); expect(material.elevation, 6.0); expect(material.color, colorScheme.surface); expect(material.surfaceTintColor, colorScheme.surfaceTint); expect(material.clipBehavior, Clip.antiAlias); final Finder findDivider = find.byType(Divider); final Container dividerContainer = tester.widget<Container>(find.descendant(of: findDivider, matching: find.byType(Container)).first); final BoxDecoration decoration = dividerContainer.decoration! as BoxDecoration; expect(decoration.border!.bottom.color, colorScheme.outline); // Default search view has a leading back button on the start of the header. expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget); // Default search view has a trailing close button on the end of the header. // It is used to clear the input in the text field. expect(find.widgetWithIcon(IconButton, Icons.close), findsOneWidget); final Text helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, colorScheme.onSurfaceVariant); expect(helperText.style?.fontSize, 16.0); expect(helperText.style?.fontFamily, 'Roboto'); expect(helperText.style?.fontWeight, FontWeight.w400); const String input = 'entered text'; await tester.enterText(find.byType(SearchBar), input); final EditableText inputText = tester.widget(find.text(input)); expect(inputText.style.color, colorScheme.onSurface); expect(inputText.style.fontSize, 16.0); expect(inputText.style.fontFamily, 'Roboto'); expect(inputText.style.fontWeight, FontWeight.w400); }); testWidgetsWithLeakTracking('The search view default size on different platforms', (WidgetTester tester) async { // The search view should be is full-screen on mobile platforms, // and have a size of (360, 2/3 screen height) on other platforms Widget buildSearchAnchor(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), home: Scaffold( body: SafeArea( child: Material( child: Align( alignment: Alignment.topLeft, child: SearchAnchor( builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ), ); } for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.android, TargetPlatform.fuchsia ]) { await tester.pumpWidget(Container()); await tester.pumpWidget(buildSearchAnchor(platform)); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(sizedBox.width, 800.0); expect(sizedBox.height, 600.0); } for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.linux, TargetPlatform.windows ]) { await tester.pumpWidget(Container()); await tester.pumpWidget(buildSearchAnchor(platform)); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(sizedBox.width, 360.0); expect(sizedBox.height, 400.0); } }); testWidgetsWithLeakTracking('SearchAnchor respects isFullScreen property', (WidgetTester tester) async { Widget buildSearchAnchor(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), home: Scaffold( body: SafeArea( child: Material( child: Align( alignment: Alignment.topLeft, child: SearchAnchor( isFullScreen: true, builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ), ); } for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.linux, TargetPlatform.windows ]) { await tester.pumpWidget(Container()); await tester.pumpWidget(buildSearchAnchor(platform)); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(sizedBox.width, 800.0); expect(sizedBox.height, 600.0); } }); testWidgetsWithLeakTracking('SearchAnchor respects controller property', (WidgetTester tester) async { const String defaultText = 'initial text'; final SearchController controller = SearchController(); addTearDown(controller.dispose); controller.text = defaultText; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( searchController: controller, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(controller.value.text, defaultText); expect(find.text(defaultText), findsOneWidget); const String updatedText = 'updated text'; await tester.enterText(find.byType(SearchBar), updatedText); expect(controller.value.text, updatedText); expect(find.text(defaultText), findsNothing); expect(find.text(updatedText), findsOneWidget); }); testWidgetsWithLeakTracking('SearchAnchor attaches and detaches controllers property', (WidgetTester tester) async { Widget builder(BuildContext context, SearchController controller) { return const Icon(Icons.search); } List<Widget> suggestionsBuilder(BuildContext context, SearchController controller) { return const <Widget>[]; } final SearchController controller1 = SearchController(); addTearDown(controller1.dispose); expect(controller1.isAttached, isFalse); await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( searchController: controller1, builder: builder, suggestionsBuilder: suggestionsBuilder, ), ), ), ); expect(controller1.isAttached, isTrue); await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( builder: builder, suggestionsBuilder: suggestionsBuilder, ), ), ), ); expect(controller1.isAttached, isFalse); final SearchController controller2 = SearchController(); addTearDown(controller2.dispose); expect(controller2.isAttached, isFalse); await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( searchController: controller2, builder: builder, suggestionsBuilder: suggestionsBuilder, ), ), ), ); expect(controller1.isAttached, isFalse); expect(controller2.isAttached, isTrue); await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( builder: builder, suggestionsBuilder: suggestionsBuilder, ), ), ), ); expect(controller1.isAttached, isFalse); expect(controller2.isAttached, isFalse); }); testWidgetsWithLeakTracking('SearchAnchor respects viewBuilder property', (WidgetTester tester) async { Widget buildAnchor({ViewBuilder? viewBuilder}) { return MaterialApp( home: Material( child: SearchAnchor( viewBuilder: viewBuilder, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ); } await tester.pumpWidget(buildAnchor()); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); // Default is a ListView. expect(find.byType(ListView), findsOneWidget); await tester.pumpWidget(Container()); await tester.pumpWidget(buildAnchor(viewBuilder: (Iterable<Widget> suggestions) => GridView.count(crossAxisCount: 5, children: suggestions.toList(),) )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(find.byType(ListView), findsNothing); expect(find.byType(GridView), findsOneWidget); }); testWidgetsWithLeakTracking('SearchAnchor respects viewLeading property', (WidgetTester tester) async { Widget buildAnchor({Widget? viewLeading}) { return MaterialApp( home: Material( child: SearchAnchor( viewLeading: viewLeading, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ); } await tester.pumpWidget(buildAnchor()); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); // Default is a icon button with arrow_back. expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget); await tester.pumpWidget(Container()); await tester.pumpWidget(buildAnchor(viewLeading: const Icon(Icons.history))); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.arrow_back), findsNothing); expect(find.byIcon(Icons.history), findsOneWidget); }); testWidgetsWithLeakTracking('SearchAnchor respects viewTrailing property', (WidgetTester tester) async { Widget buildAnchor({Iterable<Widget>? viewTrailing}) { return MaterialApp( home: Material( child: SearchAnchor( viewTrailing: viewTrailing, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ); } await tester.pumpWidget(buildAnchor()); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); // Default is a icon button with close icon. expect(find.widgetWithIcon(IconButton, Icons.close), findsOneWidget); await tester.pumpWidget(Container()); await tester.pumpWidget(buildAnchor(viewTrailing: <Widget>[const Icon(Icons.history)])); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.close), findsNothing); expect(find.byIcon(Icons.history), findsOneWidget); }); testWidgetsWithLeakTracking('SearchAnchor respects viewHintText property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( viewHintText: 'hint text', builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(find.text('hint text'), findsOneWidget); }); testWidgetsWithLeakTracking('SearchAnchor respects viewBackgroundColor property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( viewBackgroundColor: Colors.purple, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(getSearchViewMaterial(tester).color, Colors.purple); }); testWidgetsWithLeakTracking('SearchAnchor respects viewElevation property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( viewElevation: 3.0, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(getSearchViewMaterial(tester).elevation, 3.0); }); testWidgetsWithLeakTracking('SearchAnchor respects viewSurfaceTint property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( viewSurfaceTintColor: Colors.purple, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(getSearchViewMaterial(tester).surfaceTintColor, Colors.purple); }); testWidgetsWithLeakTracking('SearchAnchor respects viewSide property', (WidgetTester tester) async { const BorderSide side = BorderSide(color: Colors.purple, width: 5.0); await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( isFullScreen: false, viewSide: side, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(getSearchViewMaterial(tester).shape, RoundedRectangleBorder(side: side, borderRadius: BorderRadius.circular(28.0))); }); testWidgetsWithLeakTracking('SearchAnchor respects viewShape property', (WidgetTester tester) async { const BorderSide side = BorderSide(color: Colors.purple, width: 5.0); const OutlinedBorder shape = StadiumBorder(side: side); await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( isFullScreen: false, viewShape: shape, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); expect(getSearchViewMaterial(tester).shape, shape); }); testWidgetsWithLeakTracking('SearchAnchor respects headerTextStyle property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( headerTextStyle: theme.textTheme.bodyLarge?.copyWith(color: Colors.red), builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); await tester.enterText(find.byType(SearchBar), 'input text'); await tester.pumpAndSettle(); final EditableText inputText = tester.widget(find.text('input text')); expect(inputText.style.color, Colors.red); }); testWidgetsWithLeakTracking('SearchAnchor respects headerHintStyle property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( viewHintText: 'hint text', headerHintStyle: theme.textTheme.bodyLarge?.copyWith(color: Colors.orange), builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); final Text inputText = tester.widget(find.text('hint text')); expect(inputText.style?.color, Colors.orange); }); testWidgetsWithLeakTracking('SearchAnchor respects dividerColor property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: SearchAnchor( dividerColor: Colors.red, builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); final Finder findDivider = find.byType(Divider); final Container dividerContainer = tester.widget<Container>(find.descendant(of: findDivider, matching: find.byType(Container)).first); final BoxDecoration decoration = dividerContainer.decoration! as BoxDecoration; expect(decoration.border!.bottom.color, Colors.red); }); testWidgetsWithLeakTracking('SearchAnchor respects viewConstraints property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Center( child: SearchAnchor( isFullScreen: false, viewConstraints: BoxConstraints.tight(const Size(280.0, 390.0)), builder: (BuildContext context, SearchController controller) { return IconButton(icon: const Icon(Icons.search), onPressed: () { controller.openView(); },); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), )); await tester.tap(find.widgetWithIcon(IconButton, Icons.search)); await tester.pumpAndSettle(); final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(sizedBox.width, 280.0); expect(sizedBox.height, 390.0); }); testWidgetsWithLeakTracking('SearchAnchor respects builder property - LTR', (WidgetTester tester) async { Widget buildAnchor({required SearchAnchorChildBuilder builder}) { return MaterialApp( home: Material( child: Align( alignment: Alignment.topCenter, child: SearchAnchor( isFullScreen: false, builder: builder, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildAnchor( builder: (BuildContext context, SearchController controller) => const Icon(Icons.search) )); final Rect anchorRect = tester.getRect(find.byIcon(Icons.search)); expect(anchorRect.size, const Size(24.0, 24.0)); expect(anchorRect, equals(const Rect.fromLTRB(388.0, 0.0, 412.0, 24.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect, equals(const Rect.fromLTRB(388.0, 0.0, 748.0, 400.0))); // Search view top left should be the same as the anchor top left expect(searchViewRect.topLeft, anchorRect.topLeft); }); testWidgetsWithLeakTracking('SearchAnchor respects builder property - RTL', (WidgetTester tester) async { Widget buildAnchor({required SearchAnchorChildBuilder builder}) { return MaterialApp( home: Directionality( textDirection: TextDirection.rtl, child: Material( child: Align( alignment: Alignment.topCenter, child: SearchAnchor( isFullScreen: false, builder: builder, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ); } await tester.pumpWidget(buildAnchor(builder: (BuildContext context, SearchController controller) => const Icon(Icons.search))); final Rect anchorRect = tester.getRect(find.byIcon(Icons.search)); expect(anchorRect.size, const Size(24.0, 24.0)); expect(anchorRect, equals(const Rect.fromLTRB(388.0, 0.0, 412.0, 24.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect, equals(const Rect.fromLTRB(52.0, 0.0, 412.0, 400.0))); // Search view top right should be the same as the anchor top right expect(searchViewRect.topRight, anchorRect.topRight); }); testWidgetsWithLeakTracking('SearchAnchor respects suggestionsBuilder property', (WidgetTester tester) async { final SearchController controller = SearchController(); addTearDown(controller.dispose); const String suggestion = 'suggestion text'; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Align( alignment: Alignment.topCenter, child: SearchAnchor( searchController: controller, builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[ ListTile( title: const Text(suggestion), onTap: () { setState(() { controller.closeView(suggestion); }); }), ]; }, ), ), ); } ), )); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Finder listTile = find.widgetWithText(ListTile, suggestion); expect(listTile, findsOneWidget); await tester.tap(listTile); await tester.pumpAndSettle(); expect(controller.isOpen, false); expect(controller.value.text, suggestion); }); testWidgetsWithLeakTracking('SearchAnchor should update suggestions on changes to search controller', (WidgetTester tester) async { final SearchController controller = SearchController(); const List<String> suggestions = <String>['foo','far','bim']; addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Align( alignment: Alignment.topCenter, child: SearchAnchor( searchController: controller, builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) { final String searchText = controller.text.toLowerCase(); if (searchText.isEmpty) { return const <Widget>[ Center( child: Text('No Search'), ), ]; } final Iterable<String> filterSuggestions = suggestions.where( (String suggestion) => suggestion.toLowerCase().contains(searchText), ); return filterSuggestions.map((String suggestion) { return ListTile( title: Text(suggestion), trailing: IconButton( icon: const Icon(Icons.call_missed), onPressed: () { controller.text = suggestion; }, ), onTap: () { controller.closeView(suggestion); }, ); }).toList(); }, ), ), ); } ), )); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Finder listTile1 = find.widgetWithText(ListTile, 'foo'); final Finder listTile2 = find.widgetWithText(ListTile, 'far'); final Finder listTile3 = find.widgetWithText(ListTile, 'bim'); final Finder textWidget = find.widgetWithText(Center, 'No Search'); final Finder iconInListTile1 = find.descendant(of: listTile1, matching: find.byIcon(Icons.call_missed)); expect(textWidget,findsOneWidget); expect(listTile1, findsNothing); expect(listTile2, findsNothing); expect(listTile3, findsNothing); await tester.enterText(find.byType(SearchBar), 'f'); await tester.pumpAndSettle(); expect(textWidget,findsNothing); expect(listTile1, findsOneWidget); expect(listTile2, findsOneWidget); expect(listTile3, findsNothing); await tester.tap(iconInListTile1); await tester.pumpAndSettle(); expect(controller.value.text, 'foo'); expect(textWidget,findsNothing); expect(listTile1, findsOneWidget); expect(listTile2, findsNothing); expect(listTile3, findsNothing); await tester.tap(listTile1); await tester.pumpAndSettle(); expect(controller.isOpen, false); expect(controller.value.text, 'foo'); expect(textWidget,findsNothing); expect(listTile1, findsNothing); expect(listTile2, findsNothing); expect(listTile3, findsNothing); }); testWidgetsWithLeakTracking('SearchAnchor suggestionsBuilder property could be async', (WidgetTester tester) async { final SearchController controller = SearchController(); addTearDown(controller.dispose); const String suggestion = 'suggestion text'; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Align( alignment: Alignment.topCenter, child: SearchAnchor( searchController: controller, builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) async { return <Widget>[ ListTile( title: const Text(suggestion), onTap: () { setState(() { controller.closeView(suggestion); }); }, ), ]; }, ), ), ); }, ), )); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Finder text = find.text(suggestion); expect(text, findsOneWidget); await tester.tap(text); await tester.pumpAndSettle(); expect(controller.isOpen, false); expect(controller.value.text, suggestion); }); testWidgetsWithLeakTracking('SearchAnchor.bar has a default search bar as the anchor', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Align( alignment: Alignment.topLeft, child: SearchAnchor.bar( isFullScreen: false, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ),), ); expect(find.byType(SearchBar), findsOneWidget); final Rect anchorRect = tester.getRect(find.byType(SearchBar)); expect(anchorRect.size, const Size(800.0, 56.0)); expect(anchorRect, equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 56.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 400.0))); // Search view has same width with the default anchor(search bar). expect(searchViewRect.width, anchorRect.width); }); testWidgetsWithLeakTracking('SearchController can open/close view', (WidgetTester tester) async { final SearchController controller = SearchController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor.bar( searchController: controller, isFullScreen: false, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[ ListTile( title: const Text('item 0'), onTap: () { controller.closeView('item 0'); }, ) ]; }, ), ), ), ); expect(controller.isOpen, false); await tester.tap(find.byType(SearchBar)); await tester.pumpAndSettle(); expect(controller.isOpen, true); await tester.tap(find.widgetWithText(ListTile, 'item 0')); await tester.pumpAndSettle(); expect(controller.isOpen, false); controller.openView(); expect(controller.isOpen, true); }); testWidgetsWithLeakTracking('Search view does not go off the screen - LTR', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: Align( // Put the search anchor on the bottom-right corner of the screen to test // if the search view goes off the window. alignment: Alignment.bottomRight, child: SearchAnchor( isFullScreen: false, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ); final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search); final Rect iconButton = tester.getRect(findIconButton); // Icon button has a size of (48.0, 48.0) and the screen size is (800.0, 600.0). expect(iconButton, equals(const Rect.fromLTRB(752.0, 552.0, 800.0, 600.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect, equals(const Rect.fromLTRB(440.0, 200.0, 800.0, 600.0))); }); testWidgetsWithLeakTracking('Search view does not go off the screen - RTL', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.rtl, child: Material( child: Align( // Put the search anchor on the bottom-left corner of the screen to test // if the search view goes off the window when the text direction is right-to-left. alignment: Alignment.bottomLeft, child: SearchAnchor( isFullScreen: false, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ), ); final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search); final Rect iconButton = tester.getRect(findIconButton); expect(iconButton, equals(const Rect.fromLTRB(0.0, 552.0, 48.0, 600.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 200.0, 360.0, 600.0))); }); testWidgetsWithLeakTracking('Search view becomes smaller if the window size is smaller than the view size', (WidgetTester tester) async { addTearDown(tester.view.reset); tester.view.physicalSize = const Size(200.0, 200.0); tester.view.devicePixelRatio = 1.0; Widget buildSearchAnchor({TextDirection textDirection = TextDirection.ltr}) { return MaterialApp( home: Directionality( textDirection: textDirection, child: Material( child: SearchAnchor( isFullScreen: false, builder: (BuildContext context, SearchController controller) { return Align( alignment: Alignment.bottomRight, child: IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ), ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } // Test LTR text direction. await tester.pumpWidget(buildSearchAnchor()); final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search); final Rect iconButton = tester.getRect(findIconButton); // The icon button size is (48.0, 48.0), and the screen size is (200.0, 200.0) expect(iconButton, equals(const Rect.fromLTRB(152.0, 152.0, 200.0, 200.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0))); // Test RTL text direction. await tester.pumpWidget(Container()); await tester.pumpWidget(buildSearchAnchor(textDirection: TextDirection.rtl)); final Finder findIconButtonRTL = find.widgetWithIcon(IconButton, Icons.search); final Rect iconButtonRTL = tester.getRect(findIconButtonRTL); // The icon button size is (48.0, 48.0), and the screen size is (200.0, 200.0) expect(iconButtonRTL, equals(const Rect.fromLTRB(152.0, 152.0, 200.0, 200.0))); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRectRTL = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRectRTL, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0))); }); testWidgetsWithLeakTracking('Docked search view route is popped if the window size changes', (WidgetTester tester) async { addTearDown(tester.view.reset); tester.view.physicalSize = const Size(500.0, 600.0); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( isFullScreen: false, builder: (BuildContext context, SearchController controller) { return Align( alignment: Alignment.bottomRight, child: IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ), ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); // Open the search view await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.arrow_back), findsOneWidget); // Change window size tester.view.physicalSize = const Size(250.0, 200.0); tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byIcon(Icons.arrow_back), findsNothing); }); testWidgetsWithLeakTracking('Full-screen search view route should stay if the window size changes', (WidgetTester tester) async { addTearDown(tester.view.reset); tester.view.physicalSize = const Size(500.0, 600.0); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor( isFullScreen: true, builder: (BuildContext context, SearchController controller) { return Align( alignment: Alignment.bottomRight, child: IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ), ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); // Open a full-screen search view await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.arrow_back), findsOneWidget); // Change window size tester.view.physicalSize = const Size(250.0, 200.0); tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byIcon(Icons.arrow_back), findsOneWidget); }); testWidgetsWithLeakTracking('Search view route does not throw exception during pop animation', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/126590. await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: SearchAnchor( builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return List<Widget>.generate(5, (int index) { final String item = 'item $index'; return ListTile( leading: const Icon(Icons.history), title: Text(item), trailing: const Icon(Icons.chevron_right), onTap: () {}, ); }); }), ), ), ), ); // Open search view await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); // Pop search view route await tester.tap(find.byIcon(Icons.arrow_back)); await tester.pumpAndSettle(); // No exception. }); testWidgetsWithLeakTracking('Docked search should position itself correctly based on closest navigator', (WidgetTester tester) async { const double rootSpacing = 100.0; await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return Scaffold( body: Padding( padding: const EdgeInsets.all(rootSpacing), child: child, ), ); }, home: Material( child: SearchAnchor( isFullScreen: false, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect.topLeft, equals(const Offset(rootSpacing, rootSpacing))); }); testWidgetsWithLeakTracking('Docked search view with nested navigator does not go off the screen', (WidgetTester tester) async { addTearDown(tester.view.reset); tester.view.physicalSize = const Size(400.0, 400.0); tester.view.devicePixelRatio = 1.0; const double rootSpacing = 100.0; await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return Scaffold( body: Padding( padding: const EdgeInsets.all(rootSpacing), child: child, ), ); }, home: Material( child: Align( alignment: Alignment.bottomRight, child: SearchAnchor( isFullScreen: false, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.search), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first); expect(searchViewRect.bottomRight, equals(const Offset(300.0, 300.0))); }); // Regression tests for https://github.com/flutter/flutter/issues/128332 group('SearchAnchor text selection', () { testWidgetsWithLeakTracking('can right-click to select word', (WidgetTester tester) async { const String defaultText = 'initial text'; final SearchController controller = SearchController(); addTearDown(controller.dispose); controller.text = defaultText; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor.bar( searchController: controller, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); expect(controller.value.text, defaultText); expect(find.text(defaultText), findsOneWidget); final TestGesture gesture = await tester.startGesture( textOffsetToPosition(tester, 4) + const Offset(0.0, -9.0), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton, ); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(controller.value.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); await gesture.removePointer(); }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); testWidgetsWithLeakTracking('can click to set position', (WidgetTester tester) async { const String defaultText = 'initial text'; final SearchController controller = SearchController(); addTearDown(controller.dispose); controller.text = defaultText; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor.bar( searchController: controller, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); expect(controller.value.text, defaultText); expect(find.text(defaultText), findsOneWidget); final TestGesture gesture = await _pointGestureToSearchBar(tester); await gesture.down(textOffsetToPosition(tester, 2) + const Offset(0.0, -9.0)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(kDoubleTapTimeout); expect(controller.value.selection, const TextSelection.collapsed(offset: 2)); await gesture.down(textOffsetToPosition(tester, 9, index: 1) + const Offset(0.0, -9.0)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(controller.value.selection, const TextSelection.collapsed(offset: 9)); await gesture.removePointer(); }, variant: TargetPlatformVariant.desktop()); testWidgetsWithLeakTracking('can double-click to select word', (WidgetTester tester) async { const String defaultText = 'initial text'; final SearchController controller = SearchController(); addTearDown(controller.dispose); controller.text = defaultText; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor.bar( searchController: controller, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); expect(controller.value.text, defaultText); expect(find.text(defaultText), findsOneWidget); final TestGesture gesture = await _pointGestureToSearchBar(tester); final Offset targetPosition = textOffsetToPosition(tester, 4) + const Offset(0.0, -9.0); await gesture.down(targetPosition); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(kDoubleTapTimeout); final Offset targetPositionAfterViewOpened = textOffsetToPosition(tester, 4, index: 1) + const Offset(0.0, -9.0); await gesture.down(targetPositionAfterViewOpened); await tester.pumpAndSettle(); await gesture.up(); await tester.pump(); await gesture.down(targetPositionAfterViewOpened); await tester.pump(); await gesture.up(); await tester.pump(); expect(controller.value.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); await gesture.removePointer(); }, variant: TargetPlatformVariant.desktop()); testWidgetsWithLeakTracking('can triple-click to select field', (WidgetTester tester) async { const String defaultText = 'initial text'; final SearchController controller = SearchController(); addTearDown(controller.dispose); controller.text = defaultText; await tester.pumpWidget( MaterialApp( home: Material( child: SearchAnchor.bar( searchController: controller, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); expect(controller.value.text, defaultText); expect(find.text(defaultText), findsOneWidget); final TestGesture gesture = await _pointGestureToSearchBar(tester); final Offset targetPosition = textOffsetToPosition(tester, 4) + const Offset(0.0, -9.0); await gesture.down(targetPosition); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(kDoubleTapTimeout); final Offset targetPositionAfterViewOpened = textOffsetToPosition(tester, 4, index: 1) + const Offset(0.0, -9.0); await gesture.down(targetPositionAfterViewOpened); await tester.pump(); await gesture.up(); await tester.pump(); await gesture.down(targetPositionAfterViewOpened); await tester.pump(); await gesture.up(); await tester.pump(); await gesture.down(targetPositionAfterViewOpened); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(controller.value.selection, const TextSelection(baseOffset: 0, extentOffset: 12)); await gesture.removePointer(); }, variant: TargetPlatformVariant.desktop()); }); // Regression tests for https://github.com/flutter/flutter/issues/126623 group('Overall InputDecorationTheme does not impact SearchBar and SearchView', () { const InputDecorationTheme inputDecorationTheme = InputDecorationTheme( focusColor: Colors.green, hoverColor: Colors.blue, outlineBorder: BorderSide(color: Colors.pink, width: 10), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 20), hintStyle: TextStyle(color: Colors.purpleAccent), fillColor: Colors.tealAccent, filled: true, isCollapsed: true, border: OutlineInputBorder(), focusedBorder: UnderlineInputBorder(), enabledBorder: UnderlineInputBorder(), errorBorder: UnderlineInputBorder(), focusedErrorBorder: UnderlineInputBorder(), disabledBorder: UnderlineInputBorder(), constraints: BoxConstraints(maxWidth: 300), ); final ThemeData theme = ThemeData( useMaterial3: true, inputDecorationTheme: inputDecorationTheme ); void checkDecorationInSearchBar(WidgetTester tester) { final Finder textField = findTextField(); final InputDecoration? decoration = tester.widget<TextField>(textField).decoration; expect(decoration?.border, InputBorder.none); expect(decoration?.focusedBorder, InputBorder.none); expect(decoration?.enabledBorder, InputBorder.none); expect(decoration?.errorBorder, null); expect(decoration?.focusedErrorBorder, null); expect(decoration?.disabledBorder, null); expect(decoration?.constraints, null); expect(decoration?.isCollapsed, false); expect(decoration?.filled, false); expect(decoration?.fillColor, null); expect(decoration?.focusColor, null); expect(decoration?.hoverColor, null); expect(decoration?.contentPadding, EdgeInsets.zero); expect(decoration?.hintStyle?.color, theme.colorScheme.onSurfaceVariant); } testWidgetsWithLeakTracking('Overall InputDecorationTheme does not override text field style' ' in SearchBar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: theme, home: const Center( child: Material( child: SearchBar(hintText: 'hint text'), ), ), ), ); // Check input decoration in `SearchBar` checkDecorationInSearchBar(tester); // Check search bar defaults. final Finder searchBarMaterial = find.descendant( of: find.byType(SearchBar), matching: find.byType(Material), ); final Material material = tester.widget<Material>(searchBarMaterial); checkSearchBarDefaults(tester, theme.colorScheme, material); }); testWidgetsWithLeakTracking('Overall InputDecorationTheme does not override text field style' ' in the search view route', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Material( child: Align( alignment: Alignment.topLeft, child: SearchAnchor( viewHintText: 'hint text', builder: (BuildContext context, SearchController controller) { return const Icon(Icons.search); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ), ), ); await tester.tap(find.byIcon(Icons.search)); await tester.pumpAndSettle(); // Check input decoration in `SearchBar` checkDecorationInSearchBar(tester); // Check search bar defaults in search view route. final Finder searchBarMaterial = find.descendant( of: find.descendant(of: findViewContent(), matching: find.byType(SearchBar)), matching: find.byType(Material), ).first; final Material material = tester.widget<Material>(searchBarMaterial); expect(material.color, Colors.transparent); expect(material.elevation, 0.0); final Text hintText = tester.widget(find.text('hint text')); expect(hintText.style?.color, theme.colorScheme.onSurfaceVariant); const String input = 'entered text'; await tester.enterText(find.byType(SearchBar), input); final EditableText inputText = tester.widget(find.text(input)); expect(inputText.style.color, theme.colorScheme.onSurface); }); }); testWidgetsWithLeakTracking('SearchAnchor view respects theme brightness', (WidgetTester tester) async { Widget buildSearchAnchor(ThemeData theme) { return MaterialApp( theme: theme, home: Center( child: Material( child: SearchAnchor( builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.ac_unit), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } ThemeData theme = ThemeData(brightness: Brightness.light); await tester.pumpWidget(buildSearchAnchor(theme)); // Open the search view. await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); // Test the search view background color. Material material = getSearchViewMaterial(tester); expect(material.color, theme.colorScheme.surface); // Change the theme brightness. theme = ThemeData(brightness: Brightness.dark); await tester.pumpWidget(buildSearchAnchor(theme)); await tester.pumpAndSettle(); // Test the search view background color. material = getSearchViewMaterial(tester); expect(material.color, theme.colorScheme.surface); }); testWidgetsWithLeakTracking('Search view widgets can inherit local themes', (WidgetTester tester) async { final ThemeData globalTheme = ThemeData(colorSchemeSeed: Colors.red); final ThemeData localTheme = ThemeData( colorSchemeSeed: Colors.green, iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( backgroundColor: const Color(0xffffff00) ), ), cardTheme: const CardTheme(color: Color(0xff00ffff)), ); Widget buildSearchAnchor() { return MaterialApp( theme: globalTheme, home: Center( child: Builder( builder: (BuildContext context) { return Theme( data: localTheme, child: Material( child: SearchAnchor.bar( suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[ Card( child: ListTile( onTap: () {}, title: const Text('Item 1'), ), ), ]; }, ), ), ); } ), ), ); } await tester.pumpWidget(buildSearchAnchor()); // Open the search view. await tester.tap(find.byType(SearchBar)); await tester.pumpAndSettle(); // Test the search view background color. final Material searchViewMaterial = getSearchViewMaterial(tester); expect(searchViewMaterial.color, localTheme.colorScheme.surface); // Test the search view icons background color. final Material iconButtonMaterial = tester.widget<Material>(find.descendant( of: find.byType(IconButton), matching: find.byType(Material), ).first); expect(find.byWidget(iconButtonMaterial), findsOneWidget); expect(iconButtonMaterial.color, localTheme.iconButtonTheme.style?.backgroundColor?.resolve(<MaterialState>{})); // Test the suggestion card color. final Material suggestionMaterial = tester.widget<Material>(find.descendant( of: find.byType(Card), matching: find.byType(Material), ).first); expect(suggestionMaterial.color, localTheme.cardTheme.color); }); testWidgetsWithLeakTracking('SearchBar respects keyboardType property', (WidgetTester tester) async { Widget buildSearchBar(TextInputType keyboardType) { return MaterialApp( home: Center( child: Material( child: SearchBar( keyboardType: keyboardType, ), ), ), ); } await tester.pumpWidget(buildSearchBar(TextInputType.number)); await tester.pump(); TextField textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.number); await tester.pumpWidget(buildSearchBar(TextInputType.phone)); await tester.pump(); textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.phone); }); testWidgetsWithLeakTracking('SearchAnchor respects keyboardType property', (WidgetTester tester) async { Widget buildSearchAnchor(TextInputType keyboardType) { return MaterialApp( home: Center( child: Material( child: SearchAnchor( keyboardType: keyboardType, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.ac_unit), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildSearchAnchor(TextInputType.number)); await tester.pump(); await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); TextField textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.number); await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); await tester.pump(); await tester.pumpWidget(buildSearchAnchor(TextInputType.phone)); await tester.pump(); await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.phone); }); testWidgetsWithLeakTracking('SearchAnchor.bar respects keyboardType property', (WidgetTester tester) async { Widget buildSearchAnchor(TextInputType keyboardType) { return MaterialApp( home: Center( child: Material( child: SearchAnchor.bar( keyboardType: keyboardType, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildSearchAnchor(TextInputType.number)); await tester.pump(); await tester.tap(find.byType(SearchBar)); // Open search view. await tester.pumpAndSettle(); final Finder textFieldFinder = find.descendant(of: findViewContent(), matching: find.byType(TextField)); final TextField textFieldInView = tester.widget<TextField>(textFieldFinder); expect(textFieldInView.keyboardType, TextInputType.number); // Close search view. await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); await tester.pumpAndSettle(); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.number); }); testWidgetsWithLeakTracking('SearchBar respects textInputAction property', (WidgetTester tester) async { Widget buildSearchBar(TextInputAction textInputAction) { return MaterialApp( home: Center( child: Material( child: SearchBar( textInputAction: textInputAction, ), ), ), ); } await tester.pumpWidget(buildSearchBar(TextInputAction.previous)); await tester.pump(); TextField textField = tester.widget(find.byType(TextField)); expect(textField.textInputAction, TextInputAction.previous); await tester.pumpWidget(buildSearchBar(TextInputAction.send)); await tester.pump(); textField = tester.widget(find.byType(TextField)); expect(textField.textInputAction, TextInputAction.send); }); testWidgetsWithLeakTracking('SearchAnchor respects textInputAction property', (WidgetTester tester) async { Widget buildSearchAnchor(TextInputAction textInputAction) { return MaterialApp( home: Center( child: Material( child: SearchAnchor( textInputAction: textInputAction, builder: (BuildContext context, SearchController controller) { return IconButton( icon: const Icon(Icons.ac_unit), onPressed: () { controller.openView(); }, ); }, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildSearchAnchor(TextInputAction.previous)); await tester.pump(); await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); TextField textField = tester.widget(find.byType(TextField)); expect(textField.textInputAction, TextInputAction.previous); await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); await tester.pump(); await tester.pumpWidget(buildSearchAnchor(TextInputAction.send)); await tester.pump(); await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); await tester.pumpAndSettle(); textField = tester.widget(find.byType(TextField)); expect(textField.textInputAction, TextInputAction.send); }); testWidgetsWithLeakTracking('SearchAnchor.bar respects textInputAction property', (WidgetTester tester) async { Widget buildSearchAnchor(TextInputAction textInputAction) { return MaterialApp( home: Center( child: Material( child: SearchAnchor.bar( textInputAction: textInputAction, suggestionsBuilder: (BuildContext context, SearchController controller) { return <Widget>[]; }, ), ), ), ); } await tester.pumpWidget(buildSearchAnchor(TextInputAction.previous)); await tester.pump(); await tester.tap(find.byType(SearchBar)); // Open search view. await tester.pumpAndSettle(); final Finder textFieldFinder = find.descendant(of: findViewContent(), matching: find.byType(TextField)); final TextField textFieldInView = tester.widget<TextField>(textFieldFinder); expect(textFieldInView.textInputAction, TextInputAction.previous); // Close search view. await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); await tester.pumpAndSettle(); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.textInputAction, TextInputAction.previous); }); } Future<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async { expect(material.animationDuration, const Duration(milliseconds: 200)); expect(material.borderOnForeground, true); expect(material.borderRadius, null); expect(material.clipBehavior, Clip.none); expect(material.color, colorScheme.surface); expect(material.elevation, 6.0); expect(material.shadowColor, colorScheme.shadow); expect(material.surfaceTintColor, colorScheme.surfaceTint); expect(material.shape, const StadiumBorder()); final Text helperText = tester.widget(find.text('hint text')); expect(helperText.style?.color, colorScheme.onSurfaceVariant); expect(helperText.style?.fontSize, 16.0); expect(helperText.style?.fontFamily, 'Roboto'); expect(helperText.style?.fontWeight, FontWeight.w400); const String input = 'entered text'; await tester.enterText(find.byType(SearchBar), input); final EditableText inputText = tester.widget(find.text(input)); expect(inputText.style.color, colorScheme.onSurface); expect(inputText.style.fontSize, 16.0); expect(helperText.style?.fontFamily, 'Roboto'); expect(inputText.style.fontWeight, FontWeight.w400); } Finder findTextField() { return find.descendant( of: find.byType(SearchBar), matching: find.byType(TextField) ); } TextStyle? _iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget<RichText>( find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)), ); return iconRichText.text.style; } const Color pressedColor = Colors.red; const Color hoveredColor = Colors.orange; const Color focusedColor = Colors.yellow; const Color defaultColor = Colors.green; Color _getColor(Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedColor; } if (states.contains(MaterialState.hovered)) { return hoveredColor; } if (states.contains(MaterialState.focused)) { return focusedColor; } return defaultColor; } final ThemeData theme = ThemeData(); final TextStyle? pressedStyle = theme.textTheme.bodyLarge?.copyWith(color: pressedColor); final TextStyle? hoveredStyle = theme.textTheme.bodyLarge?.copyWith(color: hoveredColor); final TextStyle? focusedStyle = theme.textTheme.bodyLarge?.copyWith(color: focusedColor); TextStyle? _getTextStyle(Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedStyle; } if (states.contains(MaterialState.hovered)) { return hoveredStyle; } if (states.contains(MaterialState.focused)) { return focusedStyle; } return null; } Future<TestGesture> _pointGestureToSearchBar(WidgetTester tester) async { final Offset center = tester.getCenter(find.byType(SearchBar)); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, ); // On hovered. await gesture.addPointer(); await gesture.moveTo(center); return gesture; } Finder findViewContent() { return find.byWidgetPredicate((Widget widget) { return widget.runtimeType.toString() == '_ViewContent'; }); } Material getSearchViewMaterial(WidgetTester tester) { return tester.widget<Material>(find.descendant(of: findViewContent(), matching: find.byType(Material)).first); }