blob: a328dc849f58255bd44e946cef038c367e08e3aa [file] [log] [blame] [edit]
// 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 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('can split the field and options', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late AutocompleteOnSelected<String> lastOnSelected;
final GlobalKey autocompleteKey = GlobalKey();
final TextEditingController textEditingController = TextEditingController();
final FocusNode 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<String>(autocompleteKey);
},
),
),
body: Align(
alignment: Alignment.topLeft,
child: RawAutocomplete<String>(
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<String> onSelected,
Iterable<String> 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<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
const SizedBox(height: 200),
RawAutocomplete<String>(
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<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('when not enough room for options, options cover field ($openDirection)', (
WidgetTester tester,
) async {
const double 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<String>(
optionsViewOpenDirection: openDirection,
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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),
);
}
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),
);
}
});
testWidgets('correct options alignment for RTL in direction $openDirection', (
WidgetTester tester,
) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
const double 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<String>(
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<String> onSelected,
Iterable<String> 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<String>(
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<String> onSelected,
Iterable<String> 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,
});
});
}
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));
});
group('optionsViewOpenDirection', () {
testWidgets('unset (default behavior): open downward', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
fieldViewBuilder:
(
BuildContext context,
TextEditingController controller,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
return TextField(controller: controller, focusNode: focusNode);
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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<String>(
optionsViewOpenDirection:
OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
fieldViewBuilder:
(
BuildContext context,
TextEditingController controller,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
return TextField(controller: controller, focusNode: focusNode);
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.up,
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
fieldViewBuilder:
(
BuildContext context,
TextEditingController controller,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
return TextField(controller: controller, focusNode: focusNode);
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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'))),
);
});
group('fieldViewBuilder not passed', () {
testWidgets('down', (WidgetTester tester) async {
final GlobalKey autocompleteKey = GlobalKey();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextField(controller: controller, focusNode: focusNode),
RawAutocomplete<String>(
key: autocompleteKey,
textEditingController: controller,
focusNode: focusNode,
optionsViewOpenDirection:
OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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 TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RawAutocomplete<String>(
key: autocompleteKey,
textEditingController: controller,
focusNode: focusNode,
optionsViewOpenDirection: OptionsViewOpenDirection.up,
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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<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 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<String>(
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<String> onSelected,
Iterable<String> 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<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (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();
addTearDown(focusNode.dispose);
final TextEditingController 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<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();
addTearDown(focusNode.dispose);
final TextEditingController textEditingController = TextEditingController();
addTearDown(textEditingController.dispose);
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 arrow keys', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
late int lastHighlighted;
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,
) {
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<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
late int lastHighlighted;
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,
) {
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<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
late int lastHighlighted;
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,
) {
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<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);
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<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);
});
testWidgets('options width matches field width after rebuilding', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late StateSetter setState;
double 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<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) {
return Container(key: optionsKey);
},
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;
double width = 100.0;
final RawAutocomplete<String> autocomplete = RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) {
return Container(key: optionsKey);
},
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<void> 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<String>(
optionsViewOpenDirection: optionsViewOpenDirection,
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> 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 Size 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 Size 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 Size 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 Size 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 Size 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 Size 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 Size shortWindowSize = Size(1920.0, 90.0);
tester.view.physicalSize = shortWindowSize;
tester.view.devicePixelRatio = 1.0;
await tester.pumpAndSettle();
expect(find.byKey(fieldKey), findsOneWidget);
const int 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 Size 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 Size 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 Size shortWindowSize = Size(1920.0, 90.0);
tester.view.physicalSize = shortWindowSize;
tester.view.devicePixelRatio = 1.0;
await tester.pumpAndSettle();
expect(find.byKey(fieldKey), findsOneWidget);
const int 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 = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ListView(
controller: scrollController,
children: <Widget>[
const SizedBox(height: 1000.0),
RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) {
return 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 = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ListView(
controller: scrollController,
children: <Widget>[
const SizedBox(height: 1000.0),
RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) {
return 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<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);
},
),
),
),
);
const Duration longRequestDelay = Duration(milliseconds: 5000);
const Duration 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, <String>['chameleon']);
// Wait for the last timer to finish.
await tester.pump(longRequestDelay - shortRequestDelay);
expect(lastOptions, <String>['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<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);
},
),
),
),
);
focusNode.requestFocus();
const Duration firstRequestDelay = Duration(milliseconds: 1000);
const Duration secondRequestDelay = Duration(milliseconds: 2000);
const Duration 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, <String>['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<String>? lastOptions;
const Duration delay = Duration(milliseconds: 100);
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());
});
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);
},
),
),
),
);
// Setup starting point.
await tester.enterText(find.byKey(fieldKey), 'ele');
await tester.pump(delay);
expect(lastOptions, <String>['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, <String>['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<String>? lastOptions;
const Duration 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.
int timesOptionsBuilderCalledWithEle = 0;
final Iterable<String> altEleOptions = <String>['something new and crazy for ele!'];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text == 'ele') {
timesOptionsBuilderCalledWithEle += 1;
if (timesOptionsBuilderCalledWithEle > 1) {
return Future<Iterable<String>>.delayed(delay, () => altEleOptions);
}
}
final Iterable<String> options = kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
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);
},
),
),
),
);
await tester.enterText(find.byKey(fieldKey), 'ele');
await tester.pump(delay);
expect(lastOptions, <String>['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<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
const DefaultWidgetsLocalizations localizations = DefaultWidgetsLocalizations();
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 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.message, 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.message, 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<String>(
initialValue: const TextEditingValue(text: 'X'),
optionsBuilder: (TextEditingValue textEditingValue) => <String>['Y'],
fieldViewBuilder:
(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback voidCallBack,
) => TextField(controller: textEditingController),
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) => Container(),
),
),
),
),
),
);
final Finder xText = find.text('X');
expect(tester.getSize(xText).isEmpty, isTrue);
});
}