blob: 37c6f8933782ccf9538086ab5d103cfba5ff33dd [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/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 List<String> kOptions = <String>[
'aardvark',
'bobcat',
'chameleon',
'dingo',
'elephant',
'flamingo',
'goose',
'hippopotamus',
'iguana',
'jaguar',
'koala',
'lemur',
'mouse',
'northern white rhinoceros',
];
const List<User> kOptionsUsers = <User>[
User(name: 'Alice', email: 'alice@example.com'),
User(name: 'Bob', email: 'bob@example.com'),
User(name: 'Charlie', email: 'charlie123@gmail.com'),
];
testWidgets('can filter and select a list of string options', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late AutocompleteOnSelected<String> lastOnSelected;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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('tapping on an option selects it', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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('can filter and select a list of custom User options', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<User> lastOptions;
late AutocompleteOnSelected<User> lastOnSelected;
late User lastUserSelected;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<User>(
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<User> onSelected, Iterable<User> 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<User> lastOptions;
late AutocompleteOnSelected<User> lastOnSelected;
late User lastUserSelected;
String displayStringForOption(User option) => option.name;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<User>(
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<User> onSelected, Iterable<User> 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<String> lastOptions;
late VoidCallback lastOnFieldSubmitted;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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));
});
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<String>(
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<String> onSelected, Iterable<String> 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 optionsOffset = tester.getTopLeft(find.byKey(optionsKey));
Offset fieldOffset = tester.getTopLeft(find.byKey(fieldKey));
final Size fieldSize = tester.getSize(find.byKey(fieldKey));
expect(optionsOffset.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();
fieldOffset = tester.getTopLeft(find.byKey(fieldKey));
final Offset optionsOffsetOpen = tester.getTopLeft(find.byKey(optionsKey));
expect(optionsOffsetOpen.dy, isNot(equals(optionsOffset.dy)));
expect(optionsOffsetOpen.dy, fieldOffset.dy + fieldSize.height);
});
testWidgets('can prevent options from showing by returning an empty iterable', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == null || textEditingValue.text == '') {
return const Iterable<String>.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<String> onSelected, Iterable<String> 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<String> lastOptions;
final FocusNode focusNode = FocusNode();
final TextEditingController textEditingController = TextEditingController();
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<String>(
key: autocompleteKey,
focusNode: focusNode,
textEditingController: textEditingController,
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> 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<String> lastOptions;
late AutocompleteOnSelected<String> lastOnSelected;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
// 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<String> onSelected, Iterable<String> 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 = FocusNode();
final TextEditingController textEditingController = TextEditingController();
expect(
() {
RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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<String>? lastOptions;
Duration? delay;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) async {
final Iterable<String> options = kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
if (delay == null) {
return options;
}
return Future<Iterable<String>>.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<String> onSelected, Iterable<String> 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, <String>['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, <String>['dingo', 'flamingo']);
});
testWidgets('can navigate options with the keyboard', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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');
// 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');
// The selection should wrap at the top and bottom. Move up to 'mouse'
// and then back down to 'goose' and select it.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(find.byKey(fieldKey), findsOneWidget);
expect(find.byKey(optionsKey), findsNothing);
expect(textEditingController.text, 'goose');
});
testWidgets('can hide and show options with the keyboard', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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;
bool wrappingActionInvoked = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Actions(
actions: <Type, Action<Intent>>{
DismissIntent: CallbackAction<DismissIntent>(
onInvoke: (_) => wrappingActionInvoked = true,
),
},
child: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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<String> lastOptions;
late int lastHighlighted;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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);
// And move it back up
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);
// Going back up should wrap around
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(lastHighlighted, 5);
});
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<String> lastOnSelected;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
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<String> onSelected, Iterable<String> 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);
});
}