| // 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 '../widgets/semantics_tester.dart'; |
| |
| void main() { |
| late MenuController controller; |
| String? focusedMenu; |
| final List<TestMenu> selected = <TestMenu>[]; |
| final List<TestMenu> opened = <TestMenu>[]; |
| final List<TestMenu> 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'); |
| } |
| |
| 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 = 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, |
| ); |
| } |
| |
| 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.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.widgetWithText(MenuItemButton, TestMenu.subMenu10.label)), |
| equals(const Rect.fromLTRB(265.0, 40.0, 467.0, 80.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, 160.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, 72.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, 72.0))); |
| expect( |
| tester.getRect(find.widgetWithText(MenuItemButton, TestMenu.subMenu10.label)), |
| equals(const Rect.fromLTRB(249.0, 80.0, 483.0, 136.0)), |
| ); |
| expect( |
| tester.getRect( |
| find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1), |
| ), |
| equals(const Rect.fromLTRB(241.0, 64.0, 491.0, 264.0)), |
| ); |
| }); |
| |
| testWidgets('Menu defaults', (WidgetTester tester) async { |
| final ThemeData 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 = 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 ScrollbarThemeData 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: MaterialStateProperty.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: MaterialStateProperty.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 FocusNode 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, 278.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(522.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, 300.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(500.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: MaterialStateProperty.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 MenuItemButton item = MenuItemButton( |
| shortcut: SingleActivator(LogicalKeyboardKey.keyA), |
| child: Text('label2'), |
| ); |
| final MenuBar 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 DiagnosticPropertiesBuilder 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: MaterialStatePropertyAll(MaterialColor(primary value: Color(0xfff44336))), elevation: MaterialStatePropertyAll(10.0))\n' |
| 'clipBehavior: Clip.none'), |
| ); |
| }); |
| |
| 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"))')); |
| }); |
| |
| 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"))')); |
| }); |
| |
| 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('menus close on ancestor scroll', (WidgetTester tester) async { |
| final ScrollController 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 = ScrollController(); |
| addTearDown(scrollController.dispose); |
| bool 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 TestPointer 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 = ScrollController(); |
| addTearDown(scrollController.dispose); |
| final MediaQueryData 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 Size smallSize = Size(200, 200); |
| await changeSurfaceSize(tester, smallSize); |
| |
| await tester.pumpWidget(build(smallSize)); |
| await tester.pump(); |
| |
| expect(opened, isEmpty); |
| expect(closed, isNotEmpty); |
| }); |
| }); |
| |
| group('Accelerators', () { |
| const Set<TargetPlatform> apple = <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS}; |
| final Set<TargetPlatform> nonApple = TargetPlatform.values.toSet().difference(apple); |
| |
| test('Accelerator markers are stripped properly', () { |
| const Map<String, String> 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 List<int> expectedIndices = <int>[-1, 0, 0, -1, 0, -1, 24, -1]; |
| const List<bool> expectedHasAccelerator = <bool>[false, true, true, false, true, false, true, false]; |
| int acceleratorIndex = -1; |
| int 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); |
| }); |
| }); |
| |
| 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; |
| Text mnemonic1; |
| Text mnemonic2; |
| Text mnemonic3; |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label)); |
| expect(mnemonic0.data, equals('Ctrl+A')); |
| mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label)); |
| expect(mnemonic1.data, equals('Shift+B')); |
| mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label)); |
| expect(mnemonic2.data, equals('Alt+C')); |
| mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label)); |
| expect(mnemonic3.data, equals('Meta+D')); |
| case TargetPlatform.windows: |
| mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label)); |
| expect(mnemonic0.data, equals('Ctrl+A')); |
| mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label)); |
| expect(mnemonic1.data, equals('Shift+B')); |
| mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label)); |
| expect(mnemonic2.data, equals('Alt+C')); |
| mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label)); |
| expect(mnemonic3.data, equals('Win+D')); |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label)); |
| expect(mnemonic0.data, equals('⌃ A')); |
| mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label)); |
| expect(mnemonic1.data, equals('⇧ B')); |
| mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label)); |
| expect(mnemonic2.data, equals('⌥ C')); |
| mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label)); |
| 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()); |
| |
| 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('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 MenuController 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 ButtonStyle style = ButtonStyle( |
| shape: MaterialStateProperty.all<OutlinedBorder?>(const StadiumBorder()), |
| elevation: MaterialStateProperty.all<double?>(10.0), |
| backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red), |
| ); |
| final MenuStyle menuStyle = MenuStyle( |
| shape: MaterialStateProperty.all<OutlinedBorder?>(const RoundedRectangleBorder()), |
| elevation: MaterialStateProperty.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 DiagnosticPropertiesBuilder 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: MaterialStatePropertyAll(MaterialColor(primary value: Color(0xff4caf50))), elevation: MaterialStatePropertyAll(20.0), shape: MaterialStatePropertyAll(RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)))', |
| 'alignmentOffset: null', |
| 'clipBehavior: hardEdge', |
| ], |
| ), |
| ); |
| }); |
| |
| testWidgets('MenuItemButton respects closeOnActivate property', (WidgetTester tester) async { |
| final MenuController 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)); |
| }); |
| }); |
| |
| group('Layout', () { |
| List<Rect> collectMenuItemRects() { |
| final List<Rect> menuRects = <Rect>[]; |
| final List<Element> candidates = find.byType(SubmenuButton).evaluate().toList(); |
| for (final Element candidate in candidates) { |
| final RenderBox 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 List<Rect> menuRects = <Rect>[]; |
| final List<Element> candidates = findMenuPanels().evaluate().toList(); |
| for (final Element candidate in candidates) { |
| final RenderBox 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'), |
| ); |
| }, |
| ), |
| |