mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
As explained in #180121 the PR #176711 is a breaking change, which must be properly documented. Closes #180121 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
5427 lines
182 KiB
Dart
5427 lines
182 KiB
Dart
// 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 = <DropdownMenuEntry<TestMenu>>[];
|
|
final menuChildrenWithIcons = <DropdownMenuEntry<TestMenu>>[];
|
|
const leadingIconToInputPadding = 4.0;
|
|
|
|
for (final TestMenu value in TestMenu.values) {
|
|
final entry = DropdownMenuEntry<TestMenu>(value: value, label: value.label);
|
|
menuChildren.add(entry);
|
|
}
|
|
|
|
ValueKey<String> leadingIconKey(TestMenu menuEntry) =>
|
|
ValueKey<String>('leading-${menuEntry.label}');
|
|
ValueKey<String> trailingIconKey(TestMenu menuEntry) =>
|
|
ValueKey<String>('trailing-${menuEntry.label}');
|
|
|
|
for (final TestMenu value in TestMenu.values) {
|
|
final entry = DropdownMenuEntry<TestMenu>(
|
|
value: value,
|
|
label: value.label,
|
|
leadingIcon: Icon(key: leadingIconKey(value), Icons.alarm),
|
|
trailingIcon: Icon(key: trailingIconKey(value), Icons.abc),
|
|
);
|
|
menuChildrenWithIcons.add(entry);
|
|
}
|
|
|
|
Widget buildTest<T extends Enum>(
|
|
ThemeData themeData,
|
|
List<DropdownMenuEntry<T>> entries, {
|
|
double? width,
|
|
double? menuHeight,
|
|
Widget? leadingIcon,
|
|
Widget? label,
|
|
InputDecorationTheme? decorationTheme,
|
|
}) {
|
|
return MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: DropdownMenu<T>(
|
|
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<Material>(
|
|
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<Material>(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<WidgetState> states) {
|
|
if (states.contains(WidgetState.focused)) {
|
|
return focusedBackgroundColor;
|
|
}
|
|
return defaultBackgroundColor;
|
|
}),
|
|
foregroundColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.focused)) {
|
|
return focusedForegroundColor;
|
|
}
|
|
return defaultForegroundColor;
|
|
}),
|
|
iconColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.focused)) {
|
|
return focusedIconColor;
|
|
}
|
|
return defaultIconColor;
|
|
}),
|
|
overlayColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.focused)) {
|
|
return focusedOverlayColor;
|
|
}
|
|
return defaultOverlayColor;
|
|
}),
|
|
);
|
|
|
|
final styledMenuEntries = <DropdownMenuEntry<TestMenu>>[];
|
|
for (final entryWithIcons in menuChildrenWithIcons) {
|
|
styledMenuEntries.add(
|
|
DropdownMenuEntry<TestMenu>(
|
|
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<RichText>(
|
|
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<TestMenu>(
|
|
initialSelection: selectedItem,
|
|
dropdownMenuEntries: menuChildrenWithIcons,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
initialSelection: selectedItem,
|
|
dropdownMenuEntries: menuChildrenWithIcons,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
initialSelection: selectedItem,
|
|
dropdownMenuEntries: styledMenuEntries,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
initialSelection: selectedItem,
|
|
dropdownMenuEntries: styledMenuEntries,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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 = <DropdownMenuEntry<TestMenu>>[];
|
|
for (final entryWithIcons in menuChildrenWithIcons) {
|
|
partiallyStyledMenuEntries.add(
|
|
DropdownMenuEntry<TestMenu>(
|
|
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<TestMenu>(
|
|
initialSelection: selectedItem,
|
|
dropdownMenuEntries: partiallyStyledMenuEntries,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<WidgetState> states) => TextStyle(
|
|
fontWeight: states.contains(WidgetState.focused) ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
);
|
|
|
|
final menuEntries = <DropdownMenuEntry<TestMenu>>[];
|
|
for (final item in menuChildren) {
|
|
menuEntries.add(
|
|
DropdownMenuEntry<TestMenu>(
|
|
value: item.value,
|
|
label: item.label,
|
|
style: customButtonStyle,
|
|
),
|
|
);
|
|
}
|
|
|
|
TextStyle? getItemLabelStyle(String label) {
|
|
final RenderObject paragraph = tester
|
|
.element<StatelessElement>(
|
|
find.descendant(of: findMenuItemButton(label), matching: find.text(label)),
|
|
)
|
|
.renderObject!;
|
|
return (paragraph as RenderParagraph).text.style;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(
|
|
initialSelection: selectedItem,
|
|
dropdownMenuEntries: menuEntries,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(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<TestMenu>(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<TestMenu>(dropdownMenuEntries: menuChildren)),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder textField = find.byType(TextField);
|
|
final Size anchorSize = tester.getSize(textField);
|
|
expect(anchorSize, const Size(180.0, 56.0));
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder menuMaterial = find
|
|
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Material))
|
|
.first;
|
|
final Size menuSize = tester.getSize(menuMaterial);
|
|
expect(menuSize, const Size(180.0, 304.0));
|
|
|
|
// The text field should have same width as the menu
|
|
// when the width property is not null.
|
|
await tester.pumpWidget(buildTest(themeData, menuChildren, width: 200.0));
|
|
|
|
final Finder anchor = find.byType(TextField);
|
|
final double width = tester.getSize(anchor).width;
|
|
expect(width, 200.0);
|
|
|
|
await tester.tap(anchor);
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder updatedMenu = find
|
|
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Material))
|
|
.first;
|
|
final double updatedMenuWidth = tester.getSize(updatedMenu).width;
|
|
expect(updatedMenuWidth, 200.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Material3 - The width of the text field should always be the same as the menu view',
|
|
(WidgetTester tester) async {
|
|
final themeData = ThemeData();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: SafeArea(child: DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren)),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder textField = find.byType(TextField);
|
|
final double anchorWidth = tester.getSize(textField).width;
|
|
expect(anchorWidth, closeTo(184.5, 0.1));
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder menuMaterial = find
|
|
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Material))
|
|
.first;
|
|
final double menuWidth = tester.getSize(menuMaterial).width;
|
|
expect(menuWidth, closeTo(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 = <DropdownMenuEntry<ShortMenu>>[];
|
|
|
|
for (final ShortMenu value in ShortMenu.values) {
|
|
final entry = DropdownMenuEntry<ShortMenu>(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<ShortMenu>));
|
|
expect(box.size.width, customBigWidth);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<ShortMenu>));
|
|
await tester.pump();
|
|
expect(find.byType(MenuItemButton), findsNWidgets(6));
|
|
Size buttonSize = tester.getSize(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<ShortMenu>));
|
|
expect(box.size.width, customSmallWidth);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<ShortMenu>));
|
|
await tester.pump();
|
|
expect(find.byType(MenuItemButton), findsNWidgets(6));
|
|
buttonSize = tester.getSize(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 = <DropdownMenuEntry<ShortMenu>>[];
|
|
|
|
for (final ShortMenu value in ShortMenu.values) {
|
|
final entry = DropdownMenuEntry<ShortMenu>(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<ShortMenu>));
|
|
expect(box.size.width, customWidth);
|
|
|
|
// Update width
|
|
customWidth = 400.0;
|
|
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customWidth));
|
|
box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
|
|
expect(box.size.width, customWidth);
|
|
});
|
|
|
|
testWidgets('The width is determined by the menu entries', (WidgetTester tester) async {
|
|
const double entryLabelWidth = 100;
|
|
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<int>(
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(
|
|
value: 0,
|
|
label: 'Flutter',
|
|
labelWidget: SizedBox(width: entryLabelWidth),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final double width = tester.getSize(find.byType(DropdownMenu<int>)).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<int>(
|
|
label: SizedBox(width: labelWidth),
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(
|
|
value: 0,
|
|
label: 'Flutter',
|
|
labelWidget: SizedBox(width: entryLabelWidth),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final double width = tester.getSize(find.byType(DropdownMenu<int>)).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 = <DropdownMenuEntry<ShortMenu>>[];
|
|
for (final ShortMenu value in ShortMenu.values) {
|
|
final entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label);
|
|
shortMenuItems.add(entry);
|
|
}
|
|
Widget buildMenuAnchor({EdgeInsets? expandedInsets}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: parentWidth,
|
|
height: parentWidth,
|
|
child: DropdownMenu<ShortMenu>(
|
|
expandedInsets: expandedInsets,
|
|
dropdownMenuEntries: shortMenuItems,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// By default, the width of the text field is determined by the menu children.
|
|
await tester.pumpWidget(buildMenuAnchor());
|
|
RenderBox box = tester.firstRenderObject(find.byType(TextField));
|
|
expect(box.size.width, 136.0);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
Size buttonSize = tester.getSize(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 = <DropdownMenuEntry<ShortMenu>>[];
|
|
for (final ShortMenu value in ShortMenu.values) {
|
|
final entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label);
|
|
shortMenuItems.add(entry);
|
|
}
|
|
Widget buildMenuAnchor({EdgeInsetsGeometry? expandedInsets}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: parentWidth,
|
|
height: parentWidth,
|
|
child: DropdownMenu<ShortMenu>(
|
|
expandedInsets: expandedInsets,
|
|
dropdownMenuEntries: shortMenuItems,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// By default, the width of the text field is determined by the menu children.
|
|
await tester.pumpWidget(buildMenuAnchor());
|
|
RenderBox box = tester.firstRenderObject(find.byType(TextField));
|
|
expect(box.size.width, 136.0);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
Size buttonSize = tester.getSize(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<int>(
|
|
expandedInsets: EdgeInsets.zero,
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: 'Flutter'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final double dropdownWidth = tester.getSize(find.byType(DropdownMenu<int>)).width;
|
|
expect(dropdownWidth, 800);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<int>));
|
|
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<int>(
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 1, label: 'One'),
|
|
DropdownMenuEntry<int>(value: 2, label: 'Two'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The test passes if no exception is thrown during the layout phase.
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(DropdownMenu<int>), 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<TestMenu>));
|
|
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<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder updatedMenu = find
|
|
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Padding))
|
|
.first;
|
|
|
|
final Size updatedMenuSize = tester.getSize(updatedMenu);
|
|
expect(updatedMenuSize, const Size(180.0, 100.0));
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Material3 - The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list',
|
|
(WidgetTester tester) async {
|
|
final themeData = ThemeData();
|
|
await tester.pumpWidget(buildTest(themeData, menuChildren));
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder updatedMenu = find
|
|
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Padding))
|
|
.first;
|
|
|
|
final Size updatedMenuSize = tester.getSize(updatedMenu);
|
|
expect(updatedMenuSize.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<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
final Finder itemText = find.text('Item 0').last;
|
|
final Offset itemTextTopLeft = tester.getTopLeft(itemText);
|
|
|
|
expect(labelTopLeft.dx, equals(itemTextTopLeft.dx));
|
|
|
|
// Test when the text field has a leading icon.
|
|
await tester.pumpWidget(Container());
|
|
await tester.pumpWidget(
|
|
buildTest(
|
|
themeData,
|
|
menuChildren,
|
|
leadingIcon: const Icon(Icons.search),
|
|
label: const Text('label'),
|
|
),
|
|
);
|
|
|
|
final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
|
|
final double iconWidth = tester.getSize(leadingIcon).width;
|
|
final Finder updatedLabel = find.text('label').first;
|
|
final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
final Finder updatedItemText = find.text('Item 0').last;
|
|
final Offset updatedItemTextTopLeft = tester.getTopLeft(updatedItemText);
|
|
|
|
expect(updatedLabelTopLeft.dx, equals(updatedItemTextTopLeft.dx));
|
|
expect(updatedLabelTopLeft.dx, equals(iconWidth + 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<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
final Finder updatedItemText1 = find.text('Item 0').last;
|
|
final Offset updatedItemTextTopLeft1 = tester.getTopLeft(updatedItemText1);
|
|
|
|
expect(updatedLabelTopLeft1.dx, equals(updatedItemTextTopLeft1.dx));
|
|
expect(updatedLabelTopLeft1.dx, equals(largeIconWidth + 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<TestMenu>(
|
|
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<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
final Finder itemText = find.text('Item 0').last;
|
|
final Offset itemTextTopRight = tester.getTopRight(itemText);
|
|
|
|
expect(labelTopRight.dx, equals(itemTextTopRight.dx));
|
|
|
|
// Test when the text field has a leading icon.
|
|
await tester.pumpWidget(Container());
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: DropdownMenu<TestMenu>(
|
|
leadingIcon: const Icon(Icons.search),
|
|
label: const Text('label'),
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
|
|
final double iconWidth = tester.getSize(leadingIcon).width;
|
|
final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>));
|
|
final Finder updatedLabel = find.text('label').first;
|
|
final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
final Finder updatedItemText = find.text('Item 0').last;
|
|
final Offset updatedItemTextTopRight = tester.getTopRight(updatedItemText);
|
|
|
|
expect(updatedLabelTopRight.dx, equals(updatedItemTextTopRight.dx));
|
|
expect(
|
|
updatedLabelTopRight.dx,
|
|
equals(dropdownMenuTopRight.dx - iconWidth - 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<TestMenu>(
|
|
leadingIcon: const SizedBox(width: 75.0, child: Icon(Icons.search)),
|
|
label: const Text('label'),
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
final Finder largeLeadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
|
|
final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
|
|
final Offset updatedDropdownMenuTopRight = tester.getTopRight(
|
|
find.byType(DropdownMenu<TestMenu>),
|
|
);
|
|
final Finder updatedLabel1 = find.text('label').first;
|
|
final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
final Finder updatedItemText1 = find.text('Item 0').last;
|
|
final Offset updatedItemTextTopRight1 = tester.getTopRight(updatedItemText1);
|
|
|
|
expect(updatedLabelTopRight1.dx, equals(updatedItemTextTopRight1.dx));
|
|
expect(
|
|
updatedLabelTopRight1.dx,
|
|
equals(updatedDropdownMenuTopRight.dx - largeIconWidth - 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<TestMenu>(
|
|
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<TestMenu>));
|
|
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<TestMenu>(
|
|
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<TestMenu>));
|
|
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<TestMenu>(
|
|
width: MediaQuery.of(context).size.width,
|
|
dropdownMenuEntries: menuChildren,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first;
|
|
expect(iconButton, findsOneWidget);
|
|
|
|
await tester.tap(iconButton);
|
|
await tester.pump();
|
|
|
|
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<TestMenu>(
|
|
trailingIcon: const Icon(Icons.ac_unit),
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.ac_unit).first;
|
|
expect(iconButton, findsOneWidget);
|
|
|
|
await tester.tap(iconButton);
|
|
await tester.pump();
|
|
|
|
final Finder menuMaterial = find
|
|
.ancestor(of: 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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
trailingIcon: const Icon(Icons.ac_unit),
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
|
|
return entries
|
|
.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter))
|
|
.toList();
|
|
},
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
expandedInsets: EdgeInsets.zero,
|
|
requestFocusOnTap: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
expandedInsets: EdgeInsets.zero,
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
|
|
return entries
|
|
.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter))
|
|
.toList();
|
|
},
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<int>(
|
|
requestFocusOnTap: true,
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: 'ABC'),
|
|
DropdownMenuEntry<int>(value: 1, label: 'AB'),
|
|
DropdownMenuEntry<int>(value: 2, label: 'ABCD'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<int>));
|
|
await tester.pump();
|
|
|
|
// 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<TestMenu>(
|
|
enableFilter: true,
|
|
requestFocusOnTap: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
controller: controller,
|
|
dropdownMenuEntries: const <DropdownMenuEntry<TestMenu>>[
|
|
DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Good Match 1'),
|
|
DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu1, label: 'Bad Match 1'),
|
|
DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu2, label: 'Good Match 2'),
|
|
DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu3, label: 'Bad Match 2'),
|
|
DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu4, label: 'Good Match 3'),
|
|
DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu5, label: 'Bad Match 3'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
const itemLabel = 'Item 2';
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<int>(
|
|
requestFocusOnTap: true,
|
|
searchCallback: (List<DropdownMenuEntry<int>> entries, String query) {
|
|
if (query.isEmpty) {
|
|
return null;
|
|
}
|
|
final int index = entries.indexWhere(
|
|
(DropdownMenuEntry<int> entry) => entry.label.contains(query),
|
|
);
|
|
return index != -1 ? index : null;
|
|
},
|
|
dropdownMenuEntries: const <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: 'ABC'),
|
|
DropdownMenuEntry<int>(value: 1, label: 'AB'),
|
|
DropdownMenuEntry<int>(value: 2, label: 'ABCD'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<int>));
|
|
await tester.pump();
|
|
|
|
// 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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu and highlight the first item.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
|
|
|
|
// Press down key one more time to the next item.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
expect(find.widgetWithText(TextField, 'Menu 1'), findsOneWidget);
|
|
|
|
// Press down to the next item.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
expect(find.widgetWithText(TextField, 'Item 2'), findsOneWidget);
|
|
});
|
|
|
|
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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pump();
|
|
expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget);
|
|
|
|
// Press up key one more time to the upper item.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pump();
|
|
expect(find.widgetWithText(TextField, 'Item 4'), findsOneWidget);
|
|
|
|
// Press up to the upper item.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pump();
|
|
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Disabled button will be skipped while pressing up/down key while focused', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final themeData = ThemeData();
|
|
final menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu2, label: 'Item 2', enabled: false),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu3, label: 'Item 3'),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu4, label: 'Item 4'),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu5, label: 'Item 5', enabled: false),
|
|
];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
dropdownMenuEntries: menuWithDisabledItems,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
|
|
// 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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
initialSelection: TestMenu.mainMenu1,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
await tester.enterText(find.byType(TextField).first, 'Menu 1');
|
|
await tester.pumpAndSettle();
|
|
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<TestMenu>(requestFocusOnTap: true, dropdownMenuEntries: menuChildren),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
await tester.enterText(find.byType(TextField).first, 'Menu 1');
|
|
await tester.pumpAndSettle();
|
|
for (final TestMenu menu in TestMenu.values) {
|
|
// One is layout for the _DropdownMenuBody, the other one is the real button item in the menu.
|
|
expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2));
|
|
}
|
|
});
|
|
|
|
testWidgets('Enable filtering', (WidgetTester tester) async {
|
|
final themeData = ThemeData();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
await tester.enterText(find.byType(TextField).first, 'Menu 1');
|
|
await tester.pumpAndSettle();
|
|
for (final TestMenu menu in TestMenu.values) {
|
|
// 'Menu 1' should be 2, other items should only find one.
|
|
if (menu.label == TestMenu.mainMenu1.label) {
|
|
expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2));
|
|
} else {
|
|
expect(find.widgetWithText(MenuItemButton, menu.label), findsOneWidget);
|
|
}
|
|
}
|
|
});
|
|
|
|
testWidgets('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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
|
|
return entries
|
|
.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter))
|
|
.toList();
|
|
},
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
|
|
return entries
|
|
.where((DropdownMenuEntry<TestMenu> 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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
final Finder item3 = 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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Access the MenuAnchor
|
|
final MenuAnchor menuAnchor = tester.widget<MenuAnchor>(find.byType(MenuAnchor));
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
expect(menuAnchor.controller!.isOpen, true);
|
|
|
|
// Simulate `TextInputAction.done` on textfield
|
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
await tester.pumpAndSettle();
|
|
expect(menuAnchor.controller!.isOpen, false);
|
|
});
|
|
|
|
testWidgets('The onSelected gets called only when a selection is made', (
|
|
WidgetTester tester,
|
|
) async {
|
|
var selectionCount = 0;
|
|
|
|
final themeData = ThemeData();
|
|
final menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false),
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu2, label: 'Item 2'),
|
|
const DropdownMenuEntry<TestMenu>(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<TestMenu>(
|
|
dropdownMenuEntries: menuWithDisabledItems,
|
|
controller: controller,
|
|
onSelected: (_) {
|
|
setState(() {
|
|
selectionCount++;
|
|
});
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
final bool isMobile = switch (themeData.platform) {
|
|
TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.fuchsia => true,
|
|
TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => false,
|
|
};
|
|
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<TestMenu>));
|
|
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<TestMenu>));
|
|
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<TestMenu>(
|
|
initialSelection: TestMenu.mainMenu3,
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
|
|
|
|
// Open the menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
// 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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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: <Widget>[DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren)]),
|
|
),
|
|
);
|
|
|
|
// Test default condition.
|
|
await tester.pumpWidget(buildDropdownMenu());
|
|
await tester.pump();
|
|
|
|
final Finder textFieldFinder = find.byType(TextField);
|
|
final TextField result = tester.widget<TextField>(textFieldFinder);
|
|
expect(result.canRequestFocus, false);
|
|
}, variant: TargetPlatformVariant.mobile());
|
|
|
|
testWidgets('The text input field should be focused on desktop platforms '
|
|
'when it is tapped', (WidgetTester tester) async {
|
|
final themeData = ThemeData();
|
|
|
|
Widget buildDropdownMenu() => MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: Column(children: <Widget>[DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren)]),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(buildDropdownMenu());
|
|
await tester.pump();
|
|
|
|
final Finder textFieldFinder = find.byType(TextField);
|
|
final TextField result = tester.widget<TextField>(textFieldFinder);
|
|
expect(result.canRequestFocus, true);
|
|
}, variant: TargetPlatformVariant.desktop());
|
|
|
|
testWidgets('If requestFocusOnTap is true, the text input field can request focus, '
|
|
'otherwise it cannot request focus', (WidgetTester tester) async {
|
|
final themeData = ThemeData();
|
|
|
|
Widget buildDropdownMenu({required bool requestFocusOnTap}) => MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: Column(
|
|
children: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
requestFocusOnTap: requestFocusOnTap,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
// Set requestFocusOnTap to true.
|
|
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: true));
|
|
await tester.pump();
|
|
|
|
final Finder textFieldFinder = find.byType(TextField);
|
|
final TextField textField = tester.widget<TextField>(textFieldFinder);
|
|
expect(textField.canRequestFocus, true);
|
|
// Open the dropdown menu.
|
|
await tester.tap(textFieldFinder);
|
|
await tester.pump();
|
|
// Make a selection.
|
|
await tester.tap(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<TextField>(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: <Widget>[
|
|
DropdownMenu<TestMenu>(requestFocusOnTap: false, dropdownMenuEntries: menuChildren),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(buildDropdownMenu());
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder textFieldFinder = find.byType(TextField);
|
|
final TextField textField = tester.widget<TextField>(textFieldFinder);
|
|
expect(textField.canRequestFocus, false);
|
|
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
await gesture.moveTo(tester.getCenter(textFieldFinder));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.click,
|
|
);
|
|
});
|
|
|
|
testWidgets('If enabled is false, the mouse cursor should be deferred when hovered', (
|
|
WidgetTester tester,
|
|
) async {
|
|
Widget buildDropdownMenu({bool enabled = true, bool? requestFocusOnTap}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
children: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
enabled: enabled,
|
|
requestFocusOnTap: requestFocusOnTap,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Check mouse cursor dropdown menu is disabled and requestFocusOnTap is true.
|
|
await tester.pumpWidget(buildDropdownMenu(enabled: false, requestFocusOnTap: true));
|
|
await tester.pumpAndSettle();
|
|
|
|
Finder textFieldFinder = find.byType(TextField);
|
|
TextField textField = tester.widget<TextField>(textFieldFinder);
|
|
expect(textField.canRequestFocus, true);
|
|
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
await gesture.moveTo(tester.getCenter(textFieldFinder));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
|
|
// Remove the pointer.
|
|
await gesture.removePointer();
|
|
|
|
// Check mouse cursor dropdown menu is disabled and requestFocusOnTap is false.
|
|
await tester.pumpWidget(buildDropdownMenu(enabled: false, requestFocusOnTap: false));
|
|
await tester.pumpAndSettle();
|
|
|
|
textFieldFinder = find.byType(TextField);
|
|
textField = tester.widget<TextField>(textFieldFinder);
|
|
expect(textField.canRequestFocus, false);
|
|
|
|
// Add a new pointer.
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(tester.getCenter(textFieldFinder));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
|
|
// Remove the pointer.
|
|
await gesture.removePointer();
|
|
|
|
// Check enabled dropdown menu updates the mouse cursor when hovered.
|
|
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: true));
|
|
await tester.pumpAndSettle();
|
|
|
|
textFieldFinder = find.byType(TextField);
|
|
textField = tester.widget<TextField>(textFieldFinder);
|
|
expect(textField.canRequestFocus, true);
|
|
|
|
// Add a new pointer.
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(tester.getCenter(textFieldFinder));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.text,
|
|
);
|
|
});
|
|
|
|
testWidgets('The menu has the same width as the input field in ListView', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/123631
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListView(
|
|
children: <Widget>[DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren)],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Rect textInput = tester.getRect(find.byType(TextField));
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder findMenu = find.byWidgetPredicate((Widget widget) {
|
|
return widget.runtimeType.toString() == '_MenuPanel';
|
|
});
|
|
final Rect menu = tester.getRect(findMenu);
|
|
expect(textInput.width, menu.width);
|
|
|
|
await tester.pumpWidget(Container());
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListView(
|
|
children: <Widget>[
|
|
DropdownMenu<TestMenu>(width: 200, dropdownMenuEntries: menuChildren),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Rect textInput1 = tester.getRect(find.byType(TextField));
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder findMenu1 = find.byWidgetPredicate((Widget widget) {
|
|
return widget.runtimeType.toString() == '_MenuPanel';
|
|
});
|
|
final Rect menu1 = tester.getRect(findMenu1);
|
|
expect(textInput1.width, 200);
|
|
expect(menu1.width, 200);
|
|
});
|
|
|
|
testWidgets('Semantics does not include hint when input is not empty', (
|
|
WidgetTester tester,
|
|
) async {
|
|
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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
dropdownMenuEntries: menuChildren,
|
|
hintText: hintText,
|
|
onSelected: (TestMenu? value) {
|
|
setState(() {
|
|
selectedValue = value;
|
|
});
|
|
},
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final SemanticsNode node = tester.getSemantics(find.text(hintText));
|
|
|
|
expect(selectedValue?.label, null);
|
|
expect(node.label, hintText);
|
|
expect(node.value, '');
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(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<TestMenu>(
|
|
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<TestMenu>(
|
|
dropdownMenuEntries: menuChildren,
|
|
helperText: helperText,
|
|
errorText: hasError ? errorText : null,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(false));
|
|
expect(find.text(helperText), findsOneWidget);
|
|
expect(find.text(errorText), findsNothing);
|
|
|
|
await tester.pumpWidget(buildFrame(true));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text(helperText), findsNothing);
|
|
expect(find.text(errorText), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('DropdownMenu can respect helperText when helperText is not null', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final themeData = ThemeData();
|
|
const helperText = 'I am helperText';
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownMenu<TestMenu>(
|
|
dropdownMenuEntries: menuChildren,
|
|
helperText: helperText,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
expect(find.text(helperText), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('DropdownMenu can respect errorText when errorText is not null', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final themeData = ThemeData();
|
|
const errorText = 'I am errorText';
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
theme: themeData,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren, errorText: errorText),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
expect(find.text(errorText), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Can scroll to the highlighted item', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
menuHeight: 100, // Give a small number so the list can only show 2 or 3 items.
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Item 5').hitTestable(), findsNothing);
|
|
await tester.enterText(find.byType(TextField), '5');
|
|
await tester.pumpAndSettle();
|
|
// Item 5 should show up.
|
|
expect(find.text('Item 5').hitTestable(), findsOneWidget);
|
|
});
|
|
|
|
// This is a regression test for https://github.com/flutter/flutter/issues/131676.
|
|
testWidgets('Material3 - DropdownMenu uses correct text styles', (WidgetTester tester) async {
|
|
const 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<TestMenu>(
|
|
controller: controller,
|
|
width: 100,
|
|
dropdownMenuEntries: TestMenu.values.map<DropdownMenuEntry<TestMenu>>((TestMenu item) {
|
|
return DropdownMenuEntry<TestMenu>(value: item, label: '${item.label} $longText');
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Opening the width=100 menu should not crash.
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
expect(tester.takeException(), isNull);
|
|
await tester.pumpAndSettle();
|
|
|
|
Finder findMenuItemText(String label) {
|
|
final 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<TestMenu>(
|
|
key: ValueKey<int>(maxLines),
|
|
controller: controller,
|
|
width: 100,
|
|
dropdownMenuEntries: TestMenu.values.map<DropdownMenuEntry<TestMenu>>((TestMenu item) {
|
|
return DropdownMenuEntry<TestMenu>(
|
|
value: item,
|
|
label: '${item.label} $longText',
|
|
labelWidget: Text('${item.label} $longText', maxLines: maxLines),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Finder findMenuItemText(String label) {
|
|
final 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<TestMenu>));
|
|
|
|
// Actual size varies a little on web platforms.
|
|
final Matcher closeTo20 = closeTo(20, 0.05);
|
|
expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo20);
|
|
expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo20);
|
|
expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo20);
|
|
expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo20);
|
|
|
|
// Close the menu
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
expect(controller.text, ''); // nothing selected
|
|
|
|
await tester.pumpWidget(buildFrame(maxLines: 2));
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
|
|
// Actual size varies a little on web platforms.
|
|
final Matcher closeTo40 = closeTo(40, 0.05);
|
|
expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo40);
|
|
expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo40);
|
|
expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo40);
|
|
expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo40);
|
|
|
|
// Close the menu
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
expect(controller.text, ''); // nothing selected
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/131350.
|
|
testWidgets('DropdownMenuEntry.leadingIcon default layout', (WidgetTester tester) async {
|
|
// The DropdownMenu should not get extra padding in DropdownMenuEntry items
|
|
// when both text field and DropdownMenuEntry have leading icons.
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<int>(
|
|
leadingIcon: Icon(Icons.search),
|
|
hintText: 'Hint',
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: 'Item 0', leadingIcon: Icon(Icons.alarm)),
|
|
DropdownMenuEntry<int>(value: 1, label: 'Item 1'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.byType(DropdownMenu<int>));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Check text location in text field.
|
|
expect(tester.getTopLeft(find.text('Hint')).dx, 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<int>? searchCallback}) {
|
|
return MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: DropdownMenu<int>(
|
|
requestFocusOnTap: true,
|
|
searchCallback: searchCallback,
|
|
dropdownMenuEntries: const <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: 'All'),
|
|
DropdownMenuEntry<int>(value: 1, label: 'Unread'),
|
|
DropdownMenuEntry<int>(value: 2, label: 'Read'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void checkExpectedHighlight({String? searchResult, required List<String> otherItems}) {
|
|
if (searchResult != null) {
|
|
final Finder material = find.descendant(
|
|
of: findMenuItemButton(searchResult),
|
|
matching: find.byType(Material),
|
|
);
|
|
final Material itemMaterial = tester.widget<Material>(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>(material);
|
|
expect(itemMaterial.color, Colors.transparent);
|
|
}
|
|
}
|
|
|
|
// Test default.
|
|
await tester.pumpWidget(dropdownMenu());
|
|
await tester.pump();
|
|
await tester.tap(find.byType(DropdownMenu<int>));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(find.byType(TextField), 'read');
|
|
await tester.pump();
|
|
checkExpectedHighlight(
|
|
searchResult: 'Unread',
|
|
otherItems: <String>['All', 'Read'],
|
|
); // Because "Unread" contains "read".
|
|
|
|
// Test custom search algorithm.
|
|
await tester.pumpWidget(dropdownMenu(searchCallback: (_, _) => 0));
|
|
await tester.pump();
|
|
await tester.enterText(find.byType(TextField), 'read');
|
|
await tester.pump();
|
|
checkExpectedHighlight(
|
|
searchResult: 'All',
|
|
otherItems: <String>['Unread', 'Read'],
|
|
); // Because the search result should always be index 0.
|
|
|
|
// Test custom search algorithm - exact match.
|
|
await tester.pumpWidget(
|
|
dropdownMenu(
|
|
searchCallback: (List<DropdownMenuEntry<int>> entries, String query) {
|
|
if (query.isEmpty) {
|
|
return null;
|
|
}
|
|
final int index = entries.indexWhere(
|
|
(DropdownMenuEntry<int> entry) => entry.label == query,
|
|
);
|
|
|
|
return index != -1 ? index : null;
|
|
},
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.enterText(find.byType(TextField), 'read');
|
|
await tester.pump();
|
|
checkExpectedHighlight(
|
|
otherItems: <String>['All', 'Unread', 'Read'],
|
|
); // Because it's case sensitive.
|
|
await tester.enterText(find.byType(TextField), 'Read');
|
|
await tester.pump();
|
|
checkExpectedHighlight(searchResult: 'Read', otherItems: <String>['All', 'Unread']);
|
|
});
|
|
|
|
testWidgets('onSelected gets called when a selection is made in a nested menu', (
|
|
WidgetTester tester,
|
|
) async {
|
|
var selectionCount = 0;
|
|
|
|
final themeData = ThemeData();
|
|
final menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: themeData,
|
|
home: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Scaffold(
|
|
body: MenuAnchor(
|
|
menuChildren: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
dropdownMenuEntries: menuWithDisabledItems,
|
|
onSelected: (_) {
|
|
setState(() {
|
|
selectionCount++;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
builder: (BuildContext context, MenuController controller, Widget? widget) {
|
|
return IconButton(
|
|
icon: const Icon(Icons.smartphone_rounded),
|
|
onPressed: () {
|
|
controller.open();
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the first menu
|
|
await tester.tap(find.byType(IconButton));
|
|
await tester.pump();
|
|
// Open the dropdown menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
final Finder item1 = 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 = <DropdownMenuEntry<TestMenu>>[
|
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: themeData,
|
|
home: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Scaffold(
|
|
body: MenuAnchor(
|
|
menuChildren: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
dropdownMenuEntries: menuWithDisabledItems,
|
|
onSelected: (_) {
|
|
setState(() {
|
|
selectionCount++;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
builder: (BuildContext context, MenuController controller, Widget? widget) {
|
|
return IconButton(
|
|
icon: const Icon(Icons.smartphone_rounded),
|
|
onPressed: () {
|
|
controller.open();
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the first menu
|
|
await tester.tap(find.byType(IconButton));
|
|
await tester.pump();
|
|
// Open the dropdown menu
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pump();
|
|
|
|
final Finder item1 = 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<TestMenu>(dropdownMenuEntries: menuChildren);
|
|
} else {
|
|
return Container(height: 50);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.fling(find.byType(ListView), const Offset(0, -20000), 200000.0);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets('Menu shows scrollbar when height is limited', (WidgetTester tester) async {
|
|
final menuItems = <DropdownMenuEntry<TestMenu>>[
|
|
DropdownMenuEntry<TestMenu>(
|
|
value: TestMenu.mainMenu0,
|
|
label: 'Item 0',
|
|
style: MenuItemButton.styleFrom(minimumSize: const Size.fromHeight(1000)),
|
|
),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(body: DropdownMenu<TestMenu>(dropdownMenuEntries: menuItems)),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Scrollbar), findsOneWidget);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('DropdownMenu.focusNode can focus text input field', (WidgetTester tester) async {
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
final theme = ThemeData();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: DropdownMenu<String>(
|
|
focusNode: focusNode,
|
|
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
|
|
DropdownMenuEntry<String>(value: 'Yolk', label: 'Yolk'),
|
|
DropdownMenuEntry<String>(value: 'Eggbert', label: 'Eggbert'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
RenderBox box = tester.renderObject(find.byType(InputDecorator));
|
|
|
|
// Test input border when not focused.
|
|
expect(box, paints..rrect(color: theme.colorScheme.outline));
|
|
|
|
focusNode.requestFocus();
|
|
await tester.pump();
|
|
// Advance input decorator animation.
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
|
|
box = tester.renderObject(find.byType(InputDecorator));
|
|
|
|
// Test input border when focused.
|
|
expect(box, paints..rrect(color: theme.colorScheme.primary));
|
|
});
|
|
|
|
// 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: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
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<String>(
|
|
requestFocusOnTap: true,
|
|
controller: controller,
|
|
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
|
|
DropdownMenuEntry<String>(value: 'Blue', label: 'Blue'),
|
|
DropdownMenuEntry<String>(value: 'Green', label: 'Green'),
|
|
],
|
|
inputFormatters: <TextInputFormatter>[
|
|
formatter,
|
|
FilteringTextInputFormatter.deny(RegExp('[0-9]')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
|
state.updateEditingValue(const TextEditingValue(text: 'Blue'));
|
|
expect(called, 1);
|
|
expect(controller.text, 'Blue');
|
|
|
|
state.updateEditingValue(const TextEditingValue(text: 'Green'));
|
|
expect(called, 2);
|
|
expect(controller.text, 'Green');
|
|
|
|
state.updateEditingValue(const TextEditingValue(text: 'Green2'));
|
|
expect(called, 3);
|
|
expect(controller.text, 'Green');
|
|
});
|
|
|
|
// This is a regression test for https://github.com/flutter/flutter/issues/140596.
|
|
testWidgets('Long text item does not overflow', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<int>(
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(
|
|
value: 0,
|
|
label: 'This is a long text that is multiplied by 4 so it can overflow. ' * 4,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
await tester.tap(find.byType(DropdownMenu<int>));
|
|
await tester.pumpAndSettle();
|
|
|
|
// No exception should be thrown.
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
// This is a regression test for https://github.com/flutter/flutter/issues/147076.
|
|
testWidgets('Text field does not overflow parent', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: 300,
|
|
child: DropdownMenu<int>(
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(
|
|
value: 0,
|
|
label: 'This is a long text that is multiplied by 4 so it can overflow. ' * 4,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
final RenderBox box = tester.firstRenderObject(find.byType(TextField));
|
|
expect(box.size.width, 300.0);
|
|
});
|
|
|
|
// This is a regression test for https://github.com/flutter/flutter/issues/147173.
|
|
testWidgets('Text field with large helper text can be selected', (WidgetTester tester) async {
|
|
const labelText = 'MenuEntry 1';
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownMenu<int>(
|
|
hintText: 'Hint text',
|
|
helperText: 'Menu Helper text',
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
helperMaxLines: 2,
|
|
helperStyle: TextStyle(fontSize: 30),
|
|
),
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: labelText),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
await tester.tapAt(tester.getCenter(find.text('Hint text')));
|
|
await tester.pumpAndSettle();
|
|
// One is layout for the _DropdownMenuBody, the other one is the real button item in the menu.
|
|
expect(find.widgetWithText(MenuItemButton, labelText), findsNWidgets(2));
|
|
});
|
|
|
|
testWidgets('DropdownMenu allows customizing text field text align', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
children: <DropdownMenu<int>>[
|
|
DropdownMenu<int>(dropdownMenuEntries: <DropdownMenuEntry<int>>[]),
|
|
DropdownMenu<int>(
|
|
textAlign: TextAlign.center,
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final List<TextField> fields = tester.widgetList<TextField>(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<TestMenu>(
|
|
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<TestMenu>(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<String>(
|
|
alignmentOffset: alignmentOffset,
|
|
dropdownMenuEntries: <DropdownMenuEntry<String>>[
|
|
DropdownMenuEntry<String>(value: '1', label: 'One'),
|
|
DropdownMenuEntry<String>(value: '2', label: 'Two'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final MenuAnchor menuAnchor = tester.widget<MenuAnchor>(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<TestMenu>(
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
initialSelection: menuChildren[0].value,
|
|
dropdownMenuEntries: menuChildren,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
|
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<TestMenu>));
|
|
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<TestMenu>(
|
|
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: <Widget>[
|
|
ListView(
|
|
shrinkWrap: true,
|
|
children: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
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: <Widget>[
|
|
Expanded(
|
|
child: Align(
|
|
alignment: alignment,
|
|
child: DropdownMenu<TestMenu>(
|
|
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: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
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<TestMenu>), findsOneWidget);
|
|
|
|
MenuAnchor dropdownMenuAnchor = tester.widget<MenuAnchor>(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<TestMenu>), 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<TestMenu>), findsOneWidget);
|
|
|
|
// Tap the menu item to open the dropdown menu.
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
dropdownMenuAnchor = tester.widget<MenuAnchor>(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<TestMenu>), findsOneWidget);
|
|
|
|
// Tap the menu item to open the dropdown menu.
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
dropdownMenuAnchor = tester.widget<MenuAnchor>(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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(dropdownMenuEntries: menuChildren)),
|
|
),
|
|
);
|
|
TextField textField = tester.widget(find.byType(TextField));
|
|
// Default behavior.
|
|
expect(textField.maxLines, 1);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren, maxLines: null),
|
|
),
|
|
),
|
|
);
|
|
textField = tester.widget(find.byType(TextField));
|
|
expect(textField.maxLines, null);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(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<TestMenu>(dropdownMenuEntries: menuChildren)),
|
|
),
|
|
);
|
|
TextField textField = tester.widget(find.byType(TextField));
|
|
// Default behavior.
|
|
expect(textField.textInputAction, null);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(
|
|
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<TestMenu>(
|
|
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<int>(
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: longLabel),
|
|
],
|
|
menuStyle: MenuStyle(maximumSize: WidgetStatePropertyAll<Size>(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<TestMenu>(
|
|
width: 200.0,
|
|
dropdownMenuEntries: menuChildren,
|
|
menuStyle: const MenuStyle(
|
|
maximumSize: WidgetStatePropertyAll<Size>(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<TestMenu>(
|
|
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<int>(
|
|
width: 200,
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(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<int>(
|
|
expandedInsets: EdgeInsets.symmetric(horizontal: 20),
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(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<int>(
|
|
dropdownMenuEntries: <DropdownMenuEntry<int>>[
|
|
DropdownMenuEntry<int>(value: 0, label: 'Item 0'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
if (kIsWeb)
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.hasExpandedState,
|
|
],
|
|
actions: <SemanticsAction>[SemanticsAction.expand],
|
|
)
|
|
else
|
|
TestSemantics(
|
|
id: 5,
|
|
inputType: SemanticsInputType.text,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isTextField,
|
|
SemanticsFlag.isFocusable,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.isReadOnly,
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.hasExpandedState,
|
|
],
|
|
actions: <SemanticsAction>[
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<void> pumpDropdownMenu(TextEditingController? controller) {
|
|
return tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownMenu<TestMenu>(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<String>(
|
|
width: double.infinity,
|
|
enabled: isEnabled,
|
|
initialSelection: 'One',
|
|
label: const Text('Choose number'),
|
|
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
|
|
DropdownMenuEntry<String>(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<EditableText>(
|
|
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: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
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: <Widget>[
|
|
DropdownMenu<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>));
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(dropdownMenuEntries: menuChildren)),
|
|
),
|
|
);
|
|
|
|
final DropdownMenu<TestMenu> dropdownMenu = tester.firstWidget(
|
|
find.byType(DropdownMenu<TestMenu>),
|
|
);
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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<TestMenu>(
|
|
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: <Widget>[
|
|
DropdownMenu<TestMenu>(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<TestMenu>(
|
|
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<TestMenu>));
|
|
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<TestMenu>(
|
|
dropdownMenuEntries: menuChildren,
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.getSize(find.byType(DropdownMenu<TestMenu>)), 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>(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<void> 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: <Widget>[
|
|
TextButton(
|
|
focusNode: previousFocusNode,
|
|
child: const Text('Previous'),
|
|
onPressed: () {},
|
|
),
|
|
DropdownMenu<TestMenu>(
|
|
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<TestMenu>));
|
|
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, bool>{
|
|
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 = <DropdownMenuEntry<String?>>[
|
|
const DropdownMenuEntry<String?>(value: null, label: selectNoneLabel),
|
|
const DropdownMenuEntry<String?>(value: 'a', label: 'A'),
|
|
const DropdownMenuEntry<String?>(value: 'b', label: 'B'),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownMenu<String?>(
|
|
controller: controller,
|
|
requestFocusOnTap: true,
|
|
enableFilter: true,
|
|
dropdownMenuEntries: nullableMenuItems,
|
|
onSelected: (_) {
|
|
setState(() {});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open the menu.
|
|
await tester.tap(find.byType(DropdownMenu<String?>));
|
|
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;
|
|
}
|
|
}
|