| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| |
| class User { |
| const User({ |
| required this.email, |
| required this.name, |
| }); |
| |
| final String email; |
| final String name; |
| |
| @override |
| String toString() { |
| return '$name, $email'; |
| } |
| } |
| |
| void main() { |
| const List<String> kOptions = <String>[ |
| 'aardvark', |
| 'bobcat', |
| 'chameleon', |
| 'dingo', |
| 'elephant', |
| 'flamingo', |
| 'goose', |
| 'hippopotamus', |
| 'iguana', |
| 'jaguar', |
| 'koala', |
| 'lemur', |
| 'mouse', |
| 'northern white rhinoceros', |
| ]; |
| |
| const List<User> kOptionsUsers = <User>[ |
| User(name: 'Alice', email: 'alice@example.com'), |
| User(name: 'Bob', email: 'bob@example.com'), |
| User(name: 'Charlie', email: 'charlie123@gmail.com'), |
| ]; |
| |
| testWidgets('can filter and select a list of string options', (WidgetTester tester) async { |
| late String lastSelection; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<String>( |
| onSelected: (String selection) { |
| lastSelection = selection; |
| }, |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The field is always rendered, but the options are not unless needed. |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| |
| // Focus the empty field. All the options are displayed. |
| await tester.tap(find.byType(TextFormField)); |
| await tester.pump(); |
| expect(find.byType(ListView), findsOneWidget); |
| ListView list = find.byType(ListView).evaluate().first.widget as ListView; |
| expect(list.semanticChildCount, kOptions.length); |
| |
| // Enter text. The options are filtered by the text. |
| await tester.enterText(find.byType(TextFormField), 'ele'); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsOneWidget); |
| list = find.byType(ListView).evaluate().first.widget as ListView; |
| // 'chameleon' and 'elephant' are displayed. |
| expect(list.semanticChildCount, 2); |
| |
| // Select a option. The options hide and the field updates to show the |
| // selection. |
| await tester.tap(find.byType(InkWell).first); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField; |
| expect(field.controller!.text, 'chameleon'); |
| expect(lastSelection, 'chameleon'); |
| |
| // Modify the field text. The options appear again and are filtered. |
| await tester.enterText(find.byType(TextFormField), 'e'); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsOneWidget); |
| list = find.byType(ListView).evaluate().first.widget as ListView; |
| // 'chameleon', 'elephant', 'goose', 'lemur', 'mouse', and |
| // 'northern white rhinoceros' are displayed. |
| expect(list.semanticChildCount, 6); |
| }); |
| |
| testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<User>( |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptionsUsers.where((User option) { |
| return option.toString().contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The field is always rendered, but the options are not unless needed. |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| |
| // Focus the empty field. All the options are displayed. |
| await tester.tap(find.byType(TextFormField)); |
| await tester.pump(); |
| expect(find.byType(ListView), findsOneWidget); |
| ListView list = find.byType(ListView).evaluate().first.widget as ListView; |
| expect(list.semanticChildCount, kOptionsUsers.length); |
| |
| // Enter text. The options are filtered by the text. |
| await tester.enterText(find.byType(TextFormField), 'example'); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsOneWidget); |
| list = find.byType(ListView).evaluate().first.widget as ListView; |
| // 'Alice' and 'Bob' are displayed because they have "example.com" emails. |
| expect(list.semanticChildCount, 2); |
| |
| // Select a option. The options hide and the field updates to show the |
| // selection. |
| await tester.tap(find.byType(InkWell).first); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField; |
| expect(field.controller!.text, 'Alice, alice@example.com'); |
| |
| // Modify the field text. The options appear again and are filtered. |
| await tester.enterText(find.byType(TextFormField), 'B'); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsOneWidget); |
| list = find.byType(ListView).evaluate().first.widget as ListView; |
| // 'Bob' is displayed. |
| expect(list.semanticChildCount, 1); |
| }); |
| |
| testWidgets('displayStringForOption is displayed in the options', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<User>( |
| displayStringForOption: (User option) { |
| return option.name; |
| }, |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptionsUsers.where((User option) { |
| return option.toString().contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The field is always rendered, but the options are not unless needed. |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| |
| // Focus the empty field. All the options are displayed, and the string that |
| // is used comes from displayStringForOption. |
| await tester.tap(find.byType(TextFormField)); |
| await tester.pump(); |
| expect(find.byType(ListView), findsOneWidget); |
| final ListView list = find.byType(ListView).evaluate().first.widget as ListView; |
| expect(list.semanticChildCount, kOptionsUsers.length); |
| for (int i = 0; i < kOptionsUsers.length; i++) { |
| expect(find.text(kOptionsUsers[i].name), findsOneWidget); |
| } |
| |
| // Select a option. The options hide and the field updates to show the |
| // selection. The text in the field is given by displayStringForOption. |
| await tester.tap(find.byType(InkWell).first); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField; |
| expect(field.controller!.text, kOptionsUsers.first.name); |
| }); |
| |
| testWidgets('can build a custom field', (WidgetTester tester) async { |
| final GlobalKey fieldKey = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<String>( |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { |
| return Container(key: fieldKey); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The custom field is rendered and not the default TextFormField. |
| expect(find.byKey(fieldKey), findsOneWidget); |
| expect(find.byType(TextFormField), findsNothing); |
| }); |
| |
| testWidgets('can build custom options', (WidgetTester tester) async { |
| final GlobalKey optionsKey = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<String>( |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { |
| return Container(key: optionsKey); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The default field is rendered but not the options, yet. |
| expect(find.byKey(optionsKey), findsNothing); |
| expect(find.byType(TextFormField), findsOneWidget); |
| |
| // Focus the empty field. The custom options is displayed. |
| await tester.tap(find.byType(TextFormField)); |
| await tester.pump(); |
| expect(find.byKey(optionsKey), findsOneWidget); |
| }); |
| |
| testWidgets('the default Autocomplete options widget has a maximum height of 200', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp(home: Scaffold( |
| body: Autocomplete<String>( |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ))); |
| |
| final Finder listFinder = find.byType(ListView); |
| final Finder inputFinder = find.byType(TextFormField); |
| await tester.tap(inputFinder); |
| await tester.enterText(inputFinder, ''); |
| await tester.pump(); |
| final Size baseSize = tester.getSize(listFinder); |
| final double resultingHeight = baseSize.height; |
| expect(resultingHeight, equals(200)); |
| }); |
| |
| testWidgets('the options height restricts to max desired height', (WidgetTester tester) async { |
| const double desiredHeight = 150.0; |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<String>( |
| optionsMaxHeight: desiredHeight, |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ))); |
| |
| /// entering "a" returns 9 items from kOptions so basically the |
| /// height of 9 options would be beyond `desiredHeight=150`, |
| /// so height gets restricted to desiredHeight. |
| final Finder listFinder = find.byType(ListView); |
| final Finder inputFinder = find.byType(TextFormField); |
| await tester.tap(inputFinder); |
| await tester.enterText(inputFinder, 'a'); |
| await tester.pump(); |
| final Size baseSize = tester.getSize(listFinder); |
| final double resultingHeight = baseSize.height; |
| |
| /// expected desired Height =150.0 |
| expect(resultingHeight, equals(desiredHeight)); |
| }); |
| |
| testWidgets('The height of options shrinks to height of resulting items, if less than maxHeight', (WidgetTester tester) async { |
| // Returns a Future with the height of the default [Autocomplete] options widget |
| // after the provided text had been entered into the [Autocomplete] field. |
| Future<double> _getDefaultOptionsHeight( |
| WidgetTester tester, String enteredText) async { |
| final Finder listFinder = find.byType(ListView); |
| final Finder inputFinder = find.byType(TextFormField); |
| final TextFormField field = inputFinder.evaluate().first.widget as TextFormField; |
| field.controller!.clear(); |
| await tester.tap(inputFinder); |
| await tester.enterText(inputFinder, enteredText); |
| await tester.pump(); |
| final Size baseSize = tester.getSize(listFinder); |
| return baseSize.height; |
| } |
| |
| const double maxOptionsHeight = 250.0; |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<String>( |
| optionsMaxHeight: maxOptionsHeight, |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ))); |
| |
| final Finder listFinder = find.byType(ListView); |
| expect(listFinder, findsNothing); |
| |
| // Entering `a` returns 9 items(height > `maxOptionsHeight`) from the kOptions |
| // so height gets restricted to `maxOptionsHeight =250`. |
| final double nineItemsHeight = await _getDefaultOptionsHeight(tester, 'a'); |
| expect(nineItemsHeight, equals(maxOptionsHeight)); |
| |
| // Returns 2 Items (height < `maxOptionsHeight`) |
| // so options height shrinks to 2 Items combined height. |
| final double twoItemsHeight = await _getDefaultOptionsHeight(tester, 'el'); |
| expect(twoItemsHeight, lessThan(maxOptionsHeight)); |
| |
| // Returns 1 item (height < `maxOptionsHeight`) from `kOptions` |
| // so options height shrinks to 1 items height. |
| final double oneItemsHeight = await _getDefaultOptionsHeight(tester, 'elep'); |
| expect(oneItemsHeight, lessThan(twoItemsHeight)); |
| }); |
| |
| testWidgets('initialValue sets initial text field value', (WidgetTester tester) async { |
| late String lastSelection; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Autocomplete<String>( |
| initialValue: const TextEditingValue(text: 'lem'), |
| onSelected: (String selection) { |
| lastSelection = selection; |
| }, |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The field is always rendered, but the options are not unless needed. |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| expect( |
| tester.widget<TextFormField>(find.byType(TextFormField)).controller!.text, |
| 'lem', |
| ); |
| |
| // Focus the empty field. All the options are displayed. |
| await tester.tap(find.byType(TextFormField)); |
| await tester.pump(); |
| expect(find.byType(ListView), findsOneWidget); |
| final ListView list = find.byType(ListView).evaluate().first.widget as ListView; |
| // Displays just one option ('lemur'). |
| expect(list.semanticChildCount, 1); |
| |
| // Select a option. The options hide and the field updates to show the |
| // selection. |
| await tester.tap(find.byType(InkWell).first); |
| await tester.pump(); |
| expect(find.byType(TextFormField), findsOneWidget); |
| expect(find.byType(ListView), findsNothing); |
| final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField; |
| expect(field.controller!.text, 'lemur'); |
| expect(lastSelection, 'lemur'); |
| }); |
| |
| // Ensures that the option with the given label has a given background color |
| // if given, or no background if color is null. |
| void checkOptionHighlight(WidgetTester tester, String label, Color? color) { |
| final RenderBox renderBox = tester.renderObject<RenderBox>(find.ancestor(matching: find.byType(Container), of: find.text(label))); |
| if (color != null) { |
| // Check to see that the container is painted with the highlighted background color. |
| expect(renderBox, paints..rect(color: color)); |
| } else { |
| // There should only be a paragraph painted. |
| expect(renderBox, paintsExactlyCountTimes(const Symbol('drawRect'), 0)); |
| expect(renderBox, paints..paragraph()); |
| } |
| } |
| |
| testWidgets('keyboard navigation of the options properly highlights the option', (WidgetTester tester) async { |
| const Color highlightColor = Color(0xFF112233); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData.light().copyWith( |
| focusColor: highlightColor, |
| ), |
| home: Scaffold( |
| body: Autocomplete<String>( |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextFormField)); |
| await tester.enterText(find.byType(TextFormField), 'el'); |
| await tester.pump(); |
| expect(find.byType(ListView), findsOneWidget); |
| final ListView list = find.byType(ListView).evaluate().first.widget as ListView; |
| expect(list.semanticChildCount, 2); |
| |
| // Initially the first option should be highlighted |
| checkOptionHighlight(tester, 'chameleon', highlightColor); |
| checkOptionHighlight(tester, 'elephant', null); |
| |
| // Move the selection down |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| |
| // Highlight should be moved to the second item |
| checkOptionHighlight(tester, 'chameleon', null); |
| checkOptionHighlight(tester, 'elephant', highlightColor); |
| }); |
| |
| testWidgets('keyboard navigation keeps the highlighted option scrolled into view', (WidgetTester tester) async { |
| const Color highlightColor = Color(0xFF112233); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData.light().copyWith( |
| focusColor: highlightColor, |
| ), |
| home: Scaffold( |
| body: Autocomplete<String>( |
| optionsBuilder: (TextEditingValue textEditingValue) { |
| return kOptions.where((String option) { |
| return option.contains(textEditingValue.text.toLowerCase()); |
| }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextFormField)); |
| await tester.enterText(find.byType(TextFormField), 'e'); |
| await tester.pump(); |
| expect(find.byType(ListView), findsOneWidget); |
| final ListView list = find.byType(ListView).evaluate().first.widget as ListView; |
| expect(list.semanticChildCount, 6); |
| |
| // Highlighted item should be at the top |
| expect(tester.getTopLeft(find.text('chameleon')).dy, equals(64.0)); |
| checkOptionHighlight(tester, 'chameleon', highlightColor); |
| |
| // Move down the list of options |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| |
| // First item should have scrolled off the top, and not be selected. |
| expect(find.text('chameleon'), findsNothing); |
| |
| // Highlighted item 'lemur' should be centered in the options popup |
| expect(tester.getTopLeft(find.text('mouse')).dy, equals(187.0)); |
| checkOptionHighlight(tester, 'mouse', highlightColor); |
| |
| // The other items on screen should not be selected. |
| checkOptionHighlight(tester, 'goose', null); |
| checkOptionHighlight(tester, 'lemur', null); |
| checkOptionHighlight(tester, 'northern white rhinoceros', null); |
| }); |
| } |