blob: 568b1945ee0b01f4a786f96a019484882fe76c87 [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 'dart:ui' show window;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
final ValueChanged<String?> onChanged = (_) { };
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,
}) {
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 == null ? null : items.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item, key: ValueKey<String>(item + 'Text')),
);
}).toList(),
),
),
),
),
);
}
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(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({
Key? key,
required this.textDirection,
required this.child,
this.mediaSize,
}) : super(key: key);
final TextDirection textDirection;
final Widget child;
final Size? mediaSize;
@override
_TestAppState 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() {
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();
const String value = 'two';
await tester.pumpWidget(
buildFormFrame(
buttonKey: buttonKey,
value: value,
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.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: 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, 24.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),
),
],
isDense: true,
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(items: null));
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'),
disabledHint: null,
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build(items: null));
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(items: null));
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(items: null));
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, onChanged: null));
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(items: null));
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,
items: menuItems,
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,
items: menuItems,
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,
items: menuItems,
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();
try {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownButtonFormField<String>(
value: 'c',
onChanged: (String? newValue) {},
items: itemsWithDuplicateValues,
),
),
),
);
fail('Should not be possible to have duplicate item value');
} on AssertionError catch (error) {
expect(
error.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();
try {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownButton<String>(
value: 'e',
onChanged: (String? newValue) {},
items: itemsWithDuplicateValues,
),
),
),
);
fail('Should not be possible to have no items with passed in value');
} on AssertionError catch (error) {
expect(
error.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>(
child: Text(string),
value: 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('autovalidateMode and autovalidate should not be used at the same time', (WidgetTester tester) async {
Widget builder() {
return MaterialApp(
home: Material(
child: Center(
child: DropdownButtonFormField<String>(
autovalidate: true,
autovalidateMode: AutovalidateMode.always,
items: menuItems.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: onChanged,
),
),
),
);
}
expect(() => builder(), throwsAssertionError);
});
}