1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/material.dart';
6import 'package:flutter/services.dart';
7import 'package:flutter_test/flutter_test.dart';
8
9class User {
10 const User({required this.email, required this.name});
11
12 final String email;
13 final String name;
14
15 @override
16 String toString() {
17 return '$name, $email';
18 }
19}
20
21void main() {
22 const List<String> kOptions = <String>[
23 'aardvark',
24 'bobcat',
25 'chameleon',
26 'dingo',
27 'elephant',
28 'flamingo',
29 'goose',
30 'hippopotamus',
31 'iguana',
32 'jaguar',
33 'koala',
34 'lemur',
35 'mouse',
36 'northern white rhinoceros',
37 ];
38
39 const List<User> kOptionsUsers = <User>[
40 User(name: 'Alice', email: 'alice@example.com'),
41 User(name: 'Bob', email: 'bob@example.com'),
42 User(name: 'Charlie', email: 'charlie123@gmail.com'),
43 ];
44
45 testWidgets('can filter and select a list of string options', (WidgetTester tester) async {
46 late String lastSelection;
47 await tester.pumpWidget(
48 MaterialApp(
49 home: Scaffold(
50 body: Autocomplete<String>(
51 onSelected: (String selection) {
52 lastSelection = selection;
53 },
54 optionsBuilder: (TextEditingValue textEditingValue) {
55 return kOptions.where((String option) {
56 return option.contains(textEditingValue.text.toLowerCase());
57 });
58 },
59 ),
60 ),
61 ),
62 );
63
64 // The field is always rendered, but the options are not unless needed.
65 expect(find.byType(TextFormField), findsOneWidget);
66 expect(find.byType(ListView), findsNothing);
67
68 // Focus the empty field. All the options are displayed.
69 await tester.tap(find.byType(TextFormField));
70 await tester.pump();
71 expect(find.byType(ListView), findsOneWidget);
72 ListView list = find.byType(ListView).evaluate().first.widget as ListView;
73 expect(list.semanticChildCount, kOptions.length);
74
75 // Enter text. The options are filtered by the text.
76 await tester.enterText(find.byType(TextFormField), 'ele');
77 await tester.pump();
78 expect(find.byType(TextFormField), findsOneWidget);
79 expect(find.byType(ListView), findsOneWidget);
80 list = find.byType(ListView).evaluate().first.widget as ListView;
81 // 'chameleon' and 'elephant' are displayed.
82 expect(list.semanticChildCount, 2);
83
84 // Select a option. The options hide and the field updates to show the
85 // selection.
86 await tester.tap(find.byType(InkWell).first);
87 await tester.pump();
88 expect(find.byType(TextFormField), findsOneWidget);
89 expect(find.byType(ListView), findsNothing);
90 final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
91 expect(field.controller!.text, 'chameleon');
92 expect(lastSelection, 'chameleon');
93
94 // Modify the field text. The options appear again and are filtered.
95 await tester.enterText(find.byType(TextFormField), 'e');
96 await tester.pump();
97 expect(find.byType(TextFormField), findsOneWidget);
98 expect(find.byType(ListView), findsOneWidget);
99 list = find.byType(ListView).evaluate().first.widget as ListView;
100 // 'chameleon', 'elephant', 'goose', 'lemur', 'mouse', and
101 // 'northern white rhinoceros' are displayed.
102 expect(list.semanticChildCount, 6);
103 });
104
105 testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async {
106 await tester.pumpWidget(
107 MaterialApp(
108 home: Scaffold(
109 body: Autocomplete<User>(
110 optionsBuilder: (TextEditingValue textEditingValue) {
111 return kOptionsUsers.where((User option) {
112 return option.toString().contains(textEditingValue.text.toLowerCase());
113 });
114 },
115 ),
116 ),
117 ),
118 );
119
120 // The field is always rendered, but the options are not unless needed.
121 expect(find.byType(TextFormField), findsOneWidget);
122 expect(find.byType(ListView), findsNothing);
123
124 // Focus the empty field. All the options are displayed.
125 await tester.tap(find.byType(TextFormField));
126 await tester.pump();
127 expect(find.byType(ListView), findsOneWidget);
128 ListView list = find.byType(ListView).evaluate().first.widget as ListView;
129 expect(list.semanticChildCount, kOptionsUsers.length);
130
131 // Enter text. The options are filtered by the text.
132 await tester.enterText(find.byType(TextFormField), 'example');
133 await tester.pump();
134 expect(find.byType(TextFormField), findsOneWidget);
135 expect(find.byType(ListView), findsOneWidget);
136 list = find.byType(ListView).evaluate().first.widget as ListView;
137 // 'Alice' and 'Bob' are displayed because they have "example.com" emails.
138 expect(list.semanticChildCount, 2);
139
140 // Select a option. The options hide and the field updates to show the
141 // selection.
142 await tester.tap(find.byType(InkWell).first);
143 await tester.pump();
144 expect(find.byType(TextFormField), findsOneWidget);
145 expect(find.byType(ListView), findsNothing);
146 final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
147 expect(field.controller!.text, 'Alice, alice@example.com');
148
149 // Modify the field text. The options appear again and are filtered.
150 await tester.enterText(find.byType(TextFormField), 'B');
151 await tester.pump();
152 expect(find.byType(TextFormField), findsOneWidget);
153 expect(find.byType(ListView), findsOneWidget);
154 list = find.byType(ListView).evaluate().first.widget as ListView;
155 // 'Bob' is displayed.
156 expect(list.semanticChildCount, 1);
157 });
158
159 testWidgets('displayStringForOption is displayed in the options', (WidgetTester tester) async {
160 await tester.pumpWidget(
161 MaterialApp(
162 home: Scaffold(
163 body: Autocomplete<User>(
164 displayStringForOption: (User option) {
165 return option.name;
166 },
167 optionsBuilder: (TextEditingValue textEditingValue) {
168 return kOptionsUsers.where((User option) {
169 return option.toString().contains(textEditingValue.text.toLowerCase());
170 });
171 },
172 ),
173 ),
174 ),
175 );
176
177 // The field is always rendered, but the options are not unless needed.
178 expect(find.byType(TextFormField), findsOneWidget);
179 expect(find.byType(ListView), findsNothing);
180
181 // Focus the empty field. All the options are displayed, and the string that
182 // is used comes from displayStringForOption.
183 await tester.tap(find.byType(TextFormField));
184 await tester.pump();
185 expect(find.byType(ListView), findsOneWidget);
186 final ListView list = find.byType(ListView).evaluate().first.widget as ListView;
187 expect(list.semanticChildCount, kOptionsUsers.length);
188 for (int i = 0; i < kOptionsUsers.length; i++) {
189 expect(find.text(kOptionsUsers[i].name), findsOneWidget);
190 }
191
192 // Select a option. The options hide and the field updates to show the
193 // selection. The text in the field is given by displayStringForOption.
194 await tester.tap(find.byType(InkWell).first);
195 await tester.pump();
196 expect(find.byType(TextFormField), findsOneWidget);
197 expect(find.byType(ListView), findsNothing);
198 final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
199 expect(field.controller!.text, kOptionsUsers.first.name);
200 });
201
202 testWidgets('can build a custom field', (WidgetTester tester) async {
203 final GlobalKey fieldKey = GlobalKey();
204 await tester.pumpWidget(
205 MaterialApp(
206 home: Scaffold(
207 body: Autocomplete<String>(
208 optionsBuilder: (TextEditingValue textEditingValue) {
209 return kOptions.where((String option) {
210 return option.contains(textEditingValue.text.toLowerCase());
211 });
212 },
213 fieldViewBuilder:
214 (
215 BuildContext context,
216 TextEditingController textEditingController,
217 FocusNode focusNode,
218 VoidCallback onFieldSubmitted,
219 ) {
220 return Container(key: fieldKey);
221 },
222 ),
223 ),
224 ),
225 );
226
227 // The custom field is rendered and not the default TextFormField.
228 expect(find.byKey(fieldKey), findsOneWidget);
229 expect(find.byType(TextFormField), findsNothing);
230 });
231
232 testWidgets('can build custom options', (WidgetTester tester) async {
233 final GlobalKey optionsKey = GlobalKey();
234 await tester.pumpWidget(
235 MaterialApp(
236 home: Scaffold(
237 body: Autocomplete<String>(
238 optionsBuilder: (TextEditingValue textEditingValue) {
239 return kOptions.where((String option) {
240 return option.contains(textEditingValue.text.toLowerCase());
241 });
242 },
243 optionsViewBuilder:
244 (
245 BuildContext context,
246 AutocompleteOnSelected<String> onSelected,
247 Iterable<String> options,
248 ) {
249 return Container(key: optionsKey);
250 },
251 ),
252 ),
253 ),
254 );
255
256 // The default field is rendered but not the options, yet.
257 expect(find.byKey(optionsKey), findsNothing);
258 expect(find.byType(TextFormField), findsOneWidget);
259
260 // Focus the empty field. The custom options is displayed.
261 await tester.tap(find.byType(TextFormField));
262 await tester.pump();
263 expect(find.byKey(optionsKey), findsOneWidget);
264 });
265
266 testWidgets('the default Autocomplete options widget has a maximum height of 200', (
267 WidgetTester tester,
268 ) async {
269 await tester.pumpWidget(
270 MaterialApp(
271 home: Scaffold(
272 body: Autocomplete<String>(
273 optionsBuilder: (TextEditingValue textEditingValue) {
274 return kOptions.where((String option) {
275 return option.contains(textEditingValue.text.toLowerCase());
276 });
277 },
278 ),
279 ),
280 ),
281 );
282
283 final Finder listFinder = find.byType(ListView);
284 final Finder inputFinder = find.byType(TextFormField);
285 await tester.tap(inputFinder);
286 await tester.enterText(inputFinder, '');
287 await tester.pump();
288 final Size baseSize = tester.getSize(listFinder);
289 final double resultingHeight = baseSize.height;
290 expect(resultingHeight, equals(200));
291 });
292
293 testWidgets('the options height restricts to max desired height', (WidgetTester tester) async {
294 const double desiredHeight = 150.0;
295 await tester.pumpWidget(
296 MaterialApp(
297 home: Scaffold(
298 body: Autocomplete<String>(
299 optionsMaxHeight: desiredHeight,
300 optionsBuilder: (TextEditingValue textEditingValue) {
301 return kOptions.where((String option) {
302 return option.contains(textEditingValue.text.toLowerCase());
303 });
304 },
305 ),
306 ),
307 ),
308 );
309
310 /// entering "a" returns 9 items from kOptions so basically the
311 /// height of 9 options would be beyond `desiredHeight=150`,
312 /// so height gets restricted to desiredHeight.
313 final Finder listFinder = find.byType(ListView);
314 final Finder inputFinder = find.byType(TextFormField);
315 await tester.tap(inputFinder);
316 await tester.enterText(inputFinder, 'a');
317 await tester.pump();
318 final Size baseSize = tester.getSize(listFinder);
319 final double resultingHeight = baseSize.height;
320
321 /// expected desired Height =150.0
322 expect(resultingHeight, equals(desiredHeight));
323 });
324
325 testWidgets(
326 'The height of options shrinks to height of resulting items, if less than maxHeight',
327 (WidgetTester tester) async {
328 // Returns a Future with the height of the default [Autocomplete] options widget
329 // after the provided text had been entered into the [Autocomplete] field.
330 Future<double> getDefaultOptionsHeight(WidgetTester tester, String enteredText) async {
331 final Finder listFinder = find.byType(ListView);
332 final Finder inputFinder = find.byType(TextFormField);
333 final TextFormField field = inputFinder.evaluate().first.widget as TextFormField;
334 field.controller!.clear();
335 await tester.tap(inputFinder);
336 await tester.enterText(inputFinder, enteredText);
337 await tester.pump();
338 final Size baseSize = tester.getSize(listFinder);
339 return baseSize.height;
340 }
341
342 const double maxOptionsHeight = 250.0;
343 await tester.pumpWidget(
344 MaterialApp(
345 home: Scaffold(
346 body: Autocomplete<String>(
347 optionsMaxHeight: maxOptionsHeight,
348 optionsBuilder: (TextEditingValue textEditingValue) {
349 return kOptions.where((String option) {
350 return option.contains(textEditingValue.text.toLowerCase());
351 });
352 },
353 ),
354 ),
355 ),
356 );
357
358 final Finder listFinder = find.byType(ListView);
359 expect(listFinder, findsNothing);
360
361 // Entering `a` returns 9 items(height > `maxOptionsHeight`) from the kOptions
362 // so height gets restricted to `maxOptionsHeight =250`.
363 final double nineItemsHeight = await getDefaultOptionsHeight(tester, 'a');
364 expect(nineItemsHeight, equals(maxOptionsHeight));
365
366 // Returns 2 Items (height < `maxOptionsHeight`)
367 // so options height shrinks to 2 Items combined height.
368 final double twoItemsHeight = await getDefaultOptionsHeight(tester, 'el');
369 expect(twoItemsHeight, lessThan(maxOptionsHeight));
370
371 // Returns 1 item (height < `maxOptionsHeight`) from `kOptions`
372 // so options height shrinks to 1 items height.
373 final double oneItemsHeight = await getDefaultOptionsHeight(tester, 'elep');
374 expect(oneItemsHeight, lessThan(twoItemsHeight));
375 },
376 );
377
378 testWidgets('initialValue sets initial text field value', (WidgetTester tester) async {
379 late String lastSelection;
380 await tester.pumpWidget(
381 MaterialApp(
382 home: Scaffold(
383 body: Autocomplete<String>(
384 initialValue: const TextEditingValue(text: 'lem'),
385 onSelected: (String selection) {
386 lastSelection = selection;
387 },
388 optionsBuilder: (TextEditingValue textEditingValue) {
389 return kOptions.where((String option) {
390 return option.contains(textEditingValue.text.toLowerCase());
391 });
392 },
393 ),
394 ),
395 ),
396 );
397
398 // The field is always rendered, but the options are not unless needed.
399 expect(find.byType(TextFormField), findsOneWidget);
400 expect(find.byType(ListView), findsNothing);
401 expect(tester.widget<TextFormField>(find.byType(TextFormField)).controller!.text, 'lem');
402
403 // Focus the empty field. All the options are displayed.
404 await tester.tap(find.byType(TextFormField));
405 await tester.pump();
406 expect(find.byType(ListView), findsOneWidget);
407 final ListView list = find.byType(ListView).evaluate().first.widget as ListView;
408 // Displays just one option ('lemur').
409 expect(list.semanticChildCount, 1);
410
411 // Select a option. The options hide and the field updates to show the
412 // selection.
413 await tester.tap(find.byType(InkWell).first);
414 await tester.pump();
415 expect(find.byType(TextFormField), findsOneWidget);
416 expect(find.byType(ListView), findsNothing);
417 final TextFormField field = find.byType(TextFormField).evaluate().first.widget as TextFormField;
418 expect(field.controller!.text, 'lemur');
419 expect(lastSelection, 'lemur');
420 });
421
422 // Ensures that the option with the given label has a given background color
423 // if given, or no background if color is null.
424 void checkOptionHighlight(WidgetTester tester, String label, Color? color) {
425 final RenderBox renderBox = tester.renderObject<RenderBox>(
426 find.ancestor(matching: find.byType(Container), of: find.text(label)),
427 );
428 if (color != null) {
429 // Check to see that the container is painted with the highlighted background color.
430 expect(renderBox, paints..rect(color: color));
431 } else {
432 // There should only be a paragraph painted.
433 expect(renderBox, paintsExactlyCountTimes(const Symbol('drawRect'), 0));
434 expect(renderBox, paints..paragraph());
435 }
436 }
437
438 testWidgets('keyboard navigation of the options properly highlights the option', (
439 WidgetTester tester,
440 ) async {
441 const Color highlightColor = Color(0xFF112233);
442 await tester.pumpWidget(
443 MaterialApp(
444 theme: ThemeData(focusColor: highlightColor),
445 home: Scaffold(
446 body: Autocomplete<String>(
447 optionsBuilder: (TextEditingValue textEditingValue) {
448 return kOptions.where((String option) {
449 return option.contains(textEditingValue.text.toLowerCase());
450 });
451 },
452 ),
453 ),
454 ),
455 );
456
457 await tester.tap(find.byType(TextFormField));
458 await tester.enterText(find.byType(TextFormField), 'el');
459 await tester.pump();
460 expect(find.byType(ListView), findsOneWidget);
461 final ListView list = find.byType(ListView).evaluate().first.widget as ListView;
462 expect(list.semanticChildCount, 2);
463
464 // Initially the first option should be highlighted
465 checkOptionHighlight(tester, 'chameleon', highlightColor);
466 checkOptionHighlight(tester, 'elephant', null);
467
468 // Move the selection down
469 await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
470 await tester.pump();
471
472 // Highlight should be moved to the second item
473 checkOptionHighlight(tester, 'chameleon', null);
474 checkOptionHighlight(tester, 'elephant', highlightColor);
475 });
476
477 testWidgets('keyboard navigation keeps the highlighted option scrolled into view', (
478 WidgetTester tester,
479 ) async {
480 const Color highlightColor = Color(0xFF112233);
481 await tester.pumpWidget(
482 MaterialApp(
483 theme: ThemeData(focusColor: highlightColor),
484 home: Scaffold(
485 body: Autocomplete<String>(
486 optionsBuilder: (TextEditingValue textEditingValue) {
487 return kOptions.where((String option) {
488 return option.contains(textEditingValue.text.toLowerCase());
489 });
490 },
491 ),
492 ),
493 ),
494 );
495
496 await tester.tap(find.byType(TextFormField));
497 await tester.enterText(find.byType(TextFormField), 'e');
498 await tester.pump();
499 expect(find.byType(ListView), findsOneWidget);
500 final ListView list = find.byType(ListView).evaluate().first.widget as ListView;
501 expect(list.semanticChildCount, 6);
502
503 final Rect optionsGroupRect = tester.getRect(find.byType(ListView));
504 const double optionsGroupPadding = 16.0;
505
506 // Highlighted item should be at the top.
507 checkOptionHighlight(tester, 'chameleon', highlightColor);
508 expect(
509 tester.getTopLeft(find.text('chameleon')).dy,
510 equals(optionsGroupRect.top + optionsGroupPadding),
511 );
512
513 // Move down the list of options.
514 await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'elephant'.
515 await tester.pumpAndSettle();
516 await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'goose'.
517 await tester.pumpAndSettle();
518 await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'lemur'.
519 await tester.pumpAndSettle();
520
521 // Highlighted item 'lemur' should be centered in the options popup.
522 checkOptionHighlight(tester, 'lemur', highlightColor);
523 expect(tester.getCenter(find.text('lemur')).dy, equals(optionsGroupRect.center.dy));
524
525 await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'mouse'.
526 await tester.pumpAndSettle();
527
528 checkOptionHighlight(tester, 'mouse', highlightColor);
529
530 // First item should have scrolled off the top, and not be selected.
531 expect(find.text('chameleon'), findsNothing);
532
533 // The other items on screen should not be selected.
534 checkOptionHighlight(tester, 'goose', null);
535 checkOptionHighlight(tester, 'lemur', null);
536 checkOptionHighlight(tester, 'northern white rhinoceros', null);
537 });
538
539 group('optionsViewOpenDirection', () {
540 testWidgets('default (down)', (WidgetTester tester) async {
541 await tester.pumpWidget(
542 MaterialApp(
543 home: Scaffold(
544 body: Autocomplete<String>(
545 optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
546 ),
547 ),
548 ),
549 );
550 final OptionsViewOpenDirection actual = tester
551 .widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
552 .optionsViewOpenDirection;
553 expect(actual, equals(OptionsViewOpenDirection.down));
554 });
555
556 testWidgets('down', (WidgetTester tester) async {
557 await tester.pumpWidget(
558 MaterialApp(
559 home: Scaffold(
560 body: Autocomplete<String>(
561 optionsViewOpenDirection:
562 OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values
563 optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
564 ),
565 ),
566 ),
567 );
568 final OptionsViewOpenDirection actual = tester
569 .widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
570 .optionsViewOpenDirection;
571 expect(actual, equals(OptionsViewOpenDirection.down));
572 });
573
574 testWidgets('up', (WidgetTester tester) async {
575 await tester.pumpWidget(
576 MaterialApp(
577 home: Scaffold(
578 body: Center(
579 child: Autocomplete<String>(
580 optionsViewOpenDirection: OptionsViewOpenDirection.up,
581 optionsBuilder: (TextEditingValue textEditingValue) => <String>['aa'],
582 ),
583 ),
584 ),
585 ),
586 );
587 final OptionsViewOpenDirection actual = tester
588 .widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
589 .optionsViewOpenDirection;
590 expect(actual, equals(OptionsViewOpenDirection.up));
591
592 await tester.tap(find.byType(RawAutocomplete<String>));
593 await tester.enterText(find.byType(RawAutocomplete<String>), 'a');
594 await tester.pump();
595 expect(find.text('aa').hitTestable(), findsOneWidget);
596 });
597 });
598
599 testWidgets('can jump to options that are not yet built', (WidgetTester tester) async {
600 const Color highlightColor = Color(0xFF112233);
601 await tester.pumpWidget(
602 MaterialApp(
603 theme: ThemeData(focusColor: highlightColor),
604 home: Scaffold(
605 body: Autocomplete<String>(
606 optionsBuilder: (TextEditingValue textEditingValue) {
607 return kOptions.where((String option) {
608 return option.contains(textEditingValue.text.toLowerCase());
609 });
610 },
611 ),
612 ),
613 ),
614 );
615
616 await tester.tap(find.byType(TextFormField));
617 await tester.pump();
618 expect(find.byType(ListView), findsOneWidget);
619 final ListView list = find.byType(ListView).evaluate().first.widget as ListView;
620 expect(list.semanticChildCount, kOptions.length);
621
622 Finder optionFinder(int index) {
623 return find.ancestor(
624 matching: find.byType(Container),
625 of: find.text(kOptions.elementAt(index)),
626 );
627 }
628
629 expect(optionFinder(0), findsOneWidget);
630 expect(optionFinder(kOptions.length - 1), findsNothing);
631
632 // Jump to the bottom.
633 await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
634 await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
635 await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
636 await tester.pumpAndSettle();
637 expect(optionFinder(0), findsNothing);
638 expect(optionFinder(kOptions.length - 1), findsOneWidget);
639 checkOptionHighlight(tester, kOptions.last, highlightColor);
640
641 // Jump to the top.
642 await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
643 await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
644 await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
645 await tester.pumpAndSettle();
646 expect(optionFinder(0), findsOneWidget);
647 expect(optionFinder(kOptions.length - 1), findsNothing);
648 checkOptionHighlight(tester, kOptions.first, highlightColor);
649 });
650
651 testWidgets(
652 'passes textEditingController, focusNode to textEditingController, focusNode RawAutocomplete',
653 (WidgetTester tester) async {
654 final TextEditingController textEditingController = TextEditingController();
655 final FocusNode focusNode = FocusNode();
656 addTearDown(textEditingController.dispose);
657 addTearDown(focusNode.dispose);
658
659 await tester.pumpWidget(
660 MaterialApp(
661 home: Material(
662 child: Center(
663 child: Autocomplete<String>(
664 focusNode: focusNode,
665 textEditingController: textEditingController,
666 optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
667 ),
668 ),
669 ),
670 ),
671 );
672
673 final RawAutocomplete<String> rawAutocomplete = tester.widget(
674 find.byType(RawAutocomplete<String>),
675 );
676 expect(rawAutocomplete.textEditingController, textEditingController);
677 expect(rawAutocomplete.focusNode, focusNode);
678 },
679 );
680
681 testWidgets('when field scrolled offscreen, reshown selected value when scrolled back', (
682 WidgetTester tester,
683 ) async {
684 final ScrollController scrollController = ScrollController();
685 final TextEditingController textEditingController = TextEditingController();
686 final FocusNode focusNode = FocusNode();
687 addTearDown(textEditingController.dispose);
688 addTearDown(focusNode.dispose);
689 addTearDown(scrollController.dispose);
690
691 await tester.pumpWidget(
692 MaterialApp(
693 home: Scaffold(
694 body: ListView(
695 controller: scrollController,
696 children: <Widget>[
697 Autocomplete<String>(
698 focusNode: focusNode,
699 textEditingController: textEditingController,
700 optionsBuilder: (TextEditingValue textEditingValue) {
701 return kOptions.where((String option) {
702 return option.contains(textEditingValue.text.toLowerCase());
703 });
704 },
705 ),
706 const SizedBox(height: 1000.0),
707 ],
708 ),
709 ),
710 ),
711 );
712
713 /// Select an option.
714 await tester.tap(find.byType(TextField));
715 await tester.pump();
716 const String textSelection = 'chameleon';
717 await tester.tap(find.text(textSelection));
718
719 // Unfocus and scroll to deconstruct the widge
720 final TextField field = find.byType(TextField).evaluate().first.widget as TextField;
721 field.focusNode?.unfocus();
722 scrollController.jumpTo(2000.0);
723 await tester.pumpAndSettle();
724
725 /// Scroll to go back to the widget.
726 scrollController.jumpTo(0.0);
727 await tester.pumpAndSettle();
728
729 /// Checks that the option selected is still present.
730 final TextField field2 = find.byType(TextField).evaluate().first.widget as TextField;
731 expect(field2.controller!.text, textSelection);
732 });
733}
734