// 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'; void main() { late MenuController controller; String? focusedMenu; final List selected = []; final List opened = []; final List closed = []; final GlobalKey menuItemKey = GlobalKey(); late Size defaultSize; 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(); } setUpAll(() { final MediaQueryData mediaQueryData = MediaQueryData.fromWindow(TestWidgetsFlutterBinding.instance.window); defaultSize = mediaQueryData.size; }); setUp(() { focusedMenu = null; selected.clear(); opened.clear(); closed.clear(); controller = MenuController(); focusedMenu = null; }); Future changeSurfaceSize(WidgetTester tester, Size size) async { await tester.binding.setSurfaceSize(size); addTearDown(() async { await tester.binding.setSurfaceSize(defaultSize); }); } 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: [ 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 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( find.descendant(of: findMenuPanels(), matching: find.byType(Material)).first, ); } 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(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(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(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(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( 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(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(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(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(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( find.descendant(of: find.byIcon(Icons.ac_unit), matching: find.byType(RichText)), ); expect(iconRichText.text.style?.color, themeData.colorScheme.onSurface.withOpacity(0.38)); }); 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: [ Row( children: [ 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: [ 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: [ Row( children: [ 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: [ 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: [ Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ 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: [ Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ 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: [ Row( children: [ Expanded( child: MenuBar( style: MenuStyle( elevation: MaterialStateProperty.all(10), backgroundColor: const MaterialStatePropertyAll(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('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.mainMenu1])); expect(closed, isEmpty); opened.clear(); closed.clear(); await tester.tap(find.text(TestMenu.subMenu11.label)); await tester.pump(); expect(opened, equals([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.subMenu11])); opened.clear(); closed.clear(); await tester.tap(find.text(TestMenu.mainMenu0.label)); await tester.pump(); expect(opened, equals([TestMenu.mainMenu0])); expect(closed, equals([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.mainMenu1, TestMenu.subMenu11])); opened.clear(); await tester.tap(find.text(TestMenu.subSubMenu110.label)); await tester.pump(); expect(selected, equals([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(Colors.red), elevation: MaterialStatePropertyAll(10.0), ), children: const [item], ); await tester.pumpWidget( MaterialApp( home: Material( child: menuBar, ), ), ); await tester.pump(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); menuBar.debugFillProperties(builder); final List 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: [ 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.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 close on view size change', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); final MediaQueryData mediaQueryData = MediaQueryData.fromWindow(tester.binding.window); 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 apple = {TargetPlatform.macOS, TargetPlatform.iOS}; final Set nonApple = TargetPlatform.values.toSet().difference(apple); test('Accelerator markers are stripped properly', () { const Map expected = { '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 expectedIndices = [-1, 0, 0, -1, 0, -1, 24, -1]; const List expectedHasAccelerator = [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.mainMenu0])); expect(closed, equals([TestMenu.mainMenu0])); expect(selected, equals([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.mainMenu1, TestMenu.subMenu11])); expect(closed, equals([TestMenu.subMenu11, TestMenu.mainMenu1])); expect(selected, equals([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.mainMenu1, TestMenu.subMenu11])); expect(closed, equals([TestMenu.subMenu11, TestMenu.mainMenu1])); expect(selected, equals([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.mainMenu1, TestMenu.subMenu11])); expect(closed, equals([TestMenu.subMenu11, TestMenu.mainMenu1])); expect(selected, equals([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.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.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.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.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('⇧ 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')); break; 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('⇧ 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')); break; 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')); break; } await tester.pumpWidget( MaterialApp( home: Material( child: MenuBar( controller: controller, children: createTestMenus( includeExtraGroups: true, shortcuts: { 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.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: [ SubmenuButton( menuChildren: [ 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: [ SubmenuButton( menuChildren: [ 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('diagnostics', (WidgetTester tester) async { final ButtonStyle style = ButtonStyle( shape: MaterialStateProperty.all(const StadiumBorder()), elevation: MaterialStateProperty.all(10.0), backgroundColor: const MaterialStatePropertyAll(Colors.red), ); final MenuStyle menuStyle = MenuStyle( shape: MaterialStateProperty.all(const RoundedRectangleBorder()), elevation: MaterialStateProperty.all(20.0), backgroundColor: const MaterialStatePropertyAll(Colors.green), ); await tester.pumpWidget( MaterialApp( home: Material( child: MenuBar( controller: controller, children: [ SubmenuButton( style: style, menuStyle: menuStyle, menuChildren: [ 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 description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect( description, equalsIgnoringHashCodes( [ '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: none', ], ), ); }); }); group('Layout', () { List collectMenuItemRects() { final List menuRects = []; final List 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 collectSubmenuRects() { final List menuRects = []; final List 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: [ Row( children: [ 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.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: [ Row( children: [ 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.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: [ 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.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: [ 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.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) ]), ); }); Future 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: [ MenuBar( style: menuPadding != null ? MenuStyle(padding: MaterialStatePropertyAll(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.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.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.fromLTRB(145.0, 0.0, 655.0, 40.0), Rect.fromLTRB(257.0, 40.0, 467.0, 176.0), Rect.fromLTRB(467.0, 80.0, 715.0, 256.0), ]), ); }); testWidgets('submenus account for compact menu density in RTL', (WidgetTester tester) async { await buildDensityPaddingApp( tester, visualDensity: VisualDensity.compact, textDirection: TextDirection.rtl, ); expect( collectSubmenuRects(), equals(const [ Rect.fromLTRB(145.0, 0.0, 655.0, 40.0), Rect.fromLTRB(333.0, 40.0, 543.0, 176.0), Rect.fromLTRB(85.0, 80.0, 333.0, 256.0), ]), ); }); testWidgets('submenus account for padding in LTR', (WidgetTester tester) async { await buildDensityPaddingApp( tester, menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13), textDirection: TextDirection.ltr, ); expect( collectSubmenuRects(), equals(const [ Rect.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.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; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: expectedCtrl = 'Ctrl'; expectedMeta = 'Meta'; expectedAlt = 'Alt'; break; case TargetPlatform.windows: expectedCtrl = 'Ctrl'; expectedMeta = 'Win'; expectedAlt = 'Alt'; break; case TargetPlatform.iOS: case TargetPlatform.macOS: expectedCtrl = '⌃'; expectedMeta = '⌘'; expectedAlt = '⌥'; break; } const SingleActivator allModifiers = SingleActivator( LogicalKeyboardKey.keyA, control: true, meta: true, shift: true, alt: true, ); final String allExpected = '$expectedAlt $expectedCtrl $expectedMeta ⇧ A'; const CharacterActivator charShortcuts = CharacterActivator('ñ'); const String charExpected = 'ñ'; await tester.pumpWidget( MaterialApp( home: Material( child: MenuBar( controller: controller, children: [ SubmenuButton( menuChildren: [ 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: [ SubmenuButton( menuChildren: [ 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(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: [ SubmenuButton( menuChildren: [ RadioMenuButton( value: 0, groupValue: radioValue, onChanged: (int? value) { setState(() { radioValue = value; }); }, toggleable: true, child: const Text('radio 0'), ), RadioMenuButton( 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>(find.byType(RadioMenuButton).first).groupValue, null, ); await tester.tap(find.byType(RadioMenuButton).first); await tester.pumpAndSettle(); expect(radioValue, 0); await tester.tap(find.byType(SubmenuButton)); await tester.pump(); await tester.tap(find.byType(RadioMenuButton).first); await tester.pumpAndSettle(); expect(radioValue, null); await tester.tap(find.byType(SubmenuButton)); await tester.pump(); await tester.tap(find.byType(RadioMenuButton).last); await tester.pumpAndSettle(); expect(radioValue, 1); }); }); } List createTestMenus({ void Function(TestMenu)? onPressed, void Function(TestMenu)? onOpen, void Function(TestMenu)? onClose, Map shortcuts = const {}, bool includeExtraGroups = false, bool accelerators = false, }) { Widget submenuButton( TestMenu menu, { required List 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 result = [ submenuButton( TestMenu.mainMenu0, menuChildren: [ menuItemButton(TestMenu.subMenu00, leadingIcon: const Icon(Icons.add)), menuItemButton(TestMenu.subMenu01), menuItemButton(TestMenu.subMenu02), ], ), submenuButton( TestMenu.mainMenu1, menuChildren: [ menuItemButton(TestMenu.subMenu10), submenuButton( TestMenu.subMenu11, menuChildren: [ menuItemButton(TestMenu.subSubMenu110, key: UniqueKey()), menuItemButton(TestMenu.subSubMenu111), menuItemButton(TestMenu.subSubMenu112), menuItemButton(TestMenu.subSubMenu113), ], ), menuItemButton(TestMenu.subMenu12), ], ), submenuButton( TestMenu.mainMenu2, menuChildren: [ menuItemButton( TestMenu.subMenu20, leadingIcon: const Icon(Icons.ac_unit), enabled: false, ), ], ), if (includeExtraGroups) submenuButton( TestMenu.mainMenu3, menuChildren: [ menuItemButton(TestMenu.subMenu30, enabled: false), ], ), if (includeExtraGroups) submenuButton( TestMenu.mainMenu4, menuChildren: [ menuItemButton(TestMenu.subMenu40, enabled: false), menuItemButton(TestMenu.subMenu41, enabled: false), menuItemButton(TestMenu.subMenu42, enabled: false), ], ), submenuButton(TestMenu.mainMenu5, menuChildren: const []), ]; 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; } }