| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- // 结构化表达式模型的测试。
- //
- // 这套东西的难点全在编辑手感上(光标往哪落、退格先钻还是先删、括号什么时候要加),
- // 而这些"手感"出了问题在界面上看不出来,只会悄悄算错。所以边界都钉在这里。
- 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('数字后面的字母不会被并进去', () {
- 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 = docWith([LitNode('2')]);
- d.applyFunction('Sqrt');
- 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('打完 2x+1 按 ln → Log[2x+1](整槽,不是只包最后一个节点)', () {
- final d = twoXPlusOne();
- expect(d.toSymja(), '2x+1');
- d.applyFunction('Log');
- expect(d.toSymja(), 'Log[2x+1]');
- });
- test('单参数函数包完只带一个参数(不能多塞空槽)', () {
- final d = ExprDoc();
- d.insertChar('2');
- d.applyFunction('Sqrt');
- expect(d.toSymja(), 'Sqrt[2]');
- });
- test('带预置参数的:整槽当第一个参数,其余照旧', () {
- final d = twoXPlusOne();
- d.applyFunction('Integrate', prefill: ['', 'x']);
- expect(d.toSymja(), 'Integrate[2x+1, x]');
- });
- test('空槽时按 ln → 空模板,不是 Log[Null] 包 Null', () {
- final d = ExprDoc();
- d.applyFunction('Log');
- expect(d.toSymja(), 'Log[Null]');
- d.insertChar('x');
- expect(d.toSymja(), 'Log[x]');
- });
- test('槽尾是运算符时不包 —— 用户刚打完 1+ ,要的是 1+Log[...]', () {
- final d = ExprDoc();
- d.insertChar('1');
- d.insert(LitNode('+'));
- d.applyFunction('Log');
- expect(d.toSymja(), '1+Log[Null]');
- });
- test('光标停在中间时也插模板,不把整槽包走', () {
- final d = twoXPlusOne();
- d.cursor = const SlotPath(<SlotStep>[], 2); // 停在 x 之后、+ 之前
- d.applyFunction('Log');
- expect(d.toSymja(), '2xLog[Null]+1');
- });
- test('包完之后光标在函数外面(能继续往后打)', () {
- final d = twoXPlusOne();
- d.applyFunction('Log');
- d.insert(LitNode('+'));
- d.insertChar('3');
- expect(d.toSymja(), 'Log[2x+1]+3');
- });
- });
- 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);
- });
- });
- }
|