| // 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)), |
| ); |
| }); |
| } |