flutter_flutter/packages/flutter/test/material/menu_anchor_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] 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
2025-11-26 01:10:39 +00:00

5663 lines
196 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 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker/leak_tracker.dart';
import '../widgets/semantics_tester.dart';
void main() {
late MenuController controller;
String? focusedMenu;
final selected = <TestMenu>[];
final opened = <TestMenu>[];
final closed = <TestMenu>[];
final GlobalKey menuItemKey = GlobalKey();
void onPressed(TestMenu item) {
selected.add(item);
}
void onOpen(TestMenu item) {
opened.add(item);
}
void onClose(TestMenu item) {
closed.add(item);
}
void handleFocusChange() {
focusedMenu = (primaryFocus?.debugLabel ?? primaryFocus).toString();
}
setUp(() {
focusedMenu = null;
selected.clear();
opened.clear();
closed.clear();
controller = MenuController();
focusedMenu = null;
});
Future<void> changeSurfaceSize(WidgetTester tester, Size size) async {
await tester.binding.setSurfaceSize(size);
addTearDown(() async {
await tester.binding.setSurfaceSize(null);
});
}
void listenForFocusChanges() {
FocusManager.instance.addListener(handleFocusChange);
addTearDown(() => FocusManager.instance.removeListener(handleFocusChange));
}
Finder findMenuPanels() {
return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel');
}
List<RenderObject> ancestorRenderTheaters(RenderObject child) {
final results = <RenderObject>[];
RenderObject? node = child;
while (node != null) {
if (node.runtimeType.toString() == '_RenderTheater') {
results.add(node);
}
final RenderObject? parent = node.parent;
node = parent is RenderObject ? parent : null;
}
return results;
}
Finder findMenuBarItemLabels() {
return find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_MenuItemLabel',
);
}
// Finds the mnemonic associated with the menu item that has the given label.
Finder findMnemonic(String label) {
return find
.descendant(
of: find.ancestor(of: find.text(label), matching: findMenuBarItemLabels()),
matching: find.byType(Text),
)
.last;
}
Widget buildTestApp({
AlignmentGeometry? alignment,
Offset alignmentOffset = Offset.zero,
TextDirection textDirection = TextDirection.ltr,
bool consumesOutsideTap = false,
void Function(TestMenu)? onPressed,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
}) {
final focusNode = FocusNode();
addTearDown(focusNode.dispose);
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Directionality(
textDirection: textDirection,
child: Column(
children: <Widget>[
GestureDetector(
onTap: () {
onPressed?.call(TestMenu.outsideButton);
},
child: Text(TestMenu.outsideButton.label),
),
MenuAnchor(
childFocusNode: focusNode,
controller: controller,
alignmentOffset: alignmentOffset,
consumeOutsideTap: consumesOutsideTap,
style: MenuStyle(alignment: alignment),
onOpen: () {
onOpen?.call(TestMenu.anchorButton);
},
onClose: () {
onClose?.call(TestMenu.anchorButton);
},
menuChildren: <Widget>[
MenuItemButton(
key: menuItemKey,
shortcut: const SingleActivator(LogicalKeyboardKey.keyB, control: true),
onPressed: () {
onPressed?.call(TestMenu.subMenu00);
},
child: Text(TestMenu.subMenu00.label),
),
MenuItemButton(
leadingIcon: const Icon(Icons.send),
trailingIcon: const Icon(Icons.mail),
onPressed: () {
onPressed?.call(TestMenu.subMenu01);
},
child: Text(TestMenu.subMenu01.label),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return ElevatedButton(
focusNode: focusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
onPressed?.call(TestMenu.anchorButton);
},
child: child,
);
},
child: Text(TestMenu.anchorButton.label),
),
],
),
),
),
);
}
Future<TestGesture> hoverOver(WidgetTester tester, Finder finder) async {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.moveTo(tester.getCenter(finder));
await tester.pumpAndSettle();
return gesture;
}
Material getMenuBarMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(of: findMenuPanels(), matching: find.byType(Material)).first,
);
}
RenderObject getOverlayColor(WidgetTester tester) {
return tester.allRenderObjects.firstWhere(
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
);
}
TextStyle iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style!;
}
testWidgets('Menu responds to density changes', (WidgetTester tester) async {
Widget buildMenu({VisualDensity? visualDensity = VisualDensity.standard}) {
return MaterialApp(
theme: ThemeData(visualDensity: visualDensity, useMaterial3: false),
home: Material(
child: Column(
children: <Widget>[
MenuBar(children: createTestMenus(onPressed: onPressed)),
const Expanded(child: Placeholder()),
],
),
),
);
}
await tester.pumpWidget(buildMenu());
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
);
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
);
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
);
expect(
tester.getRect(find.widgetWithText(MenuItemButton, TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(257.0, 56.0, 471.0, 104.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(257.0, 48.0, 471.0, 208.0)),
);
// Test compact visual density (-2, -2).
await tester.pumpWidget(Container());
await tester.pumpWidget(buildMenu(visualDensity: VisualDensity.compact));
await tester.pump();
// The original horizontal padding with standard visual density for menu buttons are 12 px, and the total length
// for the menu bar is (655 - 145) = 510.
// There are 4 buttons in the test menu bar, and with compact visual density,
// the padding will reduce by abs(2 * (-2)) = 4. So the total length
// now should reduce by abs(4 * 2 * (-4)) = 32, which would be 510 - 32 = 478, and
// 478 = 639 - 161
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(161.0, 0.0, 639.0, 40.0)),
);
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(161.0, 0.0, 639.0, 40.0)),
);
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(161.0, 0.0, 639.0, 40.0)),
);
expect(
tester.getRect(find.widgetWithText(MenuItemButton, TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(265.0, 48.0, 467.0, 88.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(265.0, 40.0, 467.0, 176.0)),
);
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildMenu(visualDensity: const VisualDensity(horizontal: 2.0, vertical: 2.0)),
);
await tester.pump();
// Similarly, there are 4 buttons in the test menu bar, and with (2, 2) visual density,
// the padding will increase by abs(2 * 4) = 8. So the total length for buttons
// should increase by abs(4 * 2 * 8) = 64. The horizontal padding for the menu bar
// increases by 2 * 8, so the total width increases to 510 + 64 + 16 = 590, and
// 590 = 695 - 105
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(105.0, 0.0, 695.0, 56.0)),
);
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(105.0, 0.0, 695.0, 56.0)),
);
expect(
tester.getRect(find.widgetWithText(MenuItemButton, TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(257.0, 64.0, 491.0, 120.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(249.0, 56.0, 499.0, 240.0)),
);
});
testWidgets('Menu defaults', (WidgetTester tester) async {
final themeData = ThemeData();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
// Menu bar (horizontal menu).
Finder menuMaterial = find
.ancestor(of: find.byType(TextButton), matching: find.byType(Material))
.first;
Material material = tester.widget<Material>(menuMaterial);
expect(opened, isEmpty);
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))),
);
Finder buttonMaterial = find
.descendant(of: find.byType(TextButton), matching: find.byType(Material))
.first;
material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle?.color, themeData.colorScheme.onSurface);
expect(material.textStyle?.fontSize, 14.0);
expect(material.textStyle?.height, 1.43);
// Vertical menu.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
menuMaterial = find
.ancestor(
of: find.widgetWithText(TextButton, TestMenu.subMenu10.label),
matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(menuMaterial);
expect(opened.last, equals(TestMenu.mainMenu1));
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))),
);
buttonMaterial = find
.descendant(
of: find.widgetWithText(TextButton, TestMenu.subMenu10.label),
matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle?.color, themeData.colorScheme.onSurface);
expect(material.textStyle?.fontSize, 14.0);
expect(material.textStyle?.height, 1.43);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.byIcon(Icons.add), findsOneWidget);
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(Icons.add), matching: find.byType(RichText)),
);
expect(iconRichText.text.style?.color, themeData.colorScheme.onSurfaceVariant);
});
testWidgets('Menu defaults - disabled', (WidgetTester tester) async {
final themeData = ThemeData();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
// Menu bar (horizontal menu).
Finder menuMaterial = find
.ancestor(
of: find.widgetWithText(TextButton, TestMenu.mainMenu5.label),
matching: find.byType(Material),
)
.first;
Material material = tester.widget<Material>(menuMaterial);
expect(opened, isEmpty);
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))),
);
Finder buttonMaterial = find
.descendant(
of: find.widgetWithText(TextButton, TestMenu.mainMenu5.label),
matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle?.color, themeData.colorScheme.onSurface.withOpacity(0.38));
// Vertical menu.
await tester.tap(find.text(TestMenu.mainMenu2.label));
await tester.pump();
menuMaterial = find
.ancestor(
of: find.widgetWithText(TextButton, TestMenu.subMenu20.label),
matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(menuMaterial);
expect(material.color, themeData.colorScheme.surfaceContainer);
expect(material.shadowColor, themeData.colorScheme.shadow);
expect(material.surfaceTintColor, Colors.transparent);
expect(material.elevation, 3.0);
expect(
material.shape,
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))),
);
buttonMaterial = find
.descendant(
of: find.widgetWithText(TextButton, TestMenu.subMenu20.label),
matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle?.color, themeData.colorScheme.onSurface.withOpacity(0.38));
expect(find.byIcon(Icons.ac_unit), findsOneWidget);
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(Icons.ac_unit), matching: find.byType(RichText)),
);
expect(iconRichText.text.style?.color, themeData.colorScheme.onSurface.withOpacity(0.38));
});
testWidgets('Menu scrollbar inherits ScrollbarTheme', (WidgetTester tester) async {
const scrollbarTheme = ScrollbarThemeData(
thumbColor: MaterialStatePropertyAll<Color?>(Color(0xffff0000)),
thumbVisibility: MaterialStatePropertyAll<bool?>(true),
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(scrollbarTheme: scrollbarTheme),
home: Material(
child: MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
style: ButtonStyle(
minimumSize: WidgetStateProperty.all<Size>(const Size.fromHeight(1000)),
),
onPressed: () {},
child: const Text('Category'),
),
],
child: const Text('Main Menu'),
),
],
),
),
),
);
await tester.tap(find.text('Main Menu'));
await tester.pumpAndSettle();
// Test Scrollbar thumb color.
expect(find.byType(Scrollbar).last, paints..rrect(color: const Color(0xffff0000)));
// Close the menu.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(scrollbarTheme: scrollbarTheme),
home: Material(
child: ScrollbarTheme(
data: scrollbarTheme.copyWith(
thumbColor: const MaterialStatePropertyAll<Color?>(Color(0xff00ff00)),
),
child: MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
style: ButtonStyle(
minimumSize: WidgetStateProperty.all<Size>(const Size.fromHeight(1000)),
),
onPressed: () {},
child: const Text('Category'),
),
],
child: const Text('Main Menu'),
),
],
),
),
),
),
);
await tester.tap(find.text('Main Menu'));
await tester.pumpAndSettle();
// Scrollbar thumb color should be updated.
expect(find.byType(Scrollbar).last, paints..rrect(color: const Color(0xff00ff00)));
}, variant: TargetPlatformVariant.desktop());
testWidgets('Focus is returned to previous focus before invoking onPressed', (
WidgetTester tester,
) async {
final buttonFocus = FocusNode(debugLabel: 'Button Focus');
addTearDown(buttonFocus.dispose);
FocusNode? focusInOnPressed;
void onMenuSelected(TestMenu item) {
focusInOnPressed = FocusManager.instance.primaryFocus;
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBar(
controller: controller,
children: createTestMenus(onPressed: onMenuSelected),
),
ElevatedButton(
autofocus: true,
onPressed: () {},
focusNode: buttonFocus,
child: const Text('Press Me'),
),
],
),
),
),
);
await tester.pump();
expect(FocusManager.instance.primaryFocus, equals(buttonFocus));
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subSubMenu110.label));
await tester.pump();
expect(focusInOnPressed, equals(buttonFocus));
expect(FocusManager.instance.primaryFocus, equals(buttonFocus));
});
group('Menu functions', () {
testWidgets('basic menu structure', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
expect(find.text(TestMenu.subMenu10.label), findsNothing);
expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
expect(opened, isEmpty);
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
expect(find.text(TestMenu.subMenu10.label), findsOneWidget);
expect(find.text(TestMenu.subMenu11.label), findsOneWidget);
expect(find.text(TestMenu.subMenu12.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
expect(find.text(TestMenu.subSubMenu111.label), findsNothing);
expect(find.text(TestMenu.subSubMenu112.label), findsNothing);
expect(opened.last, equals(TestMenu.mainMenu1));
opened.clear();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
expect(find.text(TestMenu.subMenu10.label), findsOneWidget);
expect(find.text(TestMenu.subMenu11.label), findsOneWidget);
expect(find.text(TestMenu.subMenu12.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu110.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu111.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu112.label), findsOneWidget);
expect(opened.last, equals(TestMenu.subMenu11));
});
testWidgets('geometry', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(children: createTestMenus(onPressed: onPressed)),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(124.0, 73.0, 314.0, 87.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(112.0, 48.0, 326.0, 208.0)),
);
// Test menu bar size when not expanded.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBar(children: createTestMenus(onPressed: onPressed)),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
);
});
testWidgets('geometry with RTL direction', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(children: createTestMenus(onPressed: onPressed)),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(486.0, 73.0, 676.0, 87.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(474.0, 48.0, 688.0, 208.0)),
);
// Close and make sure it goes back where it was.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
// Test menu bar size when not expanded.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
MenuBar(children: createTestMenus(onPressed: onPressed)),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
);
});
testWidgets('menu alignment and offset in LTR', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp());
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 14.0, 472.0, 62.0)));
final Finder findMenuScope = find
.ancestor(of: find.byKey(menuItemKey), matching: find.byType(FocusScope))
.first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 62.0, 602.0, 174.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 14.0, 602.0, 126.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.center));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(400.0, 38.0, 674.0, 150.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.bottomEnd));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(472.0, 62.0, 746.0, 174.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart));
await tester.pump();
final Rect menuRect = tester.getRect(findMenuScope);
await tester.pumpWidget(
buildTestApp(
alignment: AlignmentDirectional.topStart,
alignmentOffset: const Offset(10, 20),
),
);
await tester.pump();
final Rect offsetMenuRect = tester.getRect(findMenuScope);
expect(offsetMenuRect.topLeft - menuRect.topLeft, equals(const Offset(10, 20)));
});
testWidgets('menu alignment and offset in RTL', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl));
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 14.0, 472.0, 62.0)));
final Finder findMenuScope = find
.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope))
.first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(198.0, 62.0, 472.0, 174.0)));
await tester.pumpWidget(
buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart),
);
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(198.0, 14.0, 472.0, 126.0)));
await tester.pumpWidget(
buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.center),
);
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(126.0, 38.0, 400.0, 150.0)));
await tester.pumpWidget(
buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.bottomEnd),
);
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(54.0, 62.0, 328.0, 174.0)));
await tester.pumpWidget(
buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart),
);
await tester.pump();
final Rect menuRect = tester.getRect(findMenuScope);
await tester.pumpWidget(
buildTestApp(
textDirection: TextDirection.rtl,
alignment: AlignmentDirectional.topStart,
alignmentOffset: const Offset(10, 20),
),
);
await tester.pump();
expect(
tester.getRect(findMenuScope).topLeft - menuRect.topLeft,
equals(const Offset(-10, 20)),
);
});
testWidgets('menu position in LTR', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp(alignmentOffset: const Offset(100, 50)));
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 14.0, 472.0, 62.0)));
final Finder findMenuScope = find
.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope))
.first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(
tester.getRect(findMenuScope),
equals(const Rect.fromLTRB(428.0, 112.0, 702.0, 224.0)),
);
// Now move the menu by calling open() again with a local position on the
// anchor.
controller.open(position: const Offset(200, 200));
await tester.pump();
expect(
tester.getRect(findMenuScope),
equals(const Rect.fromLTRB(526.0, 214.0, 800.0, 326.0)),
);
});
testWidgets('menu position in RTL', (WidgetTester tester) async {
await tester.pumpWidget(
buildTestApp(alignmentOffset: const Offset(100, 50), textDirection: TextDirection.rtl),
);
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 14.0, 472.0, 62.0)));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 14.0, 472.0, 62.0)));
final Finder findMenuScope = find
.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope))
.first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(98.0, 112.0, 372.0, 224.0)));
// Now move the menu by calling open() again with a local position on the
// anchor.
controller.open(position: const Offset(400, 200));
await tester.pump();
expect(
tester.getRect(findMenuScope),
equals(const Rect.fromLTRB(526.0, 214.0, 800.0, 326.0)),
);
});
testWidgets('works with Padding around menu and overlay', (WidgetTester tester) async {
await tester.pumpWidget(
Padding(
padding: const EdgeInsets.all(10.0),
child: MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: <Widget>[
Expanded(
child: MenuBar(children: createTestMenus(onPressed: onPressed)),
),
],
),
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)),
);
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)),
);
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(146.0, 95.0, 336.0, 109.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(134.0, 70.0, 348.0, 230.0)),
);
// Close and make sure it goes back where it was.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)),
);
});
testWidgets('works with Padding around menu and overlay with RTL direction', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Padding(
padding: const EdgeInsets.all(10.0),
child: MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: <Widget>[
Expanded(
child: MenuBar(children: createTestMenus(onPressed: onPressed)),
),
],
),
),
const Expanded(child: Placeholder()),
],
),
),
),
),
),
);
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)),
);
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)),
);
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(464.0, 95.0, 654.0, 109.0)),
);
expect(
tester.getRect(
find
.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material))
.at(1),
),
equals(const Rect.fromLTRB(452.0, 70.0, 666.0, 230.0)),
);
// Close and make sure it goes back where it was.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)),
);
});
testWidgets('visual attributes can be set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(
style: MenuStyle(
elevation: WidgetStateProperty.all<double?>(10),
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red),
),
children: createTestMenus(onPressed: onPressed),
),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
expect(tester.getRect(findMenuPanels()), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 48.0)));
final Material material = getMenuBarMaterial(tester);
expect(material.elevation, equals(10));
expect(material.color, equals(Colors.red));
});
testWidgets('MenuAnchor clip behavior', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
menuChildren: const <Widget>[MenuItemButton(child: Text('Button 1'))],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
controller.open();
},
child: const Text('Tap me'),
);
},
),
),
),
),
);
await tester.tap(find.text('Tap me'));
await tester.pump();
// Test default clip behavior.
expect(getMenuBarMaterial(tester).clipBehavior, equals(Clip.hardEdge));
// Close the menu.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
clipBehavior: Clip.antiAlias,
menuChildren: const <Widget>[MenuItemButton(child: Text('Button 1'))],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
controller.open();
},
child: const Text('Tap me'),
);
},
),
),
),
),
);
await tester.tap(find.text('Tap me'));
await tester.pump();
// Test custom clip behavior.
expect(getMenuBarMaterial(tester).clipBehavior, equals(Clip.antiAlias));
});
testWidgets('open and close works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
expect(opened, isEmpty);
expect(closed, isEmpty);
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1]));
expect(closed, isEmpty);
opened.clear();
closed.clear();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.subMenu11]));
expect(closed, isEmpty);
opened.clear();
closed.clear();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, isEmpty);
expect(closed, equals(<TestMenu>[TestMenu.subMenu11]));
opened.clear();
closed.clear();
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu0]));
expect(closed, equals(<TestMenu>[TestMenu.mainMenu1]));
});
testWidgets('Menus close and consume tap when open and tapped outside', (
WidgetTester tester,
) async {
await tester.pumpWidget(
buildTestApp(
consumesOutsideTap: true,
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
);
expect(opened, isEmpty);
expect(closed, isEmpty);
// Doesn't consume tap when the menu is closed.
await tester.tap(find.text(TestMenu.outsideButton.label));
await tester.pump();
expect(selected, equals(<TestMenu>[TestMenu.outsideButton]));
selected.clear();
await tester.tap(find.text(TestMenu.anchorButton.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.anchorButton]));
expect(closed, isEmpty);
expect(selected, equals(<TestMenu>[TestMenu.anchorButton]));
opened.clear();
closed.clear();
selected.clear();
await tester.tap(find.text(TestMenu.outsideButton.label));
await tester.pump();
expect(opened, isEmpty);
expect(closed, equals(<TestMenu>[TestMenu.anchorButton]));
// When the menu is open, don't expect the outside button to be selected:
// it's supposed to consume the key down.
expect(selected, isEmpty);
selected.clear();
opened.clear();
closed.clear();
});
testWidgets("Menus close and don't consume tap when open and tapped outside", (
WidgetTester tester,
) async {
await tester.pumpWidget(buildTestApp(onPressed: onPressed, onOpen: onOpen, onClose: onClose));
expect(opened, isEmpty);
expect(closed, isEmpty);
// Doesn't consume tap when the menu is closed.
await tester.tap(find.text(TestMenu.outsideButton.label));
await tester.pump();
expect(selected, equals(<TestMenu>[TestMenu.outsideButton]));
selected.clear();
await tester.tap(find.text(TestMenu.anchorButton.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.anchorButton]));
expect(closed, isEmpty);
expect(selected, equals(<TestMenu>[TestMenu.anchorButton]));
opened.clear();
closed.clear();
selected.clear();
await tester.tap(find.text(TestMenu.outsideButton.label));
await tester.pump();
expect(opened, isEmpty);
expect(closed, equals(<TestMenu>[TestMenu.anchorButton]));
// Because consumesOutsideTap is false, this is expected to receive its
// tap.
expect(selected, equals(<TestMenu>[TestMenu.outsideButton]));
selected.clear();
opened.clear();
closed.clear();
});
testWidgets('select works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
opened.clear();
await tester.tap(find.text(TestMenu.subSubMenu110.label));
await tester.pump();
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu110]));
// Selecting a non-submenu item should close all the menus.
expect(opened, isEmpty);
expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
expect(find.text(TestMenu.subMenu11.label), findsNothing);
});
testWidgets('diagnostics', (WidgetTester tester) async {
const item = MenuItemButton(
shortcut: SingleActivator(LogicalKeyboardKey.keyA),
child: Text('label2'),
);
final menuBar = MenuBar(
controller: controller,
style: const MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
elevation: MaterialStatePropertyAll<double?>(10.0),
),
children: const <Widget>[item],
);
await tester.pumpWidget(MaterialApp(home: Material(child: menuBar)));
await tester.pump();
final builder = DiagnosticPropertiesBuilder();
menuBar.debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(
description.join('\n'),
equalsIgnoringHashCodes(
'style: MenuStyle#00000(backgroundColor: WidgetStatePropertyAll(MaterialColor(primary value: ${const Color(0xfff44336)})), elevation: WidgetStatePropertyAll(10.0))\n'
'clipBehavior: Clip.none',
),
);
});
testWidgets('menus can be traversed multiple times', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/150334
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuItemButton(
autofocus: true,
onPressed: () {},
child: const Text('External Focus'),
),
MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
],
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("External Focus"))'));
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
});
testWidgets('keyboard tab traversal works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pumpAndSettle();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
opened.clear();
closed.clear();
// Test closing a menu with enter.
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(opened, isEmpty);
expect(closed, <TestMenu>[TestMenu.mainMenu0]);
});
testWidgets('keyboard directional traversal works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
// Go back, close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Move up, should close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
// Move down, should reopen the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu again.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 111"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 112"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
// Since this is a leaf off of a vertical menu, moving left should
// return to this menu's parent button.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Moving left while in a first-level submenu should focus the
// previous top-level menubar anchor.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
// Pressing arrowup from a top-level menubar anchor should focus the last
// item in that anchor's submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.pump();
// Enter the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
// Move to next top-level menu button.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
});
testWidgets('keyboard directional traversal works in RTL mode', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
// Go back, close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Move up, should close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
// Move down, should reopen the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu again.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 111"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 112"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
// Since this is a leaf off of a vertical menu, moving right should
// return to this menu's parent button.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Moving left while in a first-level submenu should focus the
// previous top-level menubar anchor.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
// Pressing arrowup from a top-level menubar anchor should focus the last
// item in that anchor's submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Enter the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
// Move to next top-level menu button.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
});
testWidgets('MenuAnchor tab traversal works', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/144381
final buttonFocusNode = FocusNode(debugLabel: TestMenu.anchorButton.label);
addTearDown(buttonFocusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuAnchor(
childFocusNode: buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('start')),
...createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
focusNode: buttonFocusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: Text(TestMenu.anchorButton.label),
);
},
),
],
),
),
),
);
listenForFocusChanges();
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals(TestMenu.anchorButton.label));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(focusedMenu, equals(TestMenu.anchorButton.label));
// Directional traversal doesn't work until a menu item is focused.
// To start focusing, hover over the first menu item.
await hoverOver(tester, find.text('start'));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("start"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("start"))'));
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
opened.clear();
closed.clear();
// Test closing a menu with enter.
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(opened, isEmpty);
expect(closed, <TestMenu>[TestMenu.mainMenu0]);
});
testWidgets('MenuAnchor LTR directional traversal works', (WidgetTester tester) async {
final buttonFocusNode = FocusNode(debugLabel: TestMenu.anchorButton.label);
addTearDown(buttonFocusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuAnchor(
childFocusNode: buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('start')),
...createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
focusNode: buttonFocusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Open'),
);
},
),
],
),
),
),
);
listenForFocusChanges();
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
expect(focusedMenu, equals(TestMenu.anchorButton.label));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(focusedMenu, equals(TestMenu.anchorButton.label));
expect(find.text('start'), findsOneWidget);
// Directional traversal doesn't work until a menu item is focused.
// To start focusing, hover over the first menu item.
await hoverOver(tester, find.text('start'));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("start"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsOne);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 00"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 01"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 02"))'));
// We're at the deepest menu on a LTR menu, so arrow right should not change focus.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 02"))'));
// Arrow left should move focus to the parent anchor.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsNothing);
// We're at the root menu, so arrow left should not change focus and
// should not open the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsNothing);
// Open the submenu again.
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsOne);
// Close all menus.
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(focusedMenu, equals(TestMenu.anchorButton.label));
expect(find.byType(MenuItemButton), findsNothing);
});
testWidgets('MenuAnchor RTL directional traversal works', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/119532
final buttonFocusNode = FocusNode(debugLabel: TestMenu.anchorButton.label);
addTearDown(buttonFocusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Column(
children: <Widget>[
MenuAnchor(
childFocusNode: buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('start')),
...createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
focusNode: buttonFocusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Open'),
);
},
),
],
),
),
),
),
);
listenForFocusChanges();
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
expect(focusedMenu, equals(TestMenu.anchorButton.label));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(focusedMenu, equals(TestMenu.anchorButton.label));
expect(find.text('start'), findsOneWidget);
// Directional traversal doesn't work until a menu item is focused.
// To start focusing, hover over the first menu item.
await hoverOver(tester, find.text('start'));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("start"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsOne);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 00"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 01"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 02"))'));
// We're at the deepest menu on a RTL menu, so arrow left should not change focus.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 02"))'));
// Arrow right should move focus to the parent anchor.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsNothing);
// We're at the root menu, so arrow right should not change focus and
// should not open the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsNothing);
// Open the submenu again.
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
expect(find.text('Sub Menu 00'), findsOne);
// Close all menus.
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(focusedMenu, equals(TestMenu.anchorButton.label));
expect(find.byType(MenuItemButton), findsNothing);
});
testWidgets('hover traversal works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
listenForFocusChanges();
// Hovering when the menu is not yet open does nothing.
await hoverOver(tester, find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(focusedMenu, isNull);
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
// Hovering when the menu is already open does nothing.
await hoverOver(tester, find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
// Hovering over the other main menu items opens them now.
await hoverOver(tester, find.text(TestMenu.mainMenu2.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await hoverOver(tester, find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
// Hovering over the menu items focuses them.
await hoverOver(tester, find.text(TestMenu.subMenu10.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await hoverOver(tester, find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await hoverOver(tester, find.text(TestMenu.subSubMenu110.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
});
testWidgets('hover traversal invalidates directional focus scope data', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/150910.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await hoverOver(tester, find.text(TestMenu.subMenu12.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
// Move pointer to disabled menu.
await hoverOver(tester, find.text(TestMenu.mainMenu5.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await hoverOver(tester, find.text(TestMenu.subMenu12.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
});
testWidgets('scrolling does not trigger hover traversal', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/150911.
final GlobalKey scrolledMenuItemKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuAnchor(
style: const MenuStyle(fixedSize: WidgetStatePropertyAll<Size>(Size.fromHeight(200))),
controller: controller,
menuChildren: <Widget>[
for (int i = 0; i < 20; i++)
MenuItemButton(
key: i == 15 ? scrolledMenuItemKey : null,
onPressed: () {},
child: Text('Item $i'),
),
],
),
),
),
);
listenForFocusChanges();
controller.open();
await tester.pumpAndSettle();
await hoverOver(tester, find.text('Item 1'));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Item 1"))'));
// Scroll the menu while the pointer is over a menu item. The focus should
// not change.
tester.renderObject(find.text('Item 15')).showOnScreen();
await tester.pumpAndSettle();
expect(focusedMenu, equals('MenuItemButton(Text("Item 1"))'));
// Traverse with the keyboard to test that the menu scrolls without hover
// focus affecting the focused menu.
for (var i = 2; i < 20; i++) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Item $i"))'));
}
});
testWidgets('menus close on ancestor scroll', (WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SingleChildScrollView(
controller: scrollController,
child: Container(
height: 1000,
alignment: Alignment.center,
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(opened, isNotEmpty);
expect(closed, isEmpty);
opened.clear();
scrollController.jumpTo(1000);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isNotEmpty);
});
testWidgets('menus do not close on root menu internal scroll', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/122168.
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
var rootOpened = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
menuButtonTheme: MenuButtonThemeData(
// Increase menu items height to make root menu scrollable.
style: TextButton.styleFrom(minimumSize: const Size.fromHeight(200)),
),
),
home: Material(
child: SingleChildScrollView(
controller: scrollController,
child: Container(
height: 1000,
alignment: Alignment.topLeft,
child: MenuAnchor(
controller: controller,
alignmentOffset: const Offset(0, 10),
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton.tonal(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Show menu'),
);
},
onOpen: () {
rootOpened = true;
},
onClose: () {
rootOpened = false;
},
menuChildren: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
includeExtraGroups: true,
),
),
),
),
),
),
);
await tester.tap(find.text('Show menu'));
await tester.pump();
expect(rootOpened, true);
// Hover the first item.
final pointer = TestPointer(1, PointerDeviceKind.mouse);
await tester.sendEventToBinding(
pointer.hover(tester.getCenter(find.text(TestMenu.mainMenu0.label))),
);
await tester.pump();
expect(opened, isNotEmpty);
// Menus do not close on internal scroll.
await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, 30.0)));
await tester.pump();
expect(rootOpened, true);
expect(closed, isEmpty);
// Menus close on external scroll.
scrollController.jumpTo(1000);
await tester.pump();
expect(rootOpened, false);
expect(closed, isNotEmpty);
});
testWidgets('menus close on view size change', (WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
final mediaQueryData = MediaQueryData.fromView(tester.view);
Widget build(Size size) {
return MaterialApp(
home: Material(
child: MediaQuery(
data: mediaQueryData.copyWith(size: size),
child: SingleChildScrollView(
controller: scrollController,
child: Container(
height: 1000,
alignment: Alignment.center,
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
),
),
);
}
await tester.pumpWidget(build(mediaQueryData.size));
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(opened, isNotEmpty);
expect(closed, isEmpty);
opened.clear();
const smallSize = Size(200, 200);
await changeSurfaceSize(tester, smallSize);
await tester.pumpWidget(build(smallSize));
await tester.pump();
expect(opened, isEmpty);
expect(closed, isNotEmpty);
});
// Regression test for
// https://github.com/flutter/flutter/issues/119532#issuecomment-2274705565.
testWidgets('Shortcuts of MenuAnchor do not rely on WidgetsApp.shortcuts', (
WidgetTester tester,
) async {
// MenuAnchor used to rely on WidgetsApp.shortcuts for menu navigation,
// which is a problem for Web because the Web uses a special set of
// default shortcuts that define arrow keys as scrolling instead of
// traversing, and therefore arrow keys won't enter submenus when the
// focus is on MenuAnchor.
//
// This test verifies that `MenuAnchor`'s shortcuts continues to work even
// when `WidgetsApp.shortcuts` contains nothing.
final childNode = FocusNode(debugLabel: 'Dropdown Inkwell');
addTearDown(childNode.dispose);
await tester.pumpWidget(
MaterialApp(
// Clear WidgetsApp.shortcuts to make sure MenuAnchor doesn't rely on
// it.
shortcuts: const <ShortcutActivator, Intent>{},
home: Scaffold(
body: MenuAnchor(
childFocusNode: childNode,
menuChildren: List<Widget>.generate(
3,
(int i) => MenuItemButton(child: Text('Submenu item $i'), onPressed: () {}),
),
builder: (BuildContext context, MenuController controller, Widget? child) {
return InkWell(
focusNode: childNode,
onTap: controller.open,
child: const Text('Main button'),
);
},
),
),
),
);
listenForFocusChanges();
// Open the drop down menu and focus on the MenuAnchor.
await tester.tap(find.text('Main button'));
await tester.pumpAndSettle();
expect(find.text('Submenu item 0'), findsOneWidget);
// Press arrowDown, and the first submenu button should be focused.
// This is the critical part. It used to not work on Web.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Submenu item 0"))'));
// Press arrowDown, and the second submenu button should be focused.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Submenu item 1"))'));
});
});
group('Accelerators', () {
const apple = <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS};
final Set<TargetPlatform> nonApple = TargetPlatform.values.toSet().difference(apple);
test('Accelerator markers are stripped properly', () {
const expected = <String, String>{
'Plain String': 'Plain String',
'&Simple Accelerator': 'Simple Accelerator',
'&Multiple &Accelerators': 'Multiple Accelerators',
'Whitespace & Accelerators': 'Whitespace Accelerators',
'&Quoted && Ampersand': 'Quoted & Ampersand',
'Ampersand at End &': 'Ampersand at End ',
'&&Multiple Ampersands &&& &&&A &&&&B &&&&': '&Multiple Ampersands & &A &&B &&',
'Bohrium 𨨏 Code point U+28A0F': 'Bohrium 𨨏 Code point U+28A0F',
};
const expectedIndices = <int>[-1, 0, 0, -1, 0, -1, 24, -1];
const expectedHasAccelerator = <bool>[false, true, true, false, true, false, true, false];
var acceleratorIndex = -1;
var count = 0;
for (final String key in expected.keys) {
expect(
MenuAcceleratorLabel.stripAcceleratorMarkers(
key,
setIndex: (int index) {
acceleratorIndex = index;
},
),
equals(expected[key]),
reason: "'$key' label doesn't match ${expected[key]}",
);
expect(
acceleratorIndex,
equals(expectedIndices[count]),
reason: "'$key' index doesn't match ${expectedIndices[count]}",
);
expect(
MenuAcceleratorLabel(key).hasAccelerator,
equals(expectedHasAccelerator[count]),
reason: "'$key' hasAccelerator isn't ${expectedHasAccelerator[count]}",
);
count += 1;
}
});
testWidgets('can invoke menu items', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.pump();
// Makes sure that identical accelerators in parent menu items don't
// shadow the ones in the children.
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu0]));
expect(closed, equals(<TestMenu>[TestMenu.mainMenu0]));
expect(selected, equals(<TestMenu>[TestMenu.subMenu00]));
// Selecting a non-submenu item should close all the menus.
expect(find.text(TestMenu.subMenu00.label), findsNothing);
opened.clear();
closed.clear();
selected.clear();
// Invoking several levels deep.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'e');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altRight);
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(closed, equals(<TestMenu>[TestMenu.subMenu11, TestMenu.mainMenu1]));
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu111]));
opened.clear();
closed.clear();
selected.clear();
}, variant: TargetPlatformVariant(nonApple));
testWidgets('can combine with regular keyboard navigation', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
// Combining accelerators and regular keyboard navigation works.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'e');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(closed, equals(<TestMenu>[TestMenu.subMenu11, TestMenu.mainMenu1]));
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu110]));
}, variant: TargetPlatformVariant(nonApple));
testWidgets('can combine with mouse', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
// Combining accelerators and regular keyboard navigation works.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'e');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.tap(find.text(TestMenu.subSubMenu112.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(closed, equals(<TestMenu>[TestMenu.subMenu11, TestMenu.mainMenu1]));
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu112]));
}, variant: TargetPlatformVariant(nonApple));
testWidgets("disabled items don't respond to accelerators", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '5');
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isEmpty);
expect(selected, isEmpty);
// Selecting a non-submenu item should close all the menus.
expect(find.text(TestMenu.subMenu00.label), findsNothing);
}, variant: TargetPlatformVariant(nonApple));
testWidgets("Apple platforms don't react to accelerators", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isEmpty);
expect(selected, isEmpty);
// Or with the option key equivalents.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'µ');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'µ');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isEmpty);
expect(selected, isEmpty);
}, variant: const TargetPlatformVariant(apple));
});
group('MenuController', () {
testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(key: UniqueKey(), controller: controller, children: createTestMenus()),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
// Now pump a new menu with a different UniqueKey to dispose of the opened
// menu's node, but keep the existing controller.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(includeExtraGroups: true),
),
),
),
);
await tester.pumpAndSettle();
});
testWidgets('closing via controller works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
),
},
),
),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, unorderedEquals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
opened.clear();
closed.clear();
// Close menus using the controller.
controller.close();
await tester.pump();
// The menu should go away,
expect(closed, unorderedEquals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(opened, isEmpty);
});
// Regression test for https://github.com/flutter/flutter/issues/176374.
testWidgets('internal controller is created when the controller is null', (
WidgetTester tester,
) async {
MenuController? testController;
await tester.pumpWidget(
MaterialApp(
home: MenuAnchor(
controller: controller,
menuChildren: const <Widget>[],
builder: (BuildContext context, MenuController controller, Widget? child) {
testController = controller;
return const Text('Anchor');
},
),
),
);
expect(testController, equals(controller));
await tester.pumpWidget(
MaterialApp(
home: MenuAnchor(
menuChildren: const <Widget>[],
builder: (BuildContext context, MenuController controller, Widget? child) {
testController = controller;
return const Text('Anchor');
},
),
),
);
expect(testController, isNotNull);
expect(testController, isNot(controller));
});
});
group('MenuItemButton', () {
testWidgets('Shortcut mnemonics are displayed', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
),
TestMenu.subSubMenu111: const SingleActivator(
LogicalKeyboardKey.keyB,
shift: true,
),
TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.keyC, alt: true),
TestMenu.subSubMenu113: const SingleActivator(
LogicalKeyboardKey.keyD,
meta: true,
),
},
),
),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
Text mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
Text mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
Text mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
Text mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
expect(mnemonic0.data, equals('Ctrl+A'));
expect(mnemonic1.data, equals('Shift+B'));
expect(mnemonic2.data, equals('Alt+C'));
expect(mnemonic3.data, equals('Meta+D'));
case TargetPlatform.windows:
expect(mnemonic0.data, equals('Ctrl+A'));
expect(mnemonic1.data, equals('Shift+B'));
expect(mnemonic2.data, equals('Alt+C'));
expect(mnemonic3.data, equals('Win+D'));
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(mnemonic0.data, equals('⌃ A'));
expect(mnemonic1.data, equals('⇧ B'));
expect(mnemonic2.data, equals('⌥ C'));
expect(mnemonic3.data, equals('⌘ D'));
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
includeExtraGroups: true,
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.arrowRight),
TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.arrowLeft),
TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.arrowUp),
TestMenu.subSubMenu113: const SingleActivator(LogicalKeyboardKey.arrowDown),
},
),
),
),
),
);
await tester.pumpAndSettle();
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals(''));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals(''));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals(''));
mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
expect(mnemonic3.data, equals(''));
// Try some weirder ones.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.escape),
TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.fn),
TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.enter),
},
),
),
),
),
);
await tester.pumpAndSettle();
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals('Esc'));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals('Fn'));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals(''));
}, variant: TargetPlatformVariant.all());
// Regression test for https://github.com/flutter/flutter/issues/145040.
testWidgets('CharacterActivator shortcut mnemonics include modifiers', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const CharacterActivator('A', control: true),
TestMenu.subSubMenu111: const CharacterActivator('B', alt: true),
TestMenu.subSubMenu112: const CharacterActivator('C', meta: true),
},
),
),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
final Text mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
final Text mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
final Text mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
expect(mnemonic0.data, equals('Ctrl+A'));
expect(mnemonic1.data, equals('Alt+B'));
expect(mnemonic2.data, equals('Meta+C'));
case TargetPlatform.windows:
expect(mnemonic0.data, equals('Ctrl+A'));
expect(mnemonic1.data, equals('Alt+B'));
expect(mnemonic2.data, equals('Win+C'));
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(mnemonic0.data, equals('⌃ A'));
expect(mnemonic1.data, equals('⌥ B'));
expect(mnemonic2.data, equals('⌘ C'));
}
}, variant: TargetPlatformVariant.all());
testWidgets('leadingIcon is used when set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
leadingIcon: const Text('leadingIcon'),
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.text('leadingIcon'), findsOneWidget);
});
testWidgets('autofocus is used when set and widget is enabled', (WidgetTester tester) async {
listenForFocusChanges();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuAnchor(
controller: controller,
menuChildren: <Widget>[
MenuItemButton(
autofocus: true,
// Required for clickability.
onPressed: () {},
child: Text(TestMenu.mainMenu0.label),
),
MenuItemButton(onPressed: () {}, child: Text(TestMenu.mainMenu1.label)),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
controller.open();
await tester.pump();
expect(controller.isOpen, equals(true));
expect(focusedMenu, equals('MenuItemButton(Text("${TestMenu.mainMenu0.label}"))'));
});
testWidgets('trailingIcon is used when set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
trailingIcon: const Text('trailingIcon'),
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.text('trailingIcon'), findsOneWidget);
});
testWidgets('SubmenuButton uses supplied controller', (WidgetTester tester) async {
final submenuController = MenuController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
controller: submenuController,
menuChildren: <Widget>[MenuItemButton(child: Text(TestMenu.subMenu00.label))],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
submenuController.open();
await tester.pump();
expect(find.text(TestMenu.subMenu00.label), findsOneWidget);
submenuController.close();
await tester.pump();
expect(find.text(TestMenu.subMenu00.label), findsNothing);
// Now remove the controller and try to control it.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[MenuItemButton(child: Text(TestMenu.subMenu00.label))],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await expectLater(() => submenuController.open(), throwsAssertionError);
await tester.pump();
expect(find.text(TestMenu.subMenu00.label), findsNothing);
});
testWidgets('diagnostics', (WidgetTester tester) async {
final style = ButtonStyle(
shape: WidgetStateProperty.all<OutlinedBorder?>(const StadiumBorder()),
elevation: WidgetStateProperty.all<double?>(10.0),
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red),
);
final menuStyle = MenuStyle(
shape: WidgetStateProperty.all<OutlinedBorder?>(const RoundedRectangleBorder()),
elevation: WidgetStateProperty.all<double?>(20.0),
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.green),
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
style: style,
menuStyle: menuStyle,
menuChildren: <Widget>[
MenuItemButton(style: style, child: Text(TestMenu.subMenu00.label)),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
final SubmenuButton submenu = tester.widget(find.byType(SubmenuButton));
final builder = DiagnosticPropertiesBuilder();
submenu.debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(
description,
equalsIgnoringHashCodes(<String>[
'focusNode: null',
'menuStyle: MenuStyle#00000(backgroundColor: WidgetStatePropertyAll(MaterialColor(primary value: ${const Color(0xff4caf50)})), elevation: WidgetStatePropertyAll(20.0), shape: WidgetStatePropertyAll(RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)))',
'alignmentOffset: null',
'clipBehavior: hardEdge',
]),
);
});
testWidgets('MenuItemButton respects closeOnActivate property', (WidgetTester tester) async {
final controller = MenuController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
controller: controller,
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Button 1')),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
controller.open();
},
child: const Text('Tap me'),
);
},
),
),
),
),
);
await tester.tap(find.text('Tap me'));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(1));
// Taps the MenuItemButton which should close the menu.
await tester.tap(find.text('Button 1'));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(0));
await tester.pumpAndSettle();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
controller: controller,
menuChildren: <Widget>[
MenuItemButton(
closeOnActivate: false,
onPressed: () {},
child: const Text('Button 1'),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
controller.open();
},
child: const Text('Tap me'),
);
},
),
),
),
),
);
await tester.tap(find.text('Tap me'));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(1));
// Taps the MenuItemButton which shouldn't close the menu.
await tester.tap(find.text('Button 1'));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(1));
});
// This is a regression test for https://github.com/flutter/flutter/issues/129439.
testWidgets('MenuItemButton does not overflow when child is long', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
child: MenuItemButton(
overflowAxis: Axis.vertical,
onPressed: () {},
child: const Text('MenuItem Button does not overflow when child is long'),
),
),
),
),
);
// No exception should be thrown.
expect(tester.takeException(), isNull);
});
testWidgets('MenuItemButton layout is updated by overflowAxis', (WidgetTester tester) async {
Widget buildMenuButton({required Axis overflowAxis, bool constrainedLayout = false}) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: constrainedLayout ? 200 : null,
child: MenuItemButton(
overflowAxis: overflowAxis,
onPressed: () {},
child: const Text('This is a very long text that will wrap to the multiple lines.'),
),
),
),
);
}
// Test a long MenuItemButton in an unconstrained layout with vertical overflow axis.
await tester.pumpWidget(buildMenuButton(overflowAxis: Axis.vertical));
expect(tester.getSize(find.byType(MenuItemButton)), const Size(800.0, 48.0));
// Test a long MenuItemButton in an unconstrained layout with horizontal overflow axis.
await tester.pumpWidget(buildMenuButton(overflowAxis: Axis.horizontal));
expect(tester.getSize(find.byType(MenuItemButton)), const Size(800.0, 48.0));
// Test a long MenuItemButton in a constrained layout with vertical overflow axis.
await tester.pumpWidget(
buildMenuButton(overflowAxis: Axis.vertical, constrainedLayout: true),
);
expect(tester.getSize(find.byType(MenuItemButton)), const Size(200.0, 120.0));
// Test a long MenuItemButton in a constrained layout with horizontal overflow axis.
await tester.pumpWidget(
buildMenuButton(overflowAxis: Axis.horizontal, constrainedLayout: true),
);
expect(tester.getSize(find.byType(MenuItemButton)), const Size(200.0, 48.0));
// This should throw an error.
final exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
});
testWidgets('MenuItemButton.styleFrom overlayColor overrides default overlay color', (
WidgetTester tester,
) async {
const overlayColor = Color(0xffff0000);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MenuItemButton(
style: MenuItemButton.styleFrom(overlayColor: overlayColor),
onPressed: () {},
child: const Text('MenuItem'),
),
),
),
);
// Hovered.
final Offset center = tester.getCenter(find.byType(MenuItemButton));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.08)));
// Highlighted (pressed).
await gesture.down(center);
await tester.pumpAndSettle();
expect(
getOverlayColor(tester),
paints
..rect(color: overlayColor.withOpacity(0.08))
..rect(color: overlayColor.withOpacity(0.08))
..rect(color: overlayColor.withOpacity(0.1)),
);
});
// Regression test for https://github.com/flutter/flutter/issues/147479.
testWidgets('MenuItemButton can build when its child is null', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: SizedBox(width: 200, child: MenuItemButton())),
),
);
expect(tester.takeException(), isNull);
});
});
group('Layout', () {
List<Rect> collectMenuItemRects() {
final menuRects = <Rect>[];
final List<Element> candidates = find.byType(SubmenuButton).evaluate().toList();
for (final candidate in candidates) {
final box = candidate.renderObject! as RenderBox;
final Offset topLeft = box.localToGlobal(box.size.topLeft(Offset.zero));
final Offset bottomRight = box.localToGlobal(box.size.bottomRight(Offset.zero));
menuRects.add(Rect.fromPoints(topLeft, bottomRight));
}
return menuRects;
}
List<Rect> collectSubmenuRects() {
final menuRects = <Rect>[];
final List<Element> candidates = findMenuPanels().evaluate().toList();
for (final candidate in candidates) {
final box = candidate.renderObject! as RenderBox;
final Offset topLeft = box.localToGlobal(box.size.topLeft(Offset.zero));
final Offset bottomRight = box.localToGlobal(box.size.bottomRight(Offset.zero));
menuRects.add(Rect.fromPoints(topLeft, bottomRight));
}
return menuRects;
}
testWidgets('unconstrained menus show up in the right place in LTR', (
WidgetTester tester,
) async {
await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(children: createTestMenus(onPressed: onPressed)),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5));
expect(
collectMenuItemRects(),
equals(const <Rect>[
Rect.fromLTRB(4.0, 0.0, 112.0, 48.0),
Rect.fromLTRB(112.0, 0.0, 220.0, 48.0),
Rect.fromLTRB(112.0, 104.0, 326.0, 152.0),
Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0),
]),
);
});
testWidgets('unconstrained menus show up in the right place in RTL', (
WidgetTester tester,
) async {
await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(children: createTestMenus(onPressed: onPressed)),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5));
expect(
collectMenuItemRects(),
equals(const <Rect>[
Rect.fromLTRB(688.0, 0.0, 796.0, 48.0),
Rect.fromLTRB(580.0, 0.0, 688.0, 48.0),
Rect.fromLTRB(474.0, 104.0, 688.0, 152.0),
Rect.fromLTRB(472.0, 0.0, 580.0, 48.0),
Rect.fromLTRB(294.0, 0.0, 472.0, 48.0),
]),
);
});
testWidgets('constrained menus show up in the right place in LTR', (WidgetTester tester) async {
await changeSurfaceSize(tester, const Size(300, 300));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Column(
children: <Widget>[
MenuBar(children: createTestMenus(onPressed: onPressed)),
const Expanded(child: Placeholder()),
],
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5));
expect(
collectMenuItemRects(),
equals(const <Rect>[
Rect.fromLTRB(4.0, 0.0, 112.0, 48.0),
Rect.fromLTRB(112.0, 0.0, 220.0, 48.0),
Rect.fromLTRB(86.0, 104.0, 300.0, 152.0),
Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0),
]),
);
});
testWidgets('tapping MenuItemButton with null focus node', (WidgetTester tester) async {
FocusNode? buttonFocusNode = FocusNode();
// Build our app and trigger a frame.
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MenuAnchor(
menuChildren: <Widget>[
MenuItemButton(
focusNode: buttonFocusNode,
closeOnActivate: false,
child: const Text('Set focus to null'),
onPressed: () {
setState(() {
buttonFocusNode?.dispose();
buttonFocusNode = null;
});
},
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('OPEN MENU'),
);
},
);
},
),
),
);
await tester.tap(find.text('OPEN MENU'));
await tester.pump();
expect(find.text('Set focus to null'), findsOneWidget);
await tester.tap(find.text('Set focus to null'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('constrained menus show up in the right place in RTL', (WidgetTester tester) async {
await changeSurfaceSize(tester, const Size(300, 300));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Column(
children: <Widget>[
MenuBar(children: createTestMenus(onPressed: onPressed)),
const Expanded(child: Placeholder()),
],
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5));
expect(
collectMenuItemRects(),
equals(const <Rect>[
Rect.fromLTRB(188.0, 0.0, 296.0, 48.0),
Rect.fromLTRB(80.0, 0.0, 188.0, 48.0),
Rect.fromLTRB(0.0, 104.0, 214.0, 152.0),
Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0),
Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0),
]),
);
});
testWidgets('constrained menus show up in the right place with offset in LTR', (
WidgetTester tester,
) async {
await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: MenuAnchor(
menuChildren: const <Widget>[
SubmenuButton(
alignmentOffset: Offset(10, 0),
menuChildren: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
SubmenuButton(
alignmentOffset: Offset(10, 0),
menuChildren: <Widget>[
SubmenuButton(
menuChildren: <Widget>[],
child: Text('SubMenuButton4'),
),
],
child: Text('SubMenuButton3'),
),
],
child: Text('SubMenuButton2'),
),
],
child: Text('SubMenuButton1'),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Tap me'),
);
},
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text('Tap me'));
await tester.pump();
await tester.tap(find.text('SubMenuButton1'));
await tester.pump();
await tester.tap(find.text('SubMenuButton2'));
await tester.pump();
await tester.tap(find.text('SubMenuButton3'));
await tester.pump();
expect(find.byType(SubmenuButton), findsNWidgets(4));
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(0.0, 48.0, 256.0, 112.0),
Rect.fromLTRB(266.0, 48.0, 522.0, 112.0),
Rect.fromLTRB(522.0, 48.0, 778.0, 112.0),
Rect.fromLTRB(256.0, 48.0, 512.0, 112.0),
]),
);
});
testWidgets('constrained menus show up in the right place with offset in RTL', (
WidgetTester tester,
) async {
await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Align(
alignment: Alignment.topRight,
child: MenuAnchor(
menuChildren: const <Widget>[
SubmenuButton(
alignmentOffset: Offset(10, 0),
menuChildren: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
SubmenuButton(
alignmentOffset: Offset(10, 0),
menuChildren: <Widget>[
SubmenuButton(
menuChildren: <Widget>[],
child: Text('SubMenuButton4'),
),
],
child: Text('SubMenuButton3'),
),
],
child: Text('SubMenuButton2'),
),
],
child: Text('SubMenuButton1'),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Tap me'),
);
},
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text('Tap me'));
await tester.pump();
await tester.tap(find.text('SubMenuButton1'));
await tester.pump();
await tester.tap(find.text('SubMenuButton2'));
await tester.pump();
await tester.tap(find.text('SubMenuButton3'));
await tester.pump();
expect(find.byType(SubmenuButton), findsNWidgets(4));
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(544.0, 48.0, 800.0, 112.0),
Rect.fromLTRB(278.0, 48.0, 534.0, 112.0),
Rect.fromLTRB(22.0, 48.0, 278.0, 112.0),
Rect.fromLTRB(288.0, 48.0, 544.0, 112.0),
]),
);
});
testWidgets('vertically constrained menus are positioned above the anchor by default', (
WidgetTester tester,
) async {
await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.bottomLeft,
child: MenuAnchor(
menuChildren: const <Widget>[MenuItemButton(child: Text('Button1'))],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Tap me'),
);
},
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text('Tap me'));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(1));
// Test the default offset (0, 0) vertical position.
expect(collectSubmenuRects(), equals(const <Rect>[Rect.fromLTRB(0.0, 488.0, 122.0, 552.0)]));
});
testWidgets(
'vertically constrained menus are positioned above the anchor with the provided offset',
(WidgetTester tester) async {
await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.bottomLeft,
child: MenuAnchor(
alignmentOffset: const Offset(0, 50),
menuChildren: const <Widget>[MenuItemButton(child: Text('Button1'))],
builder: (BuildContext context, MenuController controller, Widget? child) {
return FilledButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Tap me'),
);
},
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text('Tap me'));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(1));
// Test the offset (0, 50) vertical position.
expect(
collectSubmenuRects(),
equals(const <Rect>[Rect.fromLTRB(0.0, 438.0, 122.0, 502.0)]),
);
},
);
Future<void> buildDensityPaddingApp(
WidgetTester tester, {
required TextDirection textDirection,
VisualDensity visualDensity = VisualDensity.standard,
EdgeInsetsGeometry? menuPadding,
}) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.light(useMaterial3: false).copyWith(visualDensity: visualDensity),
home: Directionality(
textDirection: textDirection,
child: Material(
child: Column(
children: <Widget>[
MenuBar(
style: menuPadding != null
? MenuStyle(
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding),
)
: null,
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
}
testWidgets('submenus account for density in LTR', (WidgetTester tester) async {
await buildDensityPaddingApp(tester, textDirection: TextDirection.ltr);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(145.0, 0.0, 655.0, 48.0),
Rect.fromLTRB(257.0, 48.0, 471.0, 208.0),
Rect.fromLTRB(471.0, 96.0, 719.0, 304.0),
]),
);
});
testWidgets('submenus account for menu density in RTL', (WidgetTester tester) async {
await buildDensityPaddingApp(tester, textDirection: TextDirection.rtl);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(145.0, 0.0, 655.0, 48.0),
Rect.fromLTRB(329.0, 48.0, 543.0, 208.0),
Rect.fromLTRB(81.0, 96.0, 329.0, 304.0),
]),
);
});
testWidgets('submenus account for compact menu density in LTR', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
visualDensity: VisualDensity.compact,
textDirection: TextDirection.ltr,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(161.0, 0.0, 639.0, 40.0),
Rect.fromLTRB(265.0, 40.0, 467.0, 176.0),
Rect.fromLTRB(467.0, 80.0, 707.0, 256.0),
]),
);
});
testWidgets('submenus account for compact menu density in RTL', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
visualDensity: VisualDensity.compact,
textDirection: TextDirection.rtl,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(161.0, 0.0, 639.0, 40.0),
Rect.fromLTRB(333.0, 40.0, 535.0, 176.0),
Rect.fromLTRB(93.0, 80.0, 333.0, 256.0),
]),
);
});
testWidgets('submenus account for padding in LTR', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13),
textDirection: TextDirection.ltr,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(138.5, 0.0, 661.5, 73.0),
Rect.fromLTRB(256.5, 60.0, 470.5, 220.0),
Rect.fromLTRB(470.5, 108.0, 718.5, 316.0),
]),
);
});
testWidgets('submenus account for padding in RTL', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13),
textDirection: TextDirection.rtl,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(138.5, 0.0, 661.5, 73.0),
Rect.fromLTRB(329.5, 60.0, 543.5, 220.0),
Rect.fromLTRB(81.5, 108.0, 329.5, 316.0),
]),
);
});
testWidgets('Menu follows content position when a LayerLink is provided', (
WidgetTester tester,
) async {
final controller = MenuController();
final contentKey = UniqueKey();
Widget boilerplate(double bottomInsets) {
return MaterialApp(
home: MediaQuery(
data: MediaQueryData(viewInsets: EdgeInsets.only(bottom: bottomInsets)),
child: Scaffold(
body: Center(
child: MenuAnchor(
controller: controller,
layerLink: LayerLink(),
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Button 1')),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return SizedBox(key: contentKey, width: 100, height: 100);
},
),
),
),
),
);
}
// Build once without bottom insets and open the menu.
await tester.pumpWidget(boilerplate(0.0));
controller.open();
await tester.pump();
// Menu vertical position is just under the content.
expect(tester.getRect(findMenuPanels()).top, tester.getRect(find.byKey(contentKey)).bottom);
// Simulate the keyboard opening resizing the view.
await tester.pumpWidget(boilerplate(100.0));
await tester.pump();
// Menu vertical position is just under the content.
expect(tester.getRect(findMenuPanels()).top, tester.getRect(find.byKey(contentKey)).bottom);
});
testWidgets(
'Menu is correctly offset when a LayerLink is provided and alignmentOffset is set',
(WidgetTester tester) async {
final controller = MenuController();
final contentKey = UniqueKey();
const horizontalOffset = 16.0;
const verticalOffset = 20.0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: MenuAnchor(
controller: controller,
layerLink: LayerLink(),
alignmentOffset: const Offset(horizontalOffset, verticalOffset),
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Button 1')),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return SizedBox(key: contentKey, width: 100, height: 100);
},
),
),
),
),
);
controller.open();
await tester.pump();
expect(
tester.getRect(findMenuPanels()).top,
tester.getRect(find.byKey(contentKey)).bottom + verticalOffset,
);
expect(
tester.getRect(findMenuPanels()).left,
tester.getRect(find.byKey(contentKey)).left + horizontalOffset,
);
},
);
// Regression test for https://github.com/flutter/flutter/issues/171608
testWidgets('Menu vertical padding should not be reduced with compact visual density', (
WidgetTester tester,
) async {
// Helper function to get menu padding by measuring first/last items.
(double, double) getMenuPadding() {
// Find any menu items that are available.
final Finder menuItems = find.byType(SubmenuButton);
if (menuItems.evaluate().length < 2) {
return (0.0, 0.0);
}
final Rect firstItem = tester.getRect(menuItems.first);
final Rect lastItem = tester.getRect(menuItems.last);
final Rect menuPanel = tester.getRect(find.byType(Material).last);
final double topPadding = firstItem.top - menuPanel.top;
final double bottomPadding = menuPanel.bottom - lastItem.bottom;
return (topPadding, bottomPadding);
}
Future<void> buildSimpleMenuAnchor(
TextDirection textDirection, {
VisualDensity visualDensity = VisualDensity.standard,
}) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(visualDensity: visualDensity),
home: Directionality(
textDirection: textDirection,
child: Scaffold(
body: MenuAnchor(
style: const MenuStyle(
padding: WidgetStatePropertyAll<EdgeInsets>(
EdgeInsets.symmetric(vertical: 12, horizontal: 4),
),
),
menuChildren: const <Widget>[
DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Text('Text 1'),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Text('Text 2'),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('OPEN MENU'),
);
},
),
),
),
),
);
await tester.tap(find.text('OPEN MENU'));
await tester.pump();
}
// Pump widget with standard visual density.
await buildSimpleMenuAnchor(TextDirection.ltr);
final (double topStandard, double bottomStandard) = getMenuPadding();
// Pump widget with compact visual density.
await buildSimpleMenuAnchor(TextDirection.ltr, visualDensity: VisualDensity.compact);
final (double topCompact, double bottomCompact) = getMenuPadding();
// Compare standard vs compact padding.
expect(
topCompact,
equals(topStandard),
reason:
'Compact visual density should not change top padding. '
'Standard: $topStandard, Compact: $topCompact',
);
expect(
bottomCompact,
equals(bottomStandard),
reason:
'Compact visual density should not change bottom padding. '
'Standard: $bottomStandard, Compact: $bottomCompact',
);
});
group('LocalizedShortcutLabeler', () {
testWidgets('getShortcutLabel returns the right labels', (WidgetTester tester) async {
String expectedMeta;
String expectedCtrl;
String expectedAlt;
String expectedSeparator;
String expectedShift;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expectedCtrl = 'Ctrl';
expectedMeta = defaultTargetPlatform == TargetPlatform.windows ? 'Win' : 'Meta';
expectedAlt = 'Alt';
expectedShift = 'Shift';
expectedSeparator = '+';
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expectedCtrl = '';
expectedMeta = '';
expectedAlt = '';
expectedShift = '';
expectedSeparator = ' ';
}
const allModifiers = SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
meta: true,
shift: true,
alt: true,
);
late String allExpected;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
allExpected = <String>[
expectedAlt,
expectedCtrl,
expectedMeta,
expectedShift,
'A',
].join(expectedSeparator);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
allExpected = <String>[
expectedCtrl,
expectedAlt,
expectedShift,
expectedMeta,
'A',
].join(expectedSeparator);
}
const charShortcuts = CharacterActivator('ñ');
const charExpected = 'ñ';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(shortcut: allModifiers, child: Text(TestMenu.subMenu10.label)),
MenuItemButton(
shortcut: charShortcuts,
child: Text(TestMenu.subMenu11.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.text(allExpected), findsOneWidget);
expect(find.text(charExpected), findsOneWidget);
}, variant: TargetPlatformVariant.all());
});
group('CheckboxMenuButton', () {
testWidgets('tapping toggles checkbox', (WidgetTester tester) async {
bool? checkBoxValue;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
CheckboxMenuButton(
value: checkBoxValue,
onChanged: (bool? value) {
setState(() {
checkBoxValue = value;
});
},
tristate: true,
child: const Text('checkbox'),
),
],
child: const Text('submenu'),
),
],
);
},
),
),
);
await tester.tap(find.byType(SubmenuButton));
await tester.pump();
expect(tester.widget<CheckboxMenuButton>(find.byType(CheckboxMenuButton)).value, null);
await tester.tap(find.byType(CheckboxMenuButton));
await tester.pumpAndSettle();
expect(checkBoxValue, false);
await tester.tap(find.byType(SubmenuButton));
await tester.pump();
await tester.tap(find.byType(CheckboxMenuButton));
await tester.pumpAndSettle();
expect(checkBoxValue, true);
await tester.tap(find.byType(SubmenuButton));
await tester.pump();
await tester.tap(find.byType(CheckboxMenuButton));
await tester.pumpAndSettle();
expect(checkBoxValue, null);
});
});
group('RadioMenuButton', () {
testWidgets('tapping toggles radio button', (WidgetTester tester) async {
int? radioValue;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
RadioMenuButton<int>(
value: 0,
groupValue: radioValue,
onChanged: (int? value) {
setState(() {
radioValue = value;
});
},
toggleable: true,
child: const Text('radio 0'),
),
RadioMenuButton<int>(
value: 1,
groupValue: radioValue,
onChanged: (int? value) {
setState(() {
radioValue = value;
});
},
toggleable: true,
child: const Text('radio 1'),
),
],
child: const Text('submenu'),
),
],
);
},
),
),
);
await tester.tap(find.byType(SubmenuButton));
await tester.pump();
expect(
tester.widget<RadioMenuButton<int>>(find.byType(RadioMenuButton<int>).first).groupValue,
null,
);
await tester.tap(find.byType(RadioMenuButton<int>).first);
await tester.pumpAndSettle();
expect(radioValue, 0);
await tester.tap(find.byType(SubmenuButton));
await tester.pump();
await tester.tap(find.byType(RadioMenuButton<int>).first);
await tester.pumpAndSettle();
expect(radioValue, null);
await tester.tap(find.byType(SubmenuButton));
await tester.pump();
await tester.tap(find.byType(RadioMenuButton<int>).last);
await tester.pumpAndSettle();
expect(radioValue, 1);
});
});
group('Semantics', () {
testWidgets('MenuItemButton has platform-adaptive button semantics', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: MenuItemButton(
style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
onPressed: () {},
child: const Text('ABC'),
),
),
),
);
// On web, menu items should have SemanticsFlag.isButton.
// On other platforms, they should NOT have the isButton flag.
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
textDirection: TextDirection.ltr,
),
],
),
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('MenuItemButton semantics respects label', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: MenuItemButton(
semanticsLabel: 'TestWidget',
shortcut: const SingleActivator(LogicalKeyboardKey.comma),
style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
onPressed: () {},
child: const Text('ABC'),
),
),
),
);
expect(find.bySemanticsLabel('TestWidget'), findsOneWidget);
semantics.dispose();
}, variant: TargetPlatformVariant.desktop());
testWidgets('SubmenuButton has platform-adaptive button semantics', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SubmenuButton(
onHover: (bool value) {},
style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
menuChildren: const <Widget>[],
child: const Text('ABC'),
),
),
),
);
// On web, submenu buttons should have SemanticsFlag.isButton.
// On other platforms, they should NOT have the isButton flag.
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasExpandedState,
],
label: 'ABC',
textDirection: TextDirection.ltr,
),
],
),
],
),
ignoreTransform: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('SubmenuButton expanded/collapsed state', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SubmenuButton(
style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
menuChildren: <Widget>[
MenuItemButton(
style: MenuItemButton.styleFrom(fixedSize: const Size(120.0, 36.0)),
child: const Text('Item 0'),
onPressed: () {},
),
],
child: const Text('ABC'),
),
),
),
);
// Test expanded state.
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
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>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(
id: 7,
children: <TestSemantics>[
TestSemantics(
id: 8,
children: <TestSemantics>[
TestSemantics(
id: 9,
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasImplicitScrolling,
],
children: <TestSemantics>[
TestSemantics(
id: 10,
label: 'Item 0',
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
],
),
],
),
],
),
TestSemantics(
id: 5,
label: 'ABC',
flags: <SemanticsFlag>[
SemanticsFlag.isFocused,
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.hasExpandedState,
SemanticsFlag.isExpanded,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
],
),
],
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
// Test collapsed state.
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
expect(find.byType(MenuItemButton), findsNothing);
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>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(
id: 5,
label: 'ABC',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.isFocused,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.hasExpandedState,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
],
),
],
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
semantics.dispose();
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
// This is a regression test for https://github.com/flutter/flutter/issues/131676.
testWidgets('Material3 - Menu uses correct text styles', (WidgetTester tester) async {
const menuTextStyle = TextStyle(
fontSize: 18.5,
fontStyle: FontStyle.italic,
wordSpacing: 1.2,
decoration: TextDecoration.lineThrough,
);
final themeData = ThemeData(textTheme: const TextTheme(labelLarge: menuTextStyle));
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(onPressed: onPressed, onOpen: onOpen, onClose: onClose),
),
),
),
);
// Test menu button text style uses the TextTheme.labelLarge.
Finder buttonMaterial = find
.descendant(of: find.byType(TextButton), matching: find.byType(Material))
.first;
Material material = tester.widget<Material>(buttonMaterial);
expect(material.textStyle?.fontSize, menuTextStyle.fontSize);
expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle);
expect(material.textStyle?.wordSpacing, menuTextStyle.wordSpacing);
expect(material.textStyle?.decoration, menuTextStyle.decoration);
// Open the menu.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
// Test menu item text style uses the TextTheme.labelLarge.
buttonMaterial = find
.descendant(
of: find.widgetWithText(TextButton, TestMenu.subMenu10.label),
matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial);
expect(material.textStyle?.fontSize, menuTextStyle.fontSize);
expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle);
expect(material.textStyle?.wordSpacing, menuTextStyle.wordSpacing);
expect(material.textStyle?.decoration, menuTextStyle.decoration);
});
testWidgets('SubmenuButton.onFocusChange is respected', (WidgetTester tester) async {
final focusNode = FocusNode();
addTearDown(focusNode.dispose);
var onFocusChangeCalled = 0;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SubmenuButton(
focusNode: focusNode,
onFocusChange: (bool value) {
setState(() {
onFocusChangeCalled += 1;
});
},
menuChildren: const <Widget>[MenuItemButton(child: Text('item 0'))],
child: const Text('Submenu 0'),
);
},
),
),
),
);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, true);
expect(onFocusChangeCalled, 1);
focusNode.unfocus();
await tester.pump();
expect(focusNode.hasFocus, false);
expect(onFocusChangeCalled, 2);
});
testWidgets('Horizontal _MenuPanel wraps children with IntrinsicWidth', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
children: <Widget>[MenuItemButton(onPressed: () {}, child: const Text('Menu Item'))],
),
),
),
);
// Horizontal _MenuPanel wraps children with IntrinsicWidth to ensure MenuItemButton
// with vertical overflow axis is as wide as the widest child.
final Finder intrinsicWidthFinder = find.ancestor(
of: find.byType(MenuItemButton),
matching: find.byType(IntrinsicWidth),
);
expect(intrinsicWidthFinder, findsOneWidget);
});
testWidgets('SubmenuButton.styleFrom overlayColor overrides default overlay color', (
WidgetTester tester,
) async {
const overlayColor = Color(0xffff00ff);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SubmenuButton(
style: SubmenuButton.styleFrom(overlayColor: overlayColor),
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('MenuItemButton')),
],
child: const Text('Submenu'),
),
),
),
);
// Hovered.
final Offset center = tester.getCenter(find.byType(SubmenuButton));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.08)));
// Highlighted (pressed).
await gesture.down(center);
await tester.pumpAndSettle();
expect(
getOverlayColor(tester),
paints
..rect(color: overlayColor.withOpacity(0.08))
..rect(color: overlayColor.withOpacity(0.08))
..rect(color: overlayColor.withOpacity(0.1)),
);
});
testWidgets(
'Garbage collector destroys child _MenuAnchorState after parent is closed',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/149584
await tester.pumpWidget(
MaterialApp(
home: MenuAnchor(
controller: controller,
menuChildren: const <Widget>[
SubmenuButton(menuChildren: <Widget>[], child: Text('')),
],
),
),
);
controller.open();
await tester.pump();
final state = WeakReference<State>(
tester.firstState<State<SubmenuButton>>(find.byType(SubmenuButton)),
);
expect(state.target, isNotNull);
controller.close();
await tester.pump();
controller.open();
await tester.pump();
controller.close();
await tester.pump();
// Garbage collect. 1 should be enough, but 3 prevents flaky tests.
await tester.runAsync<void>(() async {
await forceGC(fullGcCycles: 3);
});
expect(state.target, isNull);
},
// Skipped on Web: [intended] ForceGC does not work in web and in release mode. See https://api.flutter.dev/flutter/package-leak_tracker_leak_tracker/forceGC.html
// Skipped for everyone else: forceGC is flaky, see https://github.com/flutter/flutter/issues/154858
skip: true,
);
// Regression test for https://github.com/flutter/flutter/issues/154798.
testWidgets('MenuItemButton.styleFrom can customize the button icon', (
WidgetTester tester,
) async {
const iconColor = Color(0xFFF000FF);
const iconSize = 32.0;
const disabledIconColor = Color(0xFFFFF000);
Widget buildButton({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: MenuItemButton(
style: MenuItemButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
disabledIconColor: disabledIconColor,
),
onPressed: enabled ? () {} : null,
trailingIcon: const Icon(Icons.add),
child: const Text('Button'),
),
),
),
);
}
// Test enabled button.
await tester.pumpWidget(buildButton());
expect(tester.getSize(find.byIcon(Icons.add)), const Size(iconSize, iconSize));
expect(iconStyle(tester, Icons.add).color, iconColor);
// Test disabled button.
await tester.pumpWidget(buildButton(enabled: false));
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
});
// Regression test for https://github.com/flutter/flutter/issues/154798.
testWidgets('SubmenuButton.styleFrom can customize the button icon', (
WidgetTester tester,
) async {
const iconColor = Color(0xFFF000FF);
const iconSize = 32.0;
const disabledIconColor = Color(0xFFFFF000);
Widget buildButton({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: SubmenuButton(
style: SubmenuButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
disabledIconColor: disabledIconColor,
),
trailingIcon: const Icon(Icons.add),
menuChildren: <Widget>[if (enabled) const Text('Item')],
child: const Text('SubmenuButton'),
),
),
),
);
}
// Test enabled button.
await tester.pumpWidget(buildButton());
expect(tester.getSize(find.byIcon(Icons.add)), const Size(iconSize, iconSize));
expect(iconStyle(tester, Icons.add).color, iconColor);
// Test disabled button.
await tester.pumpWidget(buildButton(enabled: false));
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
});
// Regression test for https://github.com/flutter/flutter/issues/155034.
testWidgets('Content is shown in the root overlay when useRootOverlay is true', (
WidgetTester tester,
) async {
final controller = MenuController();
final overlayKey = UniqueKey();
final menuItemKey = UniqueKey();
late final OverlayEntry overlayEntry;
addTearDown(() {
overlayEntry.remove();
overlayEntry.dispose();
});
Widget boilerplate() {
return MaterialApp(
home: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return Scaffold(
body: Center(
child: MenuAnchor(
useRootOverlay: true,
controller: controller,
menuChildren: <Widget>[
MenuItemButton(
key: menuItemKey,
onPressed: () {},
child: const Text('Item 1'),
),
],
),
),
);
},
),
],
),
);
}
await tester.pumpWidget(boilerplate());
expect(find.byKey(menuItemKey), findsNothing);
// Open the menu.
controller.open();
await tester.pump();
expect(find.byKey(menuItemKey), findsOne);
// Expect two overlays: the root overlay created by MaterialApp and the
// overlay created by the boilerplate code.
expect(find.byType(Overlay), findsNWidgets(2));
final Iterable<Overlay> overlays = tester.widgetList<Overlay>(find.byType(Overlay));
final Overlay nonRootOverlay = tester.widget(find.byKey(overlayKey));
final Overlay rootOverlay = overlays.firstWhere(
(Overlay overlay) => overlay != nonRootOverlay,
);
// Check that the ancestor _RenderTheater for the menu item is the one
// from the root overlay.
expect(
ancestorRenderTheaters(tester.renderObject(find.byKey(menuItemKey))).single,
tester.renderObject(find.byWidget(rootOverlay)),
);
});
testWidgets('Content is shown in the nearest ancestor overlay when useRootOverlay is false', (
WidgetTester tester,
) async {
final controller = MenuController();
final overlayKey = UniqueKey();
final menuItemKey = UniqueKey();
late final OverlayEntry overlayEntry;
addTearDown(() {
overlayEntry.remove();
overlayEntry.dispose();
});
Widget boilerplate() {
return MaterialApp(
home: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return Scaffold(
body: Center(
child: MenuAnchor(
controller: controller,
menuChildren: <Widget>[
MenuItemButton(
key: menuItemKey,
onPressed: () {},
child: const Text('Item 1'),
),
],
),
),
);
},
),
],
),
);
}
await tester.pumpWidget(boilerplate());
expect(find.byKey(menuItemKey), findsNothing);
// Open the menu.
controller.open();
await tester.pump();
expect(find.byKey(menuItemKey), findsOne);
// Expect two overlays: the root overlay created by MaterialApp and the
// overlay created by the boilerplate code.
expect(find.byType(Overlay), findsNWidgets(2));
final Overlay nonRootOverlay = tester.widget(find.byKey(overlayKey));
// Check that the ancestor _RenderTheater for the menu item is the one
// from the root overlay.
expect(
ancestorRenderTheaters(tester.renderObject(find.byKey(menuItemKey))).first,
tester.renderObject(find.byWidget(nonRootOverlay)),
);
});
// Regression test for https://github.com/flutter/flutter/issues/156572.
testWidgets('Unattached MenuController does not throw when calling close', (
WidgetTester tester,
) async {
final controller = MenuController();
controller.close();
await tester.pump();
expect(tester.takeException(), isNull);
});
testWidgets('Unattached MenuController returns false when calling isOpen', (
WidgetTester tester,
) async {
final controller = MenuController();
expect(controller.isOpen, false);
});
// Regression test for https://github.com/flutter/flutter/issues/157606.
testWidgets('MenuAnchor updates isOpen state correctly', (WidgetTester tester) async {
var isOpen = false;
var openCount = 0;
var closeCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: MenuAnchor(
menuChildren: const <Widget>[MenuItemButton(child: Text('menu item'))],
builder: (BuildContext context, MenuController controller, Widget? child) {
isOpen = controller.isOpen;
return FilledButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: Text(isOpen ? 'close' : 'open'),
);
},
onOpen: () => openCount++,
onClose: () => closeCount++,
),
),
),
),
);
expect(find.text('open'), findsOneWidget);
expect(isOpen, false);
expect(openCount, 0);
expect(closeCount, 0);
await tester.tap(find.byType(FilledButton));
await tester.pump();
expect(find.text('close'), findsOneWidget);
expect(isOpen, true);
expect(openCount, 1);
expect(closeCount, 0);
await tester.tap(find.byType(FilledButton));
await tester.pump();
expect(find.text('open'), findsOneWidget);
expect(isOpen, false);
expect(openCount, 1);
expect(closeCount, 1);
});
testWidgets('SubmenuButton.submenuIcon updates default arrow icon', (
WidgetTester tester,
) async {
const IconData disabledIcon = Icons.close;
const IconData hoveredIcon = Icons.bolt;
const IconData focusedIcon = Icons.favorite;
const IconData defaultIcon = Icons.add;
final WidgetStateProperty<Widget?> submenuIcon = WidgetStateProperty.resolveWith<Widget?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.disabled)) {
return const Icon(disabledIcon);
}
if (states.contains(WidgetState.hovered)) {
return const Icon(hoveredIcon);
}
if (states.contains(WidgetState.focused)) {
return const Icon(focusedIcon);
}
return const Icon(defaultIcon);
});
Widget buildMenu({WidgetStateProperty<Widget?>? icon, bool enabled = true}) {
return MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
SubmenuButton(
submenuIcon: icon,
menuChildren: enabled
? <Widget>[MenuItemButton(child: Text(TestMenu.mainMenu0.label))]
: <Widget>[],
child: Text(TestMenu.subSubMenu110.label),
),
],
child: Text(TestMenu.subMenu00.label),
),
],
),
),
);
}
await tester.pumpWidget(buildMenu());
await tester.tap(find.text(TestMenu.subMenu00.label));
await tester.pump();
expect(find.byIcon(Icons.arrow_right), findsOneWidget);
controller.close();
await tester.pump();
await tester.pumpWidget(buildMenu(icon: submenuIcon));
await tester.tap(find.text(TestMenu.subMenu00.label));
await tester.pump();
expect(find.byIcon(defaultIcon), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.byIcon(focusedIcon), findsOneWidget);
controller.close();
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu00.label));
await tester.pump();
await hoverOver(tester, find.text(TestMenu.subSubMenu110.label));
await tester.pump();
expect(find.byIcon(hoveredIcon), findsOneWidget);
controller.close();
await tester.pump();
await tester.pumpWidget(buildMenu(icon: submenuIcon, enabled: false));
await tester.tap(find.text(TestMenu.subMenu00.label));
await tester.pump();
expect(find.byIcon(disabledIcon), findsOneWidget);
});
});
group('Mouse cursors', () {
testWidgets('SubmenuButton has expected default mouse cursor on hover', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Test menu button item')),
],
child: const Text('Main Menu'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(1000, 1000));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
await gesture.moveTo(tester.getCenter(find.byType(SubmenuButton)));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
});
testWidgets('MenuItemButton has expected default mouse cursor on hover', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Test menu item')),
],
child: const Text('File'),
),
],
),
),
),
);
// Open SubmenuButton.
await tester.tap(find.text('File'));
await tester.pumpAndSettle();
final Finder menuItemFinder = find.byType(MenuItemButton);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(1000, 1000));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
// Move to MenuItemButton.
await gesture.moveTo(tester.getCenter(menuItemFinder));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
});
testWidgets('CheckboxMenuButton has expected default mouse cursor on hover', (
WidgetTester tester,
) async {
bool? value = false;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: CheckboxMenuButton(
value: value,
onChanged: (bool? newValue) {
value = newValue;
},
child: const Text('Checkbox'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(1000, 1000));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
await gesture.moveTo(tester.getCenter(find.byType(CheckboxMenuButton)));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
});
testWidgets('RadioMenuButton has expected default mouse cursor on hover', (
WidgetTester tester,
) async {
int? groupValue = 0;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RadioMenuButton<int>(
value: 1,
groupValue: groupValue,
onChanged: (int? newValue) {
groupValue = newValue;
},
child: const Text('Radio'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(1000, 1000));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
await gesture.moveTo(tester.getCenter(find.byType(RadioMenuButton<int>)));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
});
testWidgets('MenuItemButton has expected mouse cursor when explicitly configured', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuItemButton(
onPressed: () {},
style: ButtonStyle(
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.cell),
),
child: const Text('Menu Item'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: tester.getCenter(find.byType(MenuItemButton)));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.cell,
);
});
testWidgets('CheckboxMenuButton has expected mouse cursor when explicitly configured', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: CheckboxMenuButton(
style: ButtonStyle(
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.cell),
),
value: true,
onChanged: (bool? value) {},
child: const Text('Menu Item'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: tester.getCenter(find.byType(CheckboxMenuButton)));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.cell,
);
});
testWidgets('RadioMenuButton has expected mouse cursor when explicitly configured', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RadioMenuButton<bool>(
style: ButtonStyle(
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.cell),
),
value: false,
onChanged: (bool? value) {},
groupValue: null,
child: const Text('Menu Item'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: tester.getCenter(find.byType(RadioMenuButton<bool>)));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.cell,
);
});
testWidgets('SubmenuButton has expected mouse cursor when explicitly configured', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
children: <Widget>[
SubmenuButton(
style: ButtonStyle(
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.cell),
),
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Test menu item')),
],
child: const Text('File'),
),
],
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: tester.getCenter(find.byType(SubmenuButton)));
addTearDown(gesture.removePointer);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.cell,
);
});
});
testWidgets('Menu panel default reserved padding', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
controller: controller,
menuChildren: const <Widget>[SizedBox(width: 800, height: 24)],
builder: (BuildContext context, MenuController controller, Widget? child) {
return const SizedBox(width: 800, height: 24);
},
),
),
),
),
);
controller.open();
await tester.pump();
const defaultReservedPadding = 8.0; // See _kMenuViewPadding.
expect(tester.getRect(findMenuPanels()).width, 800.0 - defaultReservedPadding * 2);
});
testWidgets('Menu panel accepts custom reserved padding', (WidgetTester tester) async {
const EdgeInsetsGeometry reservedPadding = EdgeInsets.symmetric(horizontal: 13.0);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
controller: controller,
reservedPadding: reservedPadding,
menuChildren: const <Widget>[SizedBox(width: 800, height: 24)],
builder: (BuildContext context, MenuController controller, Widget? child) {
return const SizedBox(width: 800, height: 24);
},
),
),
),
),
);
controller.open();
await tester.pump();
expect(tester.getRect(findMenuPanels()).width, 800.0 - reservedPadding.horizontal);
});
testWidgets('MenuAcceleratorLabel does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(child: SizedBox.shrink(child: MenuAcceleratorLabel('X'))),
),
);
expect(tester.getSize(find.byType(MenuAcceleratorLabel)), Size.zero);
});
testWidgets('Layout updates when reserved padding changes', (WidgetTester tester) async {
const EdgeInsetsGeometry reservedPadding = EdgeInsets.symmetric(horizontal: 13.0);
await tester.pumpWidget(
MaterialApp(
home: MenuAnchor(
controller: controller,
menuChildren: const <Widget>[SizedBox(width: 800, height: 24)],
),
),
);
controller.open(position: Offset.zero);
await tester.pump();
await tester.pumpWidget(
MaterialApp(
home: MenuAnchor(
controller: controller,
reservedPadding: reservedPadding,
menuChildren: const <Widget>[SizedBox(width: 800, height: 24)],
),
),
);
expect(tester.getRect(findMenuPanels()).width, 800.0 - reservedPadding.horizontal);
});
testWidgets('SubmenuButton does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.shrink(child: SubmenuButton(menuChildren: <Widget>[], child: null)),
),
),
);
expect(tester.getSize(find.byType(SubmenuButton)), Size.zero);
});
testWidgets('MenuBar does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.shrink(child: MenuBar(children: <Widget>[Text('X')])),
),
),
);
expect(tester.getSize(find.byType(MenuBar)), Size.zero);
});
testWidgets('MenuItemButton does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(child: SizedBox.shrink(child: MenuItemButton())),
),
);
expect(tester.getSize(find.byType(MenuItemButton)), Size.zero);
});
testWidgets('RadioMenuButton does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox.shrink(
child: RadioMenuButton<bool>(
value: true,
groupValue: true,
onChanged: (bool? value) {},
child: null,
),
),
),
),
);
expect(tester.getSize(find.byType(RadioMenuButton<bool>)), Size.zero);
});
testWidgets('CheckboxMenuButton does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox.shrink(
child: CheckboxMenuButton(
value: true,
onChanged: (bool? value) {},
child: const Text('X'),
),
),
),
),
);
expect(tester.getSize(find.byType(CheckboxMenuButton)), Size.zero);
});
testWidgets('MenuAnchor does not crash at zero area', (WidgetTester tester) async {
tester.view.physicalSize = Size.zero;
final menuController = MenuController();
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: MenuAnchor(menuChildren: const <Widget>[Text('X')], controller: menuController),
),
),
),
);
expect(tester.getSize(find.byType(MenuAnchor)), Size.zero);
menuController.open();
await tester.pump();
expect(find.text('X'), findsOne);
});
}
List<Widget> createTestMenus({
void Function(TestMenu)? onPressed,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool includeExtraGroups = false,
bool accelerators = false,
}) {
Widget submenuButton(TestMenu menu, {required List<Widget> menuChildren}) {
return SubmenuButton(
onOpen: onOpen != null ? () => onOpen(menu) : null,
onClose: onClose != null ? () => onClose(menu) : null,
menuChildren: menuChildren,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
Widget menuItemButton(
TestMenu menu, {
bool enabled = true,
Widget? leadingIcon,
Widget? trailingIcon,
Key? key,
}) {
return MenuItemButton(
key: key,
onPressed: enabled && onPressed != null ? () => onPressed(menu) : null,
shortcut: shortcuts[menu],
leadingIcon: leadingIcon,
trailingIcon: trailingIcon,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
final result = <Widget>[
submenuButton(
TestMenu.mainMenu0,
menuChildren: <Widget>[
menuItemButton(TestMenu.subMenu00, leadingIcon: const Icon(Icons.add)),
menuItemButton(TestMenu.subMenu01),
menuItemButton(TestMenu.subMenu02),
],
),
submenuButton(
TestMenu.mainMenu1,
menuChildren: <Widget>[
menuItemButton(TestMenu.subMenu10),
submenuButton(
TestMenu.subMenu11,
menuChildren: <Widget>[
menuItemButton(TestMenu.subSubMenu110, key: UniqueKey()),
menuItemButton(TestMenu.subSubMenu111),
menuItemButton(TestMenu.subSubMenu112),
menuItemButton(TestMenu.subSubMenu113),
],
),
menuItemButton(TestMenu.subMenu12),
],
),
submenuButton(
TestMenu.mainMenu2,
menuChildren: <Widget>[
menuItemButton(TestMenu.subMenu20, leadingIcon: const Icon(Icons.ac_unit), enabled: false),
],
),
if (includeExtraGroups)
submenuButton(
TestMenu.mainMenu3,
menuChildren: <Widget>[menuItemButton(TestMenu.subMenu30, enabled: false)],
),
if (includeExtraGroups)
submenuButton(
TestMenu.mainMenu4,
menuChildren: <Widget>[
menuItemButton(TestMenu.subMenu40, enabled: false),
menuItemButton(TestMenu.subMenu41, enabled: false),
menuItemButton(TestMenu.subMenu42, enabled: false),
],
),
submenuButton(TestMenu.mainMenu5, menuChildren: const <Widget>[]),
];
return result;
}
enum TestMenu {
mainMenu0('&Menu 0'),
mainMenu1('M&enu &1'),
mainMenu2('Me&nu 2'),
mainMenu3('Men&u 3'),
mainMenu4('Menu &4'),
mainMenu5('Menu &5 && &6 &'),
subMenu00('Sub &Menu 0&0'),
subMenu01('Sub Menu 0&1'),
subMenu02('Sub Menu 0&2'),
subMenu10('Sub Menu 1&0'),
subMenu11('Sub Menu 1&1'),
subMenu12('Sub Menu 1&2'),
subMenu20('Sub Menu 2&0'),
subMenu30('Sub Menu 3&0'),
subMenu40('Sub Menu 4&0'),
subMenu41('Sub Menu 4&1'),
subMenu42('Sub Menu 4&2'),
subSubMenu110('Sub Sub Menu 11&0'),
subSubMenu111('Sub Sub Menu 11&1'),
subSubMenu112('Sub Sub Menu 11&2'),
subSubMenu113('Sub Sub Menu 11&3'),
anchorButton('Press Me'),
outsideButton('Outside');
const TestMenu(this.acceleratorLabel);
final String acceleratorLabel;
// Strip the accelerator markers.
String get label => MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel);
}