// 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 '../rendering/mock_canvas.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, }) { final FocusNode focusNode = FocusNode(); return MaterialApp( home: Material( child: Directionality( textDirection: textDirection, child: Center( child: MenuAnchor( childFocusNode: focusNode, controller: controller, alignmentOffset: alignmentOffset, style: MenuStyle(alignment: alignment), menuChildren: <Widget>[ MenuItemButton( key: menuItemKey, shortcut: const SingleActivator( LogicalKeyboardKey.keyB, control: true, ), onPressed: () {}, child: Text(TestMenu.subMenu00.label), ), MenuItemButton( leadingIcon: const Icon(Icons.send), trailingIcon: const Icon(Icons.mail), onPressed: () {}, 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(); } }, child: child, ); }, child: const Text('Press Me'), ), ), ), ), ); } 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), 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 colors', (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.surface); expect(material.shadowColor, themeData.colorScheme.shadow); expect(material.surfaceTintColor, themeData.colorScheme.surfaceTint); 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); // 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.surface); expect(material.shadowColor, themeData.colorScheme.shadow); expect(material.surfaceTintColor, themeData.colorScheme.surfaceTint); 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); 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.surface); expect(material.shadowColor, themeData.colorScheme.shadow); expect(material.surfaceTintColor, themeData.colorScheme.surfaceTint); 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.surface); expect(material.shadowColor, themeData.colorScheme.shadow); expect(material.surfaceTintColor, themeData.colorScheme.surfaceTint); 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(); expect(find.byType(Scrollbar), findsOneWidget); // Test Scrollbar thumb color. expect( find.byType(Scrollbar), 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(); expect(find.byType(Scrollbar), findsOneWidget); // Scrollbar thumb color should be updated. expect( find.byType(Scrollbar), paints..rrect(color: const Color(0xff00ff00)), ); }, variant: TargetPlatformVariant.desktop()); 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( 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( 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, 276.0, 472.0, 324.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, 324.0, 602.0, 436.0))); await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart)); await tester.pump(); expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 276.0, 602.0, 388.0))); await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.center)); await tester.pump(); expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(400.0, 300.0, 674.0, 412.0))); await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.bottomEnd)); await tester.pump(); expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(472.0, 324.0, 746.0, 436.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, 276.0, 472.0, 324.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, 324.0, 472.0, 436.0))); await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart)); await tester.pump(); expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(198.0, 276.0, 472.0, 388.0))); await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.center)); await tester.pump(); expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(126.0, 300.0, 400.0, 412.0))); await tester .pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.bottomEnd)); await tester.pump(); expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(54.0, 324.0, 328.0, 436.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, 276.0, 472.0, 324.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, 374.0, 702.0, 486.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, 476.0, 800.0, 588.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, 276.0, 472.0, 324.0))); expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.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, 374.0, 372.0, 486.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, 476.0, 800.0, 588.0))); }); testWidgets('works with Padding around menu and overlay', (WidgetTester tester) async { await tester.pumpWidget( Padding( padding: const EdgeInsets.all(10.0), child: MaterialApp( 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( 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('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(); 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(); 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(); 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>[ 'child: Text("Menu 0")', '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( 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(220.0, 0.0, 328.0, 48.0), Rect.fromLTRB(328.0, 0.0, 506.0, 48.0), Rect.fromLTRB(112.0, 104.0, 326.0, 152.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( 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(472.0, 0.0, 580.0, 48.0), Rect.fromLTRB(294.0, 0.0, 472.0, 48.0), Rect.fromLTRB(474.0, 104.0, 688.0, 152.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( 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(220.0, 0.0, 328.0, 48.0), Rect.fromLTRB(328.0, 0.0, 506.0, 48.0), Rect.fromLTRB(86.0, 104.0, 300.0, 152.0), ]), ); }); 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( 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(-28.0, 0.0, 80.0, 48.0), Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0), Rect.fromLTRB(0.0, 104.0, 214.0, 152.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( 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( 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( 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( 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().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, 160.0), Rect.fromLTRB(467.0, 72.0, 707.0, 232.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, 160.0), Rect.fromLTRB(93.0, 72.0, 333.0, 232.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), ]), ); }); }); 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 SingleActivator allModifiers = SingleActivator( LogicalKeyboardKey.keyA, control: true, meta: true, shift: true, alt: true, ); final String allExpected = <String>[expectedAlt, expectedCtrl, expectedMeta, expectedShift, 'A'].join(expectedSeparator); const CharacterActivator charShortcuts = CharacterActivator('ñ'); const String 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); }); }); } 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 List<Widget> 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'); const TestMenu(this.acceleratorLabel); final String acceleratorLabel; // Strip the accelerator markers. String get label => MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel); int get acceleratorIndex { int index = -1; MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel, setIndex: (int i) => index = i); return index; } }