// 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; void main() { const longText = 'one two three four five six seven eight nine ten eleven twelve'; final menuChildren = >[]; final menuChildrenWithIcons = >[]; const leadingIconToInputPadding = 4.0; for (final TestMenu value in TestMenu.values) { final entry = DropdownMenuEntry(value: value, label: value.label); menuChildren.add(entry); } ValueKey leadingIconKey(TestMenu menuEntry) => ValueKey('leading-${menuEntry.label}'); ValueKey trailingIconKey(TestMenu menuEntry) => ValueKey('trailing-${menuEntry.label}'); for (final TestMenu value in TestMenu.values) { final entry = DropdownMenuEntry( value: value, label: value.label, leadingIcon: Icon(key: leadingIconKey(value), Icons.alarm), trailingIcon: Icon(key: trailingIconKey(value), Icons.abc), ); menuChildrenWithIcons.add(entry); } Widget buildTest( ThemeData themeData, List> entries, { double? width, double? menuHeight, Widget? leadingIcon, Widget? label, InputDecorationTheme? decorationTheme, }) { return MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( label: label, leadingIcon: leadingIcon, width: width, menuHeight: menuHeight, dropdownMenuEntries: entries, inputDecorationTheme: decorationTheme, ), ), ); } Finder findMenuItemButton(String label) { // For each menu items there are two MenuItemButton widgets. // The last one is the real button item in the menu. // The first one is not visible, it is part of _DropdownMenuBody // which is used to compute the dropdown width. return find.widgetWithText(MenuItemButton, label).last; } Material getButtonMaterial(WidgetTester tester, String itemLabel) { return tester.widget( find.descendant(of: findMenuItemButton(itemLabel), matching: find.byType(Material)), ); } bool isItemHighlighted(WidgetTester tester, ThemeData themeData, String itemLabel) { final Color? color = getButtonMaterial(tester, itemLabel).color; return color == themeData.colorScheme.onSurface.withOpacity(0.12); } Finder findMenuPanel() { return find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MenuPanel'); } Finder findMenuMaterial() { return find.descendant(of: findMenuPanel(), matching: find.byType(Material)).first; } testWidgets('DropdownMenu defaults', (WidgetTester tester) async { final 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); Material material = tester.widget(findMenuMaterial()); 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))), ); material = getButtonMaterial(tester, TestMenu.mainMenu0.label); 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); }); group('Item style', () { const focusedBackgroundColor = Color(0xffff0000); const focusedForegroundColor = Color(0xff00ff00); const focusedIconColor = Color(0xff0000ff); const focusedOverlayColor = Color(0xffff00ff); const defaultBackgroundColor = Color(0xff00ffff); const defaultForegroundColor = Color(0xff000000); const defaultIconColor = Color(0xffffffff); const defaultOverlayColor = Color(0xffffff00); final customButtonStyle = ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.focused)) { return focusedBackgroundColor; } return defaultBackgroundColor; }), foregroundColor: WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.focused)) { return focusedForegroundColor; } return defaultForegroundColor; }), iconColor: WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.focused)) { return focusedIconColor; } return defaultIconColor; }), overlayColor: WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.focused)) { return focusedOverlayColor; } return defaultOverlayColor; }), ); final styledMenuEntries = >[]; for (final entryWithIcons in menuChildrenWithIcons) { styledMenuEntries.add( DropdownMenuEntry( value: entryWithIcons.value, label: entryWithIcons.label, leadingIcon: entryWithIcons.leadingIcon, trailingIcon: entryWithIcons.trailingIcon, style: customButtonStyle, ), ); } TextStyle? iconStyle(WidgetTester tester, Key key) { final RichText iconRichText = tester.widget( find.descendant(of: find.byKey(key), matching: find.byType(RichText)).last, ); return iconRichText.text.style; } RenderObject overlayPainter(WidgetTester tester, TestMenu menuItem) { return tester.renderObject( find .descendant( of: findMenuItemButton(menuItem.label), matching: find.byElementPredicate( (Element element) => element.renderObject.runtimeType.toString() == '_RenderInkFeatures', ), ) .last, ); } testWidgets('defaults are correct', (WidgetTester tester) async { const TestMenu selectedItem = TestMenu.mainMenu3; const TestMenu nonSelectedItem = TestMenu.mainMenu2; final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( initialSelection: selectedItem, dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Material selectedButtonMaterial = getButtonMaterial(tester, selectedItem.label); expect(selectedButtonMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); expect(selectedButtonMaterial.textStyle?.color, themeData.colorScheme.onSurface); expect( iconStyle(tester, leadingIconKey(selectedItem))?.color, themeData.colorScheme.onSurfaceVariant, ); final Material nonSelectedButtonMaterial = getButtonMaterial(tester, nonSelectedItem.label); expect(nonSelectedButtonMaterial.color, Colors.transparent); expect(nonSelectedButtonMaterial.textStyle?.color, themeData.colorScheme.onSurface); expect( iconStyle(tester, leadingIconKey(nonSelectedItem))?.color, themeData.colorScheme.onSurfaceVariant, ); // Hover the selected item. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { return gesture.removePointer(); }); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(findMenuItemButton(selectedItem.label))); await tester.pump(); expect( overlayPainter(tester, selectedItem), paints..rect(color: themeData.colorScheme.onSurface.withOpacity(0.1).withAlpha(0)), ); // Hover a non-selected item. await gesture.moveTo(tester.getCenter(findMenuItemButton(nonSelectedItem.label))); await tester.pump(); expect( overlayPainter(tester, nonSelectedItem), paints..rect(color: themeData.colorScheme.onSurface.withOpacity(0.08).withAlpha(0)), ); }); testWidgets('can be overridden at application theme level', (WidgetTester tester) async { const TestMenu selectedItem = TestMenu.mainMenu3; const TestMenu nonSelectedItem = TestMenu.mainMenu2; await tester.pumpWidget( MaterialApp( theme: ThemeData(menuButtonTheme: MenuButtonThemeData(style: customButtonStyle)), home: Scaffold( body: DropdownMenu( initialSelection: selectedItem, dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Material selectedButtonMaterial = getButtonMaterial(tester, selectedItem.label); expect(selectedButtonMaterial.color, focusedBackgroundColor); expect(selectedButtonMaterial.textStyle?.color, focusedForegroundColor); expect(iconStyle(tester, leadingIconKey(selectedItem))?.color, focusedIconColor); final Material nonSelectedButtonMaterial = getButtonMaterial(tester, nonSelectedItem.label); expect(nonSelectedButtonMaterial.color, defaultBackgroundColor); expect(nonSelectedButtonMaterial.textStyle?.color, defaultForegroundColor); expect(iconStyle(tester, leadingIconKey(nonSelectedItem))?.color, defaultIconColor); // Hover the selected item. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { return gesture.removePointer(); }); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(findMenuItemButton(selectedItem.label))); await tester.pump(); expect( overlayPainter(tester, selectedItem), paints..rect(color: focusedOverlayColor.withAlpha(0)), ); // Hover a non-selected item. await gesture.moveTo(tester.getCenter(findMenuItemButton(nonSelectedItem.label))); await tester.pump(); expect( overlayPainter(tester, nonSelectedItem), paints..rect(color: defaultOverlayColor.withAlpha(0)), ); }); testWidgets('can be overridden at menu entry level', (WidgetTester tester) async { const TestMenu selectedItem = TestMenu.mainMenu3; const TestMenu nonSelectedItem = TestMenu.mainMenu2; await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( initialSelection: selectedItem, dropdownMenuEntries: styledMenuEntries, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Material selectedButtonMaterial = getButtonMaterial(tester, selectedItem.label); expect(selectedButtonMaterial.color, focusedBackgroundColor); expect(selectedButtonMaterial.textStyle?.color, focusedForegroundColor); expect(iconStyle(tester, leadingIconKey(selectedItem))?.color, focusedIconColor); final Material nonSelectedButtonMaterial = getButtonMaterial(tester, nonSelectedItem.label); expect(nonSelectedButtonMaterial.color, defaultBackgroundColor); expect(nonSelectedButtonMaterial.textStyle?.color, defaultForegroundColor); expect(iconStyle(tester, leadingIconKey(nonSelectedItem))?.color, defaultIconColor); // Hover the selected item. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { return gesture.removePointer(); }); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(findMenuItemButton(selectedItem.label))); await tester.pump(); expect( overlayPainter(tester, selectedItem), paints..rect(color: focusedOverlayColor.withAlpha(0)), ); // Hover a non-selected item. await gesture.moveTo(tester.getCenter(findMenuItemButton(nonSelectedItem.label))); await tester.pump(); expect( overlayPainter(tester, nonSelectedItem), paints..rect(color: defaultOverlayColor.withAlpha(0)), ); }); testWidgets('defined at menu entry level takes precedence', (WidgetTester tester) async { const TestMenu selectedItem = TestMenu.mainMenu3; const TestMenu nonSelectedItem = TestMenu.mainMenu2; const luckyColor = Color(0xff777777); final singleColorButtonStyle = ButtonStyle( backgroundColor: WidgetStateProperty.all(luckyColor), foregroundColor: WidgetStateProperty.all(luckyColor), iconColor: WidgetStateProperty.all(luckyColor), overlayColor: WidgetStateProperty.all(luckyColor), ); await tester.pumpWidget( MaterialApp( theme: ThemeData(menuButtonTheme: MenuButtonThemeData(style: singleColorButtonStyle)), home: Scaffold( body: DropdownMenu( initialSelection: selectedItem, dropdownMenuEntries: styledMenuEntries, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Material selectedButtonMaterial = getButtonMaterial(tester, selectedItem.label); expect(selectedButtonMaterial.color, focusedBackgroundColor); expect(selectedButtonMaterial.textStyle?.color, focusedForegroundColor); expect(iconStyle(tester, leadingIconKey(selectedItem))?.color, focusedIconColor); final Material nonSelectedButtonMaterial = getButtonMaterial(tester, nonSelectedItem.label); expect(nonSelectedButtonMaterial.color, defaultBackgroundColor); expect(nonSelectedButtonMaterial.textStyle?.color, defaultForegroundColor); expect(iconStyle(tester, leadingIconKey(nonSelectedItem))?.color, defaultIconColor); // Hover the selected item. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { return gesture.removePointer(); }); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(findMenuItemButton(selectedItem.label))); await tester.pump(); expect( overlayPainter(tester, selectedItem), paints..rect(color: focusedOverlayColor.withAlpha(0)), ); // Hover a non-selected item. await gesture.moveTo(tester.getCenter(findMenuItemButton(nonSelectedItem.label))); await tester.pump(); expect( overlayPainter(tester, nonSelectedItem), paints..rect(color: defaultOverlayColor.withAlpha(0)), ); }); testWidgets('defined at menu entry level and application level are merged', ( WidgetTester tester, ) async { const TestMenu selectedItem = TestMenu.mainMenu3; const TestMenu nonSelectedItem = TestMenu.mainMenu2; const luckyColor = Color(0xff777777); final partialButtonStyle = ButtonStyle( backgroundColor: WidgetStateProperty.all(luckyColor), foregroundColor: WidgetStateProperty.all(luckyColor), ); final partiallyStyledMenuEntries = >[]; for (final entryWithIcons in menuChildrenWithIcons) { partiallyStyledMenuEntries.add( DropdownMenuEntry( value: entryWithIcons.value, label: entryWithIcons.label, leadingIcon: entryWithIcons.leadingIcon, trailingIcon: entryWithIcons.trailingIcon, style: partialButtonStyle, ), ); } await tester.pumpWidget( MaterialApp( theme: ThemeData(menuButtonTheme: MenuButtonThemeData(style: customButtonStyle)), home: Scaffold( body: DropdownMenu( initialSelection: selectedItem, dropdownMenuEntries: partiallyStyledMenuEntries, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Material selectedButtonMaterial = getButtonMaterial(tester, selectedItem.label); expect(selectedButtonMaterial.color, luckyColor); expect(selectedButtonMaterial.textStyle?.color, luckyColor); expect(iconStyle(tester, leadingIconKey(selectedItem))?.color, focusedIconColor); final Material nonSelectedButtonMaterial = getButtonMaterial(tester, nonSelectedItem.label); expect(nonSelectedButtonMaterial.color, luckyColor); expect(nonSelectedButtonMaterial.textStyle?.color, luckyColor); expect(iconStyle(tester, leadingIconKey(nonSelectedItem))?.color, defaultIconColor); // Hover the selected item. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { return gesture.removePointer(); }); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(findMenuItemButton(selectedItem.label))); await tester.pump(); expect( overlayPainter(tester, selectedItem), paints..rect(color: focusedOverlayColor.withAlpha(0)), ); // Hover a non-selected item. await gesture.moveTo(tester.getCenter(findMenuItemButton(nonSelectedItem.label))); await tester.pump(); expect( overlayPainter(tester, nonSelectedItem), paints..rect(color: defaultOverlayColor.withAlpha(0)), ); }); // Regression test for https://github.com/flutter/flutter/issues/177363. testWidgets('textStyle property is resolved when item is highlighted', ( WidgetTester tester, ) async { const TestMenu selectedItem = TestMenu.mainMenu3; const TestMenu nonSelectedItem = TestMenu.mainMenu2; final customButtonStyle = ButtonStyle( textStyle: WidgetStateProperty.resolveWith( (Set states) => TextStyle( fontWeight: states.contains(WidgetState.focused) ? FontWeight.bold : FontWeight.normal, ), ), ); final menuEntries = >[]; for (final item in menuChildren) { menuEntries.add( DropdownMenuEntry( value: item.value, label: item.label, style: customButtonStyle, ), ); } TextStyle? getItemLabelStyle(String label) { final RenderObject paragraph = tester .element( find.descendant(of: findMenuItemButton(label), matching: find.text(label)), ) .renderObject!; return (paragraph as RenderParagraph).text.style; } await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( initialSelection: selectedItem, dropdownMenuEntries: menuEntries, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); expect(getItemLabelStyle(selectedItem.label)?.fontWeight, FontWeight.bold); expect(getItemLabelStyle(nonSelectedItem.label)?.fontWeight, FontWeight.normal); }); }); testWidgets('Inner TextField is disabled when DropdownMenu is disabled', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SafeArea( child: DropdownMenu(enabled: false, dropdownMenuEntries: menuChildren), ), ), ), ); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.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('Inner IconButton is disabled when DropdownMenu is disabled', ( WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/149598. await tester.pumpWidget( MaterialApp( home: Scaffold( body: SafeArea( child: DropdownMenu(enabled: false, dropdownMenuEntries: menuChildren), ), ), ), ); final IconButton trailingButton = tester.widget( find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first, ); expect(trailingButton.onPressed, null); }); testWidgets( 'Material2 - The width of the text field should always be the same as the menu view', (WidgetTester tester) async { final themeData = ThemeData(useMaterial3: false); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: SafeArea(child: DropdownMenu(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)); 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(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: SafeArea(child: DropdownMenu(dropdownMenuEntries: menuChildren)), ), ), ); final Finder textField = find.byType(TextField); final double anchorWidth = tester.getSize(textField).width; expect(anchorWidth, closeTo(184.5, 0.1)); await tester.tap(find.byType(DropdownMenu)); 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(184.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(); final shortMenuItems = >[]; for (final ShortMenu value in ShortMenu.values) { final entry = DropdownMenuEntry(value: value, label: value.label); shortMenuItems.add(entry); } const customBigWidth = 250.0; await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customBigWidth)); RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu)); expect(box.size.width, customBigWidth); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); expect(find.byType(MenuItemButton), findsNWidgets(6)); Size buttonSize = tester.getSize(findMenuItemButton('I0')); expect(buttonSize.width, customBigWidth); // reset test await tester.pumpWidget(Container()); const customSmallWidth = 100.0; await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customSmallWidth)); box = tester.firstRenderObject(find.byType(DropdownMenu)); expect(box.size.width, customSmallWidth); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); expect(find.byType(MenuItemButton), findsNWidgets(6)); buttonSize = tester.getSize(findMenuItemButton('I0')); 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(); final shortMenuItems = >[]; for (final ShortMenu value in ShortMenu.values) { final entry = DropdownMenuEntry(value: value, label: value.label); shortMenuItems.add(entry); } var customWidth = 250.0; await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customWidth)); RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu)); expect(box.size.width, customWidth); // Update width customWidth = 400.0; await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customWidth)); box = tester.firstRenderObject(find.byType(DropdownMenu)); expect(box.size.width, customWidth); }); testWidgets('The width is determined by the menu entries', (WidgetTester tester) async { const double entryLabelWidth = 100; await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: >[ DropdownMenuEntry( value: 0, label: 'Flutter', labelWidget: SizedBox(width: entryLabelWidth), ), ], ), ), ), ); final double width = tester.getSize(find.byType(DropdownMenu)).width; const menuEntryPadding = 24.0; // See _kDefaultHorizontalPadding. const decorationStartGap = 4.0; // See _kInputStartGap. const leadingWidth = 16.0; const trailingWidth = 56.0; expect( width, entryLabelWidth + leadingWidth + trailingWidth + menuEntryPadding + decorationStartGap, ); }); testWidgets('The width is determined by the label when it is longer than menu entries', ( WidgetTester tester, ) async { const double labelWidth = 120; const double entryLabelWidth = 100; await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( label: SizedBox(width: labelWidth), dropdownMenuEntries: >[ DropdownMenuEntry( value: 0, label: 'Flutter', labelWidget: SizedBox(width: entryLabelWidth), ), ], ), ), ), ); final double width = tester.getSize(find.byType(DropdownMenu)).width; const leadingWidth = 16.0; const trailingWidth = 56.0; const labelPadding = 8.0; // See RenderEditable.floatingCursorAddedMargin. expect(width, labelWidth + labelPadding + leadingWidth + trailingWidth); }); testWidgets('The width of MenuAnchor respects MenuAnchor.expandedInsets', ( WidgetTester tester, ) async { const parentWidth = 500.0; final shortMenuItems = >[]; for (final ShortMenu value in ShortMenu.values) { final entry = DropdownMenuEntry(value: value, label: value.label); shortMenuItems.add(entry); } Widget buildMenuAnchor({EdgeInsets? expandedInsets}) { return MaterialApp( home: Scaffold( body: SizedBox( width: parentWidth, height: parentWidth, child: DropdownMenu( 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(findMenuItemButton('I0')); 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(findMenuItemButton('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(findMenuItemButton('I0')); expect(buttonSize.width, parentWidth - 35.0 - 20.0); }); // Regression test for https://github.com/flutter/flutter/issues/151769 testWidgets('expandedInsets can use EdgeInsets or EdgeInsetsDirectional', ( WidgetTester tester, ) async { const parentWidth = 500.0; final shortMenuItems = >[]; for (final ShortMenu value in ShortMenu.values) { final entry = DropdownMenuEntry(value: value, label: value.label); shortMenuItems.add(entry); } Widget buildMenuAnchor({EdgeInsetsGeometry? expandedInsets}) { return MaterialApp( home: Scaffold( body: SizedBox( width: parentWidth, height: parentWidth, child: DropdownMenu( 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(findMenuItemButton('I0')); expect(buttonSize.width, 136.0); // 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); Rect containerRect = tester.getRect(find.byType(SizedBox).first); Rect dropdownMenuRect = tester.getRect(find.byType(TextField)); expect(dropdownMenuRect.top, containerRect.top); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); buttonSize = tester.getSize(findMenuItemButton('I0')); expect(buttonSize.width, parentWidth - 35.0 - 20.0); // Regression test for https://github.com/flutter/flutter/issues/151769. // If expandedInsets is not zero, the width of the text field should be adjusted // based on the EdgeInsets.end and EdgeInsets.start. The top and bottom values // will be ignored. await tester.pumpWidget(Container()); await tester.pumpWidget( buildMenuAnchor( expandedInsets: const EdgeInsetsDirectional.only(start: 35.0, top: 50.0, end: 20.0), ), ); box = tester.firstRenderObject(find.byType(TextField)); expect(box.size.width, parentWidth - 35.0 - 20.0); containerRect = tester.getRect(find.byType(SizedBox).first); dropdownMenuRect = tester.getRect(find.byType(TextField)); expect(dropdownMenuRect.top, containerRect.top); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); buttonSize = tester.getSize(findMenuItemButton('I0')); expect(buttonSize.width, parentWidth - 35.0 - 20.0); }); // Regression test for https://github.com/flutter/flutter/issues/172680. testWidgets('Menu panel width can expand to full-screen width', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( expandedInsets: EdgeInsets.zero, dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: 'Flutter'), ], ), ), ), ); final double dropdownWidth = tester.getSize(find.byType(DropdownMenu)).width; expect(dropdownWidth, 800); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final double menuWidth = tester.getSize(findMenuItemButton('Flutter')).width; expect(dropdownWidth, menuWidth); }); // Regression test for https://github.com/flutter/flutter/issues/176501 testWidgets('_RenderDropdownMenuBody.computeDryLayout does not access this.constraints', ( WidgetTester tester, ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: Center( child: _TestDryLayout( child: DropdownMenu( dropdownMenuEntries: >[ DropdownMenuEntry(value: 1, label: 'One'), DropdownMenuEntry(value: 2, label: 'Two'), ], ), ), ), ), ), ); // The test passes if no exception is thrown during the layout phase. expect(tester.takeException(), isNull); expect(find.byType(DropdownMenu), findsOneWidget); }); 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(useMaterial3: false); await tester.pumpWidget(buildTest(themeData, menuChildren)); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); final Element firstItem = tester.element(findMenuItemButton('Item 0')); final firstBox = firstItem.renderObject! as RenderBox; final Offset topLeft = firstBox.localToGlobal(firstBox.size.topLeft(Offset.zero)); final Element lastItem = tester.element(findMenuItemButton('Item 5')); final 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)); 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(); await tester.pumpWidget(buildTest(themeData, menuChildren)); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); final Element firstItem = tester.element(findMenuItemButton('Item 0')); final firstBox = firstItem.renderObject! as RenderBox; final Offset topLeft = firstBox.localToGlobal(firstBox.size.topLeft(Offset.zero)); final Element lastItem = tester.element(findMenuItemButton('Item 5')); final 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.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)); 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.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(); // Default text field (without leading icon). await tester.pumpWidget(buildTest(themeData, menuChildren, label: const Text('label'))); final Finder label = find.text('label').first; final Offset labelTopLeft = tester.getTopLeft(label); await tester.tap(find.byType(DropdownMenu)); 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').first; final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel); await tester.tap(find.byType(DropdownMenu)); 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 + leadingIconToInputPadding)); // 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').first; final Offset updatedLabelTopLeft1 = tester.getTopLeft(updatedLabel1); await tester.tap(find.byType(DropdownMenu)); 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 + leadingIconToInputPadding)); }); testWidgets('The text in the menu button should be aligned with the text of ' 'the text field - RTL', (WidgetTester tester) async { final themeData = ThemeData(); // Default text field (without leading icon). await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: Directionality( textDirection: TextDirection.rtl, child: DropdownMenu( label: const Text('label'), dropdownMenuEntries: menuChildren, ), ), ), ), ); final Finder label = find.text('label').first; final Offset labelTopRight = tester.getTopRight(label); await tester.tap(find.byType(DropdownMenu)); 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( 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)); final Finder updatedLabel = find.text('label').first; final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel); await tester.tap(find.byType(DropdownMenu)); 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 - leadingIconToInputPadding), ); // 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( 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), ); final Finder updatedLabel1 = find.text('label').first; final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1); await tester.tap(find.byType(DropdownMenu)); 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 - leadingIconToInputPadding), ); }); testWidgets('The icon in the menu button should be aligned with the icon of ' 'the text field - LTR', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Directionality( textDirection: TextDirection.ltr, child: DropdownMenu( leadingIcon: const Icon(Icons.search), label: const Text('label'), dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ), ); final Finder dropdownIcon = find .descendant(of: find.byIcon(Icons.search).first, matching: find.byType(RichText)) .last; await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); final Finder itemLeadingIcon = find.byKey(leadingIconKey(TestMenu.mainMenu0)).last; expect(tester.getRect(dropdownIcon).left, tester.getRect(itemLeadingIcon).left); }); testWidgets('The icon in the menu button should be aligned with the icon of ' 'the text field - RTL', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Directionality( textDirection: TextDirection.rtl, child: DropdownMenu( leadingIcon: const Icon(Icons.search), label: const Text('label'), dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ), ); final Finder dropdownIcon = find .descendant(of: find.byIcon(Icons.search).first, matching: find.byType(RichText)) .last; await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); final Finder itemLeadingIcon = find.byKey(leadingIconKey(TestMenu.mainMenu0)).last; expect(tester.getRect(dropdownIcon).right, tester.getRect(itemLeadingIcon).right); }); testWidgets('DropdownMenu has default trailing icon button', (WidgetTester tester) async { final 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: findMenuItemButton(TestMenu.mainMenu0.label), matching: find.byType(Material)) .last; expect(menuMaterial, findsOneWidget); }); testWidgets('Trailing IconButton status test', (WidgetTester tester) async { final themeData = ThemeData(); 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('Trailing IconButton height respects InputDecorationTheme.suffixIconConstraints', ( WidgetTester tester, ) async { final themeData = ThemeData(); // Default suffix icon constraints. await tester.pumpWidget(buildTest(themeData, menuChildren)); await tester.pump(); final Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first; expect(tester.getSize(iconButton), const Size(48, 48)); // Custom suffix icon constraints. await tester.pumpWidget( buildTest( themeData, menuChildren, decorationTheme: const InputDecorationTheme( suffixIconConstraints: BoxConstraints(minWidth: 66, minHeight: 62), ), ), ); await tester.pump(); expect(tester.getSize(iconButton), const Size(66, 62)); }); testWidgets('InputDecorationTheme.isCollapsed reduces height', (WidgetTester tester) async { final themeData = ThemeData(); // Default height. await tester.pumpWidget(buildTest(themeData, menuChildren)); await tester.pump(); final Finder textField = find.byType(TextField).first; expect(tester.getSize(textField).height, 56); // Collapsed height. await tester.pumpWidget( buildTest( themeData, menuChildren, decorationTheme: const InputDecorationTheme(isCollapsed: true), ), ); await tester.pump(); expect(tester.getSize(textField).height, 48); // IconButton min height. // Collapsed height with custom suffix icon constraints. await tester.pumpWidget( buildTest( themeData, menuChildren, decorationTheme: const InputDecorationTheme( isCollapsed: true, suffixIconConstraints: BoxConstraints(maxWidth: 24, maxHeight: 24), ), ), ); await tester.pump(); expect(tester.getSize(textField).height, 24); }); testWidgets('Do not crash when resize window during menu opening', (WidgetTester tester) async { addTearDown(tester.view.reset); final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return DropdownMenu( 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(); expect(findMenuItemButton(TestMenu.mainMenu0.label), findsOne); // 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(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( 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: findMenuItemButton(TestMenu.mainMenu0.label), matching: find.byType(Material)) .last; expect(menuMaterial, findsOneWidget); }); testWidgets('Down key can highlight the menu item while focused', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, trailingIcon: const Icon(Icons.ac_unit), dropdownMenuEntries: menuChildren, ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Item 0'), true); // Press down key one more time, the highlight should move to the next item. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Menu 1'), true); // The previous item should not be highlighted. expect(isItemHighlighted(tester, themeData, 'Item 0'), false); }); testWidgets('Up key can highlight the menu item while focused', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Item 5'), true); // Press up key one more time, the highlight should move up to the item 4. await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Item 4'), true); // The previous item should not be highlighted. expect(isItemHighlighted(tester, themeData, 'Item 5'), false); }); testWidgets('Left and right keys can move text field selection', (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, filterCallback: (List> entries, String filter) { return entries .where((DropdownMenuEntry element) => element.label.contains(filter)) .toList(); }, dropdownMenuEntries: menuChildren, controller: controller, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'example'); await tester.pump(); expect(controller.text, 'example'); expect(controller.selection, const TextSelection.collapsed(offset: 7)); // Press left key, the caret should move left. await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pump(); expect(controller.selection, const TextSelection.collapsed(offset: 6)); // Press Right key, the caret should move right. await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); expect(controller.selection, const TextSelection.collapsed(offset: 7)); }); // Regression test for https://github.com/flutter/flutter/issues/156712. testWidgets('Up and down keys can highlight the menu item when expandedInsets is set', ( WidgetTester tester, ) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( expandedInsets: EdgeInsets.zero, requestFocusOnTap: true, dropdownMenuEntries: menuChildren, ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Item 5'), true); // Press up key one more time, the highlight should move up to the item 4. await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Item 4'), true); // The previous item should not be highlighted. expect(isItemHighlighted(tester, themeData, 'Item 5'), false); // Press down key, the highlight should move back to the item 5. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'Item 5'), true); }); // Regression test for https://github.com/flutter/flutter/issues/156712. testWidgets('Left and right keys can move text field selection when expandedInsets is set', ( WidgetTester tester, ) async { final controller = TextEditingController(); addTearDown(controller.dispose); final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( expandedInsets: EdgeInsets.zero, requestFocusOnTap: true, enableFilter: true, filterCallback: (List> entries, String filter) { return entries .where((DropdownMenuEntry element) => element.label.contains(filter)) .toList(); }, dropdownMenuEntries: menuChildren, controller: controller, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'example'); await tester.pump(); expect(controller.text, 'example'); expect(controller.selection, const TextSelection.collapsed(offset: 7)); // Press left key, the caret should move left. await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pump(); expect(controller.selection, const TextSelection.collapsed(offset: 6)); // Press Right key, the caret should move right. await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); expect(controller.selection, const TextSelection.collapsed(offset: 7)); }); // Regression test for https://github.com/flutter/flutter/issues/147253. testWidgets('Down key and up key can navigate while focused when a label text ' 'contains another label text', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: const Scaffold( body: DropdownMenu( requestFocusOnTap: true, dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: 'ABC'), DropdownMenuEntry(value: 1, label: 'AB'), DropdownMenuEntry(value: 2, label: 'ABCD'), ], ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Press down key three times, the highlight should move to the next item each time. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'ABC'), true); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'AB'), true); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'ABCD'), true); // Press up key two times, the highlight should up each time. await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'AB'), true); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'ABC'), true); }); // Regression test for https://github.com/flutter/flutter/issues/151878. testWidgets('Searching for non matching item does not crash', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( enableFilter: true, requestFocusOnTap: true, dropdownMenuEntries: menuChildren, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'Me'); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'Meu'); await tester.pump(); expect(tester.takeException(), isNull); }); // Regression test for https://github.com/flutter/flutter/issues/154532. testWidgets('Keyboard navigation does not throw when no entries match the filter', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, dropdownMenuEntries: menuChildren, ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'No match'); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'No match 2'); await tester.pump(); expect(tester.takeException(), isNull); }); // Regression test for https://github.com/flutter/flutter/issues/165867. testWidgets('Keyboard navigation only traverses filtered entries', (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, controller: controller, dropdownMenuEntries: const >[ DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Good Match 1'), DropdownMenuEntry(value: TestMenu.mainMenu1, label: 'Bad Match 1'), DropdownMenuEntry(value: TestMenu.mainMenu2, label: 'Good Match 2'), DropdownMenuEntry(value: TestMenu.mainMenu3, label: 'Bad Match 2'), DropdownMenuEntry(value: TestMenu.mainMenu4, label: 'Good Match 3'), DropdownMenuEntry(value: TestMenu.mainMenu5, label: 'Bad Match 3'), ], ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Filter the entries to only show the ones with 'Good Match'. await tester.enterText(find.byType(TextField), 'Good Match'); await tester.pump(); // Since the first entry is already highlighted, navigate to the second item. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(controller.text, 'Good Match 2'); // Navigate to the third item. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(controller.text, 'Good Match 3'); // Navigate back to the first item. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(controller.text, 'Good Match 1'); }); // Regression test for https://github.com/flutter/flutter/issues/147253. testWidgets('Default search prioritises the current highlight', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); const itemLabel = 'Item 2'; // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Highlight the third item by exact search. await tester.enterText(find.byType(TextField).first, itemLabel); await tester.pump(); expect(isItemHighlighted(tester, themeData, itemLabel), true); // Search something that matches multiple items. await tester.enterText(find.byType(TextField).first, 'Item'); await tester.pump(); // The third item should still be highlighted. expect(isItemHighlighted(tester, themeData, itemLabel), true); }); // Regression test for https://github.com/flutter/flutter/issues/152375. testWidgets('Down key and up key can navigate while focused when a label text contains ' 'another label text using customized search algorithm', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, searchCallback: (List> entries, String query) { if (query.isEmpty) { return null; } final int index = entries.indexWhere( (DropdownMenuEntry entry) => entry.label.contains(query), ); return index != -1 ? index : null; }, dropdownMenuEntries: const >[ DropdownMenuEntry(value: 0, label: 'ABC'), DropdownMenuEntry(value: 1, label: 'AB'), DropdownMenuEntry(value: 2, label: 'ABCD'), ], ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Press down key three times, the highlight should move to the next item each time. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'ABC'), true); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'AB'), true); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'ABCD'), true); // Press up key two times, the highlight should up each time. await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'AB'), true); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(isItemHighlighted(tester, themeData, 'ABC'), true); }); // Regression test for https://github.com/flutter/flutter/issues/152375. testWidgets('Searching can highlight entry after keyboard navigation while focused', ( WidgetTester tester, ) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); // Open the menu and highlight the first item. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); // Search for the last item. final String searchedLabel = menuChildren.last.label; await tester.enterText(find.byType(TextField).first, searchedLabel); await tester.pump(); // The corresponding menu entry is highlighted. expect(isItemHighlighted(tester, themeData, searchedLabel), true); }); testWidgets('The text input should match the label of the menu item ' 'when pressing down key while focused', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); 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); }); testWidgets('The text input should match the label of the menu item ' 'when pressing up key while focused', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); 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); }); testWidgets('Disabled button will be skipped while pressing up/down key while focused', ( WidgetTester tester, ) async { final themeData = ThemeData(); final menuWithDisabledItems = >[ const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), const DropdownMenuEntry(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false), const DropdownMenuEntry(value: TestMenu.mainMenu2, label: 'Item 2', enabled: false), const DropdownMenuEntry(value: TestMenu.mainMenu3, label: 'Item 3'), const DropdownMenuEntry(value: TestMenu.mainMenu4, label: 'Item 4'), const DropdownMenuEntry(value: TestMenu.mainMenu5, label: 'Item 5', enabled: false), ]; await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, dropdownMenuEntries: menuWithDisabledItems, ), ), ), ); await tester.pump(); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); // First item is highlighted as it's enabled. expect(isItemHighlighted(tester, themeData, 'Item 0'), true); // 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(); expect(isItemHighlighted(tester, themeData, 'Item 3'), true); }); testWidgets('Searching is enabled by default if initialSelection is non null', ( WidgetTester tester, ) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, initialSelection: TestMenu.mainMenu1, dropdownMenuEntries: menuChildren, ), ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Initial selection (Menu 1) button is highlighted. expect(isItemHighlighted(tester, themeData, 'Menu 1'), true); }); testWidgets('Highlight can move up/down starting from the searching result while focused', ( WidgetTester tester, ) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'Menu 1'); await tester.pumpAndSettle(); expect(isItemHighlighted(tester, themeData, 'Menu 1'), true); // Press up to the upper item (Item 0). await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget); expect(isItemHighlighted(tester, themeData, 'Item 0'), true); // Continue to move up to the last item (Item 5). await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget); expect(isItemHighlighted(tester, themeData, 'Item 5'), true); }); testWidgets('Filtering is disabled by default', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu(requestFocusOnTap: true, dropdownMenuEntries: menuChildren), ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); 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(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, dropdownMenuEntries: menuChildren, ), ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); 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('Enable filtering with custom filter callback that filter text case sensitive', ( WidgetTester tester, ) async { final themeData = ThemeData(); final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, filterCallback: (List> entries, String filter) { return entries .where((DropdownMenuEntry element) => element.label.contains(filter)) .toList(); }, dropdownMenuEntries: menuChildren, controller: controller, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); await tester.enterText(find.byType(TextField).first, 'item'); expect(controller.text, 'item'); await tester.pumpAndSettle(); for (final TestMenu menu in TestMenu.values) { expect(findMenuItemButton(menu.label).hitTestable(), findsNothing); } await tester.enterText(find.byType(TextField).first, 'Item'); expect(controller.text, 'Item'); await tester.pumpAndSettle(); expect(findMenuItemButton('Item 0').hitTestable(), findsOneWidget); expect(findMenuItemButton('Menu 1').hitTestable(), findsNothing); expect(findMenuItemButton('Item 2').hitTestable(), findsOneWidget); expect(findMenuItemButton('Item 3').hitTestable(), findsOneWidget); expect(findMenuItemButton('Item 4').hitTestable(), findsOneWidget); expect(findMenuItemButton('Item 5').hitTestable(), findsOneWidget); }); testWidgets( 'Throw assertion error when enable filtering with custom filter callback and enableFilter set on False', (WidgetTester tester) async { final themeData = ThemeData(); final controller = TextEditingController(); addTearDown(controller.dispose); expect(() { MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, filterCallback: (List> entries, String filter) { return entries .where((DropdownMenuEntry element) => element.label.contains(filter)) .toList(); }, dropdownMenuEntries: menuChildren, controller: controller, ), ), ); }, throwsAssertionError); }, ); testWidgets('The controller can access the value in the input field', ( WidgetTester tester, ) async { final themeData = ThemeData(); final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: themeData, home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, dropdownMenuEntries: menuChildren, controller: controller, ), ); }, ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Finder item3 = findMenuItemButton('Item 3'); 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(); final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, dropdownMenuEntries: menuChildren, controller: controller, ), ), ), ); // Access the MenuAnchor final MenuAnchor menuAnchor = tester.widget(find.byType(MenuAnchor)); // Open the menu await tester.tap(find.byType(DropdownMenu)); 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 { var selectionCount = 0; final themeData = ThemeData(); final menuWithDisabledItems = >[ const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), const DropdownMenuEntry(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false), const DropdownMenuEntry(value: TestMenu.mainMenu2, label: 'Item 2'), const DropdownMenuEntry(value: TestMenu.mainMenu3, label: 'Item 3'), ]; final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: themeData, home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: DropdownMenu( dropdownMenuEntries: menuWithDisabledItems, controller: controller, onSelected: (_) { setState(() { selectionCount++; }); }, ), ); }, ), ), ); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final bool isMobile = switch (themeData.platform) { TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.fuchsia => true, TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => false, }; var expectedCount = 1; // Test onSelected on key press await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); // On mobile platforms, the TextField cannot gain focus by default; the focus is // on a FocusNode specifically used for keyboard navigation. Therefore, // LogicalKeyboardKey.enter should be used. if (isMobile) { await tester.sendKeyEvent(LogicalKeyboardKey.enter); } else { await tester.testTextInput.receiveAction(TextInputAction.done); } await tester.pumpAndSettle(); expect(selectionCount, expectedCount); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Disabled item doesn't trigger onSelected callback. final Finder item1 = findMenuItemButton('Item 1'); await tester.tap(item1); await tester.pumpAndSettle(); expect(controller.text, 'Item 0'); expect(selectionCount, expectedCount); final Finder item2 = findMenuItemButton('Item 2'); await tester.tap(item2); await tester.pumpAndSettle(); expect(controller.text, 'Item 2'); expect(selectionCount, ++expectedCount); await tester.tap(find.byType(DropdownMenu)); await tester.pump(); final Finder item3 = findMenuItemButton('Item 3'); 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(); final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: themeData, home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: DropdownMenu( initialSelection: TestMenu.mainMenu3, dropdownMenuEntries: menuChildren, controller: controller, ), ); }, ), ), ); expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget); // Open the menu await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Validate the item 3 is highlighted. expect(isItemHighlighted(tester, themeData, 'Item 3'), true); }); testWidgets( 'When the initial selection matches a menu entry, the text field displays the corresponding value', (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: DropdownMenu( initialSelection: TestMenu.mainMenu3, dropdownMenuEntries: menuChildren, controller: controller, ), ); }, ), ), ); expect(controller.text, TestMenu.mainMenu3.label); }, ); testWidgets('Text field is empty when the initial selection does not match any menu entries', ( WidgetTester tester, ) async { final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: DropdownMenu( initialSelection: TestMenu.mainMenu3, // Use a menu entries which does not contain TestMenu.mainMenu3. dropdownMenuEntries: menuChildren.getRange(0, 1).toList(), controller: controller, ), ); }, ), ), ); expect(controller.text, isEmpty); }); testWidgets( 'Text field content is not cleared when the initial selection does not match any menu entries', (WidgetTester tester) async { final controller = TextEditingController(text: 'Flutter'); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: DropdownMenu( initialSelection: TestMenu.mainMenu3, // Use a menu entries which does not contain TestMenu.mainMenu3. dropdownMenuEntries: menuChildren.getRange(0, 1).toList(), controller: controller, ), ); }, ), ), ); expect(controller.text, 'Flutter'); }, ); testWidgets('The default text input field should not be focused on mobile platforms ' 'when it is tapped', (WidgetTester tester) async { final themeData = ThemeData(); Widget buildDropdownMenu() => MaterialApp( theme: themeData, home: Scaffold( body: Column(children: [DropdownMenu(dropdownMenuEntries: menuChildren)]), ), ); // Test default condition. await tester.pumpWidget(buildDropdownMenu()); await tester.pump(); final Finder textFieldFinder = find.byType(TextField); final TextField result = tester.widget(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(); Widget buildDropdownMenu() => MaterialApp( theme: themeData, home: Scaffold( body: Column(children: [DropdownMenu(dropdownMenuEntries: menuChildren)]), ), ); await tester.pumpWidget(buildDropdownMenu()); await tester.pump(); final Finder textFieldFinder = find.byType(TextField); final TextField result = tester.widget(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(); Widget buildDropdownMenu({required bool requestFocusOnTap}) => MaterialApp( theme: themeData, home: Scaffold( body: Column( children: [ DropdownMenu( 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(textFieldFinder); expect(textField.canRequestFocus, true); // Open the dropdown menu. await tester.tap(textFieldFinder); await tester.pump(); // Make a selection. await tester.tap(findMenuItemButton('Item 0')); await tester.pump(); expect(findMenuItemButton('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(textFieldFinder1); expect(textField1.canRequestFocus, false); // Open the dropdown menu. await tester.tap(textFieldFinder1); await tester.pump(); // Make a selection. await tester.tap(findMenuItemButton('Item 0')); 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: [ DropdownMenu(requestFocusOnTap: false, dropdownMenuEntries: menuChildren), ], ), ), ); await tester.pumpWidget(buildDropdownMenu()); await tester.pumpAndSettle(); final Finder textFieldFinder = find.byType(TextField); final TextField textField = tester.widget(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: [ DropdownMenu( 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(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(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(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: [DropdownMenu(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: [ DropdownMenu(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 { const hintText = 'I am hintText'; TestMenu? selectedValue; final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( StatefulBuilder( builder: (BuildContext context, StateSetter setState) => MaterialApp( home: Scaffold( body: Center( child: DropdownMenu( 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)); await tester.pumpAndSettle(); await tester.tap(findMenuItemButton('Item 3')); await tester.pumpAndSettle(); expect(selectedValue?.label, 'Item 3'); expect(node.label, ''); expect(node.value, 'Item 3'); }); testWidgets('Semantics does not include initial menu buttons', (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: DropdownMenu( requestFocusOnTap: true, dropdownMenuEntries: menuChildren, onSelected: (TestMenu? value) {}, controller: controller, ), ), ), ), ); // The menu buttons should not be visible and should not be in the semantics tree. for (final String label in TestMenu.values.map((TestMenu menu) => menu.label)) { expect(find.bySemanticsLabel(label), findsNothing); } // Open the menu. await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first); await tester.pump(); // The menu buttons should be visible and in the semantics tree. for (final String label in TestMenu.values.map((TestMenu menu) => menu.label)) { expect(find.bySemanticsLabel(label), findsOneWidget); } }); testWidgets('helperText is not visible when errorText is not null', (WidgetTester tester) async { final themeData = ThemeData(); const helperText = 'I am helperText'; const errorText = 'I am errorText'; Widget buildFrame(bool hasError) { return MaterialApp( theme: themeData, home: Scaffold( body: Center( child: DropdownMenu( 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(); const helperText = 'I am helperText'; Widget buildFrame() { return MaterialApp( theme: themeData, home: Scaffold( body: Center( child: DropdownMenu( 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(); const errorText = 'I am errorText'; Widget buildFrame() { return MaterialApp( theme: themeData, home: Scaffold( body: Center( child: DropdownMenu(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( 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)); 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 inputTextThemeStyle = TextStyle( fontSize: 18.5, fontStyle: FontStyle.italic, wordSpacing: 1.2, decoration: TextDecoration.lineThrough, ); const menuItemTextThemeStyle = TextStyle( fontSize: 20.5, fontStyle: FontStyle.italic, wordSpacing: 2.1, decoration: TextDecoration.underline, ); final themeData = ThemeData( 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(); // Test menu item text style uses the TextTheme.labelLarge. final Material material = getButtonMaterial(tester, TestMenu.mainMenu0.label); 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 controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( controller: controller, width: 100, dropdownMenuEntries: TestMenu.values.map>((TestMenu item) { return DropdownMenuEntry(value: item, label: '${item.label} $longText'); }).toList(), ), ), ), ); // Opening the width=100 menu should not crash. await tester.tap(find.byType(DropdownMenu)); expect(tester.takeException(), isNull); await tester.pumpAndSettle(); Finder findMenuItemText(String label) { final labelText = '$label $longText'; return find.descendant(of: findMenuItemButton(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 controller = TextEditingController(); addTearDown(controller.dispose); Widget buildFrame({required int maxLines}) { return MaterialApp( home: Scaffold( body: DropdownMenu( key: ValueKey(maxLines), controller: controller, width: 100, dropdownMenuEntries: TestMenu.values.map>((TestMenu item) { return DropdownMenuEntry( value: item, label: '${item.label} $longText', labelWidget: Text('${item.label} $longText', maxLines: maxLines), ); }).toList(), ), ), ); } Finder findMenuItemText(String label) { final labelText = '$label $longText'; return find.descendant(of: findMenuItemButton(labelText), matching: find.byType(Text)).last; } await tester.pumpWidget(buildFrame(maxLines: 1)); await tester.tap(find.byType(DropdownMenu)); // 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)); // 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( leadingIcon: Icon(Icons.search), hintText: 'Hint', dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: 'Item 0', leadingIcon: Icon(Icons.alarm)), DropdownMenuEntry(value: 1, label: 'Item 1'), ], ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); // Check text location in text field. expect(tester.getTopLeft(find.text('Hint')).dx, 52.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, 52.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, 52.0); }); testWidgets('DropdownMenu can have customized search algorithm', (WidgetTester tester) async { final theme = ThemeData(); Widget dropdownMenu({SearchCallback? searchCallback}) { return MaterialApp( theme: theme, home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, searchCallback: searchCallback, dropdownMenuEntries: const >[ DropdownMenuEntry(value: 0, label: 'All'), DropdownMenuEntry(value: 1, label: 'Unread'), DropdownMenuEntry(value: 2, label: 'Read'), ], ), ), ); } void checkExpectedHighlight({String? searchResult, required List otherItems}) { if (searchResult != null) { final Finder material = find.descendant( of: findMenuItemButton(searchResult), matching: find.byType(Material), ); final Material itemMaterial = tester.widget(material); expect(itemMaterial.color, theme.colorScheme.onSurface.withOpacity(0.12)); } for (final nonHighlight in otherItems) { final Finder material = find.descendant( of: findMenuItemButton(nonHighlight), matching: find.byType(Material), ); final Material itemMaterial = tester.widget(material); expect(itemMaterial.color, Colors.transparent); } } // Test default. await tester.pumpWidget(dropdownMenu()); await tester.pump(); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'read'); await tester.pump(); checkExpectedHighlight( searchResult: 'Unread', otherItems: ['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: ['Unread', 'Read'], ); // Because the search result should always be index 0. // Test custom search algorithm - exact match. await tester.pumpWidget( dropdownMenu( searchCallback: (List> entries, String query) { if (query.isEmpty) { return null; } final int index = entries.indexWhere( (DropdownMenuEntry entry) => entry.label == query, ); return index != -1 ? index : null; }, ), ); await tester.pump(); await tester.enterText(find.byType(TextField), 'read'); await tester.pump(); checkExpectedHighlight( otherItems: ['All', 'Unread', 'Read'], ); // Because it's case sensitive. await tester.enterText(find.byType(TextField), 'Read'); await tester.pump(); checkExpectedHighlight(searchResult: 'Read', otherItems: ['All', 'Unread']); }); testWidgets('onSelected gets called when a selection is made in a nested menu', ( WidgetTester tester, ) async { var selectionCount = 0; final themeData = ThemeData(); final menuWithDisabledItems = >[ const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), ]; await tester.pumpWidget( MaterialApp( theme: themeData, home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: MenuAnchor( menuChildren: [ DropdownMenu( 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)); await tester.pump(); final Finder item1 = findMenuItemButton('Item 0'); 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 { var selectionCount = 0; final themeData = ThemeData(); final menuWithDisabledItems = >[ const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), ]; await tester.pumpWidget( MaterialApp( theme: themeData, home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: MenuAnchor( menuChildren: [ DropdownMenu( 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)); await tester.pump(); final Finder item1 = findMenuItemButton('Item 0'); 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(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 menuItems = >[ DropdownMenuEntry( value: TestMenu.mainMenu0, label: 'Item 0', style: MenuItemButton.styleFrom(minimumSize: const Size.fromHeight(1000)), ), ]; await tester.pumpWidget( MaterialApp( home: Scaffold(body: DropdownMenu(dropdownMenuEntries: menuItems)), ), ); await tester.tap(find.byType(DropdownMenu)); 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(); addTearDown(focusNode.dispose); final theme = ThemeData(); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: DropdownMenu( focusNode: focusNode, dropdownMenuEntries: const >[ DropdownMenuEntry(value: 'Yolk', label: 'Yolk'), DropdownMenuEntry(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)); }); // Regression test for https://github.com/flutter/flutter/issues/131120. testWidgets('Focus traversal ignores non visible entries', (WidgetTester tester) async { final buttonFocusNode = FocusNode(); final textFieldFocusNode = FocusNode(); addTearDown(buttonFocusNode.dispose); addTearDown(textFieldFocusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( children: [ DropdownMenu( dropdownMenuEntries: menuChildren, focusNode: textFieldFocusNode, ), ElevatedButton( focusNode: buttonFocusNode, onPressed: () {}, child: const Text('Button'), ), ], ), ), ), ); // Move the focus to the dropdown trailing icon. primaryFocus!.nextFocus(); await tester.pump(); final Element iconButton = tester.firstElement(find.byIcon(Icons.arrow_drop_down)); expect(Focus.of(iconButton).hasFocus, isTrue); // Move the focus to the text field. primaryFocus!.nextFocus(); await tester.pump(); expect(textFieldFocusNode.hasFocus, isTrue); // Move the focus to the elevated button. primaryFocus!.nextFocus(); await tester.pump(); expect(buttonFocusNode.hasFocus, isTrue); }); testWidgets('DropdownMenu honors inputFormatters', (WidgetTester tester) async { var called = 0; final formatter = TextInputFormatter.withFunction(( TextEditingValue oldValue, TextEditingValue newValue, ) { called += 1; return newValue; }); final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, controller: controller, dropdownMenuEntries: const >[ DropdownMenuEntry(value: 'Blue', label: 'Blue'), DropdownMenuEntry(value: 'Green', label: 'Green'), ], inputFormatters: [ 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( dropdownMenuEntries: >[ DropdownMenuEntry( 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)); 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( dropdownMenuEntries: >[ DropdownMenuEntry( 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 labelText = 'MenuEntry 1'; await tester.pumpWidget( const MaterialApp( home: Scaffold( body: Center( child: DropdownMenu( hintText: 'Hint text', helperText: 'Menu Helper text', inputDecorationTheme: InputDecorationTheme( helperMaxLines: 2, helperStyle: TextStyle(fontSize: 30), ), dropdownMenuEntries: >[ DropdownMenuEntry(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)); }); testWidgets('DropdownMenu allows customizing text field text align', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: Column( children: >[ DropdownMenu(dropdownMenuEntries: >[]), DropdownMenu( textAlign: TextAlign.center, dropdownMenuEntries: >[], ), ], ), ), ), ); final List fields = tester.widgetList(find.byType(TextField)).toList(); expect(fields[0].textAlign, TextAlign.start); expect(fields[1].textAlign, TextAlign.center); }); testWidgets('DropdownMenu correctly sets keyboardType on TextField', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SafeArea( child: DropdownMenu( dropdownMenuEntries: menuChildren, keyboardType: TextInputType.number, ), ), ), ), ); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.number); }); testWidgets('DropdownMenu keyboardType defaults to TextInputType.text', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SafeArea(child: DropdownMenu(dropdownMenuEntries: menuChildren)), ), ), ); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.keyboardType, TextInputType.text); }); testWidgets('DropdownMenu passes an alignmentOffset to MenuAnchor', (WidgetTester tester) async { const alignmentOffset = Offset(0, 16); await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( alignmentOffset: alignmentOffset, dropdownMenuEntries: >[ DropdownMenuEntry(value: '1', label: 'One'), DropdownMenuEntry(value: '2', label: 'Two'), ], ), ), ), ); final MenuAnchor menuAnchor = tester.widget(find.byType(MenuAnchor)); expect(menuAnchor.alignmentOffset, alignmentOffset); }); testWidgets('DropdownMenu filter is disabled until text input', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( requestFocusOnTap: true, enableFilter: true, initialSelection: menuChildren[0].value, dropdownMenuEntries: menuChildren, ), ), ), ); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); // All entries should be available, and two buttons should be found for each entry. // One is layout for the _DropdownMenuBody, the other one is the real button item in the menu. for (final TestMenu menu in TestMenu.values) { expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2)); } // Text input would enable the filter. 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); } } // Selecting an item would disable filter again. await tester.tap(findMenuItemButton('Menu 1')); await tester.pumpAndSettle(); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); for (final TestMenu menu in TestMenu.values) { expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2)); } }); // This is a regression test for https://github.com/flutter/flutter/issues/151686. testWidgets('Setting DropdownMenu.requestFocusOnTap to false makes TextField a button', ( WidgetTester tester, ) async { const label = 'Test'; Widget buildDropdownMenu({bool? requestFocusOnTap}) { return MaterialApp( home: Scaffold( body: Center( child: DropdownMenu( requestFocusOnTap: requestFocusOnTap, dropdownMenuEntries: menuChildren, hintText: label, ), ), ), ); } await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: true)); expect( tester.getSemantics(find.byType(TextField)), matchesSemantics( hasFocusAction: true, hasTapAction: true, isTextField: true, isFocusable: true, hasEnabledState: true, isEnabled: true, label: 'Test', textDirection: TextDirection.ltr, hasExpandedState: true, ), ); await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: false)); expect( tester.getSemantics(find.byType(TextField)), kIsWeb ? matchesSemantics(isButton: true, hasExpandedState: true) : matchesSemantics( isButton: true, hasExpandedState: true, hasFocusAction: true, isTextField: true, isFocusable: true, hasEnabledState: true, isEnabled: true, label: 'Test', isReadOnly: true, textDirection: TextDirection.ltr, ), ); }); // This is a regression test for https://github.com/flutter/flutter/issues/151854. testWidgets('scrollToHighlight does not scroll parent', (WidgetTester tester) async { final controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView( controller: controller, children: [ ListView( shrinkWrap: true, children: [ DropdownMenu( initialSelection: menuChildren.last.value, dropdownMenuEntries: menuChildren, ), ], ), const SizedBox(height: 1000.0), ], ), ), ), ); await tester.tap(find.byType(TextField).first); await tester.pumpAndSettle(); expect(controller.offset, 0.0); }); testWidgets('DropdownMenu with expandedInsets can be aligned', (WidgetTester tester) async { Widget buildMenuAnchor({AlignmentGeometry alignment = Alignment.topCenter}) { return MaterialApp( home: Scaffold( body: Row( children: [ Expanded( child: Align( alignment: alignment, child: DropdownMenu( expandedInsets: const EdgeInsets.all(16), dropdownMenuEntries: menuChildren, ), ), ), ], ), ), ); } await tester.pumpWidget(buildMenuAnchor()); Offset textFieldPosition = tester.getTopLeft(find.byType(TextField)); expect(textFieldPosition, equals(const Offset(16.0, 0.0))); await tester.pumpWidget(buildMenuAnchor(alignment: Alignment.center)); textFieldPosition = tester.getTopLeft(find.byType(TextField)); expect(textFieldPosition, equals(const Offset(16.0, 272.0))); await tester.pumpWidget(buildMenuAnchor(alignment: Alignment.bottomCenter)); textFieldPosition = tester.getTopLeft(find.byType(TextField)); expect(textFieldPosition, equals(const Offset(16.0, 544.0))); }); // Regression test for https://github.com/flutter/flutter/issues/139269. testWidgets('DropdownMenu.closeBehavior controls menu closing behavior', ( WidgetTester tester, ) async { Widget buildDropdownMenu({ DropdownMenuCloseBehavior closeBehavior = DropdownMenuCloseBehavior.all, }) { return MaterialApp( home: Scaffold( body: MenuAnchor( menuChildren: [ DropdownMenu( closeBehavior: closeBehavior, dropdownMenuEntries: menuChildren, ), ], child: const Text('Open Menu'), builder: (BuildContext context, MenuController controller, Widget? child) { return ElevatedButton(onPressed: () => controller.open(), child: child); }, ), ), ); } // Test closeBehavior set to all. await tester.pumpWidget(buildDropdownMenu()); // Tap the button to open the root anchor. await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Tap the menu item to open the dropdown menu. await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(find.byType(DropdownMenu), findsOneWidget); MenuAnchor dropdownMenuAnchor = tester.widget(find.byType(MenuAnchor).last); expect(dropdownMenuAnchor.controller!.isOpen, true); // Tap the dropdown menu item. await tester.tap(findMenuItemButton(TestMenu.mainMenu0.label)); await tester.pumpAndSettle(); // All menus should be closed. expect(find.byType(DropdownMenu), findsNothing); expect(find.byType(MenuAnchor), findsOneWidget); // Test closeBehavior set to self. await tester.pumpWidget(buildDropdownMenu(closeBehavior: DropdownMenuCloseBehavior.self)); // Tap the button to open the root anchor. await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(DropdownMenu), findsOneWidget); // Tap the menu item to open the dropdown menu. await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); dropdownMenuAnchor = tester.widget(find.byType(MenuAnchor).last); expect(dropdownMenuAnchor.controller!.isOpen, true); // Tap the menu item to open the dropdown menu. await tester.tap(findMenuItemButton(TestMenu.mainMenu0.label)); await tester.pumpAndSettle(); // Only the dropdown menu should be closed. expect(dropdownMenuAnchor.controller!.isOpen, false); // Test closeBehavior set to none. await tester.pumpWidget(buildDropdownMenu(closeBehavior: DropdownMenuCloseBehavior.none)); // Tap the button to open the root anchor. await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(DropdownMenu), findsOneWidget); // Tap the menu item to open the dropdown menu. await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); dropdownMenuAnchor = tester.widget(find.byType(MenuAnchor).last); expect(dropdownMenuAnchor.controller!.isOpen, true); // Tap the dropdown menu item. await tester.tap(findMenuItemButton(TestMenu.mainMenu0.label)); await tester.pumpAndSettle(); // None of the menus should be closed. expect(dropdownMenuAnchor.controller!.isOpen, true); }); group('The menu is attached at the bottom of the TextField', () { // Define the expected text field bottom instead of querying it using // tester.getRect because when tight constraints are applied to the // Dropdown the TextField bounds are expanded while the visible size // remains 56 pixels. const textFieldBottom = 56.0; testWidgets('when given loose constraints and expandedInsets is set', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( expandedInsets: EdgeInsets.zero, initialSelection: TestMenu.mainMenu3, dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ); // Open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(tester.getRect(findMenuMaterial()).top, textFieldBottom); }); testWidgets('when given tight constraints and expandedInsets is set', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SizedBox( width: 200, height: 300, child: DropdownMenu( expandedInsets: EdgeInsets.zero, initialSelection: TestMenu.mainMenu3, dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ), ); // Open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(tester.getRect(findMenuMaterial()).top, textFieldBottom); }); // Regression test for https://github.com/flutter/flutter/issues/147076. testWidgets('when given loose constraints and expandedInsets is not set', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( initialSelection: TestMenu.mainMenu3, dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ); // Open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(tester.getRect(findMenuMaterial()).top, textFieldBottom); }); // Regression test for https://github.com/flutter/flutter/issues/147076. testWidgets('when given tight constraints and expandedInsets is not set', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SizedBox( width: 200, height: 300, child: DropdownMenu( initialSelection: TestMenu.mainMenu3, dropdownMenuEntries: menuChildrenWithIcons, ), ), ), ), ); // Open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(tester.getRect(findMenuMaterial()).top, textFieldBottom); }); }); // Regression test for https://github.com/flutter/flutter/issues/143505. testWidgets('Using keyboard navigation to select', (WidgetTester tester) async { final focusNode = FocusNode(); addTearDown(focusNode.dispose); TestMenu? selectedMenu; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: DropdownMenu( focusNode: focusNode, dropdownMenuEntries: menuChildren, onSelected: (TestMenu? menu) { selectedMenu = menu; }, ), ), ), ), ); // Adding FocusNode to IconButton causes the IconButton to receive focus. // Thus it does not matter if the TextField has a FocusNode or not. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); // Now the focus is on the icon button. final Element iconButton = tester.firstElement(find.byIcon(Icons.arrow_drop_down)); expect(Focus.of(iconButton).hasPrimaryFocus, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); expect(selectedMenu, TestMenu.mainMenu0); }, variant: TargetPlatformVariant.all()); // Regression test for https://github.com/flutter/flutter/issues/143505. testWidgets( 'Using keyboard navigation to select and without setting the FocusNode parameter', (WidgetTester tester) async { TestMenu? selectedMenu; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: DropdownMenu( dropdownMenuEntries: menuChildren, onSelected: (TestMenu? menu) { selectedMenu = menu; }, ), ), ), ), ); // Adding FocusNode to IconButton causes the IconButton to receive focus. // Thus it does not matter if the TextField has a FocusNode or not. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); // Now the focus is on the icon button. final Element iconButton = tester.firstElement(find.byIcon(Icons.arrow_drop_down)); expect(Focus.of(iconButton).hasPrimaryFocus, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); expect(selectedMenu, TestMenu.mainMenu0); }, variant: TargetPlatformVariant.all(), ); // Regression test for https://github.com/flutter/flutter/issues/177993. testWidgets('Pressing ESC key closes the menu when requestFocusOnTap is false', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: DropdownMenu( dropdownMenuEntries: menuChildren, requestFocusOnTap: false, ), ), ), ), ); // Move focus to the TextField and open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(findMenuPanel(), findsOne); // Press ESC to close the menu. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(findMenuPanel(), findsNothing); }); testWidgets('Pressing ESC key closes the menu when requestFocusOnTap is true', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: DropdownMenu( dropdownMenuEntries: menuChildren, requestFocusOnTap: true, ), ), ), ), ); // Move focus to the TextField and open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(findMenuPanel(), findsOne); // Press ESC to close the menu. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(findMenuPanel(), findsNothing); }); testWidgets( 'Pressing ESC key after changing the selected item closes the menu', (WidgetTester tester) async { final themeData = ThemeData(); await tester.pumpWidget( MaterialApp( theme: themeData, home: Material( child: Center( child: DropdownMenu( dropdownMenuEntries: menuChildren, initialSelection: menuChildren[2].value, ), ), ), ), ); // Move focus to the TextField and open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(findMenuPanel(), findsOne); // Move the selection. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(isItemHighlighted(tester, themeData, menuChildren[3].label), isTrue); // Press ESC to close the menu. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(findMenuPanel(), findsNothing); }, variant: TargetPlatformVariant.all(), ); testWidgets('DropdownMenu passes maxLines to TextField', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold(body: DropdownMenu(dropdownMenuEntries: menuChildren)), ), ); TextField textField = tester.widget(find.byType(TextField)); // Default behavior. expect(textField.maxLines, 1); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu(dropdownMenuEntries: menuChildren, maxLines: null), ), ), ); textField = tester.widget(find.byType(TextField)); expect(textField.maxLines, null); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu(dropdownMenuEntries: menuChildren, maxLines: 2), ), ), ); textField = tester.widget(find.byType(TextField)); expect(textField.maxLines, 2); }); testWidgets('DropdownMenu passes textInputAction to TextField', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold(body: DropdownMenu(dropdownMenuEntries: menuChildren)), ), ); TextField textField = tester.widget(find.byType(TextField)); // Default behavior. expect(textField.textInputAction, null); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, textInputAction: TextInputAction.next, ), ), ), ); textField = tester.widget(find.byType(TextField)); expect(textField.textInputAction, TextInputAction.next); }); // Regression test for https://github.com/flutter/flutter/issues/162539 testWidgets( 'When requestFocusOnTap is true, the TextField should gain focus after being tapped.', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, requestFocusOnTap: true, ), ), ), ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); final Element textField = tester.firstElement(find.byType(TextField)); expect(Focus.of(textField).hasFocus, isTrue); }, ); testWidgets('items can be constrainted to be smaller than the text field with menuStyle', ( WidgetTester tester, ) async { const longLabel = 'This is a long text that it can overflow.'; await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: longLabel), ], menuStyle: MenuStyle(maximumSize: WidgetStatePropertyAll(Size(150.0, 50.0))), ), ), ), ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(tester.getSize(findMenuItemButton(longLabel)).width, 150.0); // The overwrite of menuStyle is different when a width is provided, // So it needs to be tested separately. await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( width: 200.0, dropdownMenuEntries: menuChildren, menuStyle: const MenuStyle( maximumSize: WidgetStatePropertyAll(Size(150.0, 50.0)), ), ), ), ), ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(tester.getSize(findMenuItemButton(menuChildren.first.label)).width, 150.0); // The overwrite of menuStyle is different when a width is provided but maximumSize is not, // So it needs to be tested separately. await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( width: 200.0, dropdownMenuEntries: menuChildren, menuStyle: const MenuStyle(), ), ), ), ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(tester.getSize(findMenuItemButton(menuChildren.first.label)).width, 200.0); }); testWidgets( 'ensure items are constrained to intrinsic size of DropdownMenu (width or anchor) when no maximumSize', (WidgetTester tester) async { const shortLabel = 'Male'; await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( width: 200, dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: shortLabel), ], menuStyle: MenuStyle(), ), ), ), ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(tester.getSize(findMenuItemButton(shortLabel)).width, 200); // Use expandedInsets to anchor the TextField to the same size as the parent. await tester.pumpWidget( const MaterialApp( home: Scaffold( body: SizedBox( width: double.infinity, child: DropdownMenu( expandedInsets: EdgeInsets.symmetric(horizontal: 20), dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: shortLabel), ], menuStyle: MenuStyle(), ), ), ), ), ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); // Default width is 800, so the expected width is 800 - padding (20 + 20). expect(tester.getSize(findMenuItemButton(shortLabel)).width, 760.0); }, ); // Regression test for https://github.com/flutter/flutter/issues/164905. testWidgets('ensure exclude semantics for trailing button', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( const MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: >[ DropdownMenuEntry(value: 0, label: 'Item 0'), ], ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ if (kIsWeb) TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.hasExpandedState, ], actions: [SemanticsAction.expand], ) else TestSemantics( id: 5, inputType: SemanticsInputType.text, flags: [ SemanticsFlag.isTextField, SemanticsFlag.isFocusable, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isReadOnly, SemanticsFlag.isButton, SemanticsFlag.hasExpandedState, ], actions: [ SemanticsAction.focus, SemanticsAction.expand, ], textDirection: TextDirection.ltr, currentValueLength: 0, ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ignoreId: true, ), ); semantics.dispose(); }); testWidgets('restorationId is passed to inner TextField', (WidgetTester tester) async { const restorationId = 'dropdown_menu'; await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, requestFocusOnTap: true, restorationId: restorationId, ), ), ), ); expect(find.byType(TextField), findsOne); final TextField textField = tester.firstWidget(find.byType(TextField)); expect(textField.restorationId, restorationId); }); testWidgets( 'DropdownMenu does not include the default trailing icon when showTrailingIcon is false', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( showTrailingIcon: false, dropdownMenuEntries: menuChildren, ), ), ), ); await tester.pump(); final Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down); expect(iconButton, findsNothing); }, ); testWidgets( 'DropdownMenu does not include the provided trailing icon when showTrailingIcon is false', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( trailingIcon: const Icon(Icons.ac_unit), showTrailingIcon: false, dropdownMenuEntries: menuChildren, ), ), ), ); await tester.pump(); final Finder iconButton = find.widgetWithIcon(IconButton, Icons.ac_unit); expect(iconButton, findsNothing); }, ); testWidgets('Explicitly provided controllers should not be disposed when switched out.', ( WidgetTester tester, ) async { final controller1 = TextEditingController(); final controller2 = TextEditingController(); Future pumpDropdownMenu(TextEditingController? controller) { return tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu(controller: controller, dropdownMenuEntries: menuChildren), ), ), ); } await pumpDropdownMenu(controller1); await pumpDropdownMenu(controller2); controller1.dispose(); controller2.dispose(); expect(tester.takeException(), isNull); }); // Regression test for https://github.com/flutter/flutter/issues/169942. testWidgets( 'DropdownMenu disabled state applies proper styling to label and selected value text', (WidgetTester tester) async { final themeData = ThemeData(); final Color disabledColor = themeData.colorScheme.onSurface.withOpacity(0.38); Widget buildDropdownMenu({required bool isEnabled}) { return MaterialApp( theme: themeData, home: Scaffold( body: DropdownMenu( width: double.infinity, enabled: isEnabled, initialSelection: 'One', label: const Text('Choose number'), dropdownMenuEntries: const >[ DropdownMenuEntry(value: 'One', label: 'One'), ], ), ), ); } await tester.pumpWidget(buildDropdownMenu(isEnabled: true)); // Find the TextField and its EditableText from DropdownMenu. final TextField enabledTextField = tester.widget(find.byType(TextField)); final EditableText enabledEditableText = tester.widget(find.byType(EditableText)); // Verify enabled state styling for the TextField. expect(enabledTextField.enabled, isTrue); expect(enabledEditableText.style.color, isNot(disabledColor)); // Switch to the disabled state by rebuilding the widget. await tester.pumpWidget(buildDropdownMenu(isEnabled: false)); // Find the TextField and its EditableText in disabled state. final TextField textField = tester.widget(find.byType(TextField)); final EditableText disabledEditableText = tester.widget(find.byType(EditableText)); // Verify disabled state styling for the TextField. expect(textField.enabled, isFalse); expect(disabledEditableText.style.color, disabledColor); // Verify the selected value text has disabled color. final EditableText selectedValueText = tester.widget( find.descendant(of: find.byType(TextField), matching: find.byType(EditableText)), ); expect(selectedValueText.style.color, disabledColor); }, ); testWidgets('DropdownMenu trailingIconFocusNode is created when not provided', ( WidgetTester tester, ) async { final textFieldFocusNode = FocusNode(); final buttonFocusNode = FocusNode(); addTearDown(textFieldFocusNode.dispose); addTearDown(buttonFocusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( children: [ DropdownMenu( dropdownMenuEntries: menuChildren, focusNode: textFieldFocusNode, ), ElevatedButton( focusNode: buttonFocusNode, onPressed: () {}, child: const Text('Button'), ), ], ), ), ), ); primaryFocus!.nextFocus(); await tester.pump(); // Ensure the trailing icon does not have focus. // If FocusNode is not created then the TextField will have focus. final Element iconButton = tester.firstElement(find.byIcon(Icons.arrow_drop_down)); expect(Focus.of(iconButton).hasFocus, isTrue); // Ensure the TextField has focus. primaryFocus!.nextFocus(); await tester.pump(); expect(textFieldFocusNode.hasFocus, isTrue); // Ensure the button has focus. primaryFocus!.nextFocus(); await tester.pump(); expect(buttonFocusNode.hasFocus, isTrue); }); testWidgets('DropdownMenu trailingIconFocusNode is used when provided', ( WidgetTester tester, ) async { final textFieldFocusNode = FocusNode(); final trailingIconFocusNode = FocusNode(); final buttonFocusNode = FocusNode(); addTearDown(textFieldFocusNode.dispose); addTearDown(trailingIconFocusNode.dispose); addTearDown(buttonFocusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( children: [ DropdownMenu( dropdownMenuEntries: menuChildren, focusNode: textFieldFocusNode, trailingIconFocusNode: trailingIconFocusNode, ), ElevatedButton( focusNode: buttonFocusNode, onPressed: () {}, child: const Text('Button'), ), ], ), ), ), ); primaryFocus!.nextFocus(); await tester.pump(); // Ensure the trailing icon has focus. expect(trailingIconFocusNode.hasFocus, isTrue); // Ensure the TextField has focus. primaryFocus!.nextFocus(); await tester.pump(); expect(textFieldFocusNode.hasFocus, isTrue); // Ensure the button has focus. primaryFocus!.nextFocus(); await tester.pump(); expect(buttonFocusNode.hasFocus, isTrue); }); testWidgets( 'Throw assertion error when showTrailingIcon is false and trailingIconFocusNode is provided', (WidgetTester tester) async { expect(() { final focusNode = FocusNode(); addTearDown(focusNode.dispose); MaterialApp( home: Scaffold( body: DropdownMenu( showTrailingIcon: false, trailingIconFocusNode: focusNode, dropdownMenuEntries: menuChildren, ), ), ); }, throwsAssertionError); }, ); testWidgets('DropdownMenu can set cursorHeight', (WidgetTester tester) async { const cursorHeight = 4.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( cursorHeight: cursorHeight, dropdownMenuEntries: menuChildren, ), ), ), ); final EditableText editableText = tester.widget(find.byType(EditableText)); expect(editableText.cursorHeight, cursorHeight); }); testWidgets('DropdownMenu accepts a MenuController', (WidgetTester tester) async { final menuController = MenuController(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, ), ), ), ); expect(findMenuItemButton('Item 0').hitTestable(), findsNothing); menuController.open(); await tester.pumpAndSettle(); expect(findMenuItemButton('Item 0').hitTestable(), findsOne); menuController.close(); await tester.pumpAndSettle(); expect(findMenuItemButton('Item 0').hitTestable(), findsNothing); }); group('DropdownMenu.decorationBuilder', () { const labelText = 'labelText'; InputDecoration buildDecorationWithSuffixIcon(BuildContext context, MenuController controller) { return InputDecoration( labelText: labelText, suffixIcon: controller.isOpen ? const Icon(Icons.arrow_drop_up) : const Icon(Icons.arrow_drop_down), ); } InputDecoration buildDecoration(BuildContext context, MenuController controller) { return const InputDecoration(labelText: labelText); } testWidgets('Decoration properties set by decorationBuilder are applied', ( WidgetTester tester, ) async { final menuController = MenuController(); const decoration = InputDecoration( labelText: labelText, helperText: 'helperText', hintText: 'hintText', filled: true, ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, decorationBuilder: (BuildContext context, MenuController controller) { return decoration; }, ), ), ), ); final TextField textField = tester.firstWidget(find.byType(TextField)); final InputDecoration effectiveDecoration = textField.decoration!; expect(effectiveDecoration.labelText, decoration.labelText); expect(effectiveDecoration.helperText, decoration.helperText); expect(effectiveDecoration.hintText, decoration.hintText); expect(effectiveDecoration.filled, decoration.filled); }); testWidgets('Custom decorationBuilder can replace default suffixIcon', ( WidgetTester tester, ) async { final menuController = MenuController(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, decorationBuilder: buildDecorationWithSuffixIcon, ), ), ), ); expect(find.byIcon(Icons.arrow_drop_down), findsNWidgets(2)); expect(find.byType(IconButton), findsNothing); }); testWidgets('Custom decorationBuilder is called when the menu opens and closes', ( WidgetTester tester, ) async { final menuController = MenuController(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, decorationBuilder: buildDecorationWithSuffixIcon, ), ), ), ); expect(find.byIcon(Icons.arrow_drop_down), findsNWidgets(2)); expect(find.byIcon(Icons.arrow_drop_up), findsNothing); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Check that the custom decorationBuilder updated the icon. expect(find.byIcon(Icons.arrow_drop_down), findsNothing); expect(find.byIcon(Icons.arrow_drop_up), findsNWidgets(2)); }); testWidgets( 'Default IconButton is used when decorationBuilder does not set InputDecoration.suffixIcon', (WidgetTester tester) async { final menuController = MenuController(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, decorationBuilder: buildDecoration, ), ), ), ); expect(find.byType(IconButton), findsNWidgets(2)); }, ); testWidgets('Passing label and decorationBuilder throws', (WidgetTester tester) async { final menuController = MenuController(); await expectLater(() async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, label: const Text('Label'), decorationBuilder: buildDecoration, ), ), ), ); }, throwsAssertionError); }); testWidgets('Passing hintText and decorationBuilder throws', (WidgetTester tester) async { final menuController = MenuController(); await expectLater(() async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, hintText: 'hintText', decorationBuilder: buildDecoration, ), ), ), ); }, throwsAssertionError); }); testWidgets('Passing helperText and decorationBuilder throws', (WidgetTester tester) async { final menuController = MenuController(); await expectLater(() async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, hintText: 'hintText', decorationBuilder: buildDecoration, ), ), ), ); }, throwsAssertionError); }); testWidgets('Passing errorText and decorationBuilder throws', (WidgetTester tester) async { final menuController = MenuController(); await expectLater(() async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, dropdownMenuEntries: menuChildren, errorText: 'errorText', decorationBuilder: buildDecoration, ), ), ), ); }, throwsAssertionError); }); testWidgets('Preferred width takes labelText into account', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, decorationBuilder: (BuildContext context, MenuController controller) { return const InputDecoration(labelText: 'Long label text'); }, ), ), ), ); final double width = tester.getSize(find.byType(TextField)).width; expect(width, 327.5); }); testWidgets('Preferred width takes label into account', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, decorationBuilder: (BuildContext context, MenuController controller) { return const InputDecoration(label: SizedBox(width: 200)); }, ), ), ), ); final double width = tester.getSize(find.byType(TextField)).width; expect(width, 280); }); }); group('DropdownMenu.selectOnly', () { testWidgets('defaults to false on all platforms', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold(body: DropdownMenu(dropdownMenuEntries: menuChildren)), ), ); final DropdownMenu dropdownMenu = tester.firstWidget( find.byType(DropdownMenu), ); expect(dropdownMenu.selectOnly, false); }, variant: TargetPlatformVariant.all()); testWidgets('when true and requestFocusOnTap is false, makes the text field readOnly', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, selectOnly: true, requestFocusOnTap: false, ), ), ), ); final TextField textField = tester.firstWidget(find.byType(TextField)); expect(textField.readOnly, true); }); testWidgets('when true and requestFocusOnTap is true, makes the text field readOnly', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, selectOnly: true, requestFocusOnTap: true, ), ), ), ); final TextField textField = tester.firstWidget(find.byType(TextField)); expect(textField.readOnly, true); }); testWidgets( 'when true and requestFocusOnTap is false, disables text field interactive selection', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, selectOnly: true, requestFocusOnTap: false, ), ), ), ); final TextField textField = tester.firstWidget(find.byType(TextField)); expect(textField.enableInteractiveSelection, false); }, ); testWidgets( 'when true and requestFocusOnTap is true, disables text field interactive selection', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, selectOnly: true, requestFocusOnTap: true, ), ), ), ); final TextField textField = tester.firstWidget(find.byType(TextField)); expect(textField.enableInteractiveSelection, false); }, ); testWidgets( 'when true and requestFocusOnTap is false, does not make the text field focusable', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, selectOnly: true, requestFocusOnTap: false, ), ), ), ); final EditableText editableText = tester.widget(find.byType(EditableText)); expect(editableText.focusNode.hasFocus, false); // Open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(editableText.focusNode.hasFocus, false); }, ); testWidgets('when true and requestFocusOnTap is true, makes the text field focusable', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( dropdownMenuEntries: menuChildren, selectOnly: true, requestFocusOnTap: true, ), ), ), ); final EditableText editableText = tester.widget(find.byType(EditableText)); expect(editableText.focusNode.hasFocus, false); // Open the menu. await tester.tap(find.byType(TextField)); await tester.pump(); expect(editableText.focusNode.hasFocus, true); }); testWidgets('when true and the text field is focused, pressing enter opens the menu', ( WidgetTester tester, ) async { final menuController = MenuController(); final focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownMenu( menuController: menuController, focusNode: focusNode, dropdownMenuEntries: menuChildren, selectOnly: true, ), ), ), ); final EditableText editableText = tester.widget(find.byType(EditableText)); // Focus the dropdownMenu. expect(editableText.focusNode.hasFocus, false); focusNode.requestFocus(); await tester.pump(); expect(editableText.focusNode.hasFocus, true); // Pressing enter opens the menu. expect(menuController.isOpen, false); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); expect(menuController.isOpen, true); }); testWidgets('when true, the mouse cursor should be SystemMouseCursors.click when hovered', ( WidgetTester tester, ) async { Widget buildDropdownMenu() => MaterialApp( home: Scaffold( body: Column( children: [ DropdownMenu(selectOnly: true, dropdownMenuEntries: menuChildren), ], ), ), ); await tester.pumpWidget(buildDropdownMenu()); await tester.pumpAndSettle(); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.moveTo(tester.getCenter(find.byType(TextField))); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click, ); }); }); // Regression test for https://github.com/flutter/flutter/issues/174609. testWidgets( 'DropdownMenu keeps the selected item from filtered list after entries list is updated', (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return DropdownMenu( controller: controller, requestFocusOnTap: true, enableFilter: true, // toList() is used here to simulate list update. dropdownMenuEntries: menuChildren.toList(), onSelected: (_) { setState(() {}); }, ); }, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Filter the entries to only show 'Menu 1'. await tester.enterText(find.byType(TextField).first, TestMenu.mainMenu1.label); await tester.pump(); // Select the 'Menu 1' item. await tester.tap(findMenuItemButton(TestMenu.mainMenu1.label)); await tester.pumpAndSettle(); expect(controller.text, TestMenu.mainMenu1.label); }, ); testWidgets('DropdownMenu does not crash at zero area', (WidgetTester tester) async { tester.view.physicalSize = Size.zero; final controller = TextEditingController(text: 'I'); addTearDown(controller.dispose); addTearDown(tester.view.reset); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: DropdownMenu( dropdownMenuEntries: menuChildren, controller: controller, ), ), ), ), ); expect(tester.getSize(find.byType(DropdownMenu)), Size.zero); controller.selection = const TextSelection.collapsed(offset: 0); await tester.pump(); expect(find.byType(MenuItemButton), findsWidgets); }); // The variants to test in the focus handling test. final focusVariants = ValueVariant(TextInputAction.values.toSet()); // Regression test for https://github.com/flutter/flutter/issues/177009. testWidgets('Handles focus correctly when TextInputAction is invoked', ( WidgetTester tester, ) async { Future ensureCorrectFocusHandlingForAction( TextInputAction textInputAction, { required bool shouldLoseFocus, bool shouldFocusNext = false, bool shouldFocusPrevious = false, }) async { final previousFocusNode = FocusNode(); final textFieldFocusNode = FocusNode(); final nextFocusNode = FocusNode(); addTearDown(previousFocusNode.dispose); addTearDown(textFieldFocusNode.dispose); addTearDown(nextFocusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( children: [ TextButton( focusNode: previousFocusNode, child: const Text('Previous'), onPressed: () {}, ), DropdownMenu( dropdownMenuEntries: menuChildren, focusNode: textFieldFocusNode, textInputAction: textInputAction, requestFocusOnTap: true, showTrailingIcon: false, ), TextButton(focusNode: nextFocusNode, child: const Text('Next'), onPressed: () {}), ], ), ), ), ); expect(textFieldFocusNode.hasFocus, isFalse); // Tap on DropdownMenu to request focus on the TextField. await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); expect(textFieldFocusNode.hasFocus, isTrue); await tester.testTextInput.receiveAction(textInputAction); await tester.pumpAndSettle(); expect(previousFocusNode.hasFocus, equals(shouldFocusPrevious)); expect(textFieldFocusNode.hasFocus, equals(!shouldLoseFocus)); expect(nextFocusNode.hasFocus, equals(shouldFocusNext)); } // The expectations for each of the types of TextInputAction. const actionShouldLoseFocus = { TextInputAction.none: false, TextInputAction.unspecified: false, TextInputAction.done: true, TextInputAction.go: true, TextInputAction.search: true, TextInputAction.send: true, TextInputAction.continueAction: false, TextInputAction.join: false, TextInputAction.route: false, TextInputAction.emergencyCall: false, TextInputAction.newline: true, TextInputAction.next: true, TextInputAction.previous: true, }; final TextInputAction textInputAction = focusVariants.currentValue!; expect(actionShouldLoseFocus.containsKey(textInputAction), isTrue); await ensureCorrectFocusHandlingForAction( textInputAction, shouldLoseFocus: actionShouldLoseFocus[textInputAction]!, shouldFocusNext: textInputAction == TextInputAction.next, shouldFocusPrevious: textInputAction == TextInputAction.previous, ); }, variant: focusVariants); // Regression test for https://github.com/flutter/flutter/issues/180121. testWidgets('Allow null entry to clear selection', (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); const selectNoneLabel = 'Select none'; final nullableMenuItems = >[ const DropdownMenuEntry(value: null, label: selectNoneLabel), const DropdownMenuEntry(value: 'a', label: 'A'), const DropdownMenuEntry(value: 'b', label: 'B'), ]; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return DropdownMenu( controller: controller, requestFocusOnTap: true, enableFilter: true, dropdownMenuEntries: nullableMenuItems, onSelected: (_) { setState(() {}); }, ); }, ), ), ), ); // Open the menu. await tester.tap(find.byType(DropdownMenu)); await tester.pump(); // Select the 'None' item. await tester.tap(findMenuItemButton(selectNoneLabel)); await tester.pumpAndSettle(); expect(controller.text, selectNoneLabel); }); } 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; } // A helper widget that creates a render object designed to call `getDryLayout` // on its child during its own `performLayout` phase. This is used to test // that a child's `computeDryLayout` implementation is valid. class _TestDryLayout extends SingleChildRenderObjectWidget { const _TestDryLayout({super.child}); @override RenderObject createRenderObject(BuildContext context) { return _RenderTestDryLayout(); } } class _RenderTestDryLayout extends RenderProxyBox { @override void performLayout() { if (child == null) { size = constraints.smallest; return; } child!.getDryLayout(constraints); child!.layout(constraints, parentUsesSize: true); size = child!.size; } }