| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- // 结构化表达式模型的测试。
- //
- // 这套东西的难点全在编辑手感上(光标往哪落、退格先钻还是先删、括号什么时候要加),
- // 而这些"手感"出了问题在界面上看不出来,只会悄悄算错。所以边界都钉在这里。
- import 'package:flutter_test/flutter_test.dart';
- import 'package:cal/expr_node.dart';
- ExprDoc docWith(List<ExprNode> nodes) {
- final d = ExprDoc();
- for (final n in nodes) {
- d.insert(n);
- }
- return d;
- }
- void main() {
- group('插入与序列化', () {
- test('挨着插入的节点直接拼接', () {
- final d = docWith([LitNode('x'), LitNode('+'), LitNode('1')]);
- expect(d.toSymja(), 'x+1');
- });
- test('数字键并入左边那个数字,42 是一个节点不是两个', () {
- final d = ExprDoc();
- d.insertChar('4');
- d.insertChar('2');
- expect(d.root.children.length, 1);
- expect(d.toSymja(), '42');
- });
- test('小数点也并入', () {
- final d = ExprDoc();
- d.insertChar('3');
- d.insertChar('.');
- d.insertChar('1');
- d.insertChar('4');
- expect(d.toSymja(), '3.14');
- });
- test('相邻两个字面量要补乘号,否则会粘成一个符号', () {
- // Symja 里 xn 是"叫 xn 的变量",不是 x*n;PiE 同理
- final d = ExprDoc();
- d.insert(LitNode('x'));
- d.insert(LitNode('n'));
- expect(d.toSymja(), 'x*n');
- final p = ExprDoc();
- p.insert(LitNode('Pi'));
- p.insert(LitNode('E'));
- expect(p.toSymja(), 'Pi*E');
- });
- test('字母后面接数字也要补乘号', () {
- final d = ExprDoc();
- d.insert(LitNode('x'));
- d.insertChar('2');
- expect(d.toSymja(), 'x*2');
- });
- test('数字后面接字母保留 2x 的写法(Mathematica 认它是乘法)', () {
- final d = ExprDoc();
- d.insertChar('2');
- d.insert(LitNode('x'));
- expect(d.toSymja(), '2x');
- });
- test('运算符两侧不补乘号', () {
- final d = docWith([LitNode('x'), LitNode('+'), LitNode('1')]);
- expect(d.toSymja(), 'x+1');
- });
- test('函数调用后面接字母不补(Symja 认它是乘法)', () {
- final d = ExprDoc();
- d.makeFunction('Sin');
- d.insertChar('x'); // 光标退回根槽前先把参数填好
- d.cursor = const SlotPath(<SlotStep>[], 1);
- d.insert(LitNode('y'));
- expect(d.toSymja(), 'Sin[x]y');
- });
- test('数字后面的字母不会被并进去', () {
- final d = ExprDoc();
- d.insertChar('2');
- d.insertChar('x');
- expect(d.root.children.length, 2);
- expect(d.toSymja(), '2x');
- });
- test('空槽序列化成 Null(让求值明确报错,而不是静默算错)', () {
- final d = ExprDoc();
- expect(d.toSymja(), 'Null');
- });
- });
- group('幂', () {
- test('先打 x 再按 ^ ,x 成为底、光标落到指数', () {
- final d = docWith([LitNode('x')]);
- d.makePower();
- d.insertChar('2');
- expect(d.toSymja(), 'x^2');
- });
- test('没有左边的节点时造一个空底的幂', () {
- final d = ExprDoc();
- d.makePower();
- d.insertChar('2');
- expect(d.toSymja(), 'Null^2');
- });
- test('底是复合表达式时要加括号', () {
- final d = ExprDoc();
- d.insert(PowNode(
- base: SeqNode([LitNode('x'), LitNode('+'), LitNode('1')]),
- exp: SeqNode([LitNode('2')]),
- ));
- expect(d.toSymja(), '(x+1)^2');
- });
- test('底是单个字面量时不加多余括号', () {
- final d = ExprDoc();
- d.insert(PowNode(base: SeqNode([LitNode('x')]), exp: SeqNode([LitNode('2')])));
- expect(d.toSymja(), 'x^2');
- });
- });
- group('分式与括号', () {
- test('1 再按 / 再打 3 得 1/3', () {
- final d = ExprDoc();
- d.insertChar('1');
- d.makeFraction();
- d.insertChar('3');
- expect(d.toSymja(), '1/3');
- });
- test('分母是 x+1 时要加括号', () {
- final d = ExprDoc();
- d.insertChar('1');
- d.makeFraction();
- d.insertChar('x');
- d.insertChar('+');
- d.insertChar('1');
- expect(d.toSymja(), '1/(x+1)');
- });
- test('括号分组', () {
- final d = ExprDoc();
- d.insertChar('2');
- d.insertChar('*');
- d.makeGroup();
- d.insertChar('x');
- d.insertChar('+');
- d.insertChar('1');
- expect(d.toSymja(), '2*(x+1)');
- });
- });
- group('函数模板', () {
- test('空的 prefill 就是空槽,光标落在第一个槽', () {
- final d = ExprDoc();
- d.makeFunction('Integrate', prefill: ['', 'x']);
- expect(d.toSymja(), 'Integrate[Null, x]');
- // 光标应当在第一个参数里,所以接着打的字进那个槽
- d.insertChar('x');
- expect(d.toSymja(), 'Integrate[x, x]');
- });
- test('填出 Integrate[x^2, x]', () {
- final d = ExprDoc();
- d.makeFunction('Integrate', prefill: ['', 'x']);
- d.insertChar('x');
- d.makePower();
- d.insertChar('2');
- expect(d.toSymja(), 'Integrate[x^2, x]');
- });
- test('无参模板', () {
- final d = ExprDoc();
- d.makeFunction('Sin');
- expect(d.toSymja(), 'Sin[Null]');
- d.insertChar('x');
- expect(d.toSymja(), 'Sin[x]');
- });
- test('逗号切到下一个参数,没有就新建', () {
- final d = ExprDoc();
- d.makeFunction('f');
- d.insertChar('1');
- d.nextArgument();
- d.insertChar('2');
- expect(d.toSymja(), 'f[1, 2]');
- });
- test('根号键插空模板,往里填即得', () {
- final d = ExprDoc();
- d.makeFunction('Sqrt');
- expect(d.toSymja(), 'Sqrt[Null]');
- d.insertChar('2');
- expect(d.toSymja(), 'Sqrt[2]');
- });
- });
- group('退格', () {
- test('删掉左边那个字面量', () {
- final d = docWith([LitNode('1'), LitNode('2')]);
- d.backspace();
- expect(d.toSymja(), '1');
- });
- test('光标在最左边时按退格什么都不发生', () {
- final d = docWith([LitNode('1')]);
- d.cursor = SlotPath.root; // 手动把光标移到最左
- d.backspace();
- expect(d.toSymja(), '1');
- });
- test('数字逐位删,不是整个数清掉', () {
- final d = ExprDoc();
- d.insertChar('1');
- d.insertChar('2');
- d.insertChar('3');
- d.backspace();
- expect(d.toSymja(), '12');
- d.backspace();
- expect(d.toSymja(), '1');
- });
- test('住进空槽里退格:先退回父槽,顺手把没填完的模板也删掉', () {
- final d = ExprDoc();
- d.makeFunction('Integrate', prefill: ['', 'x']);
- expect(d.toSymja(), 'Integrate[Null, x]');
- d.backspace(); // 空槽最左边 → 退回根槽,并把没填完的模板一起删掉
- expect(d.toSymja(), 'Null');
- });
- test('左边是没填完的模板:先进去,再退一次才删掉', () {
- final d = ExprDoc();
- d.makeFunction('Integrate', prefill: ['', 'x']);
- d.cursor = const SlotPath(<SlotStep>[], 1); // 根槽 offset 1 = 模板右边
- expect(d.toSymja(), 'Integrate[Null, x]');
- d.backspace(); // 钻进那个空槽,内容不变
- expect(d.toSymja(), 'Integrate[Null, x]');
- d.backspace(); // 退回根槽,顺手把没填完的模板删掉
- expect(d.toSymja(), 'Null');
- });
- test('填完参数的过程里模板不会被误删', () {
- final d = ExprDoc();
- d.makeFunction('Sin');
- d.insertChar('x');
- expect(d.toSymja(), 'Sin[x]');
- // 回到参数里,且把光标停在 x **之后**(offset 1)
- d.cursor = const SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
- d.backspace(); // 删参数里的 x
- expect(d.toSymja(), 'Sin[Null]');
- });
- test('左边是填完的模板:整个删掉(否则光标永远出不来)', () {
- final d = ExprDoc();
- d.makeFunction('Integrate', prefill: ['', 'x']);
- d.insertChar('x'); // Integrate[x, x],已经填完
- d.cursor = const SlotPath(<SlotStep>[], 1); // 模板右边
- expect(d.toSymja(), 'Integrate[x, x]');
- d.backspace();
- expect(d.toSymja(), 'Null');
- });
- });
- group('光标路径', () {
- test('插入带占位槽的节点后光标在正确的槽里', () {
- final d = ExprDoc();
- d.makeFraction();
- expect(d.cursor.steps.length, 1);
- expect(d.cursor.steps.first.slot, 1); // 分母
- expect(d.cursor.offset, 0);
- });
- test('parent 回到父槽并停在刚退出的那个孩子之后', () {
- final d = ExprDoc();
- d.makeFunction('Sin');
- d.insertChar('x');
- final up = d.cursor.parent;
- expect(up, isNotNull);
- expect(up!.steps, isEmpty);
- expect(up.offset, 1); // Sin 之后
- });
- });
- group('选区与剪切粘贴', () {
- ExprDoc twoXPlusOne() {
- final d = ExprDoc();
- d.insertChar('2');
- d.insert(LitNode('x'));
- d.insert(LitNode('+'));
- d.insertChar('1');
- return d;
- }
- test('选中一个节点', () {
- final d = twoXPlusOne();
- d.selectNodeAt(SlotPath.root, 0);
- expect(d.selectionText, '2');
- });
- test('扩展选区覆盖连续节点', () {
- final d = twoXPlusOne();
- d.selectNodeAt(SlotPath.root, 1); // x
- d.extendSelectionTo(SlotPath.root, 3); // 1
- expect(d.selectionText, 'x+1');
- });
- test('往左扩展也成立(start 取更小的那个)', () {
- final d = twoXPlusOne();
- d.selectNodeAt(SlotPath.root, 3);
- d.extendSelectionTo(SlotPath.root, 0);
- expect(d.selectionText, '2x+1');
- });
- test('剪切:返回文本、删掉内容、光标落到原位', () {
- final d = twoXPlusOne();
- d.selectNodeAt(SlotPath.root, 0);
- d.extendSelectionTo(SlotPath.root, 3);
- expect(d.selectionText, '2x+1');
- d.deleteSelection();
- expect(d.toSymja(), 'Null');
- expect(d.selection, isNull);
- expect(d.cursor.offset, 0);
- });
- test('只删中间一段,两边保留', () {
- final d = twoXPlusOne();
- d.cursor = const SlotPath(<SlotStep>[], 0);
- d.selectNodeAt(SlotPath.root, 1);
- // 选中 x 与 +
- d.extendSelectionTo(SlotPath.root, 2);
- expect(d.selectionText, 'x+');
- d.deleteSelection();
- expect(d.toSymja(), '21');
- });
- test('有选区时按退格 = 删掉选中的那段', () {
- final d = twoXPlusOne();
- d.selectNodeAt(SlotPath.root, 1); // x
- d.extendSelectionTo(SlotPath.root, 2); // +
- expect(d.selectionText, 'x+');
- d.backspace();
- expect(d.toSymja(), '21');
- expect(d.selection, isNull);
- });
- test('没有选区时退格还是老行为', () {
- final d = twoXPlusOne();
- d.backspace();
- expect(d.toSymja(), '2x+');
- });
- test('插入到光标处(粘贴进 Log 的括号里)', () {
- final d = ExprDoc();
- d.makeFunction('Log'); // Log[□],光标在参数槽里
- d.insertNodesAtCursor([LitNode('2'), LitNode('x'), LitNode('+'), LitNode('1')]);
- expect(d.toSymja(), 'Log[2x+1]');
- expect(d.cursor.offset, 4);
- });
- test('跨槽扩展会被忽略,不会串到别的槽', () {
- final d = ExprDoc();
- d.makeFunction('Log'); // 光标在 Log 的参数槽
- d.selectNodeAt(SlotPath.root, 0); // 选中根槽里的 Log
- d.extendSelectionTo(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0), 0);
- expect(d.selectionText, 'Log[Null]'); // 还是原来那个选区
- });
- });
- group('矩阵', () {
- test('花括号键造一对括号,光标在里面', () {
- final d = ExprDoc();
- d.makeBrace();
- expect(d.toSymja(), '{Null}');
- d.insertChar('1');
- expect(d.toSymja(), '{1}');
- });
- test('花括号里能直接打逗号(矩阵行分隔)', () {
- final d = ExprDoc();
- d.makeBrace();
- d.insertChar('1');
- d.insert(LitNode(','));
- d.insertChar('2');
- expect(d.toSymja(), '{1,2}');
- });
- });
- group('光标槽判定', () {
- test('槽内位置不同,仍算同一个槽(渲染光标靠这个)', () {
- const a = SlotPath(<SlotStep>[], 0);
- const b = SlotPath(<SlotStep>[], 3);
- expect(a == b, isFalse); // == 比的是完整位置
- expect(a.sameSlotAs(b), isTrue); // sameSlotAs 只比槽
- });
- test('不同槽不算同一个', () {
- const a = SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
- const b = SlotPath(<SlotStep>[SlotStep(0, 1)], 1);
- expect(a.sameSlotAs(b), isFalse);
- });
- test('打一个字之后,光标仍被认为在根槽里', () {
- final d = ExprDoc();
- expect(d.cursor.sameSlotAs(SlotPath.root), isTrue);
- d.insertChar('x');
- expect(d.cursor.offset, 1);
- expect(d.cursor.sameSlotAs(SlotPath.root), isTrue,
- reason: 'offset 变了但槽没变,光标必须还画得出来');
- });
- test('进到函数参数里后,根槽不再算聚焦', () {
- final d = ExprDoc();
- d.makeFunction('Sin');
- expect(d.cursor.sameSlotAs(SlotPath.root), isFalse);
- expect(
- d.cursor.sameSlotAs(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0)),
- isTrue,
- );
- });
- });
- group('clear', () {
- test('清空后回到根槽', () {
- final d = ExprDoc();
- d.makeFunction('Integrate', prefill: ['', 'x']);
- d.clear();
- expect(d.toSymja(), 'Null');
- expect(d.cursor.steps, isEmpty);
- expect(d.root.children, isEmpty);
- });
- });
- }
|