909 lines
32 KiB
Dart
909 lines
32 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../widgets/semantics_tester.dart';
|
|
|
|
class User {
|
|
const User({required this.email, required this.name});
|
|
|
|
final String email;
|
|
final String name;
|
|
|
|
@override
|
|
String toString() {
|
|
return '$name, $email';
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
const kOptions = <String>[
|
|
'aardvark',
|
|
'bobcat',
|
|
'chameleon',
|
|
'dingo',
|
|
'elephant',
|
|
'flamingo',
|
|
'goose',
|
|
'hippopotamus',
|
|
'iguana',
|
|
'jaguar',
|
|
'koala',
|
|
'lemur',
|
|
'mouse',
|
|
'northern white rhinoceros',
|
|
];
|
|
|
|
const kOptionsUsers = <User>[
|
|
User(name: 'Alice', email: 'alice@example.com'),
|
|
User(name: 'Bob', email: 'bob@example.com'),
|
|
User(name: 'Charlie', email: 'charlie123@gmail.com'),
|
|
];
|
|
|
|
testWidgets('can filter and select a list of string options', (WidgetTester tester) async {
|
|
late String lastSelection;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
onSelected: (String selection) {
|
|
lastSelection = selection;
|
|
},
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The field is always rendered, but the options are not unless needed.
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
|
|
// Focus the empty field. All the options are displayed.
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
var list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
expect(list.semanticChildCount, kOptions.length);
|
|
|
|
// Enter text. The options are filtered by the text.
|
|
await tester.enterText(find.byType(TextFormField), 'ele');
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
// 'chameleon' and 'elephant' are displayed.
|
|
expect(list.semanticChildCount, 2);
|
|
|
|
// Select a option. The options hide and the field updates to show the
|
|
// selection.
|
|
await tester.tap(find.byType(InkWell).first);
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
final field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
|
|
expect(field.controller!.text, 'chameleon');
|
|
expect(lastSelection, 'chameleon');
|
|
|
|
// Modify the field text. The options appear again and are filtered.
|
|
await tester.enterText(find.byType(TextFormField), 'e');
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
// 'chameleon', 'elephant', 'goose', 'lemur', 'mouse', and
|
|
// 'northern white rhinoceros' are displayed.
|
|
expect(list.semanticChildCount, 6);
|
|
});
|
|
|
|
testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<User>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptionsUsers.where((User option) {
|
|
return option.toString().contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The field is always rendered, but the options are not unless needed.
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
|
|
// Focus the empty field. All the options are displayed.
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
var list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
expect(list.semanticChildCount, kOptionsUsers.length);
|
|
|
|
// Enter text. The options are filtered by the text.
|
|
await tester.enterText(find.byType(TextFormField), 'example');
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
// 'Alice' and 'Bob' are displayed because they have "example.com" emails.
|
|
expect(list.semanticChildCount, 2);
|
|
|
|
// Select a option. The options hide and the field updates to show the
|
|
// selection.
|
|
await tester.tap(find.byType(InkWell).first);
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
final field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
|
|
expect(field.controller!.text, 'Alice, alice@example.com');
|
|
|
|
// Modify the field text. The options appear again and are filtered.
|
|
await tester.enterText(find.byType(TextFormField), 'B');
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
// 'Bob' is displayed.
|
|
expect(list.semanticChildCount, 1);
|
|
});
|
|
|
|
testWidgets('displayStringForOption is displayed in the options', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<User>(
|
|
displayStringForOption: (User option) {
|
|
return option.name;
|
|
},
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptionsUsers.where((User option) {
|
|
return option.toString().contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The field is always rendered, but the options are not unless needed.
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
|
|
// Focus the empty field. All the options are displayed, and the string that
|
|
// is used comes from displayStringForOption.
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
final list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
expect(list.semanticChildCount, kOptionsUsers.length);
|
|
for (var i = 0; i < kOptionsUsers.length; i++) {
|
|
expect(find.text(kOptionsUsers[i].name), findsOneWidget);
|
|
}
|
|
|
|
// Select a option. The options hide and the field updates to show the
|
|
// selection. The text in the field is given by displayStringForOption.
|
|
await tester.tap(find.byType(InkWell).first);
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
final field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
|
|
expect(field.controller!.text, kOptionsUsers.first.name);
|
|
});
|
|
|
|
testWidgets('can build a custom field', (WidgetTester tester) async {
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
fieldViewBuilder:
|
|
(
|
|
BuildContext context,
|
|
TextEditingController textEditingController,
|
|
FocusNode focusNode,
|
|
VoidCallback onFieldSubmitted,
|
|
) {
|
|
return Container(key: fieldKey);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The custom field is rendered and not the default TextFormField.
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byType(TextFormField), findsNothing);
|
|
});
|
|
|
|
testWidgets('can build custom options', (WidgetTester tester) async {
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
optionsViewBuilder:
|
|
(
|
|
BuildContext context,
|
|
AutocompleteOnSelected<String> onSelected,
|
|
Iterable<String> options,
|
|
) {
|
|
return Container(key: optionsKey);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The default field is rendered but not the options, yet.
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
|
|
// Focus the empty field. The custom options is displayed.
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.pump();
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the default Autocomplete options widget has a maximum height of 200', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder listFinder = find.byType(ListView);
|
|
final Finder inputFinder = find.byType(TextFormField);
|
|
await tester.tap(inputFinder);
|
|
await tester.enterText(inputFinder, '');
|
|
await tester.pump();
|
|
final Size baseSize = tester.getSize(listFinder);
|
|
final double resultingHeight = baseSize.height;
|
|
expect(resultingHeight, equals(200));
|
|
});
|
|
|
|
testWidgets('the options height restricts to max desired height', (WidgetTester tester) async {
|
|
const desiredHeight = 150.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsMaxHeight: desiredHeight,
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
/// entering "a" returns 9 items from kOptions so basically the
|
|
/// height of 9 options would be beyond `desiredHeight=150`,
|
|
/// so height gets restricted to desiredHeight.
|
|
final Finder listFinder = find.byType(ListView);
|
|
final Finder inputFinder = find.byType(TextFormField);
|
|
await tester.tap(inputFinder);
|
|
await tester.enterText(inputFinder, 'a');
|
|
await tester.pump();
|
|
final Size baseSize = tester.getSize(listFinder);
|
|
final double resultingHeight = baseSize.height;
|
|
|
|
/// expected desired Height =150.0
|
|
expect(resultingHeight, equals(desiredHeight));
|
|
});
|
|
|
|
testWidgets(
|
|
'The height of options shrinks to height of resulting items, if less than maxHeight',
|
|
(WidgetTester tester) async {
|
|
// Returns a Future with the height of the default [Autocomplete] options widget
|
|
// after the provided text had been entered into the [Autocomplete] field.
|
|
Future<double> getDefaultOptionsHeight(WidgetTester tester, String enteredText) async {
|
|
final Finder listFinder = find.byType(ListView);
|
|
final Finder inputFinder = find.byType(TextFormField);
|
|
final field = inputFinder.evaluate().first.widget as TextFormField;
|
|
field.controller!.clear();
|
|
await tester.tap(inputFinder);
|
|
await tester.enterText(inputFinder, enteredText);
|
|
await tester.pump();
|
|
final Size baseSize = tester.getSize(listFinder);
|
|
return baseSize.height;
|
|
}
|
|
|
|
const maxOptionsHeight = 250.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsMaxHeight: maxOptionsHeight,
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder listFinder = find.byType(ListView);
|
|
expect(listFinder, findsNothing);
|
|
|
|
// Entering `a` returns 9 items(height > `maxOptionsHeight`) from the kOptions
|
|
// so height gets restricted to `maxOptionsHeight =250`.
|
|
final double nineItemsHeight = await getDefaultOptionsHeight(tester, 'a');
|
|
expect(nineItemsHeight, equals(maxOptionsHeight));
|
|
|
|
// Returns 2 Items (height < `maxOptionsHeight`)
|
|
// so options height shrinks to 2 Items combined height.
|
|
final double twoItemsHeight = await getDefaultOptionsHeight(tester, 'el');
|
|
expect(twoItemsHeight, lessThan(maxOptionsHeight));
|
|
|
|
// Returns 1 item (height < `maxOptionsHeight`) from `kOptions`
|
|
// so options height shrinks to 1 items height.
|
|
final double oneItemsHeight = await getDefaultOptionsHeight(tester, 'elep');
|
|
expect(oneItemsHeight, lessThan(twoItemsHeight));
|
|
},
|
|
);
|
|
|
|
testWidgets('initialValue sets initial text field value', (WidgetTester tester) async {
|
|
late String lastSelection;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
initialValue: const TextEditingValue(text: 'lem'),
|
|
onSelected: (String selection) {
|
|
lastSelection = selection;
|
|
},
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The field is always rendered, but the options are not unless needed.
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
expect(tester.widget<TextFormField>(find.byType(TextFormField)).controller!.text, 'lem');
|
|
|
|
// Focus the empty field. All the options are displayed.
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
final list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
// Displays just one option ('lemur').
|
|
expect(list.semanticChildCount, 1);
|
|
|
|
// Select a option. The options hide and the field updates to show the
|
|
// selection.
|
|
await tester.tap(find.byType(InkWell).first);
|
|
await tester.pump();
|
|
expect(find.byType(TextFormField), findsOneWidget);
|
|
expect(find.byType(ListView), findsNothing);
|
|
final field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
|
|
expect(field.controller!.text, 'lemur');
|
|
expect(lastSelection, 'lemur');
|
|
});
|
|
|
|
// Ensures that the option with the given label has a given background color
|
|
// if given, or no background if color is null.
|
|
void checkOptionHighlight(WidgetTester tester, String label, Color? color) {
|
|
final RenderBox renderBox = tester.renderObject<RenderBox>(
|
|
find.ancestor(matching: find.byType(Container), of: find.text(label)),
|
|
);
|
|
if (color != null) {
|
|
// Check to see that the container is painted with the highlighted background color.
|
|
expect(renderBox, paints..rect(color: color));
|
|
} else {
|
|
// There should only be a paragraph painted.
|
|
expect(renderBox, paintsExactlyCountTimes(const Symbol('drawRect'), 0));
|
|
expect(renderBox, paints..paragraph());
|
|
}
|
|
}
|
|
|
|
testWidgets('keyboard navigation of the options properly highlights the option', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const highlightColor = Color(0xFF112233);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(focusColor: highlightColor),
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.enterText(find.byType(TextFormField), 'el');
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
final list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
expect(list.semanticChildCount, 2);
|
|
|
|
// Initially the first option should be highlighted
|
|
checkOptionHighlight(tester, 'chameleon', highlightColor);
|
|
checkOptionHighlight(tester, 'elephant', null);
|
|
|
|
// Move the selection down
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
|
|
// Highlight should be moved to the second item
|
|
checkOptionHighlight(tester, 'chameleon', null);
|
|
checkOptionHighlight(tester, 'elephant', highlightColor);
|
|
});
|
|
|
|
testWidgets('keyboard navigation keeps the highlighted option scrolled into view', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const highlightColor = Color(0xFF112233);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(focusColor: highlightColor),
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.enterText(find.byType(TextFormField), 'e');
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
final list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
expect(list.semanticChildCount, 6);
|
|
|
|
final Rect optionsGroupRect = tester.getRect(find.byType(ListView));
|
|
const optionsGroupPadding = 16.0;
|
|
|
|
// Highlighted item should be at the top.
|
|
checkOptionHighlight(tester, 'chameleon', highlightColor);
|
|
expect(
|
|
tester.getTopLeft(find.text('chameleon')).dy,
|
|
equals(optionsGroupRect.top + optionsGroupPadding),
|
|
);
|
|
|
|
// Move down the list of options.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'elephant'.
|
|
await tester.pumpAndSettle();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'goose'.
|
|
await tester.pumpAndSettle();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'lemur'.
|
|
await tester.pumpAndSettle();
|
|
|
|
// Highlighted item 'lemur' should be centered in the options popup.
|
|
checkOptionHighlight(tester, 'lemur', highlightColor);
|
|
expect(tester.getCenter(find.text('lemur')).dy, equals(optionsGroupRect.center.dy));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'mouse'.
|
|
await tester.pumpAndSettle();
|
|
|
|
checkOptionHighlight(tester, 'mouse', highlightColor);
|
|
|
|
// First item should have scrolled off the top, and not be selected.
|
|
expect(find.text('chameleon'), findsNothing);
|
|
|
|
// The other items on screen should not be selected.
|
|
checkOptionHighlight(tester, 'goose', null);
|
|
checkOptionHighlight(tester, 'lemur', null);
|
|
checkOptionHighlight(tester, 'northern white rhinoceros', null);
|
|
});
|
|
|
|
group('optionsViewOpenDirection', () {
|
|
testWidgets('default (down)', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final OptionsViewOpenDirection actual = tester
|
|
.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
|
|
.optionsViewOpenDirection;
|
|
expect(actual, equals(OptionsViewOpenDirection.down));
|
|
});
|
|
|
|
testWidgets('down', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
// ignore: avoid_redundant_argument_values
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.down,
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final OptionsViewOpenDirection actual = tester
|
|
.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
|
|
.optionsViewOpenDirection;
|
|
expect(actual, equals(OptionsViewOpenDirection.down));
|
|
});
|
|
|
|
testWidgets('up', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Autocomplete<String>(
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.up,
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['aa'],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final OptionsViewOpenDirection actual = tester
|
|
.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
|
|
.optionsViewOpenDirection;
|
|
expect(actual, equals(OptionsViewOpenDirection.up));
|
|
|
|
await tester.tap(find.byType(RawAutocomplete<String>));
|
|
await tester.enterText(find.byType(RawAutocomplete<String>), 'a');
|
|
await tester.pump();
|
|
expect(find.text('aa').hitTestable(), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('automatic: open in the direction with more space', (WidgetTester tester) async {
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
late StateSetter setState;
|
|
Alignment alignment = Alignment.topCenter;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return Align(
|
|
alignment: alignment,
|
|
child: Autocomplete<String>(
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.mostSpace,
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a', 'b', 'c'],
|
|
fieldViewBuilder:
|
|
(
|
|
BuildContext context,
|
|
TextEditingController controller,
|
|
FocusNode focusNode,
|
|
VoidCallback onFieldSubmitted,
|
|
) {
|
|
return TextField(
|
|
key: fieldKey,
|
|
controller: controller,
|
|
focusNode: focusNode,
|
|
);
|
|
},
|
|
optionsViewBuilder:
|
|
(
|
|
BuildContext context,
|
|
AutocompleteOnSelected<String> onSelected,
|
|
Iterable<String> options,
|
|
) {
|
|
return Material(
|
|
child: ListView(
|
|
key: optionsKey,
|
|
children: options.map((String option) => Text(option)).toList(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Show the options. It should open downwards since there is more space.
|
|
await tester.tap(find.byKey(fieldKey));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
tester.getBottomLeft(find.byKey(fieldKey)),
|
|
offsetMoreOrLessEquals(tester.getTopLeft(find.byKey(optionsKey))),
|
|
);
|
|
|
|
// Move the field to the bottom.
|
|
setState(() {
|
|
alignment = Alignment.bottomCenter;
|
|
});
|
|
await tester.pump();
|
|
|
|
// The options should now open upwards, since there is more space above.
|
|
expect(
|
|
tester.getTopLeft(find.byKey(fieldKey)),
|
|
offsetMoreOrLessEquals(tester.getBottomLeft(find.byKey(optionsKey))),
|
|
);
|
|
|
|
// Move the field to the center.
|
|
setState(() {
|
|
alignment = Alignment.center;
|
|
});
|
|
await tester.pump();
|
|
|
|
// Show the options. It should open downwards since there is more space.
|
|
expect(
|
|
tester.getBottomLeft(find.byKey(fieldKey)),
|
|
offsetMoreOrLessEquals(tester.getTopLeft(find.byKey(optionsKey))),
|
|
);
|
|
});
|
|
});
|
|
|
|
testWidgets('can jump to options that are not yet built', (WidgetTester tester) async {
|
|
const highlightColor = Color(0xFF112233);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(focusColor: highlightColor),
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextFormField));
|
|
await tester.pump();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
final list = find.byType(ListView).evaluate().first.widget as ListView;
|
|
expect(list.semanticChildCount, kOptions.length);
|
|
|
|
Finder optionFinder(int index) {
|
|
return find.ancestor(
|
|
matching: find.byType(Container),
|
|
of: find.text(kOptions.elementAt(index)),
|
|
);
|
|
}
|
|
|
|
expect(optionFinder(0), findsOneWidget);
|
|
expect(optionFinder(kOptions.length - 1), findsNothing);
|
|
|
|
// Jump to the bottom.
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
|
|
await tester.pumpAndSettle();
|
|
expect(optionFinder(0), findsNothing);
|
|
expect(optionFinder(kOptions.length - 1), findsOneWidget);
|
|
checkOptionHighlight(tester, kOptions.last, highlightColor);
|
|
|
|
// Jump to the top.
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
|
|
await tester.pumpAndSettle();
|
|
expect(optionFinder(0), findsOneWidget);
|
|
expect(optionFinder(kOptions.length - 1), findsNothing);
|
|
checkOptionHighlight(tester, kOptions.first, highlightColor);
|
|
});
|
|
|
|
testWidgets(
|
|
'passes textEditingController, focusNode to textEditingController, focusNode RawAutocomplete',
|
|
(WidgetTester tester) async {
|
|
final textEditingController = TextEditingController();
|
|
final focusNode = FocusNode();
|
|
addTearDown(textEditingController.dispose);
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: Autocomplete<String>(
|
|
focusNode: focusNode,
|
|
textEditingController: textEditingController,
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RawAutocomplete<String> rawAutocomplete = tester.widget(
|
|
find.byType(RawAutocomplete<String>),
|
|
);
|
|
expect(rawAutocomplete.textEditingController, textEditingController);
|
|
expect(rawAutocomplete.focusNode, focusNode);
|
|
},
|
|
);
|
|
|
|
testWidgets('when field scrolled offscreen, reshown selected value when scrolled back', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final scrollController = ScrollController();
|
|
final textEditingController = TextEditingController();
|
|
final focusNode = FocusNode();
|
|
addTearDown(textEditingController.dispose);
|
|
addTearDown(focusNode.dispose);
|
|
addTearDown(scrollController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListView(
|
|
controller: scrollController,
|
|
children: <Widget>[
|
|
Autocomplete<String>(
|
|
focusNode: focusNode,
|
|
textEditingController: textEditingController,
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 1000.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
/// Select an option.
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pump();
|
|
const textSelection = 'chameleon';
|
|
await tester.tap(find.text(textSelection));
|
|
|
|
// Unfocus and scroll to deconstruct the widge
|
|
final field = find.byType(TextField).evaluate().first.widget as TextField;
|
|
field.focusNode?.unfocus();
|
|
scrollController.jumpTo(2000.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
/// Scroll to go back to the widget.
|
|
scrollController.jumpTo(0.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
/// Checks that the option selected is still present.
|
|
final field2 = find.byType(TextField).evaluate().first.widget as TextField;
|
|
expect(field2.controller!.text, textSelection);
|
|
});
|
|
|
|
testWidgets('Autocomplete suggestions are hit-tested before ListTiles', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
children: <Widget>[
|
|
Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
const options = <String>['Apple', 'Banana', 'Cherry'];
|
|
return options.where(
|
|
(String option) => option.toLowerCase().contains(textEditingValue.text),
|
|
);
|
|
},
|
|
),
|
|
for (int i = 0; i < 3; i++) ListTile(title: Text('Item $i'), onTap: () {}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pump();
|
|
|
|
final Finder cherryFinder = find.text('Cherry');
|
|
expect(cherryFinder, findsOneWidget);
|
|
|
|
await tester.tap(cherryFinder);
|
|
await tester.pump();
|
|
|
|
expect(find.widgetWithText(TextField, 'Cherry'), findsOneWidget);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Autocomplete renders at zero area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: SizedBox.shrink(
|
|
child: Scaffold(
|
|
body: Autocomplete<String>(
|
|
initialValue: const TextEditingValue(text: 'X'),
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['Y'],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final Finder xText = find.text('X');
|
|
expect(tester.getSize(xText), Size.zero);
|
|
});
|
|
|
|
testWidgets('autocomplete options have button semantics', (WidgetTester tester) async {
|
|
const highlightColor = Color(0xFF112233);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(focusColor: highlightColor),
|
|
home: Scaffold(
|
|
body: Autocomplete<String>(
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
return kOptions.where((String option) {
|
|
return option.contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pump();
|
|
await tester.enterText(find.byType(TextField), 'aa');
|
|
await tester.pump();
|
|
expect(
|
|
tester.getSemantics(find.text('aardvark')),
|
|
matchesSemantics(
|
|
isButton: true,
|
|
isFocusable: true,
|
|
hasTapAction: true,
|
|
hasFocusAction: true,
|
|
label: 'aardvark',
|
|
),
|
|
);
|
|
});
|
|
}
|