mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 08:34:12 +00:00
refactor: unify request approval menus and naming
This commit is contained in:
1 parent
417dcdf7bb
commit
c43443b1a1
67 files changed
+535
-559
No files matched your search
@@ -1,146 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login_method_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';
|
||||
|
||||
Future<void> openMenu(WidgetTester tester, List<String> actions) async {
|
||||
MenuEntryButton<String> entry(String title) => MenuEntryButton<String>(
|
||||
childBuilder: (style) => Text(title, style: style),
|
||||
proc: () => actions.add(title),
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(builder: (context) {
|
||||
return TextButton(
|
||||
onPressed: () => mod_menu.showMenu<String>(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(20, 20, 780, 580),
|
||||
items: [
|
||||
LoginMethodMenu(
|
||||
defaultEntry: entry('Connect'),
|
||||
methods: [entry('Without password')],
|
||||
),
|
||||
LoginMethodMenu(
|
||||
defaultEntry: entry('View camera'),
|
||||
methods: [entry('Without password')],
|
||||
),
|
||||
LoginMethodMenu(
|
||||
defaultEntry: MenuEntryButton<String>(
|
||||
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'),
|
||||
),
|
||||
methods: [entry('Without password')],
|
||||
),
|
||||
entry('Rename'),
|
||||
]
|
||||
.expand((entry) => entry.build(context,
|
||||
const MenuConfig(commonColor: Colors.black, height: 20)))
|
||||
.toList(),
|
||||
),
|
||||
child: const Text('Open menu'),
|
||||
);
|
||||
}),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('Open menu'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('main rows align and clicking them connects by default',
|
||||
(tester) async {
|
||||
for (final title in ['Connect', 'View camera', 'RDP']) {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
final left = tester.getTopLeft(find.text('Rename')).dx;
|
||||
for (final row in ['Connect', 'View camera', 'RDP']) {
|
||||
expect(tester.getTopLeft(find.text(row)).dx, left);
|
||||
}
|
||||
await tester.tap(find.text(title));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, [title]);
|
||||
expect(find.text('Without password'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'arrow offers connection without password and dismisses both menus',
|
||||
(tester) async {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
await tester.tap(find.byIcon(Icons.keyboard_arrow_right).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
expect(find.text('Without password'), findsOneWidget);
|
||||
expect(find.text('Enter password manually'), findsNothing);
|
||||
await tester.tap(find.text('Without password'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Without password']);
|
||||
expect(find.text('Without password'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
},
|
||||
variant: TargetPlatformVariant({
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.windows,
|
||||
}));
|
||||
|
||||
testWidgets('RDP edit remains independent of the connection action',
|
||||
(tester) async {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
await tester.tap(find.byIcon(Icons.edit));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Edit RDP']);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('touches around the arrow open the submenu without connecting',
|
||||
(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 = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
final arrow = find.byIcon(Icons.keyboard_arrow_right).first;
|
||||
final row = tester
|
||||
.getRect(find.ancestor(of: arrow, matching: find.byType(Row)).first);
|
||||
final target = Rect.fromCenter(
|
||||
center: tester.getCenter(arrow), width: 56, height: row.height);
|
||||
await tester.tapAt(corner.withinRect(target.deflate(2)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
expect(find.text('Without password'), findsOneWidget);
|
||||
await tester.tap(find.text('Without password'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Without password']);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
},
|
||||
variant:
|
||||
TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
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<void> openMenu(WidgetTester tester, List<String> actions,
|
||||
{ThemeData? theme, EdgeInsets? padding, bool includeRelay = false}) async {
|
||||
MenuEntryButton<String> entry(String title, {bool requestApproval = false}) =>
|
||||
MenuEntryButton<String>(
|
||||
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<String>(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(20, 20, 780, 580),
|
||||
items: [
|
||||
entry('Connect'),
|
||||
entry('Transfer file'),
|
||||
entry('View camera'),
|
||||
MenuEntryButton<String>(
|
||||
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<String>(
|
||||
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 = <String>[];
|
||||
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 = <String>[];
|
||||
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 = <String>[];
|
||||
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 = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
final row = tester.getRect(find.byType(PopupMenuChildrenItem<String>));
|
||||
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}));
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('without-password links preserve their connection type', () {
|
||||
test('request-approval links preserve their connection type', () {
|
||||
for (final type in [
|
||||
'connect',
|
||||
'play',
|
||||
@@ -15,32 +15,32 @@ void main() {
|
||||
]) {
|
||||
expect(
|
||||
urlLinkToCmdArgs(
|
||||
Uri.parse('rustdesk://$type/123456789?without-password=true')),
|
||||
['--$type', '123456789', '--without-password'],
|
||||
Uri.parse('rustdesk://$type/123456789?request-approval=true')),
|
||||
['--$type', '123456789', '--request-approval'],
|
||||
);
|
||||
}
|
||||
expect(
|
||||
urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?without-password=true')),
|
||||
['--connect', '123456789', '--without-password'],
|
||||
urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?request-approval=true')),
|
||||
['--connect', '123456789', '--request-approval'],
|
||||
);
|
||||
});
|
||||
|
||||
test('omitted and empty passwords retain automatic login', () {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789')),
|
||||
['--connect', '123456789']);
|
||||
for (final query in ['password=', 'password=&without-password=false']) {
|
||||
for (final query in ['password=', 'password=&request-approval=false']) {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')),
|
||||
['--connect', '123456789', '--password', '']);
|
||||
}
|
||||
});
|
||||
|
||||
test('without-password ignores an explicit password in either order', () {
|
||||
test('request-approval ignores an explicit password in either order', () {
|
||||
for (final query in [
|
||||
'password=provided&without-password=true',
|
||||
'without-password=true&password=provided',
|
||||
'password=provided&request-approval=true',
|
||||
'request-approval=true&password=provided',
|
||||
]) {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')),
|
||||
['--connect', '123456789', '--without-password']);
|
||||
['--connect', '123456789', '--request-approval']);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user