blob: e53f69fa4b5768a97a926f6b0e71524b63cdd87c [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
void onChanged<T>(T _) { }
final Type dropdownButtonType = DropdownButton<String>(
onChanged: (_) { },
items: const <DropdownMenuItem<String>>[],
).runtimeType;
Finder _iconRichText(Key iconKey) {
return find.descendant(
of: find.byKey(iconKey),
matching: find.byType(RichText),
);
}
Widget buildFormFrame({
Key? buttonKey,
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
int elevation = 8,
String? value = 'two',
ValueChanged<String?>? onChanged,
VoidCallback? onTap,
Widget? icon,
Color? iconDisabledColor,
Color? iconEnabledColor,
double iconSize = 24.0,
bool isDense = true,
bool isExpanded = false,
Widget? hint,
Widget? disabledHint,
Widget? underline,
List<String>? items = menuItems,
Alignment alignment = Alignment.center,
TextDirection textDirection = TextDirection.ltr,
AlignmentGeometry buttonAlignment = AlignmentDirectional.centerStart,
}) {
return TestApp(
textDirection: textDirection,
child: Material(
child: Align(
alignment: alignment,
child: RepaintBoundary(
child: DropdownButtonFormField<String>(
key: buttonKey,
autovalidateMode: autovalidateMode,
elevation: elevation,
value: value,
hint: hint,
disabledHint: disabledHint,
onChanged: onChanged,
onTap: onTap,
icon: icon,
iconSize: iconSize,
iconDisabledColor: iconDisabledColor,
iconEnabledColor: iconEnabledColor,
isDense: isDense,
isExpanded: isExpanded,
items: items?.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item, key: ValueKey<String>('${item}Text')),
);
}).toList(),
alignment: buttonAlignment,
),
),
),
),
);
}
class _TestAppState extends State<TestApp> {
@override
Widget build(BuildContext context) {
return Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(size: widget.mediaSize),
child: Directionality(
textDirection: widget.textDirection,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
assert(settings.name == '/');
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => widget.child,
);
},
),
),
),
);
}
}
class TestApp extends StatefulWidget {
const TestApp({
super.key,
required this.textDirection,
required this.child,
this.mediaSize,
});
final TextDirection textDirection;
final Widget child;
final Size? mediaSize;
@override
State<TestApp> createState() => _TestAppState();
}
void verifyPaintedShadow(Finder customPaint, int elevation) {
const Rect originalRectangle = Rect.fromLTRB(0.0, 0.0, 800, 208.0);
final List<BoxShadow> boxShadows = List<BoxShadow>.generate(3, (int index) => kElevationToShadow[elevation]![index]);
final List<RRect> rrects = List<RRect>.generate(3, (int index) {
return RRect.fromRectAndRadius(
originalRectangle.shift(
boxShadows[index].offset,
).inflate(boxShadows[index].spreadRadius),
const Radius.circular(2.0),
);
});
expect(
customPaint,
paints
..save()
..rrect(rrect: rrects[0], color: boxShadows[0].color, hasMaskFilter: true)
..rrect(rrect: rrects[1], color: boxShadows[1].color, hasMaskFilter: true)
..rrect(rrect: rrects[2], color: boxShadows[2].color, hasMaskFilter: true),
);
}
void main() {
// Regression test for https://github.com/flutter/flutter/issues/87102
testWidgets('label position test - show hint', (WidgetTester tester) async {
int? value;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
hint: const Text('Hint'),
onChanged: (int? newValue) {
value = newValue;
},
items: const <DropdownMenuItem<int?>>[
DropdownMenuItem<int?>(
value: 1,
child: Text('One'),
),
DropdownMenuItem<int?>(
value: 2,
child: Text('Two'),
),
DropdownMenuItem<int?>(
value: 3,
child: Text('Three'),
),
],
),
),
),
);
expect(value, null);
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
// Select a item.
await tester.tap(find.text('Hint'), warnIfMissed: false);
await tester.pumpAndSettle();
await tester.tap(find.text('One').last);
await tester.pumpAndSettle();
expect(value, 1);
final Offset oneValueLabel = tester.getTopLeft(find.text('labelText'));
// The position of the label does not change.
expect(hintEmptyLabel, oneValueLabel);
});
testWidgets('label position test - show disabledHint: disable', (WidgetTester tester) async {
int? value;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
onChanged: null, // this disables the menu and shows the disabledHint.
disabledHint: const Text('disabledHint'),
items: const <DropdownMenuItem<int?>>[
DropdownMenuItem<int?>(
value: 1,
child: Text('One'),
),
DropdownMenuItem<int?>(
value: 2,
child: Text('Two'),
),
DropdownMenuItem<int?>(
value: 3,
child: Text('Three'),
),
],
),
),
),
);
expect(value, null); // disabledHint shown.
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
expect(hintEmptyLabel, const Offset(0.0, 12.0));
});
testWidgets('label position test - show disabledHint: enable + null item', (WidgetTester tester) async {
int? value;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
disabledHint: const Text('disabledHint'),
onChanged: (_) {},
items: null,
),
),
),
);
expect(value, null); // disabledHint shown.
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
expect(hintEmptyLabel, const Offset(0.0, 12.0));
});
testWidgets('label position test - show disabledHint: enable + empty item', (WidgetTester tester) async {
int? value;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
disabledHint: const Text('disabledHint'),
onChanged: (_) {},
items: const <DropdownMenuItem<int?>>[],
),
),
),
);
expect(value, null); // disabledHint shown.
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
expect(hintEmptyLabel, const Offset(0.0, 12.0));
});
testWidgets('label position test - show hint: enable + empty item', (WidgetTester tester) async {
int? value;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
hint: const Text('hint'),
onChanged: (_) {},
items: const <DropdownMenuItem<int?>>[],
),
),
),
);
expect(value, null); // hint shown.
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
expect(hintEmptyLabel, const Offset(0.0, 12.0));
});
testWidgets('label position test - no hint shown: enable + no selected + disabledHint', (WidgetTester tester) async {
int? value;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
disabledHint: const Text('disabledHint'),
onChanged: (_) {},
items: const <DropdownMenuItem<int?>>[
DropdownMenuItem<int?>(
value: 1,
child: Text('One'),
),
DropdownMenuItem<int?>(
value: 2,
child: Text('Two'),
),
DropdownMenuItem<int?>(
value: 3,
child: Text('Three'),
),
],
),
),
),
);
expect(value, null);
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
expect(hintEmptyLabel, const Offset(0.0, 24.0));
});
testWidgets('label position test - show selected item: disabled + hint + disabledHint', (WidgetTester tester) async {
const int value = 1;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
hint: const Text('hint'),
onChanged: null, // disabled
disabledHint: const Text('disabledHint'),
items: const <DropdownMenuItem<int?>>[
DropdownMenuItem<int?>(
value: 1,
child: Text('One'),
),
DropdownMenuItem<int?>(
value: 2,
child: Text('Two'),
),
DropdownMenuItem<int?>(
value: 3,
child: Text('Three'),
),
],
),
),
),
);
expect(value, 1);
final Offset hintEmptyLabel = tester.getTopLeft(find.text('labelText'));
expect(hintEmptyLabel, const Offset(0.0, 12.0));
});
// Regression test for https://github.com/flutter/flutter/issues/82910
testWidgets('null value test', (WidgetTester tester) async {
int? value = 1;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<int?>(
decoration: const InputDecoration(
labelText: 'labelText',
),
value: value,
onChanged: (int? newValue) {
value = newValue;
},
items: const <DropdownMenuItem<int?>>[
DropdownMenuItem<int?>(
child: Text('None'),
),
DropdownMenuItem<int?>(
value: 1,
child: Text('One'),
),
DropdownMenuItem<int?>(
value: 2,
child: Text('Two'),
),
DropdownMenuItem<int?>(
value: 3,
child: Text('Three'),
),
],
),
),
),
);
expect(value, 1);
final Offset nonEmptyLabel = tester.getTopLeft(find.text('labelText'));
// Switch to `null` value item from value 1.
await tester.tap(find.text('One'));
await tester.pumpAndSettle();
await tester.tap(find.text('None').last);
await tester.pump();
expect(value, null);
final Offset nullValueLabel = tester.getTopLeft(find.text('labelText'));
// The position of the label does not change.
expect(nonEmptyLabel, nullValueLabel);
});
testWidgets('DropdownButtonFormField with autovalidation test', (WidgetTester tester) async {
String? value = 'one';
int validateCalled = 0;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Material(
child: DropdownButtonFormField<String>(
value: value,
hint: const Text('Select Value'),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.fastfood),
),
items: menuItems.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
value = newValue;
});
},
validator: (String? currentValue) {
validateCalled++;
return currentValue == null ? 'Must select value' : null;
},
autovalidateMode: AutovalidateMode.always,
),
),
);
},
),
);
expect(validateCalled, 1);
expect(value, equals('one'));
await tester.tap(find.text('one'));
await tester.pumpAndSettle();
await tester.tap(find.text('three').last);
await tester.pump();
expect(validateCalled, 2);
await tester.pumpAndSettle();
expect(value, equals('three'));
});
testWidgets('DropdownButtonFormField arrow icon aligns with the edge of button when expanded', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
// There shouldn't be overflow when expanded although list contains longer items.
final List<String> items = <String>[
'1234567890',
'abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890',
];
await tester.pumpWidget(
buildFormFrame(
buttonKey: buttonKey,
value: '1234567890',
isExpanded: true,
onChanged: onChanged,
items: items,
),
);
final RenderBox buttonBox = tester.renderObject<RenderBox>(
find.byKey(buttonKey),
);
expect(buttonBox.attached, isTrue);
final RenderBox arrowIcon = tester.renderObject<RenderBox>(
find.byIcon(Icons.arrow_drop_down),
);
expect(arrowIcon.attached, isTrue);
// Arrow icon should be aligned with far right of button when expanded
expect(
arrowIcon.localToGlobal(Offset.zero).dx,
buttonBox.size.centerRight(Offset(-arrowIcon.size.width, 0.0)).dx,
);
});
testWidgets('DropdownButtonFormField with isDense:true aligns selected menu item', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
await tester.pumpWidget(
buildFormFrame(
buttonKey: buttonKey,
onChanged: onChanged,
),
);
final RenderBox buttonBox = tester.renderObject<RenderBox>(
find.byKey(buttonKey),
);
expect(buttonBox.attached, isTrue);
await tester.tap(find.text('two'));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
// The selected dropdown item is both in menu we just popped up, and in
// the IndexedStack contained by the dropdown button. Both of them should
// have the same vertical center as the button.
final List<RenderBox> itemBoxes = tester.renderObjectList<RenderBox>(
find.byKey(const ValueKey<String>('two')),
).toList();
expect(itemBoxes.length, equals(2));
// When isDense is true, the button's height is reduced. The menu items'
// heights are not.
final List<double> itemBoxesHeight = itemBoxes.map<double>((RenderBox box) => box.size.height).toList();
final double menuItemHeight = itemBoxesHeight.reduce(math.max);
expect(menuItemHeight, greaterThanOrEqualTo(buttonBox.size.height));
for (final RenderBox itemBox in itemBoxes) {
expect(itemBox.attached, isTrue);
final Offset buttonBoxCenter = buttonBox.size.center(buttonBox.localToGlobal(Offset.zero));
final Offset itemBoxCenter = itemBox.size.center(itemBox.localToGlobal(Offset.zero));
expect(buttonBoxCenter.dy, equals(itemBoxCenter.dy));
}
});
testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text',
(WidgetTester tester) async {
final Key buttonKey = UniqueKey();
const String value = 'two';
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Builder(
builder: (BuildContext context) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0),
child: Material(
child: Center(
child: DropdownButtonFormField<String>(
key: buttonKey,
value: value,
onChanged: onChanged,
items: menuItems.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item,
key: ValueKey<String>('${item}Text'),
style: const TextStyle(fontSize: 20.0)),
);
}).toList(),
),
),
),
),
),
),
);
final RenderBox box =
tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
expect(box.size.height, 72.0);
});
testWidgets('DropdownButtonFormField.isDense is true by default', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/46844
final Key buttonKey = UniqueKey();
const String value = 'two';
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: DropdownButtonFormField<String>(
key: buttonKey,
value: value,
onChanged: onChanged,
items: menuItems.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item, key: ValueKey<String>('${item}Text')),
);
}).toList(),
),
),
),
),
);
final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
expect(box.size.height, 48.0);
});
testWidgets('DropdownButtonFormField - custom text style', (WidgetTester tester) async {
const String value = 'foo';
final UniqueKey itemKey = UniqueKey();
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: DropdownButtonFormField<String>(
value: value,
items: <DropdownMenuItem<String>>[
DropdownMenuItem<String>(
key: itemKey,
value: 'foo',
child: const Text(value),
),
],
onChanged: (_) { },
style: const TextStyle(
color: Colors.amber,
fontSize: 20.0,
),
),
),
),
);
final RichText richText = tester.widget<RichText>(
find.descendant(
of: find.byKey(itemKey),
matching: find.byType(RichText),
),
);
expect(richText.text.style!.color, Colors.amber);
expect(richText.text.style!.fontSize, 20.0);
});
testWidgets('DropdownButtonFormField - disabledHint displays when the items list is empty, when items is null', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String>? items }) {
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('enabled'),
disabledHint: const Text('disabled'),
);
}
// [disabledHint] should display when [items] is null
await tester.pumpWidget(build());
expect(find.text('enabled'), findsNothing);
expect(find.text('disabled'), findsOneWidget);
// [disabledHint] should display when [items] is an empty list.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('enabled'), findsNothing);
expect(find.text('disabled'), findsOneWidget);
});
testWidgets(
'DropdownButtonFormField - hint displays when the items list is '
'empty, items is null, and disabledHint is null',
(WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String>? items }) {
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build());
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
},
);
testWidgets('DropdownButtonFormField - disabledHint is null by default', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String>? items }) {
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build());
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('DropdownButtonFormField - disabledHint is null by default', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String>? items }) {
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build());
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('DropdownButtonFormField - disabledHint displays when onChanged is null', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String>? items, ValueChanged<String?>? onChanged }) {
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
onChanged: onChanged,
hint: const Text('enabled'),
disabledHint: const Text('disabled'),
);
}
await tester.pumpWidget(build(items: menuItems));
expect(find.text('enabled'), findsNothing);
expect(find.text('disabled'), findsOneWidget);
});
testWidgets('DropdownButtonFormField - disabled hint should be of same size as enabled hint', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String>? items}) {
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('enabled'),
disabledHint: const Text('disabled'),
);
}
await tester.pumpWidget(build());
final RenderBox disabledHintBox = tester.renderObject<RenderBox>(
find.byKey(buttonKey),
);
await tester.pumpWidget(build(items: menuItems));
final RenderBox enabledHintBox = tester.renderObject<RenderBox>(
find.byKey(buttonKey),
);
expect(enabledHintBox.localToGlobal(Offset.zero), equals(disabledHintBox.localToGlobal(Offset.zero)));
expect(enabledHintBox.size, equals(disabledHintBox.size));
});
testWidgets('DropdownButtonFormField - Custom icon size and colors', (WidgetTester tester) async {
final Key iconKey = UniqueKey();
final Icon customIcon = Icon(Icons.assessment, key: iconKey);
await tester.pumpWidget(buildFormFrame(
icon: customIcon,
iconSize: 30.0,
iconEnabledColor: Colors.pink,
iconDisabledColor: Colors.orange,
onChanged: onChanged,
));
// test for size
final RenderBox icon = tester.renderObject(find.byKey(iconKey));
expect(icon.size, const Size(30.0, 30.0));
// test for enabled color
final RichText enabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
expect(enabledRichText.text.style!.color, Colors.pink);
// test for disabled color
await tester.pumpWidget(buildFormFrame(
icon: customIcon,
iconSize: 30.0,
iconEnabledColor: Colors.pink,
iconDisabledColor: Colors.orange,
items: null,
));
final RichText disabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
expect(disabledRichText.text.style!.color, Colors.orange);
});
testWidgets('DropdownButtonFormField - default elevation', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
debugDisableShadows = false;
await tester.pumpWidget(buildFormFrame(
buttonKey: buttonKey,
onChanged: onChanged,
));
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
final Finder customPaint = find.ancestor(
of: find.text('one').last,
matching: find.byType(CustomPaint),
).last;
// Verifying whether or not default elevation(i.e. 8) paints desired shadow
verifyPaintedShadow(customPaint, 8);
debugDisableShadows = true;
});
testWidgets('DropdownButtonFormField - custom elevation', (WidgetTester tester) async {
debugDisableShadows = false;
final Key buttonKeyOne = UniqueKey();
final Key buttonKeyTwo = UniqueKey();
await tester.pumpWidget(buildFormFrame(
buttonKey: buttonKeyOne,
elevation: 16,
onChanged: onChanged,
));
await tester.tap(find.byKey(buttonKeyOne));
await tester.pumpAndSettle();
final Finder customPaintOne = find.ancestor(
of: find.text('one').last,
matching: find.byType(CustomPaint),
).last;
verifyPaintedShadow(customPaintOne, 16);
await tester.tap(find.text('one').last);
await tester.pumpWidget(buildFormFrame(
buttonKey: buttonKeyTwo,
elevation: 24,
onChanged: onChanged,
));
await tester.tap(find.byKey(buttonKeyTwo));
await tester.pumpAndSettle();
final Finder customPaintTwo = find.ancestor(
of: find.text('one').last,
matching: find.byType(CustomPaint),
).last;
verifyPaintedShadow(customPaintTwo, 24);
debugDisableShadows = true;
});
testWidgets('DropdownButtonFormField does not allow duplicate item values', (WidgetTester tester) async {
final List<DropdownMenuItem<String>> itemsWithDuplicateValues = <String>['a', 'b', 'c', 'c']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList();
await expectLater(
() => tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownButtonFormField<String>(
value: 'c',
onChanged: (String? newValue) {},
items: itemsWithDuplicateValues,
),
),
),
),
throwsA(isAssertionError.having(
(AssertionError error) => error.toString(),
'.toString()',
contains("There should be exactly one item with [DropdownButton]'s value"),
)),
);
});
testWidgets('DropdownButtonFormField value should only appear in one menu item', (WidgetTester tester) async {
final List<DropdownMenuItem<String>> itemsWithDuplicateValues = <String>['a', 'b', 'c', 'd']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList();
await expectLater(
() => tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownButton<String>(
value: 'e',
onChanged: (String? newValue) {},
items: itemsWithDuplicateValues,
),
),
),
),
throwsA(isAssertionError.having(
(AssertionError error) => error.toString(),
'.toString()',
contains("There should be exactly one item with [DropdownButton]'s value"),
)),
);
});
testWidgets('DropdownButtonFormField - selectedItemBuilder builds custom buttons', (WidgetTester tester) async {
const List<String> items = <String>[
'One',
'Two',
'Three',
];
String? selectedItem = items[0];
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Scaffold(
body: DropdownButtonFormField<String>(
value: selectedItem,
onChanged: (String? string) => setState(() => selectedItem = string),
selectedItemBuilder: (BuildContext context) {
int index = 0;
return items.map((String string) {
index += 1;
return Text('$string as an Arabic numeral: $index');
}).toList();
},
items: items.map((String string) {
return DropdownMenuItem<String>(
value: string,
child: Text(string),
);
}).toList(),
),
),
);
},
),
);
expect(find.text('One as an Arabic numeral: 1'), findsOneWidget);
await tester.tap(find.text('One as an Arabic numeral: 1'));
await tester.pumpAndSettle();
await tester.tap(find.text('Two'));
await tester.pumpAndSettle();
expect(find.text('Two as an Arabic numeral: 2'), findsOneWidget);
});
testWidgets('DropdownButton onTap callback is called when defined', (WidgetTester tester) async {
int dropdownButtonTapCounter = 0;
String? value = 'one';
void onChanged(String? newValue) {
value = newValue;
}
void onTap() { dropdownButtonTapCounter += 1; }
Widget build() => buildFormFrame(
value: value,
onChanged: onChanged,
onTap: onTap,
);
await tester.pumpWidget(build());
expect(dropdownButtonTapCounter, 0);
// Tap dropdown button.
await tester.tap(find.text('one'));
await tester.pumpAndSettle();
expect(value, equals('one'));
expect(dropdownButtonTapCounter, 1); // Should update counter.
// Tap dropdown menu item.
await tester.tap(find.text('three').last);
await tester.pumpAndSettle();
expect(value, equals('three'));
expect(dropdownButtonTapCounter, 1); // Should not change.
// Tap dropdown button again.
await tester.tap(find.text('three'));
await tester.pumpAndSettle();
expect(value, equals('three'));
expect(dropdownButtonTapCounter, 2); // Should update counter.
// Tap dropdown menu item.
await tester.tap(find.text('two').last);
await tester.pumpAndSettle();
expect(value, equals('two'));
expect(dropdownButtonTapCounter, 2); // Should not change.
});
testWidgets('DropdownButtonFormField should re-render if value param changes', (WidgetTester tester) async {
String currentValue = 'two';
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Material(
child: DropdownButtonFormField<String>(
value: currentValue,
onChanged: onChanged,
items: menuItems.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
onTap: () {
setState(() {
currentValue = value;
});
},
);
}).toList(),
),
),
);
},
),
);
// Make sure the rendered text value matches the initial state value.
expect(currentValue, equals('two'));
expect(find.text(currentValue), findsOneWidget);
// Tap the DropdownButtonFormField widget
await tester.tap(find.byType(dropdownButtonType));
await tester.pumpAndSettle();
// Tap the first dropdown menu item.
await tester.tap(find.text('one').last);
await tester.pumpAndSettle();
// Make sure the rendered text value matches the updated state value.
expect(currentValue, equals('one'));
expect(find.text(currentValue), findsOneWidget);
});
testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
int validateCalled = 0;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: DropdownButtonFormField<String>(
autovalidateMode: AutovalidateMode.always,
items: menuItems.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: onChanged,
validator: (String? value) {
validateCalled++;
return null;
},
),
),
),
),
);
expect(validateCalled, 1);
});
testWidgets('DropdownButtonFormField - Custom button alignment', (WidgetTester tester) async {
await tester.pumpWidget(buildFormFrame(
buttonAlignment: AlignmentDirectional.center,
items: <String>['one'],
value: 'one',
));
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byType(IndexedStack));
final RenderBox selectedItemBox = tester.renderObject(find.text('one'));
// Should be center-center aligned.
expect(
buttonBox.localToGlobal(Offset(buttonBox.size.width / 2.0, buttonBox.size.height / 2.0)),
selectedItemBox.localToGlobal(Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0)),
);
});
}