blob: c2cb13c50f85c1764638fe1070552fb3b7b51d45 [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:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const String longText = 'one two three four five six seven eight nine ten eleven twelve';
final List<DropdownMenuEntry<TestMenu>> menuChildren = <DropdownMenuEntry<TestMenu>>[];
for (final TestMenu value in TestMenu.values) {
final DropdownMenuEntry<TestMenu> entry = DropdownMenuEntry<TestMenu>(value: value, label: value.label);
menuChildren.add(entry);
}
Widget buildTest<T extends Enum>(ThemeData themeData, List<DropdownMenuEntry<T>> entries,
{double? width, double? menuHeight, Widget? leadingIcon, Widget? label}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<T>(
label: label,
leadingIcon: leadingIcon,
width: width,
menuHeight: menuHeight,
dropdownMenuEntries: entries,
),
),
);
}
testWidgets('DropdownMenu defaults', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(buildTest(themeData, menuChildren));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, themeData.textTheme.bodyLarge!.color);
expect(editableText.style.background, themeData.textTheme.bodyLarge!.background);
expect(editableText.style.shadows, themeData.textTheme.bodyLarge!.shadows);
expect(editableText.style.decoration, themeData.textTheme.bodyLarge!.decoration);
expect(editableText.style.locale, themeData.textTheme.bodyLarge!.locale);
expect(editableText.style.wordSpacing, themeData.textTheme.bodyLarge!.wordSpacing);
expect(editableText.style.fontSize, 16.0);
expect(editableText.style.height, 1.5);
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.decoration?.border, const OutlineInputBorder());
expect(textField.style?.fontSize, 16.0);
expect(textField.style?.height, 1.5);
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
await tester.pump();
expect(find.byType(MenuAnchor), findsOneWidget);
final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(TextButton, TestMenu.mainMenu0.label),
matching: find.byType(Material),
).at(1);
Material material = tester.widget<Material>(menuMaterial);
expect(material.color, themeData.colorScheme.surfaceContainer);
expect(material.shadowColor, themeData.colorScheme.shadow);
expect(material.surfaceTintColor, Colors.transparent);
expect(material.elevation, 3.0);
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
final Finder buttonMaterial = find.descendant(
of: find.byType(TextButton),
matching: find.byType(Material),
).last;
material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle?.color, themeData.colorScheme.onSurface);
expect(material.textStyle?.fontSize, 14.0);
expect(material.textStyle?.height, 1.43);
});
testWidgets('DropdownMenu can be disabled', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
body: SafeArea(
child: DropdownMenu<TestMenu>(
enabled: false,
dropdownMenuEntries: menuChildren,
),
),
),
),
);
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.decoration?.enabled, false);
final Finder menuMaterial = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Material),
);
expect(menuMaterial, findsNothing);
await tester.tap(find.byType(TextField));
await tester.pump();
final Finder updatedMenuMaterial = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Material),
);
expect(updatedMenuMaterial, findsNothing);
});
testWidgets('Material2 - The width of the text field should always be the same as the menu view',
(WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
body: SafeArea(
child: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
),
)
);
final Finder textField = find.byType(TextField);
final Size anchorSize = tester.getSize(textField);
expect(anchorSize, const Size(180.0, 56.0));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder menuMaterial = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Material),
).first;
final Size menuSize = tester.getSize(menuMaterial);
expect(menuSize, const Size(180.0, 304.0));
// The text field should have same width as the menu
// when the width property is not null.
await tester.pumpWidget(buildTest(themeData, menuChildren, width: 200.0));
final Finder anchor = find.byType(TextField);
final double width = tester.getSize(anchor).width;
expect(width, 200.0);
await tester.tap(anchor);
await tester.pumpAndSettle();
final Finder updatedMenu = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Material),
).first;
final double updatedMenuWidth = tester.getSize(updatedMenu).width;
expect(updatedMenuWidth, 200.0);
});
testWidgets('Material3 - The width of the text field should always be the same as the menu view',
(WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
body: SafeArea(
child: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
),
)
);
final Finder textField = find.byType(TextField);
final double anchorWidth = tester.getSize(textField).width;
expect(anchorWidth, closeTo(180.5, 0.1));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder menuMaterial = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Material),
).first;
final double menuWidth = tester.getSize(menuMaterial).width;
expect(menuWidth, closeTo(180.5, 0.1));
// The text field should have same width as the menu
// when the width property is not null.
await tester.pumpWidget(buildTest(themeData, menuChildren, width: 200.0));
final Finder anchor = find.byType(TextField);
final double width = tester.getSize(anchor).width;
expect(width, 200.0);
await tester.tap(anchor);
await tester.pumpAndSettle();
final Finder updatedMenu = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Material),
).first;
final double updatedMenuWidth = tester.getSize(updatedMenu).width;
expect(updatedMenuWidth, 200.0);
});
testWidgets('The width property can customize the width of the dropdown menu', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<ShortMenu>> shortMenuItems = <DropdownMenuEntry<ShortMenu>>[];
for (final ShortMenu value in ShortMenu.values) {
final DropdownMenuEntry<ShortMenu> entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label);
shortMenuItems.add(entry);
}
const double customBigWidth = 250.0;
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customBigWidth));
RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
expect(box.size.width, customBigWidth);
await tester.tap(find.byType(DropdownMenu<ShortMenu>));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
Size buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').last);
expect(buttonSize.width, customBigWidth);
// reset test
await tester.pumpWidget(Container());
const double customSmallWidth = 100.0;
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customSmallWidth));
box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
expect(box.size.width, customSmallWidth);
await tester.tap(find.byType(DropdownMenu<ShortMenu>));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').last);
expect(buttonSize.width, customSmallWidth);
});
testWidgets('The width property update test', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/120567
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<ShortMenu>> shortMenuItems = <DropdownMenuEntry<ShortMenu>>[];
for (final ShortMenu value in ShortMenu.values) {
final DropdownMenuEntry<ShortMenu> entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label);
shortMenuItems.add(entry);
}
double customWidth = 250.0;
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customWidth));
RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
expect(box.size.width, customWidth);
// Update width
customWidth = 400.0;
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customWidth));
box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
expect(box.size.width, customWidth);
});
testWidgets('The width of MenuAnchor respects MenuAnchor.expandedInsets', (WidgetTester tester) async {
const double parentWidth = 500.0;
final List<DropdownMenuEntry<ShortMenu>> shortMenuItems = <DropdownMenuEntry<ShortMenu>>[];
for (final ShortMenu value in ShortMenu.values) {
final DropdownMenuEntry<ShortMenu> entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label);
shortMenuItems.add(entry);
}
Widget buildMenuAnchor({EdgeInsets? expandedInsets}) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: parentWidth,
height: parentWidth,
child: DropdownMenu<ShortMenu>(
expandedInsets: expandedInsets,
dropdownMenuEntries: shortMenuItems,
),
),
),
);
}
// By default, the width of the text field is determined by the menu children.
await tester.pumpWidget(buildMenuAnchor());
RenderBox box = tester.firstRenderObject(find.byType(TextField));
expect(box.size.width, 136.0);
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
Size buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').hitTestable());
expect(buttonSize.width, 136.0);
// If expandedInsets is EdgeInsets.zero, the width should be the same as its parent.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildMenuAnchor(expandedInsets: EdgeInsets.zero));
box = tester.firstRenderObject(find.byType(TextField));
expect(box.size.width, parentWidth);
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0'));
expect(buttonSize.width, parentWidth);
// If expandedInsets is not zero, the width of the text field should be adjusted
// based on the EdgeInsets.left and EdgeInsets.right. The top and bottom values
// will be ignored.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildMenuAnchor(expandedInsets: const EdgeInsets.only(left: 35.0, top: 50.0, right: 20.0)));
box = tester.firstRenderObject(find.byType(TextField));
expect(box.size.width, parentWidth - 35.0 - 20.0);
final Rect containerRect = tester.getRect(find.byType(SizedBox).first);
final Rect dropdownMenuRect = tester.getRect(find.byType(TextField));
expect(dropdownMenuRect.top, containerRect.top);
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0'));
expect(buttonSize.width, parentWidth - 35.0 - 20.0);
});
testWidgets('Material2 - The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list',
(WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: false);
await tester.pumpWidget(buildTest(themeData, menuChildren));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Element firstItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 0').last);
final RenderBox firstBox = firstItem.renderObject! as RenderBox;
final Offset topLeft = firstBox.localToGlobal(firstBox.size.topLeft(Offset.zero));
final Element lastItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 5').last);
final RenderBox lastBox = lastItem.renderObject! as RenderBox;
final Offset bottomRight = lastBox.localToGlobal(lastBox.size.bottomRight(Offset.zero));
// height = height of MenuItemButton * 6 = 48 * 6
expect(bottomRight.dy - topLeft.dy, 288.0);
final Finder menuView = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Padding),
).first;
final Size menuViewSize = tester.getSize(menuView);
expect(menuViewSize, const Size(180.0, 304.0)); // 304 = 288 + vertical padding(2 * 8)
// Constrains the menu height.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildTest(themeData, menuChildren, menuHeight: 100));
await tester.pumpAndSettle();
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedMenu = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Padding),
).first;
final Size updatedMenuSize = tester.getSize(updatedMenu);
expect(updatedMenuSize, const Size(180.0, 100.0));
});
testWidgets('Material3 - The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list',
(WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true);
await tester.pumpWidget(buildTest(themeData, menuChildren));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Element firstItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 0').last);
final RenderBox firstBox = firstItem.renderObject! as RenderBox;
final Offset topLeft = firstBox.localToGlobal(firstBox.size.topLeft(Offset.zero));
final Element lastItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 5').last);
final RenderBox lastBox = lastItem.renderObject! as RenderBox;
final Offset bottomRight = lastBox.localToGlobal(lastBox.size.bottomRight(Offset.zero));
// height = height of MenuItemButton * 6 = 48 * 6
expect(bottomRight.dy - topLeft.dy, 288.0);
final Finder menuView = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Padding),
).first;
final Size menuViewSize = tester.getSize(menuView);
expect(menuViewSize.width, closeTo(180.6, 0.1));
expect(menuViewSize.height, equals(304.0)); // 304 = 288 + vertical padding(2 * 8)
// Constrains the menu height.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildTest(themeData, menuChildren, menuHeight: 100));
await tester.pumpAndSettle();
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedMenu = find.ancestor(
of: find.byType(SingleChildScrollView),
matching: find.byType(Padding),
).first;
final Size updatedMenuSize = tester.getSize(updatedMenu);
expect(updatedMenuSize.width, closeTo(180.6, 0.1));
expect(updatedMenuSize.height, equals(100.0));
});
testWidgets('The text in the menu button should be aligned with the text of '
'the text field - LTR', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
// Default text field (without leading icon).
await tester.pumpWidget(buildTest(themeData, menuChildren, label: const Text('label')));
final Finder label = find.text('label');
final Offset labelTopLeft = tester.getTopLeft(label);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder itemText = find.text('Item 0').last;
final Offset itemTextTopLeft = tester.getTopLeft(itemText);
expect(labelTopLeft.dx, equals(itemTextTopLeft.dx));
// Test when the text field has a leading icon.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildTest(themeData, menuChildren,
leadingIcon: const Icon(Icons.search),
label: const Text('label'),
));
final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double iconWidth = tester.getSize(leadingIcon).width;
final Finder updatedLabel = find.text('label');
final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText = find.text('Item 0').last;
final Offset updatedItemTextTopLeft = tester.getTopLeft(updatedItemText);
expect(updatedLabelTopLeft.dx, equals(updatedItemTextTopLeft.dx));
expect(updatedLabelTopLeft.dx, equals(iconWidth));
// Test when then leading icon is a widget with a bigger size.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildTest(themeData, menuChildren,
leadingIcon: const SizedBox(
width: 75.0,
child: Icon(Icons.search)),
label: const Text('label'),
));
final Finder largeLeadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
final Finder updatedLabel1 = find.text('label');
final Offset updatedLabelTopLeft1 = tester.getTopLeft(updatedLabel1);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText1 = find.text('Item 0').last;
final Offset updatedItemTextTopLeft1 = tester.getTopLeft(updatedItemText1);
expect(updatedLabelTopLeft1.dx, equals(updatedItemTextTopLeft1.dx));
expect(updatedLabelTopLeft1.dx, equals(largeIconWidth));
});
testWidgets('The text in the menu button should be aligned with the text of '
'the text field - RTL', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
// Default text field (without leading icon).
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: DropdownMenu<TestMenu>(
label: const Text('label'),
dropdownMenuEntries: menuChildren,
),
),
),
));
final Finder label = find.text('label');
final Offset labelTopRight = tester.getTopRight(label);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder itemText = find.text('Item 0').last;
final Offset itemTextTopRight = tester.getTopRight(itemText);
expect(labelTopRight.dx, equals(itemTextTopRight.dx));
// Test when the text field has a leading icon.
await tester.pumpWidget(Container());
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: DropdownMenu<TestMenu>(
leadingIcon: const Icon(Icons.search),
label: const Text('label'),
dropdownMenuEntries: menuChildren,
),
),
),
));
await tester.pump();
final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double iconWidth = tester.getSize(leadingIcon).width;
final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>));
final Finder updatedLabel = find.text('label');
final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText = find.text('Item 0').last;
final Offset updatedItemTextTopRight = tester.getTopRight(updatedItemText);
expect(updatedLabelTopRight.dx, equals(updatedItemTextTopRight.dx));
expect(updatedLabelTopRight.dx, equals(dropdownMenuTopRight.dx - iconWidth));
// Test when then leading icon is a widget with a bigger size.
await tester.pumpWidget(Container());
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: DropdownMenu<TestMenu>(
leadingIcon: const SizedBox(width: 75.0, child: Icon(Icons.search)),
label: const Text('label'),
dropdownMenuEntries: menuChildren,
),
),
),
));
await tester.pump();
final Finder largeLeadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
final Offset updatedDropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>));
final Finder updatedLabel1 = find.text('label');
final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText1 = find.text('Item 0').last;
final Offset updatedItemTextTopRight1 = tester.getTopRight(updatedItemText1);
expect(updatedLabelTopRight1.dx, equals(updatedItemTextTopRight1.dx));
expect(updatedLabelTopRight1.dx, equals(updatedDropdownMenuTopRight.dx - largeIconWidth));
});
testWidgets('DropdownMenu has default trailing icon button', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(buildTest(themeData, menuChildren));
await tester.pump();
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first;
expect(iconButton, findsOneWidget);
await tester.tap(iconButton);
await tester.pump();
final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label),
matching: find.byType(Material),
).last;
expect(menuMaterial, findsOneWidget);
});
testWidgets('Leading IconButton status test', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true);
await tester.pumpWidget(buildTest(themeData, menuChildren, width: 100.0, menuHeight: 100.0));
await tester.pump();
Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_up);
expect(iconButton, findsNothing);
iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first;
expect(iconButton, findsOneWidget);
await tester.tap(iconButton);
await tester.pump();
iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_up).first;
expect(iconButton, findsOneWidget);
iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down);
expect(iconButton, findsNothing);
// Tap outside
await tester.tapAt(const Offset(500.0, 500.0));
await tester.pump();
iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_up);
expect(iconButton, findsNothing);
iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first;
expect(iconButton, findsOneWidget);
});
testWidgets('Do not crash when resize window during menu opening', (WidgetTester tester) async {
addTearDown(tester.view.reset);
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter setState){
return DropdownMenu<TestMenu>(
width: MediaQuery.of(context).size.width,
dropdownMenuEntries: menuChildren,
);
},
),
),
));
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first;
expect(iconButton, findsOneWidget);
await tester.tap(iconButton);
await tester.pump();
final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label),
matching: find.byType(Material),
);
expect(menuMaterial, findsNWidgets(3));
// didChangeMetrics
tester.view.physicalSize = const Size(700.0, 700.0);
await tester.pump();
// Go without throw.
});
testWidgets('DropdownMenu can customize trailing icon button', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
trailingIcon: const Icon(Icons.ac_unit),
dropdownMenuEntries: menuChildren,
),
),
));
await tester.pump();
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.ac_unit).first;
expect(iconButton, findsOneWidget);
await tester.tap(iconButton);
await tester.pump();
final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label),
matching: find.byType(Material),
).last;
expect(menuMaterial, findsOneWidget);
});
testWidgets('Down key can highlight the menu item on desktop platforms', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
trailingIcon: const Icon(Icons.ac_unit),
dropdownMenuEntries: menuChildren,
),
),
));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
Finder button0Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
matching: find.byType(Material),
);
Material item0material = tester.widget<Material>(button0Material);
expect(item0material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
// Press down key one more time, the highlight should move to the next item.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
final Finder button1Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
matching: find.byType(Material),
);
final Material item1material = tester.widget<Material>(button1Material);
expect(item1material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
button0Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
matching: find.byType(Material),
);
item0material = tester.widget<Material>(button0Material);
expect(item0material.color, Colors.transparent); // the previous item should not be highlighted.
}, variant: TargetPlatformVariant.desktop());
testWidgets('Up key can highlight the menu item on desktop platforms', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
Finder button5Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 5').last,
matching: find.byType(Material),
);
Material item5material = tester.widget<Material>(button5Material);
expect(item5material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
// Press up key one more time, the highlight should move up to the item 4.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
final Finder button4Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 4').last,
matching: find.byType(Material),
);
final Material item4material = tester.widget<Material>(button4Material);
expect(item4material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
button5Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 5').last,
matching: find.byType(Material),
);
item5material = tester.widget<Material>(button5Material);
expect(item5material.color, Colors.transparent); // the previous item should not be highlighted.
}, variant: TargetPlatformVariant.desktop());
// Regression test for https://github.com/flutter/flutter/issues/147253.
testWidgets('Down key and up key can navigate on desktop platforms '
'when a label text contains another label text', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: const Scaffold(
body: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: 'ABC'
),
DropdownMenuEntry<int>(
value: 1,
label: 'AB'
),
DropdownMenuEntry<int>(
value: 2,
label: 'ABCD'
),
],
),
),
));
await tester.tap(find.byType(DropdownMenu<int>));
await tester.pump();
final Finder button0Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'ABC').last,
matching: find.byType(Material),
);
final Finder button1Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'AB').last,
matching: find.byType(Material),
);
final Finder button2Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'ABCD').last,
matching: find.byType(Material),
);
// Press down key three times, the highlight should move to the next item each time.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
Material item0Material = tester.widget<Material>(button0Material);
expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
Material item1Material = tester.widget<Material>(button1Material);
expect(item1Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
final Material item2Material = tester.widget<Material>(button2Material);
expect(item2Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
// Press up key two times, the highlight should up each time.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
item1Material = tester.widget<Material>(button1Material);
expect(item1Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
item0Material = tester.widget<Material>(button0Material);
expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
}, variant: TargetPlatformVariant.desktop());
// Regression test for https://github.com/flutter/flutter/issues/147253.
testWidgets('Default search prioritises the current highlight on desktop platforms',
(WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
const String itemLabel = 'Item 2';
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
// Highlight the third item by exact search.
await tester.enterText(find.byType(TextField).first, itemLabel);
await tester.pumpAndSettle();
Finder button2Material = find.descendant(
of: find.widgetWithText(MenuItemButton, itemLabel).last,
matching: find.byType(Material),
);
Material item2material = tester.widget<Material>(button2Material);
expect(item2material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
// Search something that matches multiple items.
await tester.enterText(find.byType(TextField).first, 'Item');
await tester.pumpAndSettle();
// The third item should still be highlighted.
button2Material = find.descendant(
of: find.widgetWithText(MenuItemButton, itemLabel).last,
matching: find.byType(Material),
);
item2material = tester.widget<Material>(button2Material);
expect(item2material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
}, variant: TargetPlatformVariant.desktop());
testWidgets('The text input should match the label of the menu item '
'while pressing down key on desktop platforms', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
// Press down key one more time to the next item.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.widgetWithText(TextField, 'Menu 1'), findsOneWidget);
// Press down to the next item.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 2'), findsOneWidget);
}, variant: TargetPlatformVariant.desktop());
testWidgets('The text input should match the label of the menu item '
'while pressing up key on desktop platforms', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget);
// Press up key one more time to the upper item.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 4'), findsOneWidget);
// Press up to the upper item.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
}, variant: TargetPlatformVariant.desktop());
testWidgets('Disabled button will be skipped while pressing up/down key on desktop platforms', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu2, label: 'Item 2', enabled: false),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu3, label: 'Item 3'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu4, label: 'Item 4'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu5, label: 'Item 5', enabled: false),
];
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
),
),
));
await tester.pump();
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
final Finder button0Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
matching: find.byType(Material),
);
final Material item0Material = tester.widget<Material>(button0Material);
expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // first item can be highlighted as it's enabled.
// Continue to press down key. Item 3 should be highlighted as Menu 1 and Item 2 are both disabled.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
final Finder button3Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 3').last,
matching: find.byType(Material),
);
final Material item3Material = tester.widget<Material>(button3Material);
expect(item3Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
}, variant: TargetPlatformVariant.desktop());
testWidgets('Searching is enabled by default on mobile platforms if initialSelection is non null', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
initialSelection: TestMenu.mainMenu1,
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder buttonMaterial = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Menu 1 button is highlighted.
}, variant: TargetPlatformVariant.mobile());
testWidgets('Searching is enabled by default on desktop platform', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'Menu 1');
await tester.pumpAndSettle();
final Finder buttonMaterial = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Menu 1 button is highlighted.
}, variant: TargetPlatformVariant.desktop());
testWidgets('Highlight can move up/down starting from the searching result on desktop platforms', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'Menu 1');
await tester.pumpAndSettle();
final Finder buttonMaterial = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12));
// Press up to the upper item (Item 0).
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
final Finder button0Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
matching: find.byType(Material),
);
final Material item0Material = tester.widget<Material>(button0Material);
expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Move up, the 'Item 0' is highlighted.
// Continue to move up to the last item (Item 5).
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget);
final Finder button5Material = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 5').last,
matching: find.byType(Material),
);
final Material item5Material = tester.widget<Material>(button5Material);
expect(item5Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
}, variant: TargetPlatformVariant.desktop());
testWidgets('Filtering is disabled by default', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'Menu 1');
await tester.pumpAndSettle();
for (final TestMenu menu in TestMenu.values) {
// One is layout for the _DropdownMenuBody, the other one is the real button item in the menu.
expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2));
}
});
testWidgets('Enable filtering', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
enableFilter: true,
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find
.byType(TextField)
.first, 'Menu 1');
await tester.pumpAndSettle();
for (final TestMenu menu in TestMenu.values) {
// 'Menu 1' should be 2, other items should only find one.
if (menu.label == TestMenu.mainMenu1.label) {
expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2));
} else {
expect(find.widgetWithText(MenuItemButton, menu.label), findsOneWidget);
}
}
});
testWidgets('The controller can access the value in the input field', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
enableFilter: true,
dropdownMenuEntries: menuChildren,
controller: controller,
),
);
}
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item3 = find.widgetWithText(MenuItemButton, 'Item 3').last;
await tester.tap(item3);
await tester.pumpAndSettle();
expect(controller.text, 'Item 3');
await tester.enterText(find.byType(TextField).first, 'New Item');
expect(controller.text, 'New Item');
});
testWidgets('The menu should be closed after text editing is complete', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
enableFilter: true,
dropdownMenuEntries: menuChildren,
controller: controller,
),
),
));
// Access the MenuAnchor
final MenuAnchor menuAnchor = tester.widget<MenuAnchor>(find.byType(MenuAnchor));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
expect(menuAnchor.controller!.isOpen, true);
// Simulate `TextInputAction.done` on textfield
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(menuAnchor.controller!.isOpen, false);
});
testWidgets('The onSelected gets called only when a selection is made', (WidgetTester tester) async {
int selectionCount = 0;
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 1', enabled: false),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 2'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 3'),
];
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
controller: controller,
onSelected: (_) {
setState(() {
selectionCount++;
});
},
),
);
}
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final bool isMobile = switch (themeData.platform) {
TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.fuchsia => true,
TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => false,
};
int expectedCount = isMobile ? 0 : 1;
// Test onSelected on key press
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(selectionCount, expectedCount);
// The desktop platform closed the menu when a completion action is pressed. So we need to reopen it.
if (!isMobile) {
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
}
// Disabled item doesn't trigger onSelected callback.
final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 1').last;
await tester.tap(item1);
await tester.pumpAndSettle();
expect(controller.text, isMobile ? '' : 'Item 0');
expect(selectionCount, expectedCount);
final Finder item2 = find.widgetWithText(MenuItemButton, 'Item 2').last;
await tester.tap(item2);
await tester.pumpAndSettle();
expect(controller.text, 'Item 2');
expect(selectionCount, ++expectedCount);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item3 = find.widgetWithText(MenuItemButton, 'Item 3').last;
await tester.tap(item3);
await tester.pumpAndSettle();
expect(controller.text, 'Item 3');
expect(selectionCount, ++expectedCount);
// On desktop platforms, when typing something in the text field without selecting any of the options,
// the onSelected should not be called.
if (!isMobile) {
await tester.enterText(find.byType(TextField).first, 'New Item');
expect(controller.text, 'New Item');
expect(selectionCount, expectedCount);
expect(find.widgetWithText(TextField, 'New Item'), findsOneWidget);
await tester.enterText(find.byType(TextField).first, '');
expect(selectionCount, expectedCount);
expect(controller.text.isEmpty, true);
}
}, variant: TargetPlatformVariant.all());
testWidgets('The selectedValue gives an initial text and highlights the according item', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: DropdownMenu<TestMenu>(
initialSelection: TestMenu.mainMenu3,
dropdownMenuEntries: menuChildren,
controller: controller,
),
);
}
),
));
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder buttonMaterial = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 3'),
matching: find.byType(Material),
).last;
// Validate the item 3 is highlighted.
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12));
});
testWidgets('The default text input field should not be focused on mobile platforms '
'when it is tapped', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
Widget buildDropdownMenu() => MaterialApp(
theme: themeData,
home: Scaffold(
body: Column(
children: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
],
),
),
);
// Test default condition.
await tester.pumpWidget(buildDropdownMenu());
await tester.pump();
final Finder textFieldFinder = find.byType(TextField);
final TextField result = tester.widget<TextField>(textFieldFinder);
expect(result.canRequestFocus, false);
}, variant: TargetPlatformVariant.mobile());
testWidgets('The text input field should be focused on desktop platforms '
'when it is tapped', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
Widget buildDropdownMenu() => MaterialApp(
theme: themeData,
home: Scaffold(
body: Column(
children: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
],
),
),
);
await tester.pumpWidget(buildDropdownMenu());
await tester.pump();
final Finder textFieldFinder = find.byType(TextField);
final TextField result = tester.widget<TextField>(textFieldFinder);
expect(result.canRequestFocus, true);
}, variant: TargetPlatformVariant.desktop());
testWidgets('If requestFocusOnTap is true, the text input field can request focus, '
'otherwise it cannot request focus', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
Widget buildDropdownMenu({required bool requestFocusOnTap}) => MaterialApp(
theme: themeData,
home: Scaffold(
body: Column(
children: <Widget>[
DropdownMenu<TestMenu>(
requestFocusOnTap: requestFocusOnTap,
dropdownMenuEntries: menuChildren,
),
],
),
),
);
// Set requestFocusOnTap to true.
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: true));
await tester.pump();
final Finder textFieldFinder = find.byType(TextField);
final TextField textField = tester.widget<TextField>(textFieldFinder);
expect(textField.canRequestFocus, true);
// Open the dropdown menu.
await tester.tap(textFieldFinder);
await tester.pump();
// Make a selection.
await tester.tap(find.widgetWithText(MenuItemButton, 'Item 0').last);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
// Set requestFocusOnTap to false.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: false));
await tester.pumpAndSettle();
final Finder textFieldFinder1 = find.byType(TextField);
final TextField textField1 = tester.widget<TextField>(textFieldFinder1);
expect(textField1.canRequestFocus, false);
// Open the dropdown menu.
await tester.tap(textFieldFinder1);
await tester.pump();
// Make a selection.
await tester.tap(find.widgetWithText(MenuItemButton, 'Item 0').last);
await tester.pump();
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
}, variant: TargetPlatformVariant.all());
testWidgets('If requestFocusOnTap is false, the mouse cursor should be clickable when hovered', (WidgetTester tester) async {
Widget buildDropdownMenu() => MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
DropdownMenu<TestMenu>(
requestFocusOnTap: false,
dropdownMenuEntries: menuChildren,
),
],
),
),
);
await tester.pumpWidget(buildDropdownMenu());
await tester.pumpAndSettle();
final Finder textFieldFinder = find.byType(TextField);
final TextField textField = tester.widget<TextField>(textFieldFinder);
expect(textField.canRequestFocus, false);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.moveTo(tester.getCenter(textFieldFinder));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
testWidgets('If enabled is false, the mouse cursor should be deferred when hovered', (WidgetTester tester) async {
Widget buildDropdownMenu({ bool enabled = true, bool? requestFocusOnTap }) {
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
DropdownMenu<TestMenu>(
enabled: enabled,
requestFocusOnTap: requestFocusOnTap,
dropdownMenuEntries: menuChildren,
),
],
),
),
);
}
// Check mouse cursor dropdown menu is disabled and requestFocusOnTap is true.
await tester.pumpWidget(buildDropdownMenu(enabled: false, requestFocusOnTap: true));
await tester.pumpAndSettle();
Finder textFieldFinder = find.byType(TextField);
TextField textField = tester.widget<TextField>(textFieldFinder);
expect(textField.canRequestFocus, true);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.moveTo(tester.getCenter(textFieldFinder));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Remove the pointer.
await gesture.removePointer();
// Check mouse cursor dropdown menu is disabled and requestFocusOnTap is false.
await tester.pumpWidget(buildDropdownMenu(enabled: false, requestFocusOnTap: false));
await tester.pumpAndSettle();
textFieldFinder = find.byType(TextField);
textField = tester.widget<TextField>(textFieldFinder);
expect(textField.canRequestFocus, false);
// Add a new pointer.
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(textFieldFinder));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Remove the pointer.
await gesture.removePointer();
// Check enabled dropdown menu updates the mouse cursor when hovered.
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: true));
await tester.pumpAndSettle();
textFieldFinder = find.byType(TextField);
textField = tester.widget<TextField>(textFieldFinder);
expect(textField.canRequestFocus, true);
// Add a new pointer.
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(textFieldFinder));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
testWidgets('The menu has the same width as the input field in ListView', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/123631
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ListView(
children: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
],
),
),
));
final Rect textInput = tester.getRect(find.byType(TextField));
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
final Finder findMenu = find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_MenuPanel';
});
final Rect menu = tester.getRect(findMenu);
expect(textInput.width, menu.width);
await tester.pumpWidget(Container());
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ListView(
children: <Widget>[
DropdownMenu<TestMenu>(
width: 200,
dropdownMenuEntries: menuChildren,
),
],
),
),
));
final Rect textInput1 = tester.getRect(find.byType(TextField));
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
final Finder findMenu1 = find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_MenuPanel';
});
final Rect menu1 = tester.getRect(findMenu1);
expect(textInput1.width, 200);
expect(menu1.width, 200);
});
testWidgets('Semantics does not include hint when input is not empty', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
const String hintText = 'I am hintText';
TestMenu? selectedValue;
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) => MaterialApp(
theme: themeData,
home: Scaffold(
body: Center(
child: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
dropdownMenuEntries: menuChildren,
hintText: hintText,
onSelected: (TestMenu? value) {
setState(() {
selectedValue = value;
});
},
controller: controller,
),
),
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.text(hintText));
expect(selectedValue?.label, null);
expect(node.label, hintText);
expect(node.value, '');
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(MenuItemButton, 'Item 3').last);
await tester.pumpAndSettle();
expect(selectedValue?.label, 'Item 3');
expect(node.label, '');
expect(node.value, 'Item 3');
});
testWidgets('helperText is not visible when errorText is not null', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
const String helperText = 'I am helperText';
const String errorText = 'I am errorText';
Widget buildFrame(bool hasError) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Center(
child: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
helperText: helperText,
errorText: hasError ? errorText : null,
),
),
),
);
}
await tester.pumpWidget(buildFrame(false));
expect(find.text(helperText), findsOneWidget);
expect(find.text(errorText), findsNothing);
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(find.text(helperText), findsNothing);
expect(find.text(errorText), findsOneWidget);
});
testWidgets('DropdownMenu can respect helperText when helperText is not null', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
const String helperText = 'I am helperText';
Widget buildFrame() {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Center(
child: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
helperText: helperText,
),
),
),
);
}
await tester.pumpWidget(buildFrame());
expect(find.text(helperText), findsOneWidget);
});
testWidgets('DropdownMenu can respect errorText when errorText is not null', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
const String errorText = 'I am errorText';
Widget buildFrame() {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Center(
child: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
errorText: errorText,
),
),
),
);
}
await tester.pumpWidget(buildFrame());
expect(find.text(errorText), findsOneWidget);
});
testWidgets('Can scroll to the highlighted item', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
menuHeight: 100, // Give a small number so the list can only show 2 or 3 items.
dropdownMenuEntries: menuChildren,
),
),
));
await tester.pumpAndSettle();
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
expect(find.text('Item 5').hitTestable(), findsNothing);
await tester.enterText(find.byType(TextField), '5');
await tester.pumpAndSettle();
// Item 5 should show up.
expect(find.text('Item 5').hitTestable(), findsOneWidget);
});
// This is a regression test for https://github.com/flutter/flutter/issues/131676.
testWidgets('Material3 - DropdownMenu uses correct text styles', (WidgetTester tester) async {
const TextStyle inputTextThemeStyle = TextStyle(
fontSize: 18.5,
fontStyle: FontStyle.italic,
wordSpacing: 1.2,
decoration: TextDecoration.lineThrough,
);
const TextStyle menuItemTextThemeStyle = TextStyle(
fontSize: 20.5,
fontStyle: FontStyle.italic,
wordSpacing: 2.1,
decoration: TextDecoration.underline,
);
final ThemeData themeData = ThemeData(
useMaterial3: true,
textTheme: const TextTheme(
bodyLarge: inputTextThemeStyle,
labelLarge: menuItemTextThemeStyle,
),
);
await tester.pumpWidget(buildTest(themeData, menuChildren));
// Test input text style uses the TextTheme.bodyLarge.
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.fontSize, inputTextThemeStyle.fontSize);
expect(editableText.style.fontStyle, inputTextThemeStyle.fontStyle);
expect(editableText.style.wordSpacing, inputTextThemeStyle.wordSpacing);
expect(editableText.style.decoration, inputTextThemeStyle.decoration);
// Open the menu.
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
await tester.pump();
final Finder buttonMaterial = find.descendant(
of: find.byType(TextButton),
matching: find.byType(Material),
).last;
// Test menu item text style uses the TextTheme.labelLarge.
final Material material = tester.widget<Material>(buttonMaterial);
expect(material.textStyle?.fontSize, menuItemTextThemeStyle.fontSize);
expect(material.textStyle?.fontStyle, menuItemTextThemeStyle.fontStyle);
expect(material.textStyle?.wordSpacing, menuItemTextThemeStyle.wordSpacing);
expect(material.textStyle?.decoration, menuItemTextThemeStyle.decoration);
});
testWidgets('DropdownMenuEntries do not overflow when width is specified', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/126882
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownMenu<TestMenu>(
controller: controller,
width: 100,
dropdownMenuEntries: TestMenu.values.map<DropdownMenuEntry<TestMenu>>((TestMenu item) {
return DropdownMenuEntry<TestMenu>(
value: item,
label: '${item.label} $longText',
);
}).toList(),
),
),
),
);
// Opening the width=100 menu should not crash.
await tester.tap(find.byType(DropdownMenu<TestMenu>));
expect(tester.takeException(), isNull);
await tester.pumpAndSettle();
Finder findMenuItemText(String label) {
final String labelText = '$label $longText';
return find.descendant(
of: find.widgetWithText(MenuItemButton, labelText),
matching: find.byType(Text),
).last;
}
// Actual size varies a little on web platforms.
final Matcher closeTo300 = closeTo(300, 0.25);
expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo300);
expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo300);
expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo300);
expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo300);
await tester.tap(findMenuItemText('Item 0'));
await tester.pumpAndSettle();
expect(controller.text, 'Item 0 $longText');
});
testWidgets('DropdownMenuEntry.labelWidget is Text that specifies maxLines 1 or 2', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/126882
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
Widget buildFrame({ required int maxLines }) {
return MaterialApp(
home: Scaffold(
body: DropdownMenu<TestMenu>(
key: ValueKey<int>(maxLines),
controller: controller,
width: 100,
dropdownMenuEntries: TestMenu.values.map<DropdownMenuEntry<TestMenu>>((TestMenu item) {
return DropdownMenuEntry<TestMenu>(
value: item,
label: '${item.label} $longText',
labelWidget: Text('${item.label} $longText', maxLines: maxLines),
);
}).toList(),
),
)
);
}
Finder findMenuItemText(String label) {
final String labelText = '$label $longText';
return find.descendant(
of: find.widgetWithText(MenuItemButton, labelText),
matching: find.byType(Text),
).last;
}
await tester.pumpWidget(buildFrame(maxLines: 1));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
// Actual size varies a little on web platforms.
final Matcher closeTo20 = closeTo(20, 0.05);
expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo20);
expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo20);
expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo20);
expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo20);
// Close the menu
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
expect(controller.text, ''); // nothing selected
await tester.pumpWidget(buildFrame(maxLines: 2));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
// Actual size varies a little on web platforms.
final Matcher closeTo40 = closeTo(40, 0.05);
expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo40);
expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo40);
expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo40);
expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo40);
// Close the menu
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
expect(controller.text, ''); // nothing selected
});
// Regression test for https://github.com/flutter/flutter/issues/131350.
testWidgets('DropdownMenuEntry.leadingIcon default layout', (WidgetTester tester) async {
// The DropdownMenu should not get extra padding in DropdownMenuEntry items
// when both text field and DropdownMenuEntry have leading icons.
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: DropdownMenu<int>(
leadingIcon: Icon(Icons.search),
hintText: 'Hint',
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: 'Item 0',
leadingIcon: Icon(Icons.alarm)
),
DropdownMenuEntry<int>(value: 1, label: 'Item 1'),
],
),
)
));
await tester.tap(find.byType(DropdownMenu<int>));
await tester.pumpAndSettle();
// Check text location in text field.
expect(tester.getTopLeft(find.text('Hint')).dx, 48.0);
// By default, the text of item 0 should be aligned with the text of the text field.
expect(tester.getTopLeft(find.text('Item 0').last).dx, 48.0);
// By default, the text of item 1 should be aligned with the text of the text field,
// so there are some extra padding before "Item 1".
expect(tester.getTopLeft(find.text('Item 1').last).dx, 48.0);
});
testWidgets('DropdownMenu can have customized search algorithm', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
Widget dropdownMenu({ SearchCallback<int>? searchCallback }) {
return MaterialApp(
theme: theme,
home: Scaffold(
body: DropdownMenu<int>(
requestFocusOnTap: true,
searchCallback: searchCallback,
dropdownMenuEntries: const <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'All'),
DropdownMenuEntry<int>(value: 1, label: 'Unread'),
DropdownMenuEntry<int>(value: 2, label: 'Read'),
],
),
)
);
}
void checkExpectedHighlight({String? searchResult, required List<String> otherItems}) {
if (searchResult != null) {
final Finder material = find.descendant(
of: find.widgetWithText(MenuItemButton, searchResult).last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(material);
expect(itemMaterial.color, theme.colorScheme.onSurface.withOpacity(0.12));
}
for (final String nonHighlight in otherItems) {
final Finder material = find.descendant(
of: find.widgetWithText(MenuItemButton, nonHighlight).last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(material);
expect(itemMaterial.color, Colors.transparent);
}
}
// Test default.
await tester.pumpWidget(dropdownMenu());
await tester.pump();
await tester.tap(find.byType(DropdownMenu<int>));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'read');
await tester.pump();
checkExpectedHighlight(searchResult: 'Unread', otherItems: <String>['All', 'Read']); // Because "Unread" contains "read".
// Test custom search algorithm.
await tester.pumpWidget(dropdownMenu(
searchCallback: (_, __) => 0
));
await tester.pump();
await tester.enterText(find.byType(TextField), 'read');
await tester.pump();
checkExpectedHighlight(searchResult: 'All', otherItems: <String>['Unread', 'Read']); // Because the search result should always be index 0.
// Test custom search algorithm - exact match.
await tester.pumpWidget(dropdownMenu(
searchCallback: (List<DropdownMenuEntry<int>> entries, String query) {
if (query.isEmpty) {
return null;
}
final int index = entries.indexWhere((DropdownMenuEntry<int> entry) => entry.label == query);
return index != -1 ? index : null;
},
));
await tester.pump();
await tester.enterText(find.byType(TextField), 'read');
await tester.pump();
checkExpectedHighlight(otherItems: <String>['All', 'Unread', 'Read']); // Because it's case sensitive.
await tester.enterText(find.byType(TextField), 'Read');
await tester.pump();
checkExpectedHighlight(searchResult: 'Read', otherItems: <String>['All', 'Unread']);
});
testWidgets('onSelected gets called when a selection is made in a nested menu', (WidgetTester tester) async {
int selectionCount = 0;
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
];
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: MenuAnchor(
menuChildren: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
onSelected: (_) {
setState(() {
selectionCount++;
});
},
),
],
builder: (BuildContext context, MenuController controller, Widget? widget) {
return IconButton(
icon: const Icon(Icons.smartphone_rounded),
onPressed: () {
controller.open();
},
);
},
),
);
}),
));
// Open the first menu
await tester.tap(find.byType(IconButton));
await tester.pump();
// Open the dropdown menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 0').last;
await tester.tap(item1);
await tester.pumpAndSettle();
expect(selectionCount, 1);
});
testWidgets('When onSelected is called and menu is closed, no textEditingController exception is thrown',
(WidgetTester tester) async {
int selectionCount = 0;
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
];
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: MenuAnchor(
menuChildren: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
onSelected: (_) {
setState(() {
selectionCount++;
});
},
),
],
builder: (BuildContext context, MenuController controller, Widget? widget) {
return IconButton(
icon: const Icon(Icons.smartphone_rounded),
onPressed: () {
controller.open();
},
);
},
),
);
}),
));
// Open the first menu
await tester.tap(find.byType(IconButton));
await tester.pump();
// Open the dropdown menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 0').last;
await tester.tap(item1);
await tester.pumpAndSettle();
expect(selectionCount, 1);
expect(tester.takeException(), isNull);
});
// Regression test for https://github.com/flutter/flutter/issues/139871.
testWidgets('setState is not called through addPostFrameCallback after DropdownMenu is unmounted', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: 500,
itemBuilder: (BuildContext context, int index) {
if (index == 250) {
return DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
);
} else {
return Container(height: 50);
}
},
),
),
),
);
await tester.fling(find.byType(ListView), const Offset(0, -20000), 200000.0);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('Menu shows scrollbar when height is limited', (WidgetTester tester) async {
final List<DropdownMenuEntry<TestMenu>> menuItems = <DropdownMenuEntry<TestMenu>>[
DropdownMenuEntry<TestMenu>(
value: TestMenu.mainMenu0,
label: 'Item 0',
style: MenuItemButton.styleFrom(
minimumSize: const Size.fromHeight(1000),
)
),
];
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuItems,
),
),
));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), findsOneWidget);
}, variant: TargetPlatformVariant.all());
testWidgets('DropdownMenu.focusNode can focus text input field', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final ThemeData theme = ThemeData();
await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold(
body: DropdownMenu<String>(
focusNode: focusNode,
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
DropdownMenuEntry<String>(
value: 'Yolk',
label: 'Yolk',
),
DropdownMenuEntry<String>(
value: 'Eggbert',
label: 'Eggbert',
),
],
),
),
));
RenderBox box = tester.renderObject(find.byType(InputDecorator));
// Test input border when not focused.
expect(box, paints..rrect(color: theme.colorScheme.outline));
focusNode.requestFocus();
await tester.pump();
// Advance input decorator animation.
await tester.pump(const Duration(milliseconds: 200));
box = tester.renderObject(find.byType(InputDecorator));
// Test input border when focused.
expect(box, paints..rrect(color: theme.colorScheme.primary));
});
testWidgets('DropdownMenu honors inputFormatters', (WidgetTester tester) async {
int called = 0;
final TextInputFormatter formatter = TextInputFormatter.withFunction(
(TextEditingValue oldValue, TextEditingValue newValue) {
called += 1;
return newValue;
},
);
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownMenu<String>(
controller: controller,
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
DropdownMenuEntry<String>(
value: 'Blue',
label: 'Blue',
),
DropdownMenuEntry<String>(
value: 'Green',
label: 'Green',
),
],
inputFormatters: <TextInputFormatter>[
formatter,
FilteringTextInputFormatter.deny(RegExp('[0-9]'))
],
),
),
),
);
final EditableTextState state = tester.firstState(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: 'Blue'));
expect(called, 1);
expect(controller.text, 'Blue');
state.updateEditingValue(const TextEditingValue(text: 'Green'));
expect(called, 2);
expect(controller.text, 'Green');
state.updateEditingValue(const TextEditingValue(text: 'Green2'));
expect(called, 3);
expect(controller.text, 'Green');
});
// This is a regression test for https://github.com/flutter/flutter/issues/140596.
testWidgets('Long text item does not overflow', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: 'This is a long text that is multiplied by 4 so it can overflow. ' * 4,
),
],
),
),
));
await tester.pump();
await tester.tap(find.byType(DropdownMenu<int>));
await tester.pumpAndSettle();
// No exception should be thrown.
expect(tester.takeException(), isNull);
});
// This is a regression test for https://github.com/flutter/flutter/issues/147076.
testWidgets('Text field does not overflow parent', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: SizedBox(
width: 300,
child: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: 'This is a long text that is multiplied by 4 so it can overflow. ' * 4,
),
],
),
),
),
));
await tester.pump();
final RenderBox box = tester.firstRenderObject(find.byType(TextField));
expect(box.size.width, 300.0);
});
// This is a regression test for https://github.com/flutter/flutter/issues/147173.
testWidgets('Text field with large helper text can be selected', (WidgetTester tester) async {
const String labelText = 'MenuEntry 1';
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Center(
child: DropdownMenu<int>(
hintText: 'Hint text',
helperText: 'Menu Helper text',
inputDecorationTheme: InputDecorationTheme(
helperMaxLines: 2,
helperStyle: TextStyle(fontSize: 30),
),
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: labelText,
),
],
),
),
),
));
await tester.pump();
await tester.tapAt(tester.getCenter(find.text('Hint text')));
await tester.pumpAndSettle();
// One is layout for the _DropdownMenuBody, the other one is the real button item in the menu.
expect(find.widgetWithText(MenuItemButton, labelText), findsNWidgets(2));
});
}
enum TestMenu {
mainMenu0('Item 0'),
mainMenu1('Menu 1'),
mainMenu2('Item 2'),
mainMenu3('Item 3'),
mainMenu4('Item 4'),
mainMenu5('Item 5');
const TestMenu(this.label);
final String label;
}
enum ShortMenu {
item0('I0'),
item1('I1'),
item2('I2');
const ShortMenu(this.label);
final String label;
}