From 1ab485dffed69c33749e98a2eb96dd5819e543ee Mon Sep 17 00:00:00 2001 From: CNWei Date: Sat, 1 Aug 2026 20:20:25 +0800 Subject: [PATCH] fix: wire backend<->engine data flow so content renders and input works --- desktop/integration_test/e2e_test.dart | 48 ++++++++++++++++++++++---- desktop/lib/src/terminal_screen.dart | 24 +++++++++++-- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/desktop/integration_test/e2e_test.dart b/desktop/integration_test/e2e_test.dart index a891d14..18ce4af 100644 --- a/desktop/integration_test/e2e_test.dart +++ b/desktop/integration_test/e2e_test.dart @@ -1,11 +1,14 @@ // 端到端集成测试:在真实 Windows 桌面运行完整链路。 // // 验证:RustLib.init() → 点 CONNECT → TerminalEngine(Alacritty Rust 引擎)创建 -// → 连上服务端 → 命令回显。 +// → 连上服务端 → 命令回显(内容渲染 + 输入通路)。 // // 运行前需先启动 Rust 服务端(cd .. && cargo run)。 // 运行:flutter test integration_test -d windows -import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_alacritty/flutter_alacritty.dart'; import 'package:integration_test/integration_test.dart'; @@ -33,15 +36,48 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); if (find.text('AWAITING CONNECTION').evaluate().isEmpty) break; } - expect(find.text('AWAITING CONNECTION'), findsNothing, - reason: '连接后应进入终端渲染,提示页应消失'); + reason: '连接后应进入终端渲染'); - // TerminalView(Alacritty 引擎渲染)应存在。 + // Alacritty TerminalView 应渲染。 expect(find.byType(TerminalView), findsOneWidget, reason: 'Alacritty TerminalView 应渲染'); - // 断言 TerminalScreen 仍在前台。 + // 通过引擎输出通路发送一条命令(模拟用户输入),验证: + // 服务端回显 → backend.output → engine.feed → 网格渲染。 + final engine = tester.state(find.byType(TerminalView)).widget.engine; + final token = 'RTTY_INT_${DateTime.now().millisecondsSinceEpoch}'; + engine.write(Uint8List.fromList('echo $token\r\n'.codeUnits)); + + // 轮询引擎网格,等待命令回显进入渲染。 + String? found; + final outDeadline = DateTime.now().add(const Duration(seconds: 8)); + while (DateTime.now().isBefore(outDeadline)) { + await tester.pump(const Duration(milliseconds: 200)); + final gridText = _gridText(engine); + if (gridText.contains(token)) { + found = gridText; + break; + } + } + expect(found, isNotNull, reason: '命令回显应进入引擎网格'); + debugPrint('回显捕获: ${found!.trim().split('\n').last}'); + + // 输入通路也验证:TerminalView 聚焦时按键应经 engine.write → backend → 服务端。 expect(find.byType(TerminalScreen), findsOneWidget); }); } + +/// 从引擎镜像网格提取可视文本。 +String _gridText(TerminalEngine engine) { + final grid = engine.gridForView; + final sb = StringBuffer(); + for (var r = 0; r < grid.rows; r++) { + for (var c = 0; c < grid.columns; c++) { + final cp = grid.codepointAt(r, c) as int; + sb.writeCharCode(cp == 0 ? 32 : cp); + } + sb.write('\n'); + } + return sb.toString(); +} diff --git a/desktop/lib/src/terminal_screen.dart b/desktop/lib/src/terminal_screen.dart index f8f92eb..e018d93 100644 --- a/desktop/lib/src/terminal_screen.dart +++ b/desktop/lib/src/terminal_screen.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:flutter_alacritty/flutter_alacritty.dart'; @@ -25,6 +28,8 @@ class _TerminalScreenState extends State { final FocusNode _focus = FocusNode(); RttyPtyBackend? _backend; + StreamSubscription? _engineOutputSub; + StreamSubscription? _backendOutputSub; final _host = TextEditingController(text: '127.0.0.1'); final _port = TextEditingController(text: '8080'); @@ -66,14 +71,22 @@ class _TerminalScreenState extends State { _backend = RttyPtyBackend(); }); - _backend!.onStateChanged = (s) { + final engine = _engine!; + final backend = _backend!; + + // 双向接线:backend 输出(服务端 ANSI)→ 引擎渲染; + // 引擎输出(用户输入)→ backend 发回服务端写入远端 PTY。 + _engineOutputSub = engine.output.listen(backend.write); + _backendOutputSub = backend.output.listen(engine.feed); + + backend.onStateChanged = (s) { if (!mounted) return; setState(() => _state = s); if (s == ConnState.disconnected || s == ConnState.error) { _sessionId = null; } }; - _backend!.onReady = (id, cols, rows) { + backend.onReady = (id, cols, rows) { if (!mounted) return; setState(() { _sessionId = id; @@ -82,12 +95,17 @@ class _TerminalScreenState extends State { }); }; - await _backend!.connect(host: host, port: port, session: session); + await backend.connect(host: host, port: port, session: session); } void _disconnect() => _tearDownSession(); void _tearDownSession() { + _engineOutputSub?.cancel(); + _engineOutputSub = null; + _backendOutputSub?.cancel(); + _backendOutputSub = null; + final b = _backend; if (b != null) { b.onStateChanged = null;