| // 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/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/src/foundation/diagnostics.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| TestWidgetsFlutterBinding.ensureInitialized(); |
| |
| late FakeMenuChannel fakeMenuChannel; |
| late PlatformMenuDelegate originalDelegate; |
| late DefaultPlatformMenuDelegate delegate; |
| final List<String> selected = <String>[]; |
| final List<String> opened = <String>[]; |
| final List<String> closed = <String>[]; |
| |
| void onSelected(String item) { |
| selected.add(item); |
| } |
| |
| void onOpen(String item) { |
| opened.add(item); |
| } |
| |
| void onClose(String item) { |
| closed.add(item); |
| } |
| |
| setUp(() { |
| fakeMenuChannel = FakeMenuChannel((MethodCall call) async {}); |
| delegate = DefaultPlatformMenuDelegate(channel: fakeMenuChannel); |
| originalDelegate = WidgetsBinding.instance.platformMenuDelegate; |
| WidgetsBinding.instance.platformMenuDelegate = delegate; |
| selected.clear(); |
| opened.clear(); |
| closed.clear(); |
| }); |
| |
| tearDown(() { |
| WidgetsBinding.instance.platformMenuDelegate = originalDelegate; |
| }); |
| |
| group('PlatformMenuBar', () { |
| group('basic menu structure is transmitted to platform', () { |
| testWidgets('using onSelected', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: PlatformMenuBar( |
| menus: createTestMenus( |
| onSelected: onSelected, |
| onOpen: onOpen, |
| onClose: onClose, |
| shortcuts: <String, MenuSerializableShortcut>{ |
| subSubMenu10[0]: const SingleActivator(LogicalKeyboardKey.keyA, control: true), |
| subSubMenu10[1]: const SingleActivator(LogicalKeyboardKey.keyB, shift: true), |
| subSubMenu10[2]: const SingleActivator(LogicalKeyboardKey.keyC, alt: true), |
| subSubMenu10[3]: const SingleActivator(LogicalKeyboardKey.keyD, meta: true), |
| }, |
| ), |
| child: const Center(child: Text('Body')), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| fakeMenuChannel.outgoingCalls.last.method, |
| equals('Menu.setMenus'), |
| ); |
| expect( |
| fakeMenuChannel.outgoingCalls.last.arguments, |
| equals(expectedStructure), |
| ); |
| }); |
| testWidgets('using onSelectedIntent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: PlatformMenuBar( |
| menus: createTestMenus( |
| onSelectedIntent: const DoNothingIntent(), |
| onOpen: onOpen, |
| onClose: onClose, |
| shortcuts: <String, MenuSerializableShortcut>{ |
| subSubMenu10[0]: const SingleActivator(LogicalKeyboardKey.keyA, control: true), |
| subSubMenu10[1]: const SingleActivator(LogicalKeyboardKey.keyB, shift: true), |
| subSubMenu10[2]: const SingleActivator(LogicalKeyboardKey.keyC, alt: true), |
| subSubMenu10[3]: const SingleActivator(LogicalKeyboardKey.keyD, meta: true), |
| }, |
| ), |
| child: const Center(child: Text('Body')), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| fakeMenuChannel.outgoingCalls.last.method, |
| equals('Menu.setMenus'), |
| ); |
| expect( |
| fakeMenuChannel.outgoingCalls.last.arguments, |
| equals(expectedStructure), |
| ); |
| }); |
| }); |
| testWidgets('asserts when more than one has locked the delegate', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: PlatformMenuBar( |
| menus: <PlatformMenuItem>[], |
| child: PlatformMenuBar( |
| menus: <PlatformMenuItem>[], |
| child: SizedBox(), |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.takeException(), isA<AssertionError>()); |
| }); |
| testWidgets('diagnostics', (WidgetTester tester) async { |
| const PlatformMenuItem item = PlatformMenuItem( |
| label: 'label2', |
| shortcut: SingleActivator(LogicalKeyboardKey.keyA), |
| ); |
| const PlatformMenuBar menuBar = PlatformMenuBar( |
| menus: <PlatformMenuItem>[item], |
| child: SizedBox(), |
| ); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: menuBar, |
| ), |
| ), |
| ); |
| await tester.pump(); |
| |
| expect( |
| menuBar.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'PlatformMenuBar#00000\n' |
| ' └─PlatformMenuItem#00000(label2)\n' |
| ' label: "label2"\n' |
| ' shortcut: SingleActivator#00000(keys: Key A)\n' |
| ' DISABLED\n', |
| ), |
| ); |
| }); |
| }); |
| group('MenuBarItem', () { |
| testWidgets('diagnostics', (WidgetTester tester) async { |
| const PlatformMenuItem childItem = PlatformMenuItem( |
| label: 'label', |
| ); |
| const PlatformMenu item = PlatformMenu( |
| label: 'label', |
| menus: <PlatformMenuItem>[childItem], |
| ); |
| |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| item.debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
| .map((DiagnosticsNode node) => node.toString()) |
| .toList(); |
| |
| expect(description, <String>[ |
| 'label: "label"', |
| ]); |
| }); |
| }); |
| |
| group('ShortcutSerialization', () { |
| testWidgets('character constructor', (WidgetTester tester) async { |
| final ShortcutSerialization serialization = ShortcutSerialization.character('?'); |
| expect(serialization.toChannelRepresentation(), equals(<String, Object?>{ |
| 'shortcutCharacter': '?', |
| 'shortcutModifiers': 0, |
| })); |
| final ShortcutSerialization serializationWithModifiers = ShortcutSerialization.character('?', alt: true, control: true, meta: true); |
| expect(serializationWithModifiers.toChannelRepresentation(), equals(<String, Object?>{ |
| 'shortcutCharacter': '?', |
| 'shortcutModifiers': 13, |
| })); |
| }); |
| |
| testWidgets('modifier constructor', (WidgetTester tester) async { |
| final ShortcutSerialization serialization = ShortcutSerialization.modifier(LogicalKeyboardKey.home); |
| expect(serialization.toChannelRepresentation(), equals(<String, Object?>{ |
| 'shortcutTrigger': LogicalKeyboardKey.home.keyId, |
| 'shortcutModifiers': 0, |
| })); |
| final ShortcutSerialization serializationWithModifiers = ShortcutSerialization.modifier(LogicalKeyboardKey.home, alt: true, control: true, meta: true, shift: true); |
| expect(serializationWithModifiers.toChannelRepresentation(), equals(<String, Object?>{ |
| 'shortcutTrigger': LogicalKeyboardKey.home.keyId, |
| 'shortcutModifiers': 15, |
| })); |
| }); |
| }); |
| } |
| |
| const List<String> mainMenu = <String>[ |
| 'Menu 0', |
| 'Menu 1', |
| 'Menu 2', |
| 'Menu 3', |
| ]; |
| |
| const List<String> subMenu0 = <String>[ |
| 'Sub Menu 00', |
| ]; |
| |
| const List<String> subMenu1 = <String>[ |
| 'Sub Menu 10', |
| 'Sub Menu 11', |
| 'Sub Menu 12', |
| ]; |
| |
| const List<String> subSubMenu10 = <String>[ |
| 'Sub Sub Menu 110', |
| 'Sub Sub Menu 111', |
| 'Sub Sub Menu 112', |
| 'Sub Sub Menu 113', |
| ]; |
| |
| const List<String> subMenu2 = <String>[ |
| 'Sub Menu 20', |
| ]; |
| |
| List<PlatformMenuItem> createTestMenus({ |
| void Function(String)? onSelected, |
| Intent? onSelectedIntent, |
| void Function(String)? onOpen, |
| void Function(String)? onClose, |
| Map<String, MenuSerializableShortcut> shortcuts = const <String, MenuSerializableShortcut>{}, |
| bool includeStandard = false, |
| }) { |
| final List<PlatformMenuItem> result = <PlatformMenuItem>[ |
| PlatformMenu( |
| label: mainMenu[0], |
| onOpen: onOpen != null ? () => onOpen(mainMenu[0]) : null, |
| onClose: onClose != null ? () => onClose(mainMenu[0]) : null, |
| menus: <PlatformMenuItem>[ |
| PlatformMenuItem( |
| label: subMenu0[0], |
| onSelected: onSelected != null ? () => onSelected(subMenu0[0]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subMenu0[0]], |
| ), |
| ], |
| ), |
| PlatformMenu( |
| label: mainMenu[1], |
| onOpen: onOpen != null ? () => onOpen(mainMenu[1]) : null, |
| onClose: onClose != null ? () => onClose(mainMenu[1]) : null, |
| menus: <PlatformMenuItem>[ |
| PlatformMenuItemGroup( |
| members: <PlatformMenuItem>[ |
| PlatformMenuItem( |
| label: subMenu1[0], |
| onSelected: onSelected != null ? () => onSelected(subMenu0[0]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subMenu1[0]], |
| ), |
| ], |
| ), |
| PlatformMenu( |
| label: subMenu1[1], |
| onOpen: onOpen != null ? () => onOpen(subMenu1[1]) : null, |
| onClose: onClose != null ? () => onClose(subMenu1[1]) : null, |
| menus: <PlatformMenuItem>[ |
| PlatformMenuItemGroup( |
| members: <PlatformMenuItem>[ |
| PlatformMenuItem( |
| label: subSubMenu10[0], |
| onSelected: onSelected != null ? () => onSelected(subSubMenu10[0]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subSubMenu10[0]], |
| ), |
| ], |
| ), |
| PlatformMenuItemGroup( |
| members: <PlatformMenuItem>[ |
| PlatformMenuItem( |
| label: subSubMenu10[1], |
| onSelected: onSelected != null ? () => onSelected(subSubMenu10[1]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subSubMenu10[1]], |
| ), |
| ], |
| ), |
| PlatformMenuItem( |
| label: subSubMenu10[2], |
| onSelected: onSelected != null ? () => onSelected(subSubMenu10[2]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subSubMenu10[2]], |
| ), |
| PlatformMenuItemGroup( |
| members: <PlatformMenuItem>[ |
| PlatformMenuItem( |
| label: subSubMenu10[3], |
| onSelected: onSelected != null ? () => onSelected(subSubMenu10[3]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subSubMenu10[3]], |
| ), |
| ], |
| ), |
| ], |
| ), |
| PlatformMenuItem( |
| label: subMenu1[2], |
| onSelected: onSelected != null ? () => onSelected(subMenu1[2]) : null, |
| onSelectedIntent: onSelectedIntent, |
| shortcut: shortcuts[subMenu1[2]], |
| ), |
| ], |
| ), |
| PlatformMenu( |
| label: mainMenu[2], |
| onOpen: onOpen != null ? () => onOpen(mainMenu[2]) : null, |
| onClose: onClose != null ? () => onClose(mainMenu[2]) : null, |
| menus: <PlatformMenuItem>[ |
| PlatformMenuItem( |
| // Always disabled. |
| label: subMenu2[0], |
| shortcut: shortcuts[subMenu2[0]], |
| ), |
| ], |
| ), |
| // Disabled menu |
| PlatformMenu( |
| label: mainMenu[3], |
| onOpen: onOpen != null ? () => onOpen(mainMenu[2]) : null, |
| onClose: onClose != null ? () => onClose(mainMenu[2]) : null, |
| menus: <PlatformMenuItem>[], |
| ), |
| ]; |
| return result; |
| } |
| |
| const Map<String, Object?> expectedStructure = <String, Object?>{ |
| '0': <Map<String, Object?>>[ |
| <String, Object?>{ |
| 'id': 2, |
| 'label': 'Menu 0', |
| 'enabled': true, |
| 'children': <Map<String, Object?>>[ |
| <String, Object?>{ |
| 'id': 1, |
| 'label': 'Sub Menu 00', |
| 'enabled': true, |
| }, |
| ], |
| }, |
| <String, Object?>{ |
| 'id': 18, |
| 'label': 'Menu 1', |
| 'enabled': true, |
| 'children': <Map<String, Object?>>[ |
| <String, Object?>{ |
| 'id': 4, |
| 'label': 'Sub Menu 10', |
| 'enabled': true, |
| }, |
| <String, Object?>{'id': 5, 'isDivider': true}, |
| <String, Object?>{ |
| 'id': 16, |
| 'label': 'Sub Menu 11', |
| 'enabled': true, |
| 'children': <Map<String, Object?>>[ |
| <String, Object?>{ |
| 'id': 7, |
| 'label': 'Sub Sub Menu 110', |
| 'enabled': true, |
| 'shortcutTrigger': 97, |
| 'shortcutModifiers': 8, |
| }, |
| <String, Object?>{'id': 8, 'isDivider': true}, |
| <String, Object?>{ |
| 'id': 10, |
| 'label': 'Sub Sub Menu 111', |
| 'enabled': true, |
| 'shortcutTrigger': 98, |
| 'shortcutModifiers': 2, |
| }, |
| <String, Object?>{'id': 11, 'isDivider': true}, |
| <String, Object?>{ |
| 'id': 12, |
| 'label': 'Sub Sub Menu 112', |
| 'enabled': true, |
| 'shortcutTrigger': 99, |
| 'shortcutModifiers': 4, |
| }, |
| <String, Object?>{'id': 13, 'isDivider': true}, |
| <String, Object?>{ |
| 'id': 14, |
| 'label': 'Sub Sub Menu 113', |
| 'enabled': true, |
| 'shortcutTrigger': 100, |
| 'shortcutModifiers': 1, |
| }, |
| ], |
| }, |
| <String, Object?>{ |
| 'id': 17, |
| 'label': 'Sub Menu 12', |
| 'enabled': true, |
| }, |
| ], |
| }, |
| <String, Object?>{ |
| 'id': 20, |
| 'label': 'Menu 2', |
| 'enabled': true, |
| 'children': <Map<String, Object?>>[ |
| <String, Object?>{ |
| 'id': 19, |
| 'label': 'Sub Menu 20', |
| 'enabled': false, |
| }, |
| ], |
| }, |
| <String, Object?>{'id': 21, 'label': 'Menu 3', 'enabled': false, 'children': <Map<String, Object?>>[]}, |
| ], |
| }; |
| |
| class FakeMenuChannel implements MethodChannel { |
| FakeMenuChannel(this.outgoing); |
| |
| Future<dynamic> Function(MethodCall) outgoing; |
| Future<void> Function(MethodCall)? incoming; |
| |
| List<MethodCall> outgoingCalls = <MethodCall>[]; |
| |
| @override |
| BinaryMessenger get binaryMessenger => throw UnimplementedError(); |
| |
| @override |
| MethodCodec get codec => const StandardMethodCodec(); |
| |
| @override |
| Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError(); |
| |
| @override |
| Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError(); |
| |
| @override |
| Future<T> invokeMethod<T>(String method, [dynamic arguments]) async { |
| final MethodCall call = MethodCall(method, arguments); |
| outgoingCalls.add(call); |
| return await outgoing(call) as T; |
| } |
| |
| @override |
| String get name => 'flutter/menu'; |
| |
| @override |
| void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler; |
| } |