Files
rustdesk/flutter/test/terminal_mouse_handler_test.dart
T
RustDeskandClaude Opus 5.5 9997b7a3b8 test: remove tests that do not pin user-visible behavior (#16539)
* test: remove tests that do not pin user-visible behavior

Apply the AGENTS.md test rules to the existing Rust and Flutter suites.
A test is kept when it is the only coverage of user- or API-visible
behavior, the regression test of a reported or fixed bug, a security
boundary (one test per rejection class), or a check against an external
reference. Tests that assert private state or the order of internal
steps, pin review-round corner cases, or repeat what a kept test already
covers are removed.

Rust: 679 -> 298 tests. Dart: 118 -> 45 tests.

Test-only helpers left without callers go with them
(applyLastOnlineStateForTesting, terminalKeyboardRowWidth,
debugBufferedInputCount, tokio's test-util dev feature), and cfg gates
and visibility that existed only for the deleted tests are narrowed.
Runtime behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* test: keep tests that pin security and user-visible behavior

Restore six tests that review showed are the only coverage of their
behavior:

- update metadata rejects an artifact file name with path components;
- ICE candidates from another session are dropped;
- KCP delivers every multi-segment frame queued before a close;
- set_files rejects an empty name in a multi-file transfer;
- a lopsided elongated cursor keeps its centre hotspot (#16242);
- toggling Show hidden during a pending same-path read issues a new read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* test: keep offline update and key-exchange downgrade rejections

Restore three tests that are the only coverage of their rejection class:

- offline update metadata rejects a signed artifact with an http or
  credential-bearing URL;
- offline verification rejects a renamed local file and a version that
  does not match the release;
- a key-exchange pick lowered in transit leaves both sides unable to
  decrypt, so a v0 fallback after a failed v1 decrypt cannot pass
  unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* test: keep the decomposed-grapheme and reconnect-cancel tests

Each catches a plausible wrong change that passes every kept test:
mapping the first scalar under a locked Ctrl turns é into \x05,
and a reconnect that does not fail pending reads leaves them hanging
until the 30 s timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:45:22 +08:00

206 lines
7.4 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xterm/xterm.dart';
const _terminalSize = Size(400, 120);
Widget _terminalHarness(
Terminal terminal,
TerminalController controller,
) =>
MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: _terminalSize.width,
height: _terminalSize.height,
child: TerminalMouseInteraction(
terminal,
controller: controller,
),
),
),
);
void _writeLines(Terminal terminal, int count) => terminal.write(
List.generate(count, (index) => 'line $index\r\n').join(),
);
void main() {
late Terminal terminal;
late List<String> output;
setUp(() {
output = <String>[];
terminal = Terminal(mouseHandler: const WheelButtonFixMouseHandler())
..onOutput = output.add;
});
testWidgets('Linux Ctrl+V is not paste', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
try {
final messenger = tester.binding.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(
SystemChannels.platform,
(_) async => {'text': 'clipboard'},
);
final controller = TerminalController();
addTearDown(controller.dispose);
await tester.pumpWidget(_terminalHarness(terminal, controller));
await tester.tap(find.byType(TerminalView));
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
final controlVOutput = List.of(output);
output.clear();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(controlVOutput, ['\x16']);
expect(output, ['clipboard']);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
String? report(
TerminalMouseButton button, [
TerminalMouseButtonState state = TerminalMouseButtonState.down,
]) {
output.clear();
terminal.mouseInput(button, state, const CellOffset(10, 5));
return output.isEmpty ? null : output.single;
}
test('reports SGR wheel buttons without the Shift modifier bit', () {
terminal.write('\x1b[?1000h\x1b[?1006h');
expect(report(TerminalMouseButton.wheelUp), '\x1b[<64;11;6M');
expect(report(TerminalMouseButton.wheelDown), '\x1b[<65;11;6M');
expect(report(TerminalMouseButton.wheelLeft), '\x1b[<66;11;6M');
expect(report(TerminalMouseButton.wheelRight), '\x1b[<67;11;6M');
});
test('reports normal-encoding wheel buttons in the 64..67 range', () {
terminal.write('\x1b[?1000h');
expect(
report(TerminalMouseButton.wheelUp),
'\x1b[M${String.fromCharCode(32 + 64)}'
'${String.fromCharCode(32 + 11)}${String.fromCharCode(32 + 6)}',
);
expect(
report(TerminalMouseButton.wheelDown),
'\x1b[M${String.fromCharCode(32 + 65)}'
'${String.fromCharCode(32 + 11)}${String.fromCharCode(32 + 6)}',
);
});
test('leaves non-wheel buttons to the upstream handler', () {
terminal.write('\x1b[?1000h\x1b[?1006h');
expect(report(TerminalMouseButton.left), '\x1b[<0;11;6M');
expect(report(TerminalMouseButton.middle), '\x1b[<1;11;6M');
expect(
report(TerminalMouseButton.right, TerminalMouseButtonState.up),
'\x1b[<2;11;6m',
);
});
testWidgets('dragging below scrolls and extends selection', (tester) async {
final controller = TerminalController();
_writeLines(terminal, 80);
await tester.pumpWidget(_terminalHarness(terminal, controller));
final terminalView =
tester.state<TerminalViewState>(find.byType(TerminalView));
final scrollController = terminalView.widget.scrollController!;
scrollController.jumpTo(0);
await tester.pump();
final renderTerminal = terminalView.renderTerminal;
const localStart = Offset(20, 20);
final startCell = renderTerminal.getCellOffset(localStart);
final mouse = TestPointer(1, PointerDeviceKind.mouse);
final outside = Offset(20, renderTerminal.size.height);
await tester.handlePointerEventRecord([
PointerEventRecord(Duration.zero, [
mouse.down(renderTerminal.localToGlobal(localStart)),
mouse.move(renderTerminal.localToGlobal(outside)),
]),
PointerEventRecord(const Duration(milliseconds: 150), [
mouse.move(
renderTerminal.localToGlobal(outside + const Offset(1, 1)),
),
mouse.up(),
]),
]);
expect(scrollController.offset, greaterThan(0));
expect(controller.selection!.begin, startCell);
expect(controller.selection!.end.y, greaterThan(startCell.y));
final releasedOffset = scrollController.offset;
await tester.pump(const Duration(milliseconds: 100));
expect(scrollController.offset, releasedOffset);
});
testWidgets('tmux mouse input is reported without local selection',
(tester) async {
final controller = TerminalController();
terminal.write('\x1b[?1049h\x1b[?1002h\x1b[?1006hword');
await tester.pumpWidget(_terminalHarness(terminal, controller));
final renderTerminal = tester
.state<TerminalViewState>(find.byType(TerminalView))
.renderTerminal;
const wheel = Offset(120, 40);
await tester.sendEventToBinding(
PointerScrollEvent(
position: renderTerminal.localToGlobal(wheel),
scrollDelta: const Offset(0, 40),
),
);
await tester.pump();
final wheelCell = renderTerminal.getCellOffset(wheel);
expect(output.first, '\x1b[<65;${wheelCell.x + 1};${wheelCell.y + 1}M');
output.clear();
final clickPosition =
renderTerminal.getOffset(const CellOffset(0, 0)) + const Offset(1, 1);
final clickCell = renderTerminal.getCellOffset(clickPosition);
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
await mouse.down(renderTerminal.localToGlobal(clickPosition));
await mouse.up();
await tester.pump();
await mouse.down(renderTerminal.localToGlobal(clickPosition));
await mouse.up();
await tester.pump(kDoubleTapTimeout);
expect(output, [
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}M',
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}m',
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}M',
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}m',
]);
expect(controller.selection, isNull);
expect(controller.suspendedPointerInputs, isFalse);
output.clear();
const start = Offset(40, 40);
const end = Offset(240, 80);
final startCell = renderTerminal.getCellOffset(start);
final endCell = renderTerminal.getCellOffset(end);
await mouse.down(renderTerminal.localToGlobal(start));
await mouse.moveTo(renderTerminal.localToGlobal(end));
await tester.pump();
expect(output, [
'\x1b[<0;${startCell.x + 1};${startCell.y + 1}M',
'\x1b[<32;${endCell.x + 1};${endCell.y + 1}M',
]);
expect(controller.selection, isNull);
await mouse.up();
expect(output.last, '\x1b[<0;${endCell.x + 1};${endCell.y + 1}m');
expect(controller.suspendedPointerInputs, isFalse);
});
}