| // 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<TestMenu> selected = <TestMenu>[]; |
| final List<TestMenu> opened = <TestMenu>[]; |
| final List<TestMenu> closed = <TestMenu>[]; |
| 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<void> 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: <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 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)); |
| }); |
| |
| 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('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 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<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')); |
| 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('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')); |
| 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, 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('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: none', |
| ], |
| ), |
| ); |
| }); |
| }); |
| |
| 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) |
| ]), |
| ); |
| }); |
| |
| 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(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>[ |
| 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>[ |
| 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 = '+'; |
| break; |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| expectedCtrl = '⌃'; |
| expectedMeta = '⌘'; |
| expectedAlt = '⌥'; |
| expectedShift = '⇧'; |
| expectedSeparator = ' '; |
| break; |
| } |
| |
| 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; |
| } |
| } |