Преглед изворни кода

键盘重排成 8 排(∫ 收进 d/dx、运算符收进变量行);设置里加"近似值位数"

键盘
- 求和/求积一层、求导/求积分一层:Σ 的 shift 是 ∏,d/dx 的 shift 是 ∫,
  ∫ 不再单独占一个键(原来"∫ 自己一个键、shift 却是 ∏")。空出来的位置给 √
- 变量那一行 x y a t n 的 shift 收下整排运算符(=、==、/.→、,、{),
  于是键盘从 9 排压到 8 排,键也更高更好按
- 基座只留常用的:数字、. + − × / ^、括号、⇧ ⌫ CLR ANS ✓、π e i
- 测试里加了 pressShift():按 shift 层的键要先按 ⇧

设置
- 新增"近似值位数"滑动条(2~20,默认 15)。计算器顶栏的"近似"和工具页里
  所有"(近似)"那行都听它 —— 位数只有一个来源(AppSettings,落到会话上)
- 近似值改成先 Rationalize 再取小数:输入写成 0.2/0.3 时机器浮点会带出
  `3.5999999999999996` 这种尾巴,还原成分数再取小数就是干净的 3.6
- 各工具的 N[...] 位数统一从硬编码换成设置里的值

Co-Authored-By: Claude Code <noreply@anthropic.com>
nonvioce пре 3 недеља
родитељ
комит
36c6214498

+ 29 - 33
lib/calculator_page.dart

@@ -458,21 +458,25 @@ class _CalculatorPageState extends State<CalculatorPage> {
     return LayoutBuilder(
       builder: (context, box) {
         final rows = <List<_Key>>[
-          // 1 大运算符
+          // 1 大运算符。∫ 收在 d/dx 的 shift 里 —— 求和/求积一层、求导/求积分一层,
+          //   同族的放一起,比原来"∫ 自己一个键、shift 却是 ∏"顺。
           [
-            _Key('∫', _Act.bigOp, payload: 'Integrate', prefill: ['x'],
+            _Key('Σ', _Act.bigOp,
+                payload: 'Sum', prefill: ['n', '1', 'Infinity'],
                 shifted: _Key('∏', _Act.bigOp,
                     payload: 'Product', prefill: ['n', '1', 'Infinity'])),
-            _Key('Σ', _Act.bigOp,
-                payload: 'Sum', prefill: ['n', '1', 'Infinity']),
-            _Key('d/dx', _Act.fnPrefixed, payload: 'D', prefill: ['', 'x']),
+            _Key('d/dx', _Act.fnPrefixed, payload: 'D', prefill: ['', 'x'],
+                shifted: _Key('∫', _Act.bigOp,
+                    payload: 'Integrate', prefill: ['x'])),
             _Key('lim', _Act.bigOp, payload: 'Limit', prefill: ['x', '0']),
             _Key('solve', _Act.fnPrefixed, payload: 'Solve',
                 prefill: ['', 'x'],
                 shifted: _Key('nsolve', _Act.fnPrefixed, payload: 'NSolve',
                     prefill: ['', 'x'])),
+            _Key('√', _Act.sqrt,
+                shifted: _Key('|x|', _Act.fn, payload: 'Abs')),
           ],
-          // 2 常用函数
+          // 2 函数。! 也放这排(它本来就是个函数),shift 是组合数
           [
             _Key('sin', _Act.fn, payload: 'Sin',
                 shifted: _Key('asin', _Act.fn, payload: 'ArcSin')),
@@ -482,29 +486,25 @@ class _CalculatorPageState extends State<CalculatorPage> {
                 shifted: _Key('atan', _Act.fn, payload: 'ArcTan')),
             _Key('ln', _Act.fn, payload: 'Log',
                 shifted: _Key('lg', _Act.fn, payload: 'Log10')),
-            _Key('√', _Act.sqrt,
-                shifted: _Key('|x|', _Act.fn, payload: 'Abs')),
-          ],
-          // 3 变量
-          [
-            _Key('x', _Act.char, payload: 'x', mathVar: true),
-            _Key('y', _Act.char, payload: 'y', mathVar: true),
-            _Key('a', _Act.char, payload: 'a', mathVar: true),
-            _Key('t', _Act.char, payload: 't', mathVar: true),
-            _Key('n', _Act.char, payload: 'n', mathVar: true),
+            _Key('!', _Act.postfix,
+                shifted: _Key('C', _Act.fnPrefixed,
+                    payload: 'Binomial', prefill: ['', ''])),
           ],
-          // 4 运算符:解方程、赋值、代入、定义函数全靠它们。
-          // 都是"运算符"不是"函数",所以放键盘上而不是函数面板里。
+          // 3 变量。原来那排运算符(赋值、等号、代入、逗号、矩阵)收进它们的 shift,
+          //   于是整排省掉一个 —— 键盘从 9 排压到 8 排,键也更高。
           [
-            _Key('=', _Act.char, payload: '=', tint: true),
-            _Key('==', _Act.char, payload: '==', tint: true),
-            // /. 和 -> 永远成对出现(代入就是 `式子 /. x -> 值`),
-            // 合成一个键直接给出骨架,省一个位置给逗号(矩阵行分隔)
-            _Key('/.→', _Act.subst, tint: true),
-            _Key(',', _Act.comma),
-            _Key('{', _Act.brace, tint: true),
+            _Key('x', _Act.char, payload: 'x', mathVar: true,
+                shifted: _Key('=', _Act.char, payload: '=', tint: true)),
+            _Key('y', _Act.char, payload: 'y', mathVar: true,
+                shifted: _Key('==', _Act.char, payload: '==', tint: true)),
+            _Key('a', _Act.char, payload: 'a', mathVar: true,
+                shifted: _Key('/.→', _Act.subst, tint: true)),
+            _Key('t', _Act.char, payload: 't', mathVar: true,
+                shifted: _Key(',', _Act.comma)),
+            _Key('n', _Act.char, payload: 'n', mathVar: true,
+                shifted: _Key('{', _Act.brace, tint: true)),
           ],
-          // 5 数字与括号
+          // 4 数字与括号
           [
             _Key('7', _Act.char, payload: '7', mergeDigit: true),
             _Key('8', _Act.char, payload: '8', mergeDigit: true),
@@ -512,9 +512,7 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('(', _Act.group,
                 shifted: _Key('P', _Act.fnPrefixed,
                     payload: 'FactorialPower', prefill: ['', ''])),
-            _Key('!', _Act.postfix,
-                shifted: _Key('C', _Act.fnPrefixed,
-                    payload: 'Binomial', prefill: ['', ''])),
+            _Key('π', _Act.char, payload: 'Pi', tint: true, display: 'π'),
           ],
           [
             _Key('4', _Act.char, payload: '4', mergeDigit: true),
@@ -530,22 +528,20 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('+', _Act.char, payload: '+'),
             _Key('−', _Act.char, payload: '-'),
           ],
-          // 7 花括号:矩阵用
           [
             _Key('0', _Act.char, payload: '0', mergeDigit: true),
             _Key('.', _Act.char, payload: '.', mergeDigit: true),
             _Key('^', _Act.power),
             // 常量。编辑框显示 π / e,送进引擎的是 Pi / E(和 ANS 同一套机制)
-            _Key('π', _Act.char, payload: 'Pi', tint: true, display: 'π'),
             _Key('e', _Act.char, payload: 'E', tint: true, display: 'e'),
+            // 虚数单位。送进 Symja 的是 I,编辑框里显示小写 i
+            _Key('i', _Act.char, payload: 'I', tint: true, display: 'i'),
           ],
           [
             // ⇧ 开着的时候这个键是深色的 —— 一眼能看出"现在是 shift 层"
             _Key('⇧', _Act.shift, tint: true, primary: _shift),
             _Key('⌫', _Act.back, tint: true),
             _Key('CLR', _Act.clear, tint: true),
-            // 虚数单位。送进 Symja 的是 I,编辑框里显示小写 i
-            _Key('i', _Act.char, payload: 'I', tint: true, display: 'i'),
             _Key('ANS', _Act.char, payload: '%', tint: true, display: 'ANS'),
             // 键盘负责"敲字符",✓ 负责"执行"。两者都叫 = 会撞车。
             _Key('✓', _Act.commit, primary: true),

+ 5 - 1
lib/main.dart

@@ -30,7 +30,11 @@ class _App extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final eInk = context.watch<AppSettings>().eInk;
+    final settings = context.watch<AppSettings>();
+    final eInk = settings.eInk;
+    // 近似位数只有一个来源(设置里那个滑动条):读出来直接给会话,
+    // 计算器的"近似"和工具页的"(近似)"就都是这个位数。
+    context.read<SymjaSession>().approximateDigits = settings.approxDigits;
 
     return MaterialApp(
       title: '某人的计算器2',

+ 43 - 1
lib/settings.dart

@@ -16,13 +16,22 @@ class AppSettings extends ChangeNotifier {
   static const int minPort = 1024;
   static const int maxPort = 65535;
 
+  /// 近似值保留几位有效数字(工具页和计算器的"近似"都听这个)。
+  static const int defaultApproxDigits = 15;
+  static const int minDigits = 2;
+  static const int maxDigits = 20;
+
   int _mcpPort = defaultMcpPort;
+  int _approxDigits = defaultApproxDigits;
   bool _eInk = false;
   bool _loaded = false;
 
   /// MCP 服务器监听的端口。
   int get mcpPort => _mcpPort;
 
+  /// 近似值位数。计算器顶栏的"近似"用它,工具页的"(近似)"那行也用它。
+  int get approxDigits => _approxDigits;
+
   /// 是否已从存档读过一次(界面据此决定要不要提示"设置已恢复")。
   bool get loaded => _loaded;
 
@@ -37,6 +46,8 @@ class AppSettings extends ChangeNotifier {
     if (data != null) {
       final p = (data['mcpPort'] as num?)?.toInt();
       if (p != null && p >= minPort && p <= maxPort) _mcpPort = p;
+      final d = (data['approxDigits'] as num?)?.toInt();
+      if (d != null && d >= minDigits && d <= maxDigits) _approxDigits = d;
       _eInk = data['eInk'] == true;
     }
     _loaded = true;
@@ -50,6 +61,15 @@ class AppSettings extends ChangeNotifier {
     await _save();
   }
 
+  Future<void> setApproxDigits(int digits) async {
+    if (digits < minDigits || digits > maxDigits || digits == _approxDigits) {
+      return;
+    }
+    _approxDigits = digits;
+    notifyListeners();
+    await _save();
+  }
+
   Future<void> setEInk(bool on) async {
     if (on == _eInk) return;
     _eInk = on;
@@ -59,6 +79,7 @@ class AppSettings extends ChangeNotifier {
 
   Future<void> _save() => _store.save(<String, dynamic>{
         'mcpPort': _mcpPort,
+        'approxDigits': _approxDigits,
         'eInk': _eInk,
       });
 }
@@ -105,7 +126,9 @@ class _SettingsSheetState extends State<_SettingsSheet> {
 
   @override
   Widget build(BuildContext context) {
-    final eInk = context.watch<AppSettings>().eInk;
+    final settings = context.watch<AppSettings>();
+    final eInk = settings.eInk;
+    final digits = settings.approxDigits;
 
     return SafeArea(
       child: Padding(
@@ -138,6 +161,25 @@ class _SettingsSheetState extends State<_SettingsSheet> {
               ),
             ),
             const SizedBox(height: 8),
+            Row(
+              children: [
+                const Text('近似值位数'),
+                const Spacer(),
+                Text('$digits',
+                    style: const TextStyle(fontFamily: 'monospace')),
+              ],
+            ),
+            Slider(
+              value: digits.toDouble(),
+              min: AppSettings.minDigits.toDouble(),
+              max: AppSettings.maxDigits.toDouble(),
+              divisions: AppSettings.maxDigits - AppSettings.minDigits,
+              label: '$digits',
+              onChanged: (v) => _settings.setApproxDigits(v.round()),
+            ),
+            const Text('工具页的「(近似)」和计算器的「近似」都用这个位数。',
+                style: TextStyle(fontSize: 12)),
+            const SizedBox(height: 8),
             SwitchListTile(
               contentPadding: EdgeInsets.zero,
               value: eInk,

+ 2 - 1
lib/tools/binomial_distribution.dart

@@ -54,7 +54,8 @@ class _BinomialDistributionPageState extends State<BinomialDistributionPage> {
         'Table[Binomial[n,i]*p^i*(1-p)^(n-i), {i,0,n}]]]',
       );
       final numeric = await session.compute(
-        'With[{n=$n,p=$p}, N[Table[Binomial[n,i]*p^i*(1-p)^(n-i), {i,0,n}], 8]]',
+        'With[{n=$n,p=$p}, N[Table[Binomial[n,i]*p^i*(1-p)^(n-i), {i,0,n}], '
+        '${session.approximateDigits}]]',
       );
       final probs = parseSymjaList(exact.text);
       final nums = parseSymjaList(numeric.text);

+ 2 - 2
lib/tools/circle_equation.dart

@@ -59,11 +59,11 @@ class _CircleEquationPageState extends State<CircleEquationPage> {
 
       // 精确值 + 近似值都要:系数和圆心半径都是分数,小数形式有时更直观
       final defsN = parseSymjaList(
-        (await session.compute('N[$defExpr, 10]')).text,
+        (await session.compute('N[Rationalize[$defExpr], ${session.approximateDigits}]')).text,
       );
       final center = parseSymjaList((await session.compute(centerExpr)).text);
       final centerN = parseSymjaList(
-        (await session.compute('N[$centerExpr, 10]')).text,
+        (await session.compute('N[Rationalize[$centerExpr], ${session.approximateDigits}]')).text,
       );
 
       String at(List<Object> l, int i) => i < l.length ? '${l[i]}' : '';

+ 3 - 2
lib/tools/convolution_calculator.dart

@@ -151,7 +151,7 @@ class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
         if (ps == null) continue;
         final total = await session.compute('Total[{${ps.join(', ')}}]');
         final totalN = await session.compute(
-          'N[Total[{${ps.join(', ')}}], 10]',
+          'N[Rationalize[Total[{${ps.join(', ')}}]], ${session.approximateDigits}]',
         );
         groupSums.add(total.text);
         // 精确分数 + 小数:概率这种"看一眼量级"的值,两者都有用
@@ -162,7 +162,8 @@ class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
 
       final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
       final grandN = await session.compute(
-        'N[Total[{${groupSums.join(', ')}}], 10]',
+        'N[Rationalize[Total[{${groupSums.join(', ')}}]], '
+          '${session.approximateDigits}]',
       );
       outputs.add(ToolText('概率总和', withApprox(grand.text, grandN.text)));
       return outputs;

+ 2 - 2
lib/tools/cuo_wei_xiang_jian.dart

@@ -55,8 +55,8 @@ class _CuoWeiXiangJianPageState extends State<CuoWeiXiangJianPage> {
       // **值全是 Symja 算的**,这里只负责把数字摆进公式、分数写成 \frac。
       final s = sFormula(a: av, b: bv, q: qv);
 
-      final aApprox = await session.compute('N[$aExpr, 10]');
-      final bApprox = await session.compute('N[$bExpr, 10]');
+      final aApprox = await session.compute('N[Rationalize[$aExpr], ${session.approximateDigits}]');
+      final bApprox = await session.compute('N[Rationalize[$bExpr], ${session.approximateDigits}]');
 
       return <ToolOutput>[
         ToolText('结果 Sₙ = (A·n + B)·qⁿ − B', s.plain, tex: s.tex),

+ 1 - 1
lib/tools/hypergeometric_distribution.dart

@@ -72,7 +72,7 @@ class _HypergeometricDistributionPageState
       );
       final numeric = await session.compute(
         '$wrap N[Table[Binomial[mm,j]*Binomial[nn-mm,kk-j]/Binomial[nn,kk], '
-        '{j, Max[0,kk-(nn-mm)], Min[kk,mm]}], 8]]',
+        '{j, Max[0,kk-(nn-mm)], Min[kk,mm]}], ${session.approximateDigits}]]',
       );
       final probs = parseSymjaList(exact.text);
       final nums = parseSymjaList(numeric.text);

+ 2 - 1
lib/tools/regression.dart

@@ -43,7 +43,8 @@ class _RegressionPageState extends State<RegressionPage> {
           'sxx=Total[(x-xb)^2]; syy=Total[(y-yb)^2]; '
           'sxy=Total[(x-xb)*(y-yb)]; '
           'b=sxy/sxx; aa=yb-b*xb; se2=Total[(y-(aa+b*x))^2]; '
-          '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]], 10]';
+          '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]], '
+              '${session.approximateDigits}]';
       final stats = parseSymjaList((await session.compute(statsExpr)).text)
           .map((e) => _clean('$e'))
           .toList();

+ 2 - 2
lib/tools/symmetry.dart

@@ -84,7 +84,7 @@ class _SymmetryPageState extends State<SymmetryPage> {
               'Module[{t=(A*x+B*y+C)/(A^2+B^2)}, {x-2*A*t, y-2*B*t}]]';
           final r = parseSymjaList((await session.compute(expr)).text);
           final rN = parseSymjaList(
-            (await session.compute('N[$expr, 10]')).text,
+            (await session.compute('N[Rationalize[$expr], ${session.approximateDigits}]')).text,
           );
           return [ToolText('对称点', '(${_pair(r, rN, 0)}, ${_pair(r, rN, 1)})')];
         }
@@ -120,7 +120,7 @@ class _SymmetryPageState extends State<SymmetryPage> {
   /// 精确值 + 近似值:每个分量各自配上自己的小数。
   Future<List<Object>> _withApprox(SymjaSession session, String expr) async {
     final r = parseSymjaList((await session.compute(expr)).text);
-    final n = parseSymjaList((await session.compute('N[$expr, 10]')).text);
+    final n = parseSymjaList((await session.compute('N[Rationalize[$expr], ${session.approximateDigits}]')).text);
     return [
       for (var i = 0; i < r.length; i++)
         withApprox('${r[i]}', i < n.length ? '${n[i]}' : ''),

+ 9 - 2
lib/tools/tool_kit.dart

@@ -406,7 +406,12 @@ Future<List<ToolOutput>> exactAndApprox(
   String expr,
 ) async {
   final exact = await session.compute('Rationalize[$expr]', applyMode: false);
-  final approx = await session.compute('N[$expr, 10]', applyMode: false);
+  // 近似值也从**有理化之后**的数转小数:输入写成 0.2/0.3 时机器浮点会带出
+  // `3.5999999999999996` 这种尾巴,先还原成分数再取小数就干净了(3.6)。
+  // 位数听设置里那个滑动条(计算器的"近似"也是同一个值)。
+  final approx = await session.compute(
+      'N[Rationalize[$expr], ${session.approximateDigits}]',
+      applyMode: false);
   final same = _sameValue(exact.text, approx.text);
   return [
     ToolText(label, exact.text, tex: exact.tex),
@@ -438,7 +443,9 @@ Future<ToolOutput> numeric(
   String label,
   String expr,
 ) async {
-  final r = await session.compute('N[$expr, 12]', applyMode: false);
+  final r = await session.compute(
+      'N[Rationalize[$expr], ${session.approximateDigits}]',
+      applyMode: false);
   return ToolText(label, r.text, tex: null);
 }
 

+ 44 - 10
test/widget_test.dart

@@ -8,6 +8,7 @@ import 'package:cal/calculator_page.dart';
 import 'package:cal/console_page.dart';
 import 'package:cal/expr_editor.dart';
 import 'package:cal/main.dart';
+import 'package:cal/settings.dart';
 import 'package:cal/tools/cuo_wei_xiang_jian.dart';
 import 'package:cal/tools/cycle_coloring.dart';
 import 'package:cal/tools/group_allocation.dart';
@@ -110,6 +111,12 @@ void main() {
     await tester.pumpAndSettle();
   }
 
+  /// 按一个**在 shift 层里**的键:先按 ⇧ 再按它。
+  Future<void> pressShift(WidgetTester tester, String label) async {
+    await press(tester, '⇧');
+    await press(tester, label);
+  }
+
   /// 打开工具页里的某个工具(会连带把 App 起起来)。
   ///
   /// 先用搜索框把它筛出来:网格是懒加载的,靠后的卡片在测试视口外根本不在树里。
@@ -169,7 +176,7 @@ void main() {
   group('计算器(结构化输入)', () {
     testWidgets('按 ∫ 插出 Integrate 模板,并在显示区露出序列化结果', (tester) async {
       await pumpApp(tester);
-      await press(tester, '∫');
+      await pressShift(tester, '∫');
 
       // 空槽序列化成 Null,用户一眼能看出还没填
       expect(find.text('Integrate[Null, x]'), findsOneWidget);
@@ -177,7 +184,7 @@ void main() {
 
     testWidgets('模板槽里能接着打字(光标落在第一个参数)', (tester) async {
       await pumpApp(tester);
-      await press(tester, '∫');
+      await pressShift(tester, '∫');
       await press(tester, 'x');
 
       expect(await lastExpr(tester), 'Integrate[x, x]');
@@ -308,7 +315,7 @@ void main() {
     testWidgets('/.→ 一个键给出代入骨架(原来 /. 和 -> 是两个键)', (tester) async {
       await pumpApp(tester);
       await press(tester, 'x');
-      await press(tester, '/.→');
+      await pressShift(tester, '/.→');
       await press(tester, '3');
 
       // 骨架自带变量 x 和箭头,用户只要接着打要代进去的值
@@ -321,7 +328,7 @@ void main() {
       await press(tester, 'x');
       await press(tester, '−');
       await press(tester, '1');
-      await press(tester, '==');
+      await pressShift(tester, '==');
       await press(tester, '0');
 
       // 说明:这里刻意不用 ^。按了 ^ 之后光标在**指数槽**里,
@@ -342,10 +349,10 @@ void main() {
 
       // 拼一行矩阵:{{1,2}}
       // (没有"换行键"了,多行矩阵要把光标点回外层花括号再加逗号和 `{`)
-      await press(tester, '{');
-      await press(tester, '{');
+      await pressShift(tester, '{');
+      await pressShift(tester, '{');
       await press(tester, '1');
-      await press(tester, ',');
+      await pressShift(tester, ',');
       await press(tester, '2');
 
       expect(await lastExpr(tester), 'Det[{{1,2}}]');
@@ -357,7 +364,7 @@ void main() {
       await press(tester, '⇧');
       await press(tester, 'C'); // Shif 层的 Binomial[□, □]
       await press(tester, '5');
-      await press(tester, ',');
+      await pressShift(tester, ',');
       await press(tester, '2');
       expect(await lastExpr(tester), 'Binomial[5, 2]');
 
@@ -365,7 +372,7 @@ void main() {
       await press(tester, '⇧');
       await press(tester, 'P'); // FactorialPower[□, □]
       await press(tester, '5');
-      await press(tester, ',');
+      await pressShift(tester, ',');
       await press(tester, '2');
       expect(await lastExpr(tester), 'FactorialPower[5, 2]');
     });
@@ -394,7 +401,7 @@ void main() {
     testWidgets('赋值:x = 1', (tester) async {
       await pumpApp(tester);
       await press(tester, 'x');
-      await press(tester, '=');
+      await pressShift(tester, '=');
       await press(tester, '1');
       expect(await lastExpr(tester), 'x=1');
     });
@@ -778,6 +785,33 @@ void main() {
       expect(app.theme!.splashFactory, NoSplash.splashFactory);
     });
 
+    testWidgets('近似值位数:滑动条能调,计算器的 N[...] 跟着变', (tester) async {
+      await pumpApp(tester);
+      await goTo(tester, '控制台');
+      await tester.tap(find.byIcon(Icons.settings_outlined));
+      await tester.pumpAndSettle();
+
+      await tester.drag(find.byType(Slider), const Offset(-400, 0));
+      await tester.pumpAndSettle();
+      final digits =
+          tester.widget<Slider>(find.byType(Slider)).value.round();
+      expect(digits, AppSettings.minDigits);
+
+      await tester.tapAt(const Offset(10, 10)); // 关面板
+      await tester.pumpAndSettle();
+
+      // 计算器切到「近似」,送进引擎的表达式应当带上这个位数
+      await goTo(tester, '计算器');
+      await tester.tap(find.text('精确'));
+      await tester.pumpAndSettle();
+      await press(tester, '2');
+
+      expect(
+        calls.lastWhere((c) => c.method == 'evaluate').arguments['expr'],
+        'N[2, $digits]',
+      );
+    });
+
     testWidgets('设置的 MCP 端口会传给原生侧', (tester) async {
       await pumpApp(tester);
       await goTo(tester, '控制台');