fix: address unimplemented/broken issues across server and desktop

This commit is contained in:
2026-08-01 18:58:12 +08:00
parent 93ee801f27
commit 7e4236e2f3
7 changed files with 239 additions and 24 deletions

View File

@@ -320,12 +320,12 @@ class _StatusBar extends StatelessWidget {
),
),
SizedBox(width: 20),
_Hint('鼠标拖选复制', RttyTheme.textFaint),
_Hint('拖选复制', RttyTheme.textFaint),
SizedBox(width: 14),
_Hint('右键粘贴', RttyTheme.textFaint),
_Hint('Ctrl+Shift+C / V 复制粘贴', RttyTheme.textFaint),
Spacer(),
Text(
'FLUTTER × XTERM × ALCATTERM ENGINE',
'FLUTTER × XTERM TERMINAL',
style: TextStyle(color: RttyTheme.textFaint, fontSize: 10),
),
],

View File

@@ -2,18 +2,22 @@
//
// 运行前需先启动 Rust 服务端cd .. && cargo run
// 运行flutter test test/e2e_ws_test.dart
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xterm/xterm.dart';
import '../lib/src/rtty_client.dart';
import 'package:rtty_desktop/src/rtty_client.dart';
import 'support.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('RttyClient connects to Rust server and renders command output',
() async {
if (!await isServerUp('127.0.0.1', 8080)) {
markTestSkipped('Rtty server not running');
return;
}
final terminal = Terminal();
final client = RttyClient(terminal);

View File

@@ -0,0 +1,77 @@
// 多端解耦 + 控制权强制回归验证(连真实 Rust 服务端)。
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:web_socket_channel/io.dart';
import 'support.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('多端解耦 + 控制权强制 + 会话清理', () async {
if (!await isServerUp('127.0.0.1', 8080)) {
markTestSkipped('Rtty server not running');
return;
}
final base = 'ws://127.0.0.1:8080/ws';
// 1. 桌面端client=desktop收 raw 二进制,不收 snapshot。
final desktop = IOWebSocketChannel.connect(Uri.parse('$base?client=desktop'));
final dOut = <String>[];
desktop.stream.listen((m) {
if (m is List<int>) {
dOut.add('binary:${utf8.decode(m, allowMalformed: true)}');
} else if (m is String) {
dOut.add('text:${m.length > 100 ? m.substring(0, 100) : m}');
} else {
dOut.add('$m');
}
});
await Future<void>.delayed(const Duration(milliseconds: 700));
final dHasBinary = dOut.any((e) => e.startsWith('binary:'));
final dHasSnapshot = dOut.any((e) => e.contains('mobile_snapshot'));
expect(dHasBinary, isTrue, reason: '桌面端应收到原始 ANSI 二进制 $dOut');
expect(dHasSnapshot, isFalse, reason: '桌面端不应收到语义快照 $dOut');
// 2. 移动端client=mobile收 snapshot不收 binary。
final mobile = IOWebSocketChannel.connect(Uri.parse('$base?client=mobile'));
final mOut = <String>[];
mobile.stream.listen((m) {
if (m is List<int>) {
mOut.add('binary:${utf8.decode(m, allowMalformed: true)}');
} else if (m is String) {
mOut.add('text:${m.length > 100 ? m.substring(0, 100) : m}');
} else {
mOut.add('$m');
}
});
await Future<void>.delayed(const Duration(milliseconds: 700));
final mHasSnapshot = mOut.any((e) => e.contains('mobile_snapshot'));
final mHasBinary = mOut.any((e) => e.startsWith('binary:'));
expect(mHasSnapshot, isTrue, reason: '移动端应收到语义快照 $mOut');
expect(mHasBinary, isFalse, reason: '移动端不应收到二进制 $mOut');
// 3. 桌面端发命令,确认 raw 输出回显。
desktop.sink.add(jsonEncode({'type': 'input', 'data': 'echo D_E2E\r\n'}));
await Future<void>.delayed(const Duration(milliseconds: 900));
expect(dOut.join('\n').contains('D_E2E'), isTrue, reason: '桌面端命令回显');
// 4. 控制权强制:桌面端 claim 后,移动端输入被拒。
desktop.sink.add(jsonEncode({'type': 'claim_control'}));
await Future<void>.delayed(const Duration(milliseconds: 400));
mobile.sink
.add(jsonEncode({'type': 'input', 'data': 'echo BLOCKED_SHOULD_NOT_SHOW\r\n'}));
await Future<void>.delayed(const Duration(milliseconds: 900));
final both = (dOut.join('\n') + mOut.join('\n'));
expect(both.contains('BLOCKED_SHOULD_NOT_SHOW'), isFalse,
reason: '非控制者输入应被阻止');
// 5. 会话清理exit 触发会话移除。
desktop.sink.add(jsonEncode({'type': 'input', 'data': 'exit\r\n'}));
await Future<void>.delayed(const Duration(milliseconds: 1300));
desktop.sink.close();
mobile.sink.close();
});
}

14
desktop/test/support.dart Normal file
View File

@@ -0,0 +1,14 @@
// 集成测试共享工具。
import 'package:web_socket_channel/io.dart';
/// 探测 Rtty 服务端是否可达;返回 false 时集成测试应被跳过。
Future<bool> isServerUp(String host, int port) async {
try {
final ws = IOWebSocketChannel.connect(Uri.parse('ws://$host:$port/ws'));
await ws.ready.timeout(const Duration(seconds: 2));
ws.sink.close();
return true;
} catch (_) {
return false;
}
}