// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; 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 kOptions = [ 'aardvark', 'bobcat', 'chameleon', 'dingo', 'elephant', 'flamingo', 'goose', 'hippopotamus', 'iguana', 'jaguar', 'koala', 'lemur', 'mouse', 'northern white rhinoceros', ]; const kOptionsUsers = [ 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 { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late AutocompleteOnSelected lastOnSelected; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; lastOnSelected = onSelected; return Container(key: optionsKey); }, ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Focus the empty field. All the options are displayed. focusNode.requestFocus(); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, kOptions.length); // Enter text. The options are filtered by the text. textEditingController.value = const TextEditingValue( text: 'ele', selection: TextSelection(baseOffset: 3, extentOffset: 3), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); // Select an option. The options hide and the field updates to show the // selection. final String selection = lastOptions.elementAt(1); lastOnSelected(selection); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, selection); // Modify the field text. The options appear again and are filtered. textEditingController.value = const TextEditingValue( text: 'e', selection: TextSelection(baseOffset: 1, extentOffset: 1), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 6); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); expect(lastOptions.elementAt(2), 'goose'); expect(lastOptions.elementAt(3), 'lemur'); expect(lastOptions.elementAt(4), 'mouse'); expect(lastOptions.elementAt(5), 'northern white rhinoceros'); }); testWidgets('can split the field and options', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late AutocompleteOnSelected lastOnSelected; final GlobalKey autocompleteKey = GlobalKey(); final textEditingController = TextEditingController(); final focusNode = FocusNode(); addTearDown(textEditingController.dispose); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( appBar: AppBar( // The field is in the AppBar, not actually a child of RawAutocomplete. title: TextFormField( key: fieldKey, controller: textEditingController, focusNode: focusNode, decoration: const InputDecoration(hintText: 'Split RawAutocomplete App'), onFieldSubmitted: (String value) { RawAutocomplete.onFieldSubmitted(autocompleteKey); }, ), ), body: Align( alignment: Alignment.topLeft, child: RawAutocomplete( key: autocompleteKey, focusNode: focusNode, textEditingController: textEditingController, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }).toList(); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; lastOnSelected = onSelected; return Material( key: optionsKey, elevation: 4.0, child: ListView( children: options .map( (String option) => GestureDetector( onTap: () { onSelected(option); }, child: ListTile(title: Text(option)), ), ) .toList(), ), ); }, ), ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Focus the empty field. All the options are displayed. focusNode.requestFocus(); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, kOptions.length); expect(tester.getSize(find.byKey(optionsKey)).width, greaterThan(0.0)); // Enter text. The options are filtered by the text. textEditingController.value = const TextEditingValue( text: 'ele', selection: TextSelection(baseOffset: 3, extentOffset: 3), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); // Select an option. The options hide and the field updates to show the // selection. final String selection = lastOptions.elementAt(1); lastOnSelected(selection); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, selection); // Modify the field text. The options appear again and are filtered. textEditingController.value = const TextEditingValue( text: 'e', selection: TextSelection(baseOffset: 1, extentOffset: 1), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 6); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); expect(lastOptions.elementAt(2), 'goose'); expect(lastOptions.elementAt(3), 'lemur'); expect(lastOptions.elementAt(4), 'mouse'); expect(lastOptions.elementAt(5), 'northern white rhinoceros'); }); for (final OptionsViewOpenDirection openDirection in OptionsViewOpenDirection.values) { testWidgets('tapping on an option selects it ($openDirection)', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( children: [ const SizedBox(height: 200), RawAutocomplete( optionsViewOpenDirection: openDirection, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Material( elevation: 4.0, child: ListView.builder( key: optionsKey, padding: const EdgeInsets.all(8.0), itemCount: options.length, itemBuilder: (BuildContext context, int index) { final String option = options.elementAt(index); return GestureDetector( onTap: () { onSelected(option); }, child: ListTile(title: Text(option)), ); }, ), ); }, ), ], ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Tap on the text field to open the options. await tester.tap(find.byKey(fieldKey)); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, kOptions.length); await tester.tap(find.text(kOptions[2])); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, equals(kOptions[2])); }); testWidgets('when not enough room for options, options cover field ($openDirection)', ( WidgetTester tester, ) async { const padding = 32.0; final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late StateSetter setState; Alignment alignment = Alignment.bottomCenter; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return Padding( padding: const EdgeInsets.symmetric(horizontal: padding), child: Align( alignment: alignment, child: RawAutocomplete( optionsViewOpenDirection: openDirection, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return ListView.builder( key: optionsKey, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { final String option = options.elementAt(index); return InkWell( onTap: () { onSelected(option); }, child: Padding( padding: const EdgeInsets.all(16.0), child: Text(option), ), ); }, ); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, ), ), ); }, ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byKey(fieldKey)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); await tester.enterText(find.byKey(fieldKey), 'go'); // 3 results. await tester.pump(); switch (openDirection) { case OptionsViewOpenDirection.up: // Options are positioned and sized like normal. expect(find.byType(InkWell), findsNWidgets(3)); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; final double topOfField = tester.getTopLeft(find.byKey(fieldKey)).dy; expect( tester.getTopLeft(find.byType(InkWell).first), Offset(padding, topOfField - 3 * optionHeight), ); expect(tester.getBottomLeft(find.byType(InkWell).at(2)), Offset(padding, topOfField)); case OptionsViewOpenDirection.down: expect(find.byType(InkWell), findsNWidgets(1)); final Size optionsSize = tester.getSize(find.byKey(optionsKey)); expect(optionsSize.height, kMinInteractiveDimension); // Options are positioned as low as possible while still fitting on screen. final double bottomOfField = tester.getBottomLeft(find.byKey(optionsKey)).dy; expect( tester.getTopLeft(find.byKey(optionsKey)), Offset(padding, bottomOfField - optionsSize.height), ); case OptionsViewOpenDirection.mostSpace: // Behaves like OptionsViewOpenDirection.up. expect(find.byType(InkWell), findsNWidgets(3)); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; final double topOfField = tester.getTopLeft(find.byKey(fieldKey)).dy; expect( tester.getTopLeft(find.byType(InkWell).first), Offset(padding, topOfField - 3 * optionHeight), ); expect(tester.getBottomLeft(find.byType(InkWell).at(2)), Offset(padding, topOfField)); } setState(() { alignment = Alignment.topCenter; }); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); switch (openDirection) { case OptionsViewOpenDirection.up: // Options are positioned as high as possible while still fitting on // the screen. expect(find.byType(InkWell), findsNWidgets(1)); final Size optionsSize = tester.getSize(find.byKey(optionsKey)); expect(optionsSize.height, kMinInteractiveDimension); expect(tester.getTopLeft(find.byKey(optionsKey)), const Offset(padding, 0.0)); expect(tester.getBottomLeft(find.byKey(optionsKey)), Offset(padding, optionsSize.height)); case OptionsViewOpenDirection.down: // Options are positioned and sized like normal. expect(find.byType(InkWell), findsNWidgets(3)); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; final double bottomOfField = tester.getBottomLeft(find.byKey(fieldKey)).dy; expect(tester.getTopLeft(find.byType(InkWell).first), Offset(padding, bottomOfField)); expect( tester.getBottomLeft(find.byType(InkWell).at(2)), Offset(padding, bottomOfField + 3 * optionHeight), ); case OptionsViewOpenDirection.mostSpace: // Behaves like OptionsViewOpenDirection.down. expect(find.byType(InkWell), findsNWidgets(3)); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; final double bottomOfField = tester.getBottomLeft(find.byKey(fieldKey)).dy; expect(tester.getTopLeft(find.byType(InkWell).first), Offset(padding, bottomOfField)); expect( tester.getBottomLeft(find.byType(InkWell).at(2)), Offset(padding, bottomOfField + 3 * optionHeight), ); } }); testWidgets('correct options alignment for RTL in direction $openDirection', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); const kOptionsWidth = 100.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Directionality( textDirection: TextDirection.rtl, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: RawAutocomplete( optionsViewOpenDirection: openDirection, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return SizedBox(width: kOptionsWidth, key: optionsKey); }, ), ), ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byType(TextField)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); expect(optionsBox.size.width, kOptionsWidth); expect( tester.getTopRight(find.byKey(optionsKey)).dx, tester.getTopRight(find.byKey(fieldKey)).dx, ); }); testWidgets('options width matches field width with open direction $openDirection', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: RawAutocomplete( optionsViewOpenDirection: openDirection, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Container(key: optionsKey); }, ), ), ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byType(TextField)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); final RenderBox fieldBox = tester.renderObject(find.byKey(fieldKey)); final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); expect(optionsBox.size.width, equals(fieldBox.size.width)); expect(tester.getTopLeft(find.byKey(optionsKey)).dy, switch (openDirection) { OptionsViewOpenDirection.down => tester.getTopLeft(find.byKey(fieldKey)).dy + fieldBox.size.height, OptionsViewOpenDirection.up => tester.getTopLeft(find.byKey(fieldKey)).dy - optionsBox.size.height, OptionsViewOpenDirection.mostSpace => tester.getTopLeft(find.byKey(fieldKey)).dy + fieldBox.size.height, }); }); } testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late AutocompleteOnSelected lastOnSelected; late User lastUserSelected; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptionsUsers.where((User option) { return option.toString().contains(textEditingValue.text.toLowerCase()); }); }, onSelected: (User selected) { lastUserSelected = selected; }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: fieldTextEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; lastOnSelected = onSelected; return Container(key: optionsKey); }, ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Enter text. The options are filtered by the text. focusNode.requestFocus(); textEditingController.value = const TextEditingValue( text: 'example', selection: TextSelection(baseOffset: 7, extentOffset: 7), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), kOptionsUsers[0]); expect(lastOptions.elementAt(1), kOptionsUsers[1]); // Select an option. The options hide and onSelected is called. final User selection = lastOptions.elementAt(1); lastOnSelected(selection); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(lastUserSelected, selection); expect(textEditingController.text, selection.toString()); // Modify the field text. The options appear again and are filtered, this // time by name instead of email. textEditingController.value = const TextEditingValue( text: 'B', selection: TextSelection(baseOffset: 1, extentOffset: 1), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 1); expect(lastOptions.elementAt(0), kOptionsUsers[1]); }); testWidgets('can specify a custom display string for a list of custom User options', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late AutocompleteOnSelected lastOnSelected; late User lastUserSelected; String displayStringForOption(User option) => option.name; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptionsUsers.where((User option) { return option.toString().contains(textEditingValue.text.toLowerCase()); }); }, displayStringForOption: displayStringForOption, onSelected: (User selected) { lastUserSelected = selected; }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { textEditingController = fieldTextEditingController; focusNode = fieldFocusNode; return TextField( key: fieldKey, focusNode: focusNode, controller: fieldTextEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; lastOnSelected = onSelected; return Container(key: optionsKey); }, ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Enter text. The options are filtered by the text. focusNode.requestFocus(); textEditingController.value = const TextEditingValue( text: 'example', selection: TextSelection(baseOffset: 7, extentOffset: 7), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), kOptionsUsers[0]); expect(lastOptions.elementAt(1), kOptionsUsers[1]); // Select an option. The options hide and onSelected is called. The field // has its text set to the selection's display string. final User selection = lastOptions.elementAt(1); lastOnSelected(selection); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(lastUserSelected, selection); expect(textEditingController.text, selection.name); // Modify the field text. The options appear again and are filtered, this // time by name instead of email. textEditingController.value = const TextEditingValue( text: 'B', selection: TextSelection(baseOffset: 1, extentOffset: 1), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 1); expect(lastOptions.elementAt(0), kOptionsUsers[1]); }); testWidgets('onFieldSubmitted selects the first option', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late VoidCallback lastOnFieldSubmitted; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { textEditingController = fieldTextEditingController; focusNode = fieldFocusNode; lastOnFieldSubmitted = onFieldSubmitted; return TextField( key: fieldKey, focusNode: focusNode, controller: fieldTextEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Enter text. The options are filtered by the text. focusNode.requestFocus(); textEditingController.value = const TextEditingValue( text: 'ele', selection: TextSelection(baseOffset: 3, extentOffset: 3), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); // Select the current string, as if the field was submitted. The options // hide and the field updates to show the selection. lastOnFieldSubmitted(); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, lastOptions.elementAt(0)); }); group('optionsViewOpenDirection', () { testWidgets('unset (default behavior): open downward', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) => ['a'], fieldViewBuilder: ( BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted, ) { return TextField(controller: controller, focusNode: focusNode); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return const Text('a'); }, ), ), ), ), ); await tester.showKeyboard(find.byType(TextField)); await tester.pump(); expect( tester.getBottomLeft(find.byType(TextField)), offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))), ); }); testWidgets('down: open downward', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: RawAutocomplete( // ignore: avoid_redundant_argument_values optionsViewOpenDirection: OptionsViewOpenDirection.down, optionsBuilder: (TextEditingValue textEditingValue) => ['a'], fieldViewBuilder: ( BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted, ) { return TextField(controller: controller, focusNode: focusNode); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return const Text('a'); }, ), ), ), ), ); await tester.showKeyboard(find.byType(TextField)); await tester.pump(); expect( tester.getBottomLeft(find.byType(TextField)), offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))), ); }); testWidgets('up: open upward', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: RawAutocomplete( optionsViewOpenDirection: OptionsViewOpenDirection.up, optionsBuilder: (TextEditingValue textEditingValue) => ['a'], fieldViewBuilder: ( BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted, ) { return TextField(controller: controller, focusNode: focusNode); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return const Text('a'); }, ), ), ), ), ); await tester.showKeyboard(find.byType(TextField)); await tester.pump(); expect( tester.getTopLeft(find.byType(TextField)), offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a'))), ); }); testWidgets('auto: open in the direction with more space', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late StateSetter setState; Alignment alignment = Alignment.topCenter; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return Align( alignment: alignment, child: RawAutocomplete( key: fieldKey, optionsViewOpenDirection: OptionsViewOpenDirection.mostSpace, optionsBuilder: (TextEditingValue textEditingValue) => ['a', 'b', 'c'], fieldViewBuilder: ( BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted, ) { return TextField(controller: controller, focusNode: focusNode); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Material( child: ListView( key: optionsKey, children: options.map((String option) => Text(option)).toList(), ), ); }, ), ); }, ), ), ), ); // Show the options. It should open downwards since there is more space. await tester.tap(find.byType(TextField)); await tester.pump(); expect( tester.getBottomLeft(find.byKey(fieldKey)), offsetMoreOrLessEquals(tester.getTopLeft(find.byKey(optionsKey))), ); // Move the field to the bottom. setState(() { alignment = Alignment.bottomCenter; }); await tester.pump(); // The options should now open upwards, since there is more space above. expect( tester.getTopLeft(find.byKey(fieldKey)), offsetMoreOrLessEquals(tester.getBottomLeft(find.byKey(optionsKey))), ); }); group('fieldViewBuilder not passed', () { testWidgets('down', (WidgetTester tester) async { final GlobalKey autocompleteKey = GlobalKey(); final controller = TextEditingController(); addTearDown(controller.dispose); final focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField(controller: controller, focusNode: focusNode), RawAutocomplete( key: autocompleteKey, textEditingController: controller, focusNode: focusNode, // ignore: avoid_redundant_argument_values optionsViewOpenDirection: OptionsViewOpenDirection.down, optionsBuilder: (TextEditingValue textEditingValue) => ['a'], optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return const Text('a'); }, ), ], ), ), ), ); await tester.showKeyboard(find.byType(TextField)); await tester.pump(); expect( tester.getBottomLeft(find.byKey(autocompleteKey)), offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))), ); }); testWidgets('up', (WidgetTester tester) async { final GlobalKey autocompleteKey = GlobalKey(); final controller = TextEditingController(); addTearDown(controller.dispose); final focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ RawAutocomplete( key: autocompleteKey, textEditingController: controller, focusNode: focusNode, optionsViewOpenDirection: OptionsViewOpenDirection.up, optionsBuilder: (TextEditingValue textEditingValue) => ['a'], optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return const Text('a'); }, ), TextField(controller: controller, focusNode: focusNode), ], ), ), ), ); await tester.showKeyboard(find.byType(TextField)); await tester.pump(); expect( tester.getTopLeft(find.byKey(autocompleteKey)), offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a'))), ); }); }); }); testWidgets('options follow field when it moves', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late StateSetter setState; Alignment alignment = Alignment.center; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return Align( alignment: alignment, child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextFormField( controller: fieldTextEditingController, focusNode: focusNode, key: fieldKey, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Container(key: optionsKey); }, ), ); }, ), ), ), ); // Field is shown but not options. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Enter text to show the options. focusNode.requestFocus(); textEditingController.value = const TextEditingValue( text: 'ele', selection: TextSelection(baseOffset: 3, extentOffset: 3), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); // Options are just below the field. final Offset optionsTopLeft = tester.getTopLeft(find.byKey(optionsKey)); final Offset fieldOffset = tester.getTopLeft(find.byKey(fieldKey)); final Size fieldSize = tester.getSize(find.byKey(fieldKey)); expect(optionsTopLeft.dy, fieldOffset.dy + fieldSize.height); // Move the field (similar to as if the keyboard opened). The options move // to follow the field. setState(() { alignment = Alignment.topCenter; }); await tester.pump(); final Offset fieldOffsetFrame1 = tester.getTopLeft(find.byKey(fieldKey)); final Offset optionsTopLeftOpenFrame1 = tester.getTopLeft(find.byKey(optionsKey)); expect(fieldOffsetFrame1.dy, lessThan(fieldOffset.dy)); expect(optionsTopLeftOpenFrame1.dy, isNot(equals(optionsTopLeft.dy))); expect(optionsTopLeftOpenFrame1.dy, fieldOffsetFrame1.dy + fieldSize.height); }); testWidgets('options are shown one frame after tapping in field', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Align( alignment: Alignment.topCenter, child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { return TextFormField( controller: fieldTextEditingController, focusNode: fieldFocusNode, key: fieldKey, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return ListView( key: optionsKey, children: options.map((String option) => Text(option)).toList(), ); }, ), ), ), ), ); // Field is shown but not options. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(find.text('aardvark'), findsNothing); // Tap to show the options. await tester.tap(find.byKey(fieldKey)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(find.text('aardvark'), findsOneWidget); }); testWidgets('can prevent options from showing by returning an empty iterable', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { return const Iterable.empty(); } return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: fieldTextEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Focus the empty field. The options are not displayed because // optionsBuilder returns nothing for an empty field query. focusNode.requestFocus(); textEditingController.value = const TextEditingValue( selection: TextSelection(baseOffset: 0, extentOffset: 0), ); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); // Enter text. Now the options appear, filtered by the text. textEditingController.value = const TextEditingValue( text: 'ele', selection: TextSelection(baseOffset: 3, extentOffset: 3), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); }); testWidgets('can create a field outside of fieldViewBuilder', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); final GlobalKey autocompleteKey = GlobalKey(); late Iterable lastOptions; final focusNode = FocusNode(); addTearDown(focusNode.dispose); final textEditingController = TextEditingController(); addTearDown(textEditingController.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( appBar: AppBar( // This is where the real field is being built. title: TextFormField( key: fieldKey, controller: textEditingController, focusNode: focusNode, onFieldSubmitted: (String value) { RawAutocomplete.onFieldSubmitted(autocompleteKey); }, ), ), body: RawAutocomplete( key: autocompleteKey, focusNode: focusNode, textEditingController: textEditingController, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Enter text. The options are filtered by the text. focusNode.requestFocus(); textEditingController.value = const TextEditingValue( text: 'ele', selection: TextSelection(baseOffset: 3, extentOffset: 3), ); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); // Submit the field. The options hide and the field updates to show the // selection. await tester.showKeyboard(find.byType(TextFormField)); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, lastOptions.elementAt(0)); }); testWidgets('initialValue sets initial text field value', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late AutocompleteOnSelected lastOnSelected; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( // Should initialize text field with 'lem'. initialValue: const TextEditingValue(text: 'lem'), optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; lastOnSelected = onSelected; return Container(key: optionsKey); }, ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // The text editing controller value starts off with initialized value. expect(textEditingController.text, 'lem'); // Focus the empty field. All the options are displayed. focusNode.requestFocus(); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.elementAt(0), 'lemur'); // Select an option. The options hide and the field updates to show the // selection. final String selection = lastOptions.elementAt(0); lastOnSelected(selection); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, selection); }); testWidgets('initialValue cannot be defined if TextEditingController is defined', ( WidgetTester tester, ) async { final focusNode = FocusNode(); addTearDown(focusNode.dispose); final textEditingController = TextEditingController(); addTearDown(textEditingController.dispose); expect(() { RawAutocomplete( focusNode: focusNode, // Both [initialValue] and [textEditingController] cannot be // simultaneously defined. initialValue: const TextEditingValue(text: 'lemur'), textEditingController: textEditingController, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Container(); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { return TextField(focusNode: focusNode, controller: textEditingController); }, ); }, throwsAssertionError); }); testWidgets('support asynchronous options builder', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; late TextEditingController textEditingController; Iterable? lastOptions; Duration? delay; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) async { final Iterable options = kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); if (delay == null) { return options; } return Future>.delayed(delay, () => options); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // Enter text to build the options with delay. focusNode.requestFocus(); delay = const Duration(milliseconds: 500); await tester.enterText(find.byKey(fieldKey), 'go'); await tester.pumpAndSettle(); // The options have not yet been built. expect(find.byKey(optionsKey), findsNothing); expect(lastOptions, isNull); // Await asynchronous options builder. await tester.pumpAndSettle(delay); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions, ['dingo', 'flamingo', 'goose']); // Enter text to rebuild the options without delay. delay = null; await tester.enterText(find.byKey(fieldKey), 'ngo'); await tester.pump(); expect(lastOptions, ['dingo', 'flamingo']); }); testWidgets('can navigate options with the arrow keys', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; late int lastHighlighted; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextFormField( key: fieldKey, focusNode: focusNode, controller: textEditingController, onFieldSubmitted: (String value) { onFieldSubmitted(); }, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastHighlighted = AutocompleteHighlightedOption.of(context); lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // Enter text. The options are filtered by the text. focusNode.requestFocus(); await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); // Move the highlighted option to the second item 'elephant' and select it await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Can't use the key event for enter to submit to the text field using // the test framework, so this appears to be the equivalent. await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(textEditingController.text, 'elephant'); // Modify the field text. The options appear again and are filtered. focusNode.requestFocus(); textEditingController.clear(); await tester.enterText(find.byKey(fieldKey), 'e'); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 6); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); expect(lastOptions.elementAt(2), 'goose'); expect(lastOptions.elementAt(3), 'lemur'); expect(lastOptions.elementAt(4), 'mouse'); expect(lastOptions.elementAt(5), 'northern white rhinoceros'); // Selection does not wrap (going up). await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 0); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 0); // Selection does not wrap (going down). await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 1); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 2); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 3); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 4); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 5); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 5); }); testWidgets('can jump to ends with keyboard', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; late int lastHighlighted; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextFormField( key: fieldKey, focusNode: focusNode, controller: textEditingController, onFieldSubmitted: (String value) { onFieldSubmitted(); }, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastHighlighted = AutocompleteHighlightedOption.of(context); lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // Enter text. The options are filtered by the text. focusNode.requestFocus(); textEditingController.clear(); await tester.enterText(find.byKey(fieldKey), 'e'); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 6); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); expect(lastOptions.elementAt(2), 'goose'); expect(lastOptions.elementAt(3), 'lemur'); expect(lastOptions.elementAt(4), 'mouse'); expect(lastOptions.elementAt(5), 'northern white rhinoceros'); // Jump to the bottom. await tester.sendKeyDownEvent(LogicalKeyboardKey.control); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyUpEvent(LogicalKeyboardKey.control); await tester.pump(); expect(lastHighlighted, 5); // Doesn't wrap down. await tester.sendKeyDownEvent(LogicalKeyboardKey.control); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyUpEvent(LogicalKeyboardKey.control); await tester.pump(); expect(lastHighlighted, 5); // Jump to the top. await tester.sendKeyDownEvent(LogicalKeyboardKey.control); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyUpEvent(LogicalKeyboardKey.control); await tester.pump(); expect(lastHighlighted, 0); // Doesn't wrap up. await tester.sendKeyDownEvent(LogicalKeyboardKey.control); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyUpEvent(LogicalKeyboardKey.control); await tester.pump(); expect(lastHighlighted, 0); }); testWidgets('can navigate with page up/down keys', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; late int lastHighlighted; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextFormField( key: fieldKey, focusNode: focusNode, controller: textEditingController, onFieldSubmitted: (String value) { onFieldSubmitted(); }, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastHighlighted = AutocompleteHighlightedOption.of(context); lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // Enter text. The options are filtered by the text. focusNode.requestFocus(); textEditingController.clear(); await tester.enterText(find.byKey(fieldKey), 'e'); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 6); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); expect(lastOptions.elementAt(2), 'goose'); expect(lastOptions.elementAt(3), 'lemur'); expect(lastOptions.elementAt(4), 'mouse'); expect(lastOptions.elementAt(5), 'northern white rhinoceros'); // Jump down. Stops at the bottom and doesn't wrap. await tester.sendKeyEvent(LogicalKeyboardKey.pageDown); await tester.pump(); expect(lastHighlighted, 4); await tester.sendKeyEvent(LogicalKeyboardKey.pageDown); await tester.pump(); expect(lastHighlighted, 5); await tester.sendKeyEvent(LogicalKeyboardKey.pageDown); await tester.pump(); expect(lastHighlighted, 5); // Jump to the bottom and then jump up a page. await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.pump(); expect(lastHighlighted, 1); await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.pump(); expect(lastHighlighted, 0); await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.pump(); expect(lastHighlighted, 0); }); testWidgets('can hide and show options with the keyboard', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextFormField( key: fieldKey, focusNode: focusNode, controller: textEditingController, onFieldSubmitted: (String value) { onFieldSubmitted(); }, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // Enter text. The options are filtered by the text. focusNode.requestFocus(); await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 2); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); // Hide the options. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Show the options again by pressing arrow keys await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); // Show the options again by re-focusing the field. focusNode.unfocus(); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); focusNode.requestFocus(); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); // Show the options again by editing the text (but not when selecting text // or moving the caret). await tester.enterText(find.byKey(fieldKey), 'elep'); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); textEditingController.selection = TextSelection.fromPosition(const TextPosition(offset: 3)); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); }); testWidgets('re-invokes DismissIntent if options not shown', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; var wrappingActionInvoked = false; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Actions( actions: >{ DismissIntent: CallbackAction( onInvoke: (_) => wrappingActionInvoked = true, ), }, child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; return TextFormField( key: fieldKey, focusNode: focusNode, controller: fieldTextEditingController, onFieldSubmitted: (String value) { onFieldSubmitted(); }, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Container(key: optionsKey); }, ), ), ), ), ); // Enter text to show options. focusNode.requestFocus(); await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); // Hide the options. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(wrappingActionInvoked, false); // Ensure the wrapping Actions can receive the DismissIntent. await tester.sendKeyEvent(LogicalKeyboardKey.escape); expect(wrappingActionInvoked, true); }); testWidgets( 'optionsViewBuilders can use AutocompleteHighlightedOption to highlight selected option', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late int lastHighlighted; late FocusNode focusNode; late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextFormField( key: fieldKey, focusNode: focusNode, controller: textEditingController, onFieldSubmitted: (String value) { onFieldSubmitted(); }, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; lastHighlighted = AutocompleteHighlightedOption.of(context); return Container(key: optionsKey); }, ), ), ), ); // Enter text. The options are filtered by the text. focusNode.requestFocus(); await tester.enterText(find.byKey(fieldKey), 'e'); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, 6); expect(lastOptions.elementAt(0), 'chameleon'); expect(lastOptions.elementAt(1), 'elephant'); expect(lastOptions.elementAt(2), 'goose'); expect(lastOptions.elementAt(3), 'lemur'); expect(lastOptions.elementAt(4), 'mouse'); expect(lastOptions.elementAt(5), 'northern white rhinoceros'); // Move the highlighted option down and check the highlighted index expect(lastHighlighted, 0); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 1); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 2); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 3); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 4); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(lastHighlighted, 5); // And move it back up await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 4); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 3); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 2); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 1); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 0); // Arrow keys do not wrap. await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(lastHighlighted, 0); }, ); testWidgets('floating menu goes away on select', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99749. final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late AutocompleteOnSelected lastOnSelected; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { return TextField( key: fieldKey, focusNode: fieldFocusNode, controller: fieldTextEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOnSelected = onSelected; return Container(key: optionsKey); }, ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.enterText(find.byKey(fieldKey), kOptions[0]); await tester.pumpAndSettle(); expect(find.byKey(optionsKey), findsOneWidget); // Pretend that the only option is selected. This does not change the // text in the text field. lastOnSelected(kOptions[0]); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); }); testWidgets('options width matches field width after rebuilding', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late StateSetter setState; var width = 100.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: StatefulBuilder( builder: (BuildContext context, StateSetter localStateSetter) { setState = localStateSetter; return SizedBox( width: width, child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Container(key: optionsKey); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, ), ); }, ), ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); final RenderBox fieldBox = tester.renderObject(find.byKey(fieldKey)); expect(fieldBox.size.width, 100.0); await tester.tap(find.byType(TextField)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); expect(optionsBox.size.width, 100.0); setState(() { width = 200.0; }); await tester.pump(); // The options width changes to match the field width. expect(fieldBox.size.width, 200.0); expect(optionsBox.size.width, 200.0); }); testWidgets('options width matches field width after changing', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late StateSetter setState; var width = 100.0; final autocomplete = RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Container(key: optionsKey); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return StatefulBuilder( builder: (BuildContext context, StateSetter localStateSetter) { setState = localStateSetter; return SizedBox( width: width, child: TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ), ); }, ); }, ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Padding(padding: const EdgeInsets.symmetric(horizontal: 32.0), child: autocomplete), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); final RenderBox fieldBox = tester.renderObject(find.byKey(fieldKey)); expect(fieldBox.size.width, 100.0); await tester.tap(find.byType(TextField)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); expect(fieldBox.size.width, 100.0); expect(optionsBox.size.width, 100.0); setState(() { width = 200.0; }); await tester.pump(); // The options width changes to match the field width. expect(fieldBox.size.width, 200.0); expect(optionsBox.size.width, 200.0); }); group('screen size', () { Future pumpRawAutocomplete( WidgetTester tester, { GlobalKey? fieldKey, GlobalKey? optionsKey, OptionsViewOpenDirection optionsViewOpenDirection = OptionsViewOpenDirection.down, Alignment alignment = Alignment.topLeft, }) { return tester.pumpWidget( MaterialApp( home: Scaffold( body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Align( alignment: alignment, child: RawAutocomplete( optionsViewOpenDirection: optionsViewOpenDirection, optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return ListView.builder( key: optionsKey, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { final String option = options.elementAt(index); return InkWell( onTap: () { onSelected(option); }, child: Padding( padding: const EdgeInsets.all(16.0), child: Text(option), ), ); }, ); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, ), ), ), ), ), ); } testWidgets('options when screen changes landscape to portrait', (WidgetTester tester) async { // Start with a portrait-sized window, with enough space for all of the // options. const wideWindowSize = Size(1920.0, 1080.0); tester.view.physicalSize = wideWindowSize; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(find.byType(InkWell), findsNWidgets(kOptions.length)); final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); expect( optionsTopLeft1, Offset( tester.getTopLeft(find.byKey(fieldKey)).dx, tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height, ), ); final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; expect( optionsBottomRight1, Offset( tester.getTopLeft(find.byKey(fieldKey)).dx + fieldSize1.width, tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height + optionHeight * kOptions.length, ), ); // Change the screen size to portrait. const narrowWindowSize = Size(1070.0, 1770.0); tester.view.physicalSize = narrowWindowSize; tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byType(InkWell), findsNWidgets(kOptions.length)); expect(tester.getTopLeft(find.byKey(optionsKey)), optionsTopLeft1); final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); expect(fieldSize1.width, greaterThan(fieldSize2.width)); expect(fieldSize1.height, fieldSize2.height); final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsBottomRight2.dx, lessThan(optionsBottomRight1.dx)); expect(optionsBottomRight2.dy, optionsBottomRight1.dy); expect( optionsBottomRight2, Offset( fieldTopLeft2.dx + fieldSize2.width, fieldTopLeft2.dy + fieldSize2.height + optionHeight * kOptions.length, ), ); }); testWidgets('options when screen changes portrait to landscape and overflows', ( WidgetTester tester, ) async { // Start with a portrait-sized window, with enough space for all of the // options. const narrowWindowSize = Size(1070.0, 1770.0); tester.view.physicalSize = narrowWindowSize; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); expect(find.byType(InkWell), findsNWidgets(kOptions.length)); final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); expect( optionsTopLeft1, Offset( tester.getTopLeft(find.byKey(fieldKey)).dx, tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height, ), ); final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; expect( optionsBottomRight1, Offset( tester.getTopLeft(find.byKey(fieldKey)).dx + fieldSize1.width, tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height + optionHeight * kOptions.length, ), ); // Change the screen size to landscape where the options can't all fit on // the screen. const wideWindowSize = Size(1920.0, 580.0); tester.view.physicalSize = wideWindowSize; tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); final int visibleOptions = (wideWindowSize.height / optionHeight).floor(); expect(visibleOptions, lessThan(kOptions.length)); expect(find.byType(InkWell), findsNWidgets(visibleOptions)); expect(tester.getTopLeft(find.byKey(optionsKey)), optionsTopLeft1); final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); expect(fieldSize1.width, lessThan(fieldSize2.width)); expect(fieldSize1.height, fieldSize2.height); final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx)); expect(optionsBottomRight2.dy, lessThan(optionsBottomRight1.dy)); expect( optionsBottomRight2, Offset( fieldTopLeft2.dx + fieldSize2.width, // Options are taking all available space below the field. wideWindowSize.height, ), ); }); testWidgets('screen changes portrait to landscape and overflows', (WidgetTester tester) async { // Start with a portrait-sized window, with enough space for all of the // options. const narrowWindowSize = Size(1070.0, 1770.0); tester.view.physicalSize = narrowWindowSize; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; final double optionsHeight1 = tester.getSize(find.byKey(optionsKey)).height; final int visibleOptions1 = (optionsHeight1 / optionHeight).ceil(); expect(find.byType(InkWell), findsNWidgets(visibleOptions1)); final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); final Offset fieldTopLeft1 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsTopLeft1, Offset(fieldTopLeft1.dx, fieldTopLeft1.dy + fieldSize1.height)); final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); expect( optionsBottomRight1, Offset( fieldTopLeft1.dx + fieldSize1.width, fieldTopLeft1.dy + fieldSize1.height + optionsHeight1, ), ); // Change the screen size to landscape where the options can't all fit on // the screen. const wideWindowSize = Size(1920.0, 580.0); tester.view.physicalSize = wideWindowSize; tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); final double optionsHeight2 = tester.getSize(find.byKey(optionsKey)).height; final int visibleOptions2 = (optionsHeight2 / optionHeight).ceil(); expect(visibleOptions2, lessThan(kOptions.length)); expect(find.byType(InkWell), findsNWidgets(visibleOptions2)); final Offset optionsTopLeft2 = tester.getTopLeft(find.byKey(optionsKey)); expect(optionsTopLeft2, optionsTopLeft1); final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); expect(fieldSize1.width, lessThan(fieldSize2.width)); expect(fieldSize1.height, fieldSize2.height); final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx)); expect( optionsBottomRight2, Offset( fieldTopLeft2.dx + fieldSize2.width, // Options are taking all available space below the field. wideWindowSize.height, ), ); // Shrink the screen further so that the options become smaller than // kMinInteractiveDimension and move to overlap the field. const shortWindowSize = Size(1920.0, 90.0); tester.view.physicalSize = shortWindowSize; tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); const visibleOptions3 = 1; expect(find.byType(InkWell), findsNWidgets(visibleOptions3)); final Offset optionsTopLeft3 = tester.getTopLeft(find.byKey(optionsKey)); expect(optionsTopLeft3.dx, optionsTopLeft1.dx); // The options have moved up, overlapping the field, to still be able to // show kMinInteractiveDimension. expect(optionsTopLeft3.dy, lessThan(optionsTopLeft1.dy)); final Size fieldSize3 = tester.getSize(find.byKey(fieldKey)); final Offset fieldTopLeft3 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsTopLeft3.dy, lessThan(fieldTopLeft3.dy + fieldSize3.height)); expect(fieldSize3.width, fieldSize2.width); expect(fieldSize1.height, fieldSize3.height); final Offset optionsBottomRight3 = tester.getBottomRight(find.byKey(optionsKey)); expect(optionsBottomRight3.dx, greaterThan(optionsBottomRight1.dx)); expect( optionsBottomRight3, Offset(fieldTopLeft3.dx + fieldSize3.width, shortWindowSize.height), ); }); testWidgets('when opening up screen changes portrait to landscape and overflows', ( WidgetTester tester, ) async { // Start with a portrait-sized window, with enough space for all of the // options. const narrowWindowSize = Size(1070.0, 1770.0); tester.view.physicalSize = narrowWindowSize; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); await pumpRawAutocomplete( tester, fieldKey: fieldKey, optionsKey: optionsKey, optionsViewOpenDirection: OptionsViewOpenDirection.up, alignment: Alignment.center, ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); final double optionHeight = tester.getSize(find.byType(InkWell).first).height; final double optionsHeight1 = tester.getSize(find.byKey(optionsKey)).height; final int visibleOptions1 = (optionsHeight1 / optionHeight).ceil(); expect(find.byType(InkWell), findsNWidgets(visibleOptions1)); final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); final Offset fieldTopLeft1 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsTopLeft1, Offset(fieldTopLeft1.dx, fieldTopLeft1.dy - optionsHeight1)); expect(optionsTopLeft1.dy, greaterThan(0.0)); final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); expect(optionsBottomRight1, Offset(fieldTopLeft1.dx + fieldSize1.width, fieldTopLeft1.dy)); // Change the screen size to landscape where the options can't all fit on // the screen. const wideWindowSize = Size(1920.0, 580.0); tester.view.physicalSize = wideWindowSize; tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); final double optionsHeight2 = tester.getSize(find.byKey(optionsKey)).height; expect(optionsHeight2, lessThan(optionsHeight1)); final int visibleOptions2 = (optionsHeight2 / optionHeight).ceil(); expect(visibleOptions2, lessThan(visibleOptions1)); expect(find.byType(InkWell), findsNWidgets(visibleOptions2)); final Offset optionsTopLeft2 = tester.getTopLeft(find.byKey(optionsKey)); final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsTopLeft2, Offset(optionsTopLeft1.dx, fieldTopLeft2.dy - optionsHeight2)); final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); expect(fieldSize1.width, lessThan(fieldSize2.width)); expect(fieldSize1.height, fieldSize2.height); final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx)); expect(optionsBottomRight2, Offset(fieldTopLeft2.dx + fieldSize2.width, fieldTopLeft2.dy)); // Shrink the screen further so that the options become smaller than // kMinInteractiveDimension and move to overlap the field. const shortWindowSize = Size(1920.0, 90.0); tester.view.physicalSize = shortWindowSize; tester.view.devicePixelRatio = 1.0; await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); const visibleOptions3 = 1; expect(find.byType(InkWell), findsNWidgets(visibleOptions3)); final Offset optionsTopLeft3 = tester.getTopLeft(find.byKey(optionsKey)); expect(optionsTopLeft3.dx, optionsTopLeft1.dx); // The options have moved down, overlapping the field, to still be able to // show kMinInteractiveDimension. expect(optionsTopLeft3.dy, lessThan(optionsTopLeft1.dy)); final Size fieldSize3 = tester.getSize(find.byKey(fieldKey)); final Offset fieldTopLeft3 = tester.getTopLeft(find.byKey(fieldKey)); expect(optionsTopLeft3.dy, lessThan(fieldTopLeft3.dy + fieldSize3.height)); expect(fieldSize3.width, fieldSize2.width); expect(fieldSize1.height, fieldSize3.height); final Offset optionsBottomRight3 = tester.getBottomRight(find.byKey(optionsKey)); expect(optionsBottomRight3.dx, greaterThan(optionsBottomRight1.dx)); expect(optionsBottomRight3.dy, greaterThan(fieldTopLeft3.dy)); expect(optionsBottomRight3.dx, fieldTopLeft3.dx + fieldSize3.width); }); }); testWidgets( 'when field scrolled offscreen, options are hidden and not reshown when scrolled back on desktop and web', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView( controller: scrollController, children: [ const SizedBox(height: 1000.0), RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return ListView.builder( key: optionsKey, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { final String option = options.elementAt(index); return InkWell( onTap: () { onSelected(option); }, child: Padding( padding: const EdgeInsets.all(16.0), child: Text(option), ), ); }, ); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, ), const SizedBox(height: 1000.0), ], ), ), ), ); expect(find.byKey(fieldKey), findsNothing); expect(find.byKey(optionsKey), findsNothing); await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byKey(fieldKey)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); // Jump to the beginning. The field is off screen and the options are not // showing either. scrollController.jumpTo(0.0); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsNothing); expect(find.byKey(optionsKey), findsNothing); // Scroll back to the field and ensure it is visible. await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); await tester.pumpAndSettle(); // The options are no longer visible on desktop and web. expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); // Jump to the end. The field is hidden again. scrollController.jumpTo(2000.0); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsNothing); expect(find.byKey(optionsKey), findsNothing); }, variant: TargetPlatformVariant.desktop(), ); testWidgets( 'when field scrolled offscreen, options are hidden and reshown when scrolled back on mobile', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView( controller: scrollController, children: [ const SizedBox(height: 1000.0), RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return ListView.builder( key: optionsKey, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { final String option = options.elementAt(index); return InkWell( onTap: () { onSelected(option); }, child: Padding( padding: const EdgeInsets.all(16.0), child: Text(option), ), ); }, ); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onSubmitted, ) { return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, ), const SizedBox(height: 1000.0), ], ), ), ), ); expect(find.byKey(fieldKey), findsNothing); expect(find.byKey(optionsKey), findsNothing); await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); await tester.tap(find.byKey(fieldKey)); await tester.pump(); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsOneWidget); // Jump to the beginning. The field is off screen and the options are not // showing either. scrollController.jumpTo(0.0); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsNothing); expect(find.byKey(optionsKey), findsNothing); // Scroll back to the field and ensure it is visible. await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); await tester.pumpAndSettle(); // The options remain visible on mobile, but not on web. expect(find.byKey(fieldKey), findsOneWidget); kIsWeb ? expect(find.byKey(optionsKey), findsNothing) : expect(find.byKey(optionsKey), findsOneWidget); // Jump to the end. The field is hidden again. scrollController.jumpTo(2000.0); await tester.pumpAndSettle(); expect(find.byKey(fieldKey), findsNothing); expect(find.byKey(optionsKey), findsNothing); }, variant: TargetPlatformVariant.mobile(), ); testWidgets('can prevent older optionsBuilder results from replacing the new ones', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; late TextEditingController textEditingController; Iterable? lastOptions; Duration? delay; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) async { final Iterable options = kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); if (delay == null) { return options; } return Future>.delayed(delay, () => options); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); const longRequestDelay = Duration(milliseconds: 5000); const shortRequestDelay = Duration(milliseconds: 1000); focusNode.requestFocus(); // Enter the first letter. delay = longRequestDelay; await tester.enterText(find.byKey(fieldKey), 'c'); await tester.pump(); expect(lastOptions, null); // Enter the second letter which resolves faster. delay = shortRequestDelay; await tester.enterText(find.byKey(fieldKey), 'ch'); await tester.pump(); expect(lastOptions, null); // Wait for the short request to resolve. await tester.pump(shortRequestDelay); // lastOptions must contain results from the last request. expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions, ['chameleon']); // Wait for the last timer to finish. await tester.pump(longRequestDelay - shortRequestDelay); expect(lastOptions, ['chameleon']); }); testWidgets('updates result only from the last call made to optionsBuilder', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; late TextEditingController textEditingController; Iterable? lastOptions; Duration? delay; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) async { final Iterable options = kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); if (delay == null) { return options; } return Future>.delayed(delay, () => options); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); focusNode.requestFocus(); const firstRequestDelay = Duration(milliseconds: 1000); const secondRequestDelay = Duration(milliseconds: 2000); const thirdRequestDelay = Duration(milliseconds: 3000); // Enter the first letter. delay = firstRequestDelay; await tester.enterText(find.byKey(fieldKey), 'l'); await tester.pump(); expect(lastOptions, null); // Enter the second letter which resolves slower. delay = secondRequestDelay; await tester.enterText(find.byKey(fieldKey), 'le'); await tester.pump(); expect(lastOptions, null); // Enter the third letter which resolves the slowest. delay = thirdRequestDelay; await tester.enterText(find.byKey(fieldKey), 'lem'); await tester.pump(); expect(lastOptions, null); // lastOptions should get updated only from the last request. await tester.pump(firstRequestDelay); expect(find.byKey(optionsKey), findsNothing); expect(lastOptions, null); await tester.pump(secondRequestDelay - firstRequestDelay); expect(find.byKey(optionsKey), findsNothing); expect(lastOptions, null); await tester.pump(thirdRequestDelay - secondRequestDelay); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions, ['lemur']); }); testWidgets('update options view when field input changes return to the starting keyword', ( WidgetTester tester, ) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; late TextEditingController textEditingController; Iterable? lastOptions; const delay = Duration(milliseconds: 100); await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) async { final Iterable options = kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); return Future>.delayed(delay, () => options); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); // Setup starting point. await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pump(delay); expect(lastOptions, ['chameleon', 'elephant']); expect(find.byKey(optionsKey), findsOneWidget); // Hide options. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); // Enter another letter and then immediately erase it. await tester.enterText(find.byKey(fieldKey), 'eleo'); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pump(delay); expect(lastOptions, ['chameleon', 'elephant']); // Options dropdown should be visible after the last optionsBuilder // call is resolved. expect(find.byKey(optionsKey), findsOneWidget); }); testWidgets('optionsBuilder does not have to be a pure function', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; late TextEditingController textEditingController; Iterable? lastOptions; const delay = Duration(milliseconds: 100); // This is used to tell optionsBuilder to return something different after // being called with "ele" the second time. I.e. it is not a pure function. var timesOptionsBuilderCalledWithEle = 0; final Iterable altEleOptions = ['something new and crazy for ele!']; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) async { if (textEditingValue.text == 'ele') { timesOptionsBuilderCalledWithEle += 1; if (timesOptionsBuilderCalledWithEle > 1) { return Future>.delayed(delay, () => altEleOptions); } } final Iterable options = kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); return Future>.delayed(delay, () => options); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pump(delay); expect(lastOptions, ['chameleon', 'elephant']); expect(find.byKey(optionsKey), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); await tester.enterText(find.byKey(fieldKey), 'eleo'); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); await tester.enterText(find.byKey(fieldKey), 'ele'); await tester.pump(delay); expect(lastOptions, altEleOptions); expect(find.byKey(optionsKey), findsOneWidget); }); testWidgets('Autocomplete Semantics announcement', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; late FocusNode focusNode; late TextEditingController textEditingController; const localizations = DefaultWidgetsLocalizations(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; textEditingController = fieldTextEditingController; return TextField( key: fieldKey, focusNode: focusNode, controller: textEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOptions = options; return Container(key: optionsKey); }, ), ), ), ); expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); expect(tester.takeAnnouncements(), isEmpty); focusNode.requestFocus(); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, kOptions.length); expect( tester.takeAnnouncements().first, isAccessibilityAnnouncement(localizations.searchResultsFound), ); await tester.enterText(find.byKey(fieldKey), 'a'); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, greaterThan(0)); expect(tester.takeAnnouncements(), isEmpty); await tester.enterText(find.byKey(fieldKey), 'zzzz'); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); expect( tester.takeAnnouncements().first, isAccessibilityAnnouncement(localizations.noResultsFound), ); handle.dispose(); }); testWidgets('RawAutocomplete renders at zero area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: SizedBox.shrink( child: Scaffold( body: RawAutocomplete( initialValue: const TextEditingValue(text: 'X'), optionsBuilder: (TextEditingValue textEditingValue) => ['Y'], fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback voidCallBack, ) => TextField(controller: textEditingController), optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) => Container(), ), ), ), ), ), ); final Finder xText = find.text('X'); expect(tester.getSize(xText).isEmpty, isTrue); }); testWidgets( 'floating menu is displayed when the field is focused and already has a selected value', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/177429. const fieldKey = Key('field'); const optionsKey = Key('options'); late AutocompleteOnSelected lastOnSelected; late FocusNode focusNode; await tester.pumpWidget( MaterialApp( home: Scaffold( body: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { return kOptions.where((String option) { return option.contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { focusNode = fieldFocusNode; return TextField( key: fieldKey, focusNode: fieldFocusNode, controller: fieldTextEditingController, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { lastOnSelected = onSelected; return Container(key: optionsKey); }, ), ), ), ); // The field is always rendered, but the options are not unless needed. expect(find.byKey(fieldKey), findsOne); expect(find.byKey(optionsKey), findsNothing); await tester.enterText(find.byKey(fieldKey), kOptions[0]); await tester.pumpAndSettle(); expect(find.byKey(optionsKey), findsOne); lastOnSelected(kOptions[0]); await tester.pump(); expect(find.byKey(optionsKey), findsNothing); focusNode.unfocus(); await tester.pump(); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(find.byKey(optionsKey), findsOne); }, ); }