fix: wire backend<->engine data flow so content renders and input works

This commit is contained in:
2026-08-01 20:20:25 +08:00
parent 76ce9a01b1
commit 1ab485dffe
2 changed files with 63 additions and 9 deletions

View File

@@ -1,11 +1,14 @@
// 端到端集成测试:在真实 Windows 桌面运行完整链路。 // 端到端集成测试:在真实 Windows 桌面运行完整链路。
// //
// 验证RustLib.init() → 点 CONNECT → TerminalEngineAlacritty Rust 引擎)创建 // 验证RustLib.init() → 点 CONNECT → TerminalEngineAlacritty Rust 引擎)创建
// → 连上服务端 → 命令回显。 // → 连上服务端 → 命令回显(内容渲染 + 输入通路)
// //
// 运行前需先启动 Rust 服务端cd .. && cargo run // 运行前需先启动 Rust 服务端cd .. && cargo run
// 运行flutter test integration_test -d windows // 运行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_test/flutter_test.dart';
import 'package:flutter_alacritty/flutter_alacritty.dart'; import 'package:flutter_alacritty/flutter_alacritty.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@@ -33,15 +36,48 @@ void main() {
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
if (find.text('AWAITING CONNECTION').evaluate().isEmpty) break; if (find.text('AWAITING CONNECTION').evaluate().isEmpty) break;
} }
expect(find.text('AWAITING CONNECTION'), findsNothing, expect(find.text('AWAITING CONNECTION'), findsNothing,
reason: '连接后应进入终端渲染,提示页应消失'); reason: '连接后应进入终端渲染');
// TerminalViewAlacritty 引擎渲染)应存在 // Alacritty TerminalView 应渲染
expect(find.byType(TerminalView), findsOneWidget, expect(find.byType(TerminalView), findsOneWidget,
reason: 'Alacritty TerminalView 应渲染'); reason: 'Alacritty TerminalView 应渲染');
// 断言 TerminalScreen 仍在前台。 // 通过引擎输出通路发送一条命令(模拟用户输入),验证:
// 服务端回显 → backend.output → engine.feed → 网格渲染。
final engine = tester.state<TerminalViewState>(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); 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();
}

View File

@@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_alacritty/flutter_alacritty.dart'; import 'package:flutter_alacritty/flutter_alacritty.dart';
@@ -25,6 +28,8 @@ class _TerminalScreenState extends State<TerminalScreen> {
final FocusNode _focus = FocusNode(); final FocusNode _focus = FocusNode();
RttyPtyBackend? _backend; RttyPtyBackend? _backend;
StreamSubscription<Uint8List>? _engineOutputSub;
StreamSubscription<Uint8List>? _backendOutputSub;
final _host = TextEditingController(text: '127.0.0.1'); final _host = TextEditingController(text: '127.0.0.1');
final _port = TextEditingController(text: '8080'); final _port = TextEditingController(text: '8080');
@@ -66,14 +71,22 @@ class _TerminalScreenState extends State<TerminalScreen> {
_backend = RttyPtyBackend(); _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; if (!mounted) return;
setState(() => _state = s); setState(() => _state = s);
if (s == ConnState.disconnected || s == ConnState.error) { if (s == ConnState.disconnected || s == ConnState.error) {
_sessionId = null; _sessionId = null;
} }
}; };
_backend!.onReady = (id, cols, rows) { backend.onReady = (id, cols, rows) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_sessionId = id; _sessionId = id;
@@ -82,12 +95,17 @@ class _TerminalScreenState extends State<TerminalScreen> {
}); });
}; };
await _backend!.connect(host: host, port: port, session: session); await backend.connect(host: host, port: port, session: session);
} }
void _disconnect() => _tearDownSession(); void _disconnect() => _tearDownSession();
void _tearDownSession() { void _tearDownSession() {
_engineOutputSub?.cancel();
_engineOutputSub = null;
_backendOutputSub?.cancel();
_backendOutputSub = null;
final b = _backend; final b = _backend;
if (b != null) { if (b != null) {
b.onStateChanged = null; b.onStateChanged = null;