blob: 1aaa30c8c6ddb844a486faf53944d8560c449b79 [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 'dart:async';
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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.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);
}
testWidgets('SearchBar defaults', (WidgetTester tester) async {
final theme = ThemeData();
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);
});
testWidgets('SearchBar respects controller property', (WidgetTester tester) async {
const defaultText = 'default text';
final 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 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);
});
testWidgets('SearchBar respects focusNode property', (WidgetTester tester) async {
final 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);
});
testWidgets('SearchBar focusNode is hot swappable', (WidgetTester tester) async {
final 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 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);
});
testWidgets('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));
});
testWidgets('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));
});
testWidgets('SearchBar respects hintText property', (WidgetTester tester) async {
const hintText = 'hint text';
await tester.pumpWidget(
const MaterialApp(
home: Material(child: SearchBar(hintText: hintText)),
),
);
expect(find.text(hintText), findsOneWidget);
});
testWidgets('SearchBar respects leading property', (WidgetTester tester) async {
final 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.
});
testWidgets('SearchBar respects trailing property', (WidgetTester tester) async {
final 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.
});
testWidgets('SearchBar respects onTap property', (WidgetTester tester) async {
var 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);
});
testWidgets('SearchBar respects onChanged property', (WidgetTester tester) async {
var 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);
});
testWidgets('SearchBar respects onSubmitted property', (WidgetTester tester) async {
var 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'));
});
testWidgets('SearchBar respects constraints property', (WidgetTester tester) async {
const 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));
});
testWidgets('SearchBar respects elevation property', (WidgetTester tester) async {
const pressedElevation = 0.0;
const hoveredElevation = 1.0;
const focusedElevation = 2.0;
const defaultElevation = 3.0;
double getElevation(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return pressedElevation;
}
if (states.contains(WidgetState.hovered)) {
return hoveredElevation;
}
if (states.contains(WidgetState.focused)) {
return focusedElevation;
}
return defaultElevation;
}
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(elevation: WidgetStateProperty.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);
});
testWidgets('SearchBar respects backgroundColor property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(backgroundColor: WidgetStateProperty.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);
});
testWidgets('SearchBar respects shadowColor property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(shadowColor: WidgetStateProperty.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);
});
testWidgets('SearchBar respects surfaceTintColor property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(surfaceTintColor: WidgetStateProperty.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);
});
testWidgets('SearchBar respects overlayColor property', (WidgetTester tester) async {
final focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(
focusNode: focusNode,
overlayColor: WidgetStateProperty.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)),
);
});
testWidgets('SearchBar respects side and shape properties', (WidgetTester tester) async {
const pressedSide = BorderSide(width: 2.0);
const hoveredSide = BorderSide(width: 3.0);
const focusedSide = BorderSide(width: 4.0);
const defaultSide = BorderSide(width: 5.0);
const OutlinedBorder pressedShape = RoundedRectangleBorder();
const OutlinedBorder hoveredShape = ContinuousRectangleBorder();
const OutlinedBorder focusedShape = CircleBorder();
const OutlinedBorder defaultShape = StadiumBorder();
BorderSide getSide(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return pressedSide;
}
if (states.contains(WidgetState.hovered)) {
return hoveredSide;
}
if (states.contains(WidgetState.focused)) {
return focusedSide;
}
return defaultSide;
}
OutlinedBorder getShape(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return pressedShape;
}
if (states.contains(WidgetState.hovered)) {
return hoveredShape;
}
if (states.contains(WidgetState.focused)) {
return focusedShape;
}
return defaultShape;
}
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(
side: WidgetStateProperty.resolveWith<BorderSide>(getSide),
shape: WidgetStateProperty.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));
});
testWidgets('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);
});
testWidgets('SearchBar respects hintStyle property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(
hintText: 'hint text',
hintStyle: WidgetStateProperty.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);
});
testWidgets('SearchBar respects textStyle property', (WidgetTester tester) async {
final controller = TextEditingController(text: 'input text');
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(
controller: controller,
textStyle: WidgetStateProperty.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);
});
testWidgets('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);
});
testWidgets('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.backButton());
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);
});
testWidgets('SearchAnchor respects viewOnChanged and viewOnSubmitted properties', (
WidgetTester tester,
) async {
final controller = SearchController();
addTearDown(controller.dispose);
var onChangedCalled = 0;
var 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);
});
testWidgets('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.backButton());
await tester.pumpAndSettle();
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.textCapitalization, TextCapitalization.characters);
});
testWidgets('SearchAnchor.bar respects onChanged and onSubmitted properties', (
WidgetTester tester,
) async {
final controller = SearchController();
addTearDown(controller.dispose);
var onChangedCalled = 0;
var 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);
});
// Regression test for https://github.com/flutter/flutter/issues/178719.
testWidgets('SearchAnchor.bar anchor loses focus after view closes', (WidgetTester tester) async {
final controller = SearchController();
addTearDown(controller.dispose);
var 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++;
});
controller.closeView(value);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
);
},
),
),
);
// Tap to open the search view.
await tester.tap(find.byType(SearchBar));
await tester.pumpAndSettle();
expect(controller.isOpen, true);
// Find the anchor SearchBar's TextField.
final Finder anchorTextField = find.descendant(
of: find.byType(SearchBar).first,
matching: find.byType(TextField),
);
// Find the view SearchBar's TextField.
final Finder viewTextField = find.descendant(
of: findViewContent(),
matching: find.byType(TextField),
);
// View SearchBar should have focus.
expect(tester.widget<TextField>(viewTextField).focusNode?.hasFocus, isTrue);
// Close the view (use receiveAction because sendKeyEvent
// doesn't trigger onSubmitted in tests - the text input action needs to
// be sent directly to properly simulate the IME behavior).
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(onSubmittedCalled, 1);
expect(controller.isOpen, false);
// After search view is closed, anchor SearchBar should NOT have focus.
expect(tester.widget<TextField>(anchorTextField).focusNode?.hasFocus, isFalse);
// Simulate the IME behavior via input action again .
// It should not trigger onSubmitted because field is unfocused
// and the text input connection is closed.
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(onSubmittedCalled, 1); // Still 1, not incremented.
});
testWidgets('hintStyle can override textStyle for hintText', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchBar(
hintText: 'hint text',
hintStyle: WidgetStateProperty.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.
testWidgets('The text is still centered when SearchBar text field is smaller than 48', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: 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);
});
testWidgets('The search view defaults', (WidgetTester tester) async {
final theme = ThemeData();
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.surfaceContainerHigh);
expect(material.surfaceTintColor, Colors.transparent);
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 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.backButton(), 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 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);
});
testWidgets('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 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 Size size = tester.getSize(
find.descendant(of: findViewContent(), matching: find.byType(ConstrainedBox)).first,
);
expect(size.width, 800.0);
expect(size.height, 600.0);
}
for (final 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 Size size = tester.getSize(
find.descendant(of: findViewContent(), matching: find.byType(ConstrainedBox)).first,
);
expect(size.width, 360.0);
expect(size.height, 400.0);
}
});
testWidgets('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 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 Size size = tester.getSize(
find.descendant(of: findViewContent(), matching: find.byType(ConstrainedBox)).first,
);
expect(size.width, 800.0);
expect(size.height, 600.0);
}
});
testWidgets('SearchAnchor respects controller property', (WidgetTester tester) async {
const defaultText = 'initial text';
final 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 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);
});
testWidgets('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 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 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);
});
testWidgets('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);
});
testWidgets('SearchAnchor.bar respects viewBuilder property', (WidgetTester tester) async {
Widget buildAnchor({ViewBuilder? viewBuilder}) {
return MaterialApp(
home: Material(
child: SearchAnchor.bar(
viewBuilder: viewBuilder,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
);
}
await tester.pumpWidget(buildAnchor());
await tester.tap(find.byIcon(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.byIcon(Icons.search));
await tester.pumpAndSettle();
expect(find.byType(ListView), findsNothing);
expect(find.byType(GridView), findsOneWidget);
});
testWidgets('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.backButton(), 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.backButton(), findsNothing);
expect(find.byIcon(Icons.history), findsOneWidget);
});
testWidgets('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 when input is not empty.
await tester.enterText(findTextField(), 'a');
await tester.pump();
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);
});
testWidgets('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);
});
testWidgets('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);
});
testWidgets('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);
});
testWidgets('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);
});
testWidgets('SearchAnchor respects viewSide property', (WidgetTester tester) async {
const 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)),
);
});
testWidgets('SearchAnchor respects viewShape property', (WidgetTester tester) async {
const 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);
});
testWidgets('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);
});
testWidgets('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);
});
testWidgets('SearchAnchor respects viewPadding property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: false,
viewPadding: const EdgeInsets.all(16.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 Padding padding = tester.widget<Padding>(
find.descendant(of: findViewContent(), matching: find.byType(Padding)).first,
);
expect(padding.padding, const EdgeInsets.all(16.0));
});
testWidgets('SearchAnchor ignores viewPadding property if full screen', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: true,
viewPadding: const EdgeInsets.all(16.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 Padding padding = tester.widget<Padding>(
find.descendant(of: findViewContent(), matching: find.byType(Padding)).first,
);
expect(padding.padding, EdgeInsets.zero);
});
testWidgets('SearchAnchor respects shrinkWrap property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: false,
shrinkWrap: true,
viewConstraints: const BoxConstraints(),
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
onPressed: () {
controller.openView();
},
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<Widget>.generate(
controller.text.length,
(int index) => ListTile(title: Text('Item $index')),
);
},
),
),
),
);
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
final Finder findDivider = find.descendant(
of: findViewContent(),
matching: find.byType(Divider),
);
// Divider should not be shown if there are no suggestions
expect(findDivider, findsNothing);
final Finder findMaterial = find
.descendant(of: findViewContent(), matching: find.byType(Material))
.first;
final Rect materialRectWithoutSuggestions = tester.getRect(findMaterial);
expect(materialRectWithoutSuggestions, equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 56.0)));
await tester.enterText(find.byType(SearchBar), 'a');
await tester.pumpAndSettle();
expect(findDivider, findsOneWidget);
final Rect materialRectWithSuggestions = tester.getRect(findMaterial);
expect(materialRectWithSuggestions, equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 113.0)));
});
testWidgets('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 decoration = dividerContainer.decoration! as BoxDecoration;
expect(decoration.border!.bottom.color, Colors.red);
});
testWidgets('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 Size size = tester.getSize(
find.descendant(of: findViewContent(), matching: find.byType(ConstrainedBox)).first,
);
expect(size.width, 280.0);
expect(size.height, 390.0);
});
testWidgets('SearchAnchor respects viewBarPadding property', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor(
viewBarPadding: const EdgeInsets.symmetric(horizontal: 16.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 Finder findSearchBar = find
.descendant(of: findViewContent(), matching: find.byType(SearchBar))
.first;
final Padding padding = tester.widget<Padding>(
find.descendant(of: findSearchBar, matching: find.byType(Padding)).first,
);
expect(padding.padding, const EdgeInsets.symmetric(horizontal: 16.0));
});
testWidgets('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(ConstrainedBox)).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);
});
testWidgets('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(ConstrainedBox)).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);
});
testWidgets('SearchAnchor respects suggestionsBuilder property', (WidgetTester tester) async {
final controller = SearchController();
addTearDown(controller.dispose);
const 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);
});
testWidgets('SearchAnchor should update suggestions on changes to search controller', (
WidgetTester tester,
) async {
final controller = SearchController();
const 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);
});
testWidgets('SearchAnchor suggestionsBuilder property could be async', (
WidgetTester tester,
) async {
final controller = SearchController();
addTearDown(controller.dispose);
const 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);
});
testWidgets('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(ConstrainedBox)).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);
});
testWidgets('SearchController can open/close view', (WidgetTester tester) async {
final 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);
});
testWidgets('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(ConstrainedBox)).first,
);
expect(searchViewRect, equals(const Rect.fromLTRB(440.0, 200.0, 800.0, 600.0)));
});
testWidgets('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(ConstrainedBox)).first,
);
expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 200.0, 360.0, 600.0)));
});
testWidgets('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(ConstrainedBox)).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(ConstrainedBox)).first,
);
expect(searchViewRectRTL, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0)));
});
testWidgets('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.backButton(), findsOneWidget);
// Change window size
tester.view.physicalSize = const Size(250.0, 200.0);
tester.view.devicePixelRatio = 1.0;
await tester.pumpAndSettle();
expect(find.backButton(), findsNothing);
});
testWidgets('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.backButton(), findsOneWidget);
// Change window size
tester.view.physicalSize = const Size(250.0, 200.0);
tester.view.devicePixelRatio = 1.0;
await tester.pumpAndSettle();
expect(find.backButton(), findsOneWidget);
});
testWidgets('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 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.backButton());
await tester.pumpAndSettle();
// No exception.
});
testWidgets('Docked search should position itself correctly based on closest navigator', (
WidgetTester tester,
) async {
const 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(ConstrainedBox)).first,
);
expect(searchViewRect.topLeft, equals(const Offset(rootSpacing, rootSpacing)));
});
testWidgets('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 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(ConstrainedBox)).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', () {
testWidgets('can right-click to select word', (WidgetTester tester) async {
const defaultText = 'initial text';
final 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));
testWidgets('can click to set position', (WidgetTester tester) async {
const defaultText = 'initial text';
final 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());
testWidgets('can double-click to select word', (WidgetTester tester) async {
const defaultText = 'initial text';
final 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());
testWidgets('can triple-click to select field', (WidgetTester tester) async {
const defaultText = 'initial text';
final 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 InputDecorationThemeData does not impact SearchBar and SearchView', () {
const inputDecorationTheme = InputDecorationThemeData(
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 theme = ThemeData(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);
}
testWidgets('Overall InputDecorationThemeData 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);
});
testWidgets('Overall InputDecorationThemeData 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 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);
});
});
testWidgets('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>[];
},
),
),
),
);
}
var 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.surfaceContainerHigh);
// 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.surfaceContainerHigh);
});
testWidgets('Search view widgets can inherit local themes', (WidgetTester tester) async {
final globalTheme = ThemeData(colorSchemeSeed: Colors.red);
final localTheme = ThemeData(
colorSchemeSeed: Colors.green,
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(backgroundColor: const Color(0xffffff00)),
),
cardTheme: const CardThemeData(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.surfaceContainerHigh);
// 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(<WidgetState>{}),
);
// 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);
});
testWidgets('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);
});
testWidgets('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.backButton());
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);
});
testWidgets('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.backButton());
await tester.pumpAndSettle();
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.keyboardType, TextInputType.number);
});
testWidgets('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);
});
testWidgets('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.backButton());
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);
});
testWidgets('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.backButton());
await tester.pumpAndSettle();
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.textInputAction, TextInputAction.previous);
});
testWidgets('Block entering text on disabled widget', (WidgetTester tester) async {
const initValue = 'init';
final controller = TextEditingController(text: initValue);
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(child: SearchBar(controller: controller, enabled: false)),
),
),
);
const testValue = 'abcdefghi';
await tester.enterText(find.byType(SearchBar), testValue);
expect(controller.value.text, initValue);
});
testWidgets('Block entering text on disabled widget with SearchAnchor.bar', (
WidgetTester tester,
) async {
const initValue = 'init';
final controller = TextEditingController(text: initValue);
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor.bar(
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
enabled: false,
),
),
),
),
);
const testValue = 'abcdefghi';
await tester.enterText(find.byType(SearchBar), testValue);
expect(controller.value.text, initValue);
});
testWidgets('Disabled SearchBar semantics node still contains value and inputType', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
final controller = TextEditingController(text: 'text');
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(child: SearchBar(controller: controller, enabled: false)),
),
),
);
expect(
semantics,
includesNodeWith(
actions: <SemanticsAction>[],
value: 'text',
inputType: SemanticsInputType.search,
),
);
semantics.dispose();
});
testWidgets('SearchBar semantics node has search input type', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(child: Center(child: SearchBar())),
),
);
expect(semantics, includesNodeWith(inputType: SemanticsInputType.search));
semantics.dispose();
});
testWidgets('Check SearchBar opacity when disabled', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(child: Center(child: SearchBar(enabled: false))),
),
);
final Finder searchBarFinder = find.byType(SearchBar);
expect(searchBarFinder, findsOneWidget);
final Finder opacityFinder = find.descendant(
of: searchBarFinder,
matching: find.byType(Opacity),
);
expect(opacityFinder, findsOneWidget);
final Opacity opacityWidget = tester.widget<Opacity>(opacityFinder);
expect(opacityWidget.opacity, 0.38);
});
testWidgets('Check SearchAnchor opacity when disabled', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchAnchor(
enabled: false,
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
);
final Finder searchBarFinder = find.byType(SearchAnchor);
expect(searchBarFinder, findsOneWidget);
final Finder opacityFinder = find.descendant(
of: searchBarFinder,
matching: find.byType(AnimatedOpacity),
);
expect(opacityFinder, findsOneWidget);
final AnimatedOpacity opacityWidget = tester.widget<AnimatedOpacity>(opacityFinder);
expect(opacityWidget.opacity, 0.38);
});
testWidgets('SearchAnchor tap failed when disabled', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchAnchor(
enabled: false,
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
);
final Finder searchBarFinder = find.byType(SearchAnchor);
expect(searchBarFinder, findsOneWidget);
expect(searchBarFinder.hitTestable().tryEvaluate(), false);
});
testWidgets('SearchAnchor respects headerHeight', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchAnchor(
isFullScreen: true,
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
headerHeight: 32,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
);
await tester.pump();
await tester.tap(find.byIcon(Icons.search)); // Open search view.
await tester.pumpAndSettle();
final Finder findHeader = find.descendant(
of: findViewContent(),
matching: find.byType(SearchBar),
);
expect(tester.getSize(findHeader).height, 32);
});
testWidgets('SearchAnchor.bar respects viewHeaderHeight', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchAnchor.bar(
isFullScreen: true,
viewHeaderHeight: 32,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
);
await tester.pump();
await tester.tap(find.byType(SearchBar)); // Open search view.
await tester.pumpAndSettle();
final Finder findHeader = find.descendant(
of: findViewContent(),
matching: find.byType(SearchBar),
);
final RenderBox box = tester.renderObject(findHeader);
expect(box.size.height, 32);
});
testWidgets(
'Tapping outside searchbar should unfocus the searchbar on mobile',
(WidgetTester tester) async {
final focusNode = FocusNode(debugLabel: 'Test Node');
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return SearchBar(
controller: controller,
onTap: () {
controller.openView();
},
onTapOutside: (PointerDownEvent event) {
focusNode.unfocus();
},
onChanged: (_) {
controller.openView();
},
autoFocus: true,
focusNode: focusNode,
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<ListTile>.generate(5, (int index) {
final item = 'item $index';
return ListTile(title: Text(item));
});
},
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.tapAt(const Offset(50, 50));
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets('The default clear button only shows when text input is not empty '
'on the search view', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
);
await tester.pump();
await tester.tap(find.byIcon(Icons.search)); // Open search view.
await tester.pumpAndSettle();
expect(find.widgetWithIcon(IconButton, Icons.close), findsNothing);
await tester.enterText(findTextField(), 'a');
await tester.pump();
expect(find.widgetWithIcon(IconButton, Icons.close), findsOneWidget);
await tester.enterText(findTextField(), '');
await tester.pump();
expect(find.widgetWithIcon(IconButton, Icons.close), findsNothing);
});
// This is a regression test for https://github.com/flutter/flutter/issues/139880.
testWidgets('suggestionsBuilder with Future is not called twice on layout resize', (
WidgetTester tester,
) async {
var suggestionsLoadingCount = 0;
Future<List<String>> createListData() async {
return List<String>.generate(1000, (int index) {
return 'Hello World - $index';
});
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[
FutureBuilder<List<String>>(
future: createListData(),
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const LinearProgressIndicator();
}
final List<String>? result = snapshot.data;
if (result == null) {
return const LinearProgressIndicator();
}
suggestionsLoadingCount++;
return SingleChildScrollView(
child: Column(
children: result.map((String text) {
return ListTile(title: Text(text));
}).toList(),
),
);
},
),
];
},
),
),
),
),
);
await tester.pump();
await tester.tap(find.byIcon(Icons.search)); // Open search view.
await tester.pumpAndSettle();
// Simulate the keyboard opening resizing the view.
tester.view.viewInsets = const FakeViewPadding(bottom: 500.0);
addTearDown(tester.view.reset);
await tester.pumpAndSettle();
expect(suggestionsLoadingCount, 1);
});
// This is a regression test for https://github.com/flutter/flutter/issues/139880.
testWidgets('suggestionsBuilder is not called when the search value does not change', (
WidgetTester tester,
) async {
var suggestionsBuilderCalledCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
suggestionsBuilderCalledCount++;
return <Widget>[];
},
),
),
),
),
);
await tester.pump();
await tester.tap(find.byIcon(Icons.search)); // Open search view.
await tester.pumpAndSettle();
// Simulate the keyboard opening resizing the view.
tester.view.viewInsets = const FakeViewPadding(bottom: 500.0);
addTearDown(tester.view.reset);
// Show the keyboard.
await tester.showKeyboard(find.byType(TextField));
await tester.pumpAndSettle();
expect(suggestionsBuilderCalledCount, 2);
// Remove the viewInset, as if the keyboard were hidden.
tester.view.resetViewInsets();
// Hide the keyboard.
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(suggestionsBuilderCalledCount, 2);
});
testWidgets('Suggestions gets refreshed after long API call', (WidgetTester tester) async {
Timer? debounceTimer;
const apiCallDuration = Duration(seconds: 1);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) async {
final completer = Completer<List<String>>();
debounceTimer?.cancel();
debounceTimer = Timer(apiCallDuration, () {
completer.complete(List<String>.generate(10, (int index) => 'Item - $index'));
});
final List<String> options = await completer.future;
final suggestions = List<Widget>.generate(options.length, (int index) {
final String item = options[index];
return ListTile(title: Text(item));
});
return suggestions;
},
),
),
),
),
);
await tester.tap(find.byIcon(Icons.search)); // Open search view.
await tester.pumpAndSettle();
// Simulate the keyboard opening resizing the view.
tester.view.viewInsets = const FakeViewPadding(bottom: 500.0);
addTearDown(tester.view.reset);
// Show the keyboard.
await tester.showKeyboard(find.byType(TextField));
await tester.pumpAndSettle(apiCallDuration);
expect(find.text('Item - 1'), findsOneWidget);
});
testWidgets('SearchBar.scrollPadding is passed through to EditableText', (
WidgetTester tester,
) async {
const EdgeInsets scrollPadding = EdgeInsets.zero;
await tester.pumpWidget(
const MaterialApp(
home: Material(child: SearchBar(scrollPadding: scrollPadding)),
),
);
expect(find.byType(EditableText), findsOneWidget);
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.scrollPadding, scrollPadding);
});
testWidgets('SearchAnchor.bar.scrollPadding is passed through to EditableText', (
WidgetTester tester,
) async {
const EdgeInsets scrollPadding = EdgeInsets.zero;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
scrollPadding: scrollPadding,
),
),
),
);
expect(find.byType(EditableText), findsOneWidget);
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.scrollPadding, scrollPadding);
});
group('contextMenuBuilder', () {
setUp(() async {
if (!kIsWeb) {
return;
}
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.contextMenu,
(MethodCall call) {
// Just complete successfully, so that BrowserContextMenu thinks that
// the engine successfully received its call.
return Future<void>.value();
},
);
await BrowserContextMenu.disableContextMenu();
});
tearDown(() async {
if (!kIsWeb) {
return;
}
await BrowserContextMenu.enableContextMenu();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.contextMenu,
null,
);
});
testWidgets('SearchAnchor.bar.contextMenuBuilder is passed through to EditableText', (
WidgetTester tester,
) async {
Widget contextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
return const Placeholder();
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
contextMenuBuilder: contextMenuBuilder,
),
),
),
);
expect(find.byType(EditableText), findsOneWidget);
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.contextMenuBuilder, contextMenuBuilder);
expect(find.byType(Placeholder), findsNothing);
await tester.tap(find.byType(SearchBar), buttons: kSecondaryButton);
await tester.pumpAndSettle();
expect(find.byType(Placeholder), findsOneWidget);
});
testWidgets(
'iOS uses the system context menu by default if supported',
(WidgetTester tester) async {
tester.platformDispatcher.supportsShowingSystemContextMenu = true;
addTearDown(() {
tester.platformDispatcher.resetSupportsShowingSystemContextMenu();
tester.view.reset();
});
final controller = TextEditingController(text: 'one two three');
addTearDown(controller.dispose);
await tester.pumpWidget(
// Don't wrap with the global View so that the change to
// platformDispatcher is read.
wrapWithView: false,
View(
view: tester.view,
child: MaterialApp(
home: Material(child: TextField(controller: controller)),
),
),
);
// No context menu shown.
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
expect(find.byType(SystemContextMenu), findsNothing);
// Double tap to select the first word and show the menu.
await tester.tapAt(textOffsetToPosition(tester, 1));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 1));
await tester.pump(SelectionOverlay.fadeDuration);
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
expect(find.byType(SystemContextMenu), findsOneWidget);
},
skip: kIsWeb, // [intended] on web the browser handles the context menu.
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
});
testWidgets('SearchAnchor does not dispose external SearchController', (
WidgetTester tester,
) async {
final controller = SearchController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
searchController: controller,
builder: (BuildContext context, SearchController controller) {
return IconButton(
onPressed: () async {
controller.openView();
},
icon: const Icon(Icons.search),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
await tester.pumpWidget(const MaterialApp(home: Material(child: Text('disposed'))));
expect(tester.takeException(), isNull);
ChangeNotifier.debugAssertNotDisposed(controller);
});
testWidgets('SearchAnchor gracefully closes its search view when disposed', (
WidgetTester tester,
) async {
var disposed = false;
late StateSetter setState;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
if (disposed) {
return const Text('disposed');
}
return SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return IconButton(
onPressed: () async {
controller.openView();
},
icon: const Icon(Icons.search),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[const Text('suggestion')];
},
);
},
),
),
),
);
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
setState(() {
disposed = true;
});
await tester.pump();
// The search menu starts to close but is not disposed yet.
final EditableText editableText = tester.widget(find.byType(EditableText));
final TextEditingController controller = editableText.controller;
ChangeNotifier.debugAssertNotDisposed(controller);
await tester.pumpAndSettle();
// The search menu and the internal search controller are now disposed.
expect(tester.takeException(), isNull);
expect(find.byType(TextField), findsNothing);
FlutterError? error;
try {
ChangeNotifier.debugAssertNotDisposed(controller);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error, isFlutterError);
expect(
error!.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A SearchController was used after being disposed.\n'
' Once you have called dispose() on a SearchController, it can no\n'
' longer be used.\n',
),
);
});
// Regression test for https://github.com/flutter/flutter/issues/155180.
testWidgets('disposing SearchAnchor during search view exit animation does not crash', (
WidgetTester tester,
) async {
final key = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: key,
home: Material(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return IconButton(
onPressed: () async {
controller.openView();
},
icon: const Icon(Icons.search),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[const Text('suggestion')];
},
),
),
),
);
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
key.currentState!.pop();
await tester.pump();
await tester.pumpWidget(
MaterialApp(
navigatorKey: key,
home: const Material(child: Text('disposed')),
),
);
await tester.pump();
expect(tester.takeException(), isNull);
});
testWidgets('SearchAnchor viewOnClose function test', (WidgetTester tester) async {
var name = 'silva';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor(
viewOnClose: () {
name = 'Pedro';
},
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 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();
expect(name, 'silva');
// Pop search view route
await tester.tap(find.backButton());
await tester.pumpAndSettle();
expect(name, 'Pedro');
// No exception.
});
testWidgets('SearchAnchor.bar viewOnClose function test', (WidgetTester tester) async {
var name = 'silva';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
onClose: () {
name = 'Pedro';
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[
ListTile(
title: const Text('item 0'),
onTap: () {
controller.closeView('item 0');
},
),
];
},
),
),
),
);
// Open search view
await tester.tap(find.byType(SearchBar));
await tester.pumpAndSettle();
expect(name, 'silva');
// Pop search view route
await tester.tap(find.backButton());
await tester.pumpAndSettle();
expect(name, 'Pedro');
// No exception.
});
testWidgets('SearchAnchor viewOnOpen is called when the search view is opened', (
WidgetTester tester,
) async {
var searchViewState = 'Idle';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor(
viewOnClose: () {
searchViewState = 'Closed';
},
viewOnOpen: () {
searchViewState = 'Opened';
},
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 item = 'item $index';
return ListTile(
leading: const Icon(Icons.history),
title: Text(item),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
);
});
},
),
),
),
),
);
expect(find.byIcon(Icons.search), findsOneWidget);
// Open search view.
await tester.tap(find.byIcon(Icons.search));
await tester.pump();
expect(searchViewState, 'Opened');
// Pop search view route.
await tester.tap(find.backButton());
await tester.pump();
expect(searchViewState, 'Closed');
});
testWidgets('SearchAnchor.bar onOpen is called when the search view is opened', (
WidgetTester tester,
) async {
var searchViewState = 'Idle';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor.bar(
onClose: () {
searchViewState = 'Closed';
},
onOpen: () {
searchViewState = 'Opened';
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<Widget>.generate(5, (int index) {
final item = 'item $index';
return ListTile(
leading: const Icon(Icons.history),
title: Text(item),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
);
});
},
),
),
),
),
);
expect(find.byIcon(Icons.search), findsOneWidget);
// Open search view.
await tester.tap(find.byIcon(Icons.search));
await tester.pump();
expect(searchViewState, 'Opened');
// Pop search view route.
await tester.tap(find.backButton());
await tester.pump();
expect(searchViewState, 'Closed');
});
testWidgets(
'The last element of the suggestion list should be visible when scrolling to the end of list',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: SearchAnchor.bar(
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<Widget>.generate(30, (int index) {
return ListTile(
titleAlignment: ListTileTitleAlignment.center,
title: Text('Item $index'),
);
});
},
),
),
);
// Open search view.
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
const fakeKeyboardHeight = 500.0;
final double physicalBottomPadding = fakeKeyboardHeight * tester.view.devicePixelRatio;
// Simulate the keyboard opening resizing the view.
tester.view.viewInsets = FakeViewPadding(bottom: physicalBottomPadding);
addTearDown(tester.view.reset);
// Scroll down to the end of the list.
expect(find.byType(ListView), findsOne);
await tester.fling(find.byType(ListView), const Offset(0, -5000), 5000);
await tester.pumpAndSettle();
// The last item should not be hidden by the keyboard.
final double lastItemBottom = tester.getRect(find.text('Item 29')).bottom;
final double fakeKeyboardTop =
tester.getSize(find.byType(MaterialApp)).height - fakeKeyboardHeight;
expect(lastItemBottom, lessThanOrEqualTo(fakeKeyboardTop));
},
);
testWidgets(
'readOnly disallows SystemContextMenu',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/170521.
tester.platformDispatcher.supportsShowingSystemContextMenu = true;
final controller = TextEditingController(text: 'abcdefghijklmnopqr');
addTearDown(() {
tester.platformDispatcher.resetSupportsShowingSystemContextMenu();
tester.view.reset();
controller.dispose();
});
var readOnly = true;
late StateSetter setState;
await tester.pumpWidget(
// Don't wrap with the global View so that the change to
// platformDispatcher is read.
wrapWithView: false,
View(
view: tester.view,
child: MaterialApp(
home: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return SearchBar(controller: controller, readOnly: readOnly);
},
),
),
),
),
);
final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout
? SelectionOverlay.fadeDuration
: kDoubleTapTimeout;
// Double tap to select the text.
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(waitDuration);
// No error as in https://github.com/flutter/flutter/issues/170521.
// The Flutter-drawn context menu is shown. The SystemContextMenu is not
// shown because readOnly is true.
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
expect(find.byType(SystemContextMenu), findsNothing);
// Turn off readOnly and hide the context menu.
setState(() {
readOnly = false;
});
await tester.tap(find.text('Copy'));
await tester.pump(waitDuration);
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
expect(find.byType(SystemContextMenu), findsNothing);
// Double tap to show the context menu again.
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(waitDuration);
// Now iOS is showing the SystemContextMenu while others continue to show
// the Flutter-drawn context menu.
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
expect(find.byType(SystemContextMenu), findsOneWidget);
case TargetPlatform.macOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
}
},
variant: TargetPlatformVariant.all(),
skip: kIsWeb, // [intended] on web the browser handles the context menu.
);
testWidgets('smartDashesType and smartQuotesType are properly forwarded to inner text fields', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
smartDashesType: SmartDashesType.disabled,
smartQuotesType: SmartQuotesType.disabled,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
// Check anchor's text field.
final TextField anchorTextField = tester.widget(find.byType(TextField));
expect(anchorTextField.smartDashesType, SmartDashesType.disabled);
expect(anchorTextField.smartQuotesType, SmartQuotesType.disabled);
// Open view and check view's text field.
await tester.tap(find.byType(SearchBar));
await tester.pumpAndSettle();
final Finder viewTextFieldFinder = find.descendant(
of: find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_ViewContent',
),
matching: find.byType(TextField),
);
expect(viewTextFieldFinder, findsOneWidget);
final TextField viewTextField = tester.widget(viewTextFieldFinder);
expect(viewTextField.smartDashesType, SmartDashesType.disabled);
expect(viewTextField.smartQuotesType, SmartQuotesType.disabled);
});
testWidgets('SearchAnchor does not crash at zero area', (WidgetTester tester) async {
tester.view.physicalSize = Size.zero;
final controller = SearchController();
addTearDown(controller.dispose);
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SearchAnchor(
searchController: controller,
builder: (_, _) => const Text('X'),
suggestionsBuilder: (_, _) => <Widget>[const Text('Y')],
),
),
),
);
expect(tester.getSize(find.byType(SearchAnchor)), Size.zero);
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pump();
expect(find.byType(Text), findsWidgets);
});
testWidgets('SearchBar does not crash at zero area', (WidgetTester tester) async {
tester.view.physicalSize = Size.zero;
final controller = TextEditingController(text: 'X');
addTearDown(controller.dispose);
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: Center(child: SearchBar(controller: controller)),
),
);
expect(tester.getSize(find.byType(SearchBar)), Size.zero);
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pump();
expect(find.text('X'), findsOne);
});
}
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.surfaceContainerHigh);
expect(material.elevation, 6.0);
expect(material.shadowColor, colorScheme.shadow);
expect(material.surfaceTintColor, Colors.transparent);
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 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<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return pressedColor;
}
if (states.contains(WidgetState.hovered)) {
return hoveredColor;
}
if (states.contains(WidgetState.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<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return pressedStyle;
}
if (states.contains(WidgetState.hovered)) {
return hoveredStyle;
}
if (states.contains(WidgetState.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,
);
}