fix: wire backend<->engine data flow so content renders and input works
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
// 端到端集成测试:在真实 Windows 桌面运行完整链路。
|
// 端到端集成测试:在真实 Windows 桌面运行完整链路。
|
||||||
//
|
//
|
||||||
// 验证:RustLib.init() → 点 CONNECT → TerminalEngine(Alacritty Rust 引擎)创建
|
// 验证:RustLib.init() → 点 CONNECT → TerminalEngine(Alacritty 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: '连接后应进入终端渲染');
|
||||||
|
|
||||||
// TerminalView(Alacritty 引擎渲染)应存在。
|
// 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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user