import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_hbb/common/widgets/request_approval_menu.dart'; import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; const connectionTypes = [ 'Connect', 'Transfer file', 'View camera', 'Terminal', 'Terminal (Run as administrator)', 'TCP tunneling', 'RDP', ]; Future openMenu(WidgetTester tester, List actions, {ThemeData? theme, EdgeInsets? padding, bool includeRelay = false}) async { MenuEntryButton entry(String title, {bool requestApproval = false}) => MenuEntryButton( childBuilder: (style) => Text(title, style: style), proc: () => actions.add(requestApproval ? '$title:request-approval' : title), padding: padding, dismissOnClicked: true, ); await tester.pumpWidget(MaterialApp( theme: theme, home: Scaffold( body: Builder(builder: (context) { return TextButton( onPressed: () => mod_menu.showMenu( context: context, position: const RelativeRect.fromLTRB(20, 20, 780, 580), items: [ entry('Connect'), entry('Transfer file'), entry('View camera'), MenuEntryButton( childBuilder: (style) => Row(children: [ Text('RDP', style: style), const Spacer(), IconButton( icon: const Icon(Icons.edit), onPressed: () { Navigator.pop(context); actions.add('Edit RDP'); }, ), ]), proc: () => actions.add('RDP'), padding: padding, dismissOnClicked: true, ), RequestApprovalMenu( title: 'Request approval', padding: padding, entries: [ for (final title in connectionTypes) entry(title, requestApproval: true), ], ), if (includeRelay) MenuEntrySwitchSync( switchType: SwitchType.scheckbox, text: 'Always connect via relay', currentValue: false, setter: (_) async {}, padding: padding, dismissOnClicked: true, ), entry('Rename'), ] .expand((entry) => entry.build(context, const MenuConfig(commonColor: Colors.black, height: 20))) .toList(), ), child: const Text('Open menu'), ); }), ), )); await tester.pumpAndSettle(); await tester.tap(find.text('Open menu')); await tester.pumpAndSettle(); } void main() { testWidgets('tapping outside the approval submenu closes both menus', (tester) async { final actions = []; await openMenu(tester, actions); await tester.tap(find.text('Request approval')); await tester.pumpAndSettle(); await tester.tapAt(const Offset(2, 2)); await tester.pumpAndSettle(); expect(actions, isEmpty); expect(find.text('Request approval'), findsNothing); expect(find.text('Rename'), findsNothing); expect(find.text('Open menu'), findsOneWidget); }); testWidgets('approval text and arrow align with normal menu items', (tester) async { for (final (theme, padding) in [ (ThemeData(), null), ( ThemeData( useMaterial3: false, platform: TargetPlatform.windows, visualDensity: VisualDensity.compact), const EdgeInsets.only(left: 12, right: 3) ), ( ThemeData(useMaterial3: false), const EdgeInsets.symmetric(horizontal: 12) ), ( ThemeData( useMaterial3: false, textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 20)), )), null ), ]) { await openMenu(tester, [], theme: theme, padding: padding, includeRelay: true); expect(tester.getTopLeft(find.text('Request approval')).dx, tester.getTopLeft(find.text('Connect')).dx); expect(tester.getCenter(find.byIcon(Icons.keyboard_arrow_right)).dx, closeTo(tester.getCenter(find.byType(Checkbox)).dx, 0.01)); await tester.tap(find.text('Rename')); await tester.pumpAndSettle(); } }); testWidgets('normal connections and RDP editing remain direct actions', (tester) async { for (final title in [ 'Connect', 'Transfer file', 'View camera', 'RDP', 'Edit RDP' ]) { final actions = []; await openMenu(tester, actions); expect(find.byIcon(Icons.keyboard_arrow_right), findsOneWidget); final left = tester.getTopLeft(find.text('Rename')).dx; for (final row in ['Connect', 'Transfer file', 'View camera', 'RDP']) { expect(tester.getTopLeft(find.text(row)).dx, left); } await tester.tap( title == 'Edit RDP' ? find.byIcon(Icons.edit) : find.text(title)); await tester.pumpAndSettle(); expect(actions, [title]); expect(find.text('Request approval'), findsNothing); expect(find.text('Rename'), findsNothing); } }); testWidgets( 'one approval menu offers all connection types and closes both menus', (tester) async { for (var i = 0; i < connectionTypes.length; i++) { final actions = []; await openMenu(tester, actions); await tester.tap(i.isEven ? find.text('Request approval') : find.byIcon(Icons.keyboard_arrow_right)); await tester.pumpAndSettle(); expect(actions, isEmpty); for (final title in connectionTypes) { expect(find.text(title), findsWidgets); } await tester.tap(find.text(connectionTypes[i]).last); await tester.pumpAndSettle(); expect(actions, ['${connectionTypes[i]}:request-approval']); expect(find.text('Request approval'), findsNothing); expect(find.text('Rename'), findsNothing); } }, variant: TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.linux, TargetPlatform.macOS, TargetPlatform.windows, })); testWidgets('the whole approval row opens the submenu on a narrow screen', (tester) async { tester.view.physicalSize = const Size(360, 640); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); for (final corner in [ Alignment.topLeft, Alignment.topRight, Alignment.bottomLeft, Alignment.bottomRight, ]) { final actions = []; await openMenu(tester, actions); final row = tester.getRect(find.byType(PopupMenuChildrenItem)); await tester.tapAt(corner.withinRect(row.deflate(2))); await tester.pumpAndSettle(); expect(actions, isEmpty); expect(find.text('Terminal (Run as administrator)'), findsOneWidget); await tester.tap(find.text('Connect').last); await tester.pumpAndSettle(); expect(actions, ['Connect:request-approval']); expect(find.text('Rename'), findsNothing); } }, variant: TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); }