3538 lines
132 KiB
Dart
3538 lines
132 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/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
class User {
|
|
const User({required this.email, required this.name});
|
|
|
|
final String email;
|
|
final String name;
|
|
|
|
@override
|
|
String toString() {
|
|
return '$name, $email';
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
const kOptions = <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 {
|
|
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();
|
|
final focusNode = FocusNode();
|
|
addTearDown(textEditingController.dispose);
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
// The field is in the AppBar, not actually a child of RawAutocomplete.
|
|
title: TextFormField(
|
|
key: fieldKey,
|
|
controller: textEditingController,
|
|
focusNode: focusNode,
|
|
decoration: const InputDecoration(hintText: 'Split RawAutocomplete App'),
|
|
onFieldSubmitted: (String value) {
|
|
RawAutocomplete.onFieldSubmitted<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 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),
|
|
);
|
|
case OptionsViewOpenDirection.mostSpace:
|
|
// Behaves like OptionsViewOpenDirection.up.
|
|
expect(find.byType(InkWell), findsNWidgets(3));
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
final double topOfField = tester.getTopLeft(find.byKey(fieldKey)).dy;
|
|
expect(
|
|
tester.getTopLeft(find.byType(InkWell).first),
|
|
Offset(padding, topOfField - 3 * optionHeight),
|
|
);
|
|
expect(tester.getBottomLeft(find.byType(InkWell).at(2)), Offset(padding, topOfField));
|
|
}
|
|
|
|
setState(() {
|
|
alignment = Alignment.topCenter;
|
|
});
|
|
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
|
|
switch (openDirection) {
|
|
case OptionsViewOpenDirection.up:
|
|
// Options are positioned as high as possible while still fitting on
|
|
// the screen.
|
|
expect(find.byType(InkWell), findsNWidgets(1));
|
|
final Size optionsSize = tester.getSize(find.byKey(optionsKey));
|
|
expect(optionsSize.height, kMinInteractiveDimension);
|
|
expect(tester.getTopLeft(find.byKey(optionsKey)), const Offset(padding, 0.0));
|
|
expect(tester.getBottomLeft(find.byKey(optionsKey)), Offset(padding, optionsSize.height));
|
|
case OptionsViewOpenDirection.down:
|
|
// Options are positioned and sized like normal.
|
|
expect(find.byType(InkWell), findsNWidgets(3));
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
final double bottomOfField = tester.getBottomLeft(find.byKey(fieldKey)).dy;
|
|
expect(tester.getTopLeft(find.byType(InkWell).first), Offset(padding, bottomOfField));
|
|
expect(
|
|
tester.getBottomLeft(find.byType(InkWell).at(2)),
|
|
Offset(padding, bottomOfField + 3 * optionHeight),
|
|
);
|
|
case OptionsViewOpenDirection.mostSpace:
|
|
// Behaves like OptionsViewOpenDirection.down.
|
|
expect(find.byType(InkWell), findsNWidgets(3));
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
final double bottomOfField = tester.getBottomLeft(find.byKey(fieldKey)).dy;
|
|
expect(tester.getTopLeft(find.byType(InkWell).first), Offset(padding, bottomOfField));
|
|
expect(
|
|
tester.getBottomLeft(find.byType(InkWell).at(2)),
|
|
Offset(padding, bottomOfField + 3 * optionHeight),
|
|
);
|
|
}
|
|
});
|
|
|
|
testWidgets('correct options alignment for RTL in direction $openDirection', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
const kOptionsWidth = 100.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
|
child: RawAutocomplete<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,
|
|
OptionsViewOpenDirection.mostSpace =>
|
|
tester.getTopLeft(find.byKey(fieldKey)).dy + fieldBox.size.height,
|
|
});
|
|
});
|
|
}
|
|
|
|
testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async {
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
late Iterable<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>(
|
|
// ignore: avoid_redundant_argument_values
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.down,
|
|
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'))),
|
|
);
|
|
});
|
|
|
|
testWidgets('auto: open in the direction with more space', (WidgetTester tester) async {
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
late StateSetter setState;
|
|
Alignment alignment = Alignment.topCenter;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return Align(
|
|
alignment: alignment,
|
|
child: RawAutocomplete<String>(
|
|
key: fieldKey,
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.mostSpace,
|
|
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a', 'b', 'c'],
|
|
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 Material(
|
|
child: ListView(
|
|
key: optionsKey,
|
|
children: options.map((String option) => Text(option)).toList(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Show the options. It should open downwards since there is more space.
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
tester.getBottomLeft(find.byKey(fieldKey)),
|
|
offsetMoreOrLessEquals(tester.getTopLeft(find.byKey(optionsKey))),
|
|
);
|
|
|
|
// Move the field to the bottom.
|
|
setState(() {
|
|
alignment = Alignment.bottomCenter;
|
|
});
|
|
await tester.pump();
|
|
|
|
// The options should now open upwards, since there is more space above.
|
|
expect(
|
|
tester.getTopLeft(find.byKey(fieldKey)),
|
|
offsetMoreOrLessEquals(tester.getBottomLeft(find.byKey(optionsKey))),
|
|
);
|
|
});
|
|
|
|
group('fieldViewBuilder not passed', () {
|
|
testWidgets('down', (WidgetTester tester) async {
|
|
final GlobalKey autocompleteKey = GlobalKey();
|
|
final controller = TextEditingController();
|
|
addTearDown(controller.dispose);
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
TextField(controller: controller, focusNode: focusNode),
|
|
RawAutocomplete<String>(
|
|
key: autocompleteKey,
|
|
textEditingController: controller,
|
|
focusNode: focusNode,
|
|
// ignore: avoid_redundant_argument_values
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.down,
|
|
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 controller = TextEditingController();
|
|
addTearDown(controller.dispose);
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <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();
|
|
addTearDown(focusNode.dispose);
|
|
final textEditingController = TextEditingController();
|
|
addTearDown(textEditingController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
// This is where the real field is being built.
|
|
title: TextFormField(
|
|
key: fieldKey,
|
|
controller: textEditingController,
|
|
focusNode: focusNode,
|
|
onFieldSubmitted: (String value) {
|
|
RawAutocomplete.onFieldSubmitted(autocompleteKey);
|
|
},
|
|
),
|
|
),
|
|
body: RawAutocomplete<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();
|
|
addTearDown(focusNode.dispose);
|
|
final 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;
|
|
var 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;
|
|
|
|
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,
|
|
) {
|
|
return TextField(
|
|
key: fieldKey,
|
|
focusNode: fieldFocusNode,
|
|
controller: fieldTextEditingController,
|
|
);
|
|
},
|
|
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;
|
|
var width = 100.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter localStateSetter) {
|
|
setState = localStateSetter;
|
|
return SizedBox(
|
|
width: width,
|
|
child: RawAutocomplete<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;
|
|
var width = 100.0;
|
|
|
|
final 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 wideWindowSize = Size(1920.0, 1080.0);
|
|
tester.view.physicalSize = wideWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
|
|
await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey);
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
expect(find.byType(InkWell), findsNWidgets(kOptions.length));
|
|
|
|
final Size fieldSize1 = tester.getSize(find.byKey(fieldKey));
|
|
final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey));
|
|
expect(
|
|
optionsTopLeft1,
|
|
Offset(
|
|
tester.getTopLeft(find.byKey(fieldKey)).dx,
|
|
tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height,
|
|
),
|
|
);
|
|
final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey));
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
expect(
|
|
optionsBottomRight1,
|
|
Offset(
|
|
tester.getTopLeft(find.byKey(fieldKey)).dx + fieldSize1.width,
|
|
tester.getTopLeft(find.byKey(fieldKey)).dy +
|
|
fieldSize1.height +
|
|
optionHeight * kOptions.length,
|
|
),
|
|
);
|
|
|
|
// Change the screen size to portrait.
|
|
const narrowWindowSize = Size(1070.0, 1770.0);
|
|
tester.view.physicalSize = narrowWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byType(InkWell), findsNWidgets(kOptions.length));
|
|
expect(tester.getTopLeft(find.byKey(optionsKey)), optionsTopLeft1);
|
|
final Size fieldSize2 = tester.getSize(find.byKey(fieldKey));
|
|
expect(fieldSize1.width, greaterThan(fieldSize2.width));
|
|
expect(fieldSize1.height, fieldSize2.height);
|
|
final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey));
|
|
final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsBottomRight2.dx, lessThan(optionsBottomRight1.dx));
|
|
expect(optionsBottomRight2.dy, optionsBottomRight1.dy);
|
|
expect(
|
|
optionsBottomRight2,
|
|
Offset(
|
|
fieldTopLeft2.dx + fieldSize2.width,
|
|
fieldTopLeft2.dy + fieldSize2.height + optionHeight * kOptions.length,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('options when screen changes portrait to landscape and overflows', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Start with a portrait-sized window, with enough space for all of the
|
|
// options.
|
|
const narrowWindowSize = Size(1070.0, 1770.0);
|
|
tester.view.physicalSize = narrowWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
|
|
await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey);
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
expect(find.byType(InkWell), findsNWidgets(kOptions.length));
|
|
|
|
final Size fieldSize1 = tester.getSize(find.byKey(fieldKey));
|
|
final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey));
|
|
expect(
|
|
optionsTopLeft1,
|
|
Offset(
|
|
tester.getTopLeft(find.byKey(fieldKey)).dx,
|
|
tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height,
|
|
),
|
|
);
|
|
final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey));
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
expect(
|
|
optionsBottomRight1,
|
|
Offset(
|
|
tester.getTopLeft(find.byKey(fieldKey)).dx + fieldSize1.width,
|
|
tester.getTopLeft(find.byKey(fieldKey)).dy +
|
|
fieldSize1.height +
|
|
optionHeight * kOptions.length,
|
|
),
|
|
);
|
|
|
|
// Change the screen size to landscape where the options can't all fit on
|
|
// the screen.
|
|
const wideWindowSize = Size(1920.0, 580.0);
|
|
tester.view.physicalSize = wideWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
|
|
final int visibleOptions = (wideWindowSize.height / optionHeight).floor();
|
|
expect(visibleOptions, lessThan(kOptions.length));
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions));
|
|
expect(tester.getTopLeft(find.byKey(optionsKey)), optionsTopLeft1);
|
|
final Size fieldSize2 = tester.getSize(find.byKey(fieldKey));
|
|
expect(fieldSize1.width, lessThan(fieldSize2.width));
|
|
expect(fieldSize1.height, fieldSize2.height);
|
|
final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey));
|
|
final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx));
|
|
expect(optionsBottomRight2.dy, lessThan(optionsBottomRight1.dy));
|
|
expect(
|
|
optionsBottomRight2,
|
|
Offset(
|
|
fieldTopLeft2.dx + fieldSize2.width,
|
|
// Options are taking all available space below the field.
|
|
wideWindowSize.height,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('screen changes portrait to landscape and overflows', (WidgetTester tester) async {
|
|
// Start with a portrait-sized window, with enough space for all of the
|
|
// options.
|
|
const narrowWindowSize = Size(1070.0, 1770.0);
|
|
tester.view.physicalSize = narrowWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
|
|
await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey);
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
final double optionsHeight1 = tester.getSize(find.byKey(optionsKey)).height;
|
|
final int visibleOptions1 = (optionsHeight1 / optionHeight).ceil();
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions1));
|
|
final Size fieldSize1 = tester.getSize(find.byKey(fieldKey));
|
|
final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey));
|
|
final Offset fieldTopLeft1 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsTopLeft1, Offset(fieldTopLeft1.dx, fieldTopLeft1.dy + fieldSize1.height));
|
|
final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey));
|
|
expect(
|
|
optionsBottomRight1,
|
|
Offset(
|
|
fieldTopLeft1.dx + fieldSize1.width,
|
|
fieldTopLeft1.dy + fieldSize1.height + optionsHeight1,
|
|
),
|
|
);
|
|
|
|
// Change the screen size to landscape where the options can't all fit on
|
|
// the screen.
|
|
const wideWindowSize = Size(1920.0, 580.0);
|
|
tester.view.physicalSize = wideWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
|
|
final double optionsHeight2 = tester.getSize(find.byKey(optionsKey)).height;
|
|
final int visibleOptions2 = (optionsHeight2 / optionHeight).ceil();
|
|
expect(visibleOptions2, lessThan(kOptions.length));
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions2));
|
|
final Offset optionsTopLeft2 = tester.getTopLeft(find.byKey(optionsKey));
|
|
expect(optionsTopLeft2, optionsTopLeft1);
|
|
final Size fieldSize2 = tester.getSize(find.byKey(fieldKey));
|
|
expect(fieldSize1.width, lessThan(fieldSize2.width));
|
|
expect(fieldSize1.height, fieldSize2.height);
|
|
final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey));
|
|
final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx));
|
|
expect(
|
|
optionsBottomRight2,
|
|
Offset(
|
|
fieldTopLeft2.dx + fieldSize2.width,
|
|
// Options are taking all available space below the field.
|
|
wideWindowSize.height,
|
|
),
|
|
);
|
|
|
|
// Shrink the screen further so that the options become smaller than
|
|
// kMinInteractiveDimension and move to overlap the field.
|
|
const shortWindowSize = Size(1920.0, 90.0);
|
|
tester.view.physicalSize = shortWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
|
|
const visibleOptions3 = 1;
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions3));
|
|
final Offset optionsTopLeft3 = tester.getTopLeft(find.byKey(optionsKey));
|
|
expect(optionsTopLeft3.dx, optionsTopLeft1.dx);
|
|
// The options have moved up, overlapping the field, to still be able to
|
|
// show kMinInteractiveDimension.
|
|
expect(optionsTopLeft3.dy, lessThan(optionsTopLeft1.dy));
|
|
final Size fieldSize3 = tester.getSize(find.byKey(fieldKey));
|
|
final Offset fieldTopLeft3 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsTopLeft3.dy, lessThan(fieldTopLeft3.dy + fieldSize3.height));
|
|
expect(fieldSize3.width, fieldSize2.width);
|
|
expect(fieldSize1.height, fieldSize3.height);
|
|
final Offset optionsBottomRight3 = tester.getBottomRight(find.byKey(optionsKey));
|
|
expect(optionsBottomRight3.dx, greaterThan(optionsBottomRight1.dx));
|
|
expect(
|
|
optionsBottomRight3,
|
|
Offset(fieldTopLeft3.dx + fieldSize3.width, shortWindowSize.height),
|
|
);
|
|
});
|
|
|
|
testWidgets('when opening up screen changes portrait to landscape and overflows', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Start with a portrait-sized window, with enough space for all of the
|
|
// options.
|
|
const narrowWindowSize = Size(1070.0, 1770.0);
|
|
tester.view.physicalSize = narrowWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
|
|
await pumpRawAutocomplete(
|
|
tester,
|
|
fieldKey: fieldKey,
|
|
optionsKey: optionsKey,
|
|
optionsViewOpenDirection: OptionsViewOpenDirection.up,
|
|
alignment: Alignment.center,
|
|
);
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
|
|
final double optionHeight = tester.getSize(find.byType(InkWell).first).height;
|
|
final double optionsHeight1 = tester.getSize(find.byKey(optionsKey)).height;
|
|
final int visibleOptions1 = (optionsHeight1 / optionHeight).ceil();
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions1));
|
|
final Size fieldSize1 = tester.getSize(find.byKey(fieldKey));
|
|
final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey));
|
|
final Offset fieldTopLeft1 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsTopLeft1, Offset(fieldTopLeft1.dx, fieldTopLeft1.dy - optionsHeight1));
|
|
expect(optionsTopLeft1.dy, greaterThan(0.0));
|
|
final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey));
|
|
expect(optionsBottomRight1, Offset(fieldTopLeft1.dx + fieldSize1.width, fieldTopLeft1.dy));
|
|
|
|
// Change the screen size to landscape where the options can't all fit on
|
|
// the screen.
|
|
const wideWindowSize = Size(1920.0, 580.0);
|
|
tester.view.physicalSize = wideWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
|
|
final double optionsHeight2 = tester.getSize(find.byKey(optionsKey)).height;
|
|
expect(optionsHeight2, lessThan(optionsHeight1));
|
|
final int visibleOptions2 = (optionsHeight2 / optionHeight).ceil();
|
|
expect(visibleOptions2, lessThan(visibleOptions1));
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions2));
|
|
final Offset optionsTopLeft2 = tester.getTopLeft(find.byKey(optionsKey));
|
|
final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsTopLeft2, Offset(optionsTopLeft1.dx, fieldTopLeft2.dy - optionsHeight2));
|
|
final Size fieldSize2 = tester.getSize(find.byKey(fieldKey));
|
|
expect(fieldSize1.width, lessThan(fieldSize2.width));
|
|
expect(fieldSize1.height, fieldSize2.height);
|
|
final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey));
|
|
expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx));
|
|
expect(optionsBottomRight2, Offset(fieldTopLeft2.dx + fieldSize2.width, fieldTopLeft2.dy));
|
|
|
|
// Shrink the screen further so that the options become smaller than
|
|
// kMinInteractiveDimension and move to overlap the field.
|
|
const shortWindowSize = Size(1920.0, 90.0);
|
|
tester.view.physicalSize = shortWindowSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(fieldKey), findsOneWidget);
|
|
const visibleOptions3 = 1;
|
|
expect(find.byType(InkWell), findsNWidgets(visibleOptions3));
|
|
final Offset optionsTopLeft3 = tester.getTopLeft(find.byKey(optionsKey));
|
|
expect(optionsTopLeft3.dx, optionsTopLeft1.dx);
|
|
// The options have moved down, overlapping the field, to still be able to
|
|
// show kMinInteractiveDimension.
|
|
expect(optionsTopLeft3.dy, lessThan(optionsTopLeft1.dy));
|
|
final Size fieldSize3 = tester.getSize(find.byKey(fieldKey));
|
|
final Offset fieldTopLeft3 = tester.getTopLeft(find.byKey(fieldKey));
|
|
expect(optionsTopLeft3.dy, lessThan(fieldTopLeft3.dy + fieldSize3.height));
|
|
expect(fieldSize3.width, fieldSize2.width);
|
|
expect(fieldSize1.height, fieldSize3.height);
|
|
final Offset optionsBottomRight3 = tester.getBottomRight(find.byKey(optionsKey));
|
|
expect(optionsBottomRight3.dx, greaterThan(optionsBottomRight1.dx));
|
|
expect(optionsBottomRight3.dy, greaterThan(fieldTopLeft3.dy));
|
|
expect(optionsBottomRight3.dx, fieldTopLeft3.dx + fieldSize3.width);
|
|
});
|
|
});
|
|
|
|
testWidgets(
|
|
'when field scrolled offscreen, options are hidden and not reshown when scrolled back on desktop and web',
|
|
(WidgetTester tester) async {
|
|
final GlobalKey fieldKey = GlobalKey();
|
|
final GlobalKey optionsKey = GlobalKey();
|
|
final scrollController = ScrollController();
|
|
addTearDown(scrollController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListView(
|
|
controller: scrollController,
|
|
children: <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();
|
|
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 longRequestDelay = Duration(milliseconds: 5000);
|
|
const shortRequestDelay = Duration(milliseconds: 1000);
|
|
focusNode.requestFocus();
|
|
|
|
// Enter the first letter.
|
|
delay = longRequestDelay;
|
|
await tester.enterText(find.byKey(fieldKey), 'c');
|
|
await tester.pump();
|
|
expect(lastOptions, null);
|
|
|
|
// Enter the second letter which resolves faster.
|
|
delay = shortRequestDelay;
|
|
await tester.enterText(find.byKey(fieldKey), 'ch');
|
|
await tester.pump();
|
|
expect(lastOptions, null);
|
|
|
|
// Wait for the short request to resolve.
|
|
await tester.pump(shortRequestDelay);
|
|
|
|
// lastOptions must contain results from the last request.
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
expect(lastOptions, <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 firstRequestDelay = Duration(milliseconds: 1000);
|
|
const secondRequestDelay = Duration(milliseconds: 2000);
|
|
const thirdRequestDelay = Duration(milliseconds: 3000);
|
|
|
|
// Enter the first letter.
|
|
delay = firstRequestDelay;
|
|
await tester.enterText(find.byKey(fieldKey), 'l');
|
|
await tester.pump();
|
|
expect(lastOptions, null);
|
|
|
|
// Enter the second letter which resolves slower.
|
|
delay = secondRequestDelay;
|
|
await tester.enterText(find.byKey(fieldKey), 'le');
|
|
await tester.pump();
|
|
expect(lastOptions, null);
|
|
|
|
// Enter the third letter which resolves the slowest.
|
|
delay = thirdRequestDelay;
|
|
await tester.enterText(find.byKey(fieldKey), 'lem');
|
|
await tester.pump();
|
|
expect(lastOptions, null);
|
|
|
|
// lastOptions should get updated only from the last request.
|
|
await tester.pump(firstRequestDelay);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
expect(lastOptions, null);
|
|
|
|
await tester.pump(secondRequestDelay - firstRequestDelay);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
expect(lastOptions, null);
|
|
|
|
await tester.pump(thirdRequestDelay - secondRequestDelay);
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
expect(lastOptions, <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 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 delay = Duration(milliseconds: 100);
|
|
|
|
// This is used to tell optionsBuilder to return something different after
|
|
// being called with "ele" the second time. I.e. it is not a pure function.
|
|
var timesOptionsBuilderCalledWithEle = 0;
|
|
final Iterable<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 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,
|
|
isAccessibilityAnnouncement(localizations.searchResultsFound),
|
|
);
|
|
|
|
await tester.enterText(find.byKey(fieldKey), 'a');
|
|
await tester.pump();
|
|
expect(find.byKey(optionsKey), findsOneWidget);
|
|
expect(lastOptions.length, greaterThan(0));
|
|
expect(tester.takeAnnouncements(), isEmpty);
|
|
|
|
await tester.enterText(find.byKey(fieldKey), 'zzzz');
|
|
await tester.pump();
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
expect(
|
|
tester.takeAnnouncements().first,
|
|
isAccessibilityAnnouncement(localizations.noResultsFound),
|
|
);
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
testWidgets('RawAutocomplete renders at zero area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: SizedBox.shrink(
|
|
child: Scaffold(
|
|
body: RawAutocomplete<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);
|
|
});
|
|
|
|
testWidgets(
|
|
'floating menu is displayed when the field is focused and already has a selected value',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/177429.
|
|
const fieldKey = Key('field');
|
|
const optionsKey = Key('options');
|
|
late AutocompleteOnSelected<String> lastOnSelected;
|
|
late FocusNode focusNode;
|
|
|
|
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;
|
|
return TextField(
|
|
key: fieldKey,
|
|
focusNode: fieldFocusNode,
|
|
controller: fieldTextEditingController,
|
|
);
|
|
},
|
|
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), findsOne);
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
|
|
await tester.enterText(find.byKey(fieldKey), kOptions[0]);
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(optionsKey), findsOne);
|
|
|
|
lastOnSelected(kOptions[0]);
|
|
await tester.pump();
|
|
expect(find.byKey(optionsKey), findsNothing);
|
|
|
|
focusNode.unfocus();
|
|
await tester.pump();
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(optionsKey), findsOne);
|
|
},
|
|
);
|
|
}
|