// Dart 侧验证:把 Symja 通道 mock 掉,检查外壳、计算器按键、控制台命令、工具页。 // 真机端到端验证另做(本构建机没有 KVM,跑不了模拟器)。 import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:cal/calculator_page.dart'; import 'package:cal/console_page.dart'; import 'package:cal/main.dart'; import 'package:cal/tools_page.dart'; void main() { const symja = MethodChannel('com.nonvioce.cal/symja'); const mcp = MethodChannel('com.nonvioce.cal/mcp'); final calls = []; setUp(() { calls.clear(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(symja, (call) async { calls.add(call); switch (call.method) { case 'evaluate': final expr = call.arguments['expr'] as String; if (expr == 'boom') { return {'ok': false, 'error': '模拟的失败'}; } return { 'ok': true, 'text': 'x^3/3', 'tex': r'\frac{{x}^{3}}{3}', 'ms': 12, }; case 'variables': return { 'ok': true, 'vars': [ {'name': 'a', 'value': '42'}, ], }; case 'exportSession': return {'ok': true, 'defs': ['a=42']}; case 'importSession': return {'ok': true, 'applied': 1, 'failed': []}; case 'clearVariable': case 'clearAll': return {'ok': true}; case 'sample': return { 'ok': true, 'ys': [0.0, 1.0, 0.0], 'ms': 5, }; case 'cancel': return null; default: return null; } }); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(mcp, (call) async { calls.add(call); switch (call.method) { case 'status': return { 'running': false, 'port': 8734, 'tools': ['evaluate', 'solve'], }; default: return null; } }); }); tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(symja, null); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(mcp, null); }); Future pumpApp(WidgetTester tester) async { await tester.pumpWidget(const CalApp()); await tester.pumpAndSettle(); } Future goTo(WidgetTester tester, String tab) async { await tester.tap(find.text(tab)); await tester.pumpAndSettle(); } /// IndexedStack 会让三个页面同时存在,所以查找必须限定在某一页里。 Finder inPage(Type page, Finder matching) => find.descendant(of: find.byType(page), matching: matching); Future submit(WidgetTester tester, String text) async { await tester.enterText(inPage(ConsolePage, find.byType(TextField)), text); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); } group('外壳', () { testWidgets('三个界面:计算器 / 工具 / 控制台', (tester) async { await pumpApp(tester); expect(find.text('计算器'), findsWidgets); expect(find.text('工具'), findsWidgets); expect(find.text('控制台'), findsWidgets); // 默认进计算器 expect(find.byType(CalculatorPage), findsOneWidget); }); }); group('计算器', () { testWidgets('按键把表达式插进显示区,并触发预览', (tester) async { await pumpApp(tester); await tester.tap(find.text('∫')); // Integrate[□, x] 模板 await tester.pumpAndSettle(); final tf = tester.widget( inPage(CalculatorPage, find.byType(TextField))); expect(tf.controller!.text, 'Integrate[, x]'); // 光标应停在方括号里 expect(tf.controller!.selection.baseOffset, 'Integrate['.length); }); testWidgets('预览走 preview 标志,不污染 %', (tester) async { await pumpApp(tester); await tester.enterText( inPage(CalculatorPage, find.byType(TextField)), 'Sin[x]'); // 等防抖 await tester.pump(const Duration(milliseconds: 700)); await tester.pumpAndSettle(); final c = calls.firstWhere((c) => c.method == 'evaluate'); expect(c.arguments['preview'], true); }); }); group('控制台', () { testWidgets('提交表达式会送到通道并渲染结果', (tester) async { await pumpApp(tester); await goTo(tester, '控制台'); await submit(tester, 'Integrate[x^2, x]'); final eval = calls.lastWhere((c) => c.method == 'evaluate'); expect(eval.arguments['expr'], 'Integrate[x^2, x]'); expect(eval.arguments['preview'], false); expect(find.text('x^3/3'), findsWidgets); }); testWidgets('help 是本地命令,不求值', (tester) async { await pumpApp(tester); await goTo(tester, '控制台'); await submit(tester, 'help'); expect(calls.where((c) => c.method == 'evaluate' && c.arguments['preview'] == false), isEmpty); expect(find.textContaining('命令(小写'), findsOneWidget); }); testWidgets('vars 命令列出变量', (tester) async { await pumpApp(tester); await goTo(tester, '控制台'); await submit(tester, 'vars'); expect(find.text('a = 42'), findsOneWidget); }); testWidgets('失败时显示错误', (tester) async { await pumpApp(tester); await goTo(tester, '控制台'); await submit(tester, 'boom'); expect(find.text('模拟的失败'), findsOneWidget); }); }); group('工具页', () { testWidgets('列出插件,并能按名字过滤', (tester) async { await pumpApp(tester); await goTo(tester, '工具'); // 网格是懒加载的,靠后的卡片在测试视口外,所以只断言前几个 expect(find.text('错位相减法'), findsOneWidget); expect(find.text('二项分布'), findsOneWidget); await tester.enterText(inPage(ToolsPage, find.byType(TextField)), '回归'); await tester.pumpAndSettle(); expect(find.text('线性回归'), findsOneWidget); expect(find.text('二项分布'), findsNothing); }); testWidgets('拼音首字母也能搜到', (tester) async { await pumpApp(tester); await goTo(tester, '工具'); await tester.enterText(inPage(ToolsPage, find.byType(TextField)), 'xxhg'); await tester.pumpAndSettle(); expect(find.text('线性回归'), findsOneWidget); }); }); }