expr_node_test.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // 结构化表达式模型的测试。
  2. //
  3. // 这套东西的难点全在编辑手感上(光标往哪落、退格先钻还是先删、括号什么时候要加),
  4. // 而这些"手感"出了问题在界面上看不出来,只会悄悄算错。所以边界都钉在这里。
  5. import 'package:flutter_test/flutter_test.dart';
  6. import 'package:cal/expr_node.dart';
  7. ExprDoc docWith(List<ExprNode> nodes) {
  8. final d = ExprDoc();
  9. for (final n in nodes) {
  10. d.insert(n);
  11. }
  12. return d;
  13. }
  14. void main() {
  15. group('插入与序列化', () {
  16. test('挨着插入的节点直接拼接', () {
  17. final d = docWith([LitNode('x'), LitNode('+'), LitNode('1')]);
  18. expect(d.toSymja(), 'x+1');
  19. });
  20. test('数字键并入左边那个数字,42 是一个节点不是两个', () {
  21. final d = ExprDoc();
  22. d.insertChar('4');
  23. d.insertChar('2');
  24. expect(d.root.children.length, 1);
  25. expect(d.toSymja(), '42');
  26. });
  27. test('小数点也并入', () {
  28. final d = ExprDoc();
  29. d.insertChar('3');
  30. d.insertChar('.');
  31. d.insertChar('1');
  32. d.insertChar('4');
  33. expect(d.toSymja(), '3.14');
  34. });
  35. test('数字后面的字母不会被并进去', () {
  36. final d = ExprDoc();
  37. d.insertChar('2');
  38. d.insertChar('x');
  39. expect(d.root.children.length, 2);
  40. expect(d.toSymja(), '2x');
  41. });
  42. test('空槽序列化成 Null(让求值明确报错,而不是静默算错)', () {
  43. final d = ExprDoc();
  44. expect(d.toSymja(), 'Null');
  45. });
  46. });
  47. group('幂', () {
  48. test('先打 x 再按 ^ ,x 成为底、光标落到指数', () {
  49. final d = docWith([LitNode('x')]);
  50. d.makePower();
  51. d.insertChar('2');
  52. expect(d.toSymja(), 'x^2');
  53. });
  54. test('没有左边的节点时造一个空底的幂', () {
  55. final d = ExprDoc();
  56. d.makePower();
  57. d.insertChar('2');
  58. expect(d.toSymja(), 'Null^2');
  59. });
  60. test('底是复合表达式时要加括号', () {
  61. final d = ExprDoc();
  62. d.insert(PowNode(
  63. base: SeqNode([LitNode('x'), LitNode('+'), LitNode('1')]),
  64. exp: SeqNode([LitNode('2')]),
  65. ));
  66. expect(d.toSymja(), '(x+1)^2');
  67. });
  68. test('底是单个字面量时不加多余括号', () {
  69. final d = ExprDoc();
  70. d.insert(PowNode(base: SeqNode([LitNode('x')]), exp: SeqNode([LitNode('2')])));
  71. expect(d.toSymja(), 'x^2');
  72. });
  73. });
  74. group('分式与括号', () {
  75. test('1 再按 / 再打 3 得 1/3', () {
  76. final d = ExprDoc();
  77. d.insertChar('1');
  78. d.makeFraction();
  79. d.insertChar('3');
  80. expect(d.toSymja(), '1/3');
  81. });
  82. test('分母是 x+1 时要加括号', () {
  83. final d = ExprDoc();
  84. d.insertChar('1');
  85. d.makeFraction();
  86. d.insertChar('x');
  87. d.insertChar('+');
  88. d.insertChar('1');
  89. expect(d.toSymja(), '1/(x+1)');
  90. });
  91. test('括号分组', () {
  92. final d = ExprDoc();
  93. d.insertChar('2');
  94. d.insertChar('*');
  95. d.makeGroup();
  96. d.insertChar('x');
  97. d.insertChar('+');
  98. d.insertChar('1');
  99. expect(d.toSymja(), '2*(x+1)');
  100. });
  101. });
  102. group('函数模板', () {
  103. test('空的 prefill 就是空槽,光标落在第一个槽', () {
  104. final d = ExprDoc();
  105. d.makeFunction('Integrate', prefill: ['', 'x']);
  106. expect(d.toSymja(), 'Integrate[Null, x]');
  107. // 光标应当在第一个参数里,所以接着打的字进那个槽
  108. d.insertChar('x');
  109. expect(d.toSymja(), 'Integrate[x, x]');
  110. });
  111. test('填出 Integrate[x^2, x]', () {
  112. final d = ExprDoc();
  113. d.makeFunction('Integrate', prefill: ['', 'x']);
  114. d.insertChar('x');
  115. d.makePower();
  116. d.insertChar('2');
  117. expect(d.toSymja(), 'Integrate[x^2, x]');
  118. });
  119. test('无参模板', () {
  120. final d = ExprDoc();
  121. d.makeFunction('Sin');
  122. expect(d.toSymja(), 'Sin[Null]');
  123. d.insertChar('x');
  124. expect(d.toSymja(), 'Sin[x]');
  125. });
  126. test('逗号切到下一个参数,没有就新建', () {
  127. final d = ExprDoc();
  128. d.makeFunction('f');
  129. d.insertChar('1');
  130. d.nextArgument();
  131. d.insertChar('2');
  132. expect(d.toSymja(), 'f[1, 2]');
  133. });
  134. test('根式把左边的节点收进被开方数', () {
  135. final d = docWith([LitNode('2')]);
  136. d.applyFunction('Sqrt');
  137. expect(d.toSymja(), 'Sqrt[2]');
  138. });
  139. });
  140. group('退格', () {
  141. test('删掉左边那个字面量', () {
  142. final d = docWith([LitNode('1'), LitNode('2')]);
  143. d.backspace();
  144. expect(d.toSymja(), '1');
  145. });
  146. test('光标在最左边时按退格什么都不发生', () {
  147. final d = docWith([LitNode('1')]);
  148. d.cursor = SlotPath.root; // 手动把光标移到最左
  149. d.backspace();
  150. expect(d.toSymja(), '1');
  151. });
  152. test('数字逐位删,不是整个数清掉', () {
  153. final d = ExprDoc();
  154. d.insertChar('1');
  155. d.insertChar('2');
  156. d.insertChar('3');
  157. d.backspace();
  158. expect(d.toSymja(), '12');
  159. d.backspace();
  160. expect(d.toSymja(), '1');
  161. });
  162. test('住进空槽里退格:先退回父槽,顺手把没填完的模板也删掉', () {
  163. final d = ExprDoc();
  164. d.makeFunction('Integrate', prefill: ['', 'x']);
  165. expect(d.toSymja(), 'Integrate[Null, x]');
  166. d.backspace(); // 空槽最左边 → 退回根槽,并把没填完的模板一起删掉
  167. expect(d.toSymja(), 'Null');
  168. });
  169. test('左边是没填完的模板:先进去,再退一次才删掉', () {
  170. final d = ExprDoc();
  171. d.makeFunction('Integrate', prefill: ['', 'x']);
  172. d.cursor = const SlotPath(<SlotStep>[], 1); // 根槽 offset 1 = 模板右边
  173. expect(d.toSymja(), 'Integrate[Null, x]');
  174. d.backspace(); // 钻进那个空槽,内容不变
  175. expect(d.toSymja(), 'Integrate[Null, x]');
  176. d.backspace(); // 退回根槽,顺手把没填完的模板删掉
  177. expect(d.toSymja(), 'Null');
  178. });
  179. test('填完参数的过程里模板不会被误删', () {
  180. final d = ExprDoc();
  181. d.makeFunction('Sin');
  182. d.insertChar('x');
  183. expect(d.toSymja(), 'Sin[x]');
  184. // 回到参数里,且把光标停在 x **之后**(offset 1)
  185. d.cursor = const SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
  186. d.backspace(); // 删参数里的 x
  187. expect(d.toSymja(), 'Sin[Null]');
  188. });
  189. test('左边是填完的模板:整个删掉(否则光标永远出不来)', () {
  190. final d = ExprDoc();
  191. d.makeFunction('Integrate', prefill: ['', 'x']);
  192. d.insertChar('x'); // Integrate[x, x],已经填完
  193. d.cursor = const SlotPath(<SlotStep>[], 1); // 模板右边
  194. expect(d.toSymja(), 'Integrate[x, x]');
  195. d.backspace();
  196. expect(d.toSymja(), 'Null');
  197. });
  198. });
  199. group('光标路径', () {
  200. test('插入带占位槽的节点后光标在正确的槽里', () {
  201. final d = ExprDoc();
  202. d.makeFraction();
  203. expect(d.cursor.steps.length, 1);
  204. expect(d.cursor.steps.first.slot, 1); // 分母
  205. expect(d.cursor.offset, 0);
  206. });
  207. test('parent 回到父槽并停在刚退出的那个孩子之后', () {
  208. final d = ExprDoc();
  209. d.makeFunction('Sin');
  210. d.insertChar('x');
  211. final up = d.cursor.parent;
  212. expect(up, isNotNull);
  213. expect(up!.steps, isEmpty);
  214. expect(up.offset, 1); // Sin 之后
  215. });
  216. });
  217. group('函数键:把整个槽包进去', () {
  218. ExprDoc twoXPlusOne() {
  219. final d = ExprDoc();
  220. d.insertChar('2');
  221. d.insert(LitNode('x'));
  222. d.insert(LitNode('+'));
  223. d.insertChar('1');
  224. return d;
  225. }
  226. test('打完 2x+1 按 ln → Log[2x+1](整槽,不是只包最后一个节点)', () {
  227. final d = twoXPlusOne();
  228. expect(d.toSymja(), '2x+1');
  229. d.applyFunction('Log');
  230. expect(d.toSymja(), 'Log[2x+1]');
  231. });
  232. test('单参数函数包完只带一个参数(不能多塞空槽)', () {
  233. final d = ExprDoc();
  234. d.insertChar('2');
  235. d.applyFunction('Sqrt');
  236. expect(d.toSymja(), 'Sqrt[2]');
  237. });
  238. test('带预置参数的:整槽当第一个参数,其余照旧', () {
  239. final d = twoXPlusOne();
  240. d.applyFunction('Integrate', prefill: ['', 'x']);
  241. expect(d.toSymja(), 'Integrate[2x+1, x]');
  242. });
  243. test('空槽时按 ln → 空模板,不是 Log[Null] 包 Null', () {
  244. final d = ExprDoc();
  245. d.applyFunction('Log');
  246. expect(d.toSymja(), 'Log[Null]');
  247. d.insertChar('x');
  248. expect(d.toSymja(), 'Log[x]');
  249. });
  250. test('槽尾是运算符时不包 —— 用户刚打完 1+ ,要的是 1+Log[...]', () {
  251. final d = ExprDoc();
  252. d.insertChar('1');
  253. d.insert(LitNode('+'));
  254. d.applyFunction('Log');
  255. expect(d.toSymja(), '1+Log[Null]');
  256. });
  257. test('光标停在中间时也插模板,不把整槽包走', () {
  258. final d = twoXPlusOne();
  259. d.cursor = const SlotPath(<SlotStep>[], 2); // 停在 x 之后、+ 之前
  260. d.applyFunction('Log');
  261. expect(d.toSymja(), '2xLog[Null]+1');
  262. });
  263. test('包完之后光标在函数外面(能继续往后打)', () {
  264. final d = twoXPlusOne();
  265. d.applyFunction('Log');
  266. d.insert(LitNode('+'));
  267. d.insertChar('3');
  268. expect(d.toSymja(), 'Log[2x+1]+3');
  269. });
  270. });
  271. group('光标槽判定', () {
  272. test('槽内位置不同,仍算同一个槽(渲染光标靠这个)', () {
  273. const a = SlotPath(<SlotStep>[], 0);
  274. const b = SlotPath(<SlotStep>[], 3);
  275. expect(a == b, isFalse); // == 比的是完整位置
  276. expect(a.sameSlotAs(b), isTrue); // sameSlotAs 只比槽
  277. });
  278. test('不同槽不算同一个', () {
  279. const a = SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
  280. const b = SlotPath(<SlotStep>[SlotStep(0, 1)], 1);
  281. expect(a.sameSlotAs(b), isFalse);
  282. });
  283. test('打一个字之后,光标仍被认为在根槽里', () {
  284. final d = ExprDoc();
  285. expect(d.cursor.sameSlotAs(SlotPath.root), isTrue);
  286. d.insertChar('x');
  287. expect(d.cursor.offset, 1);
  288. expect(d.cursor.sameSlotAs(SlotPath.root), isTrue,
  289. reason: 'offset 变了但槽没变,光标必须还画得出来');
  290. });
  291. test('进到函数参数里后,根槽不再算聚焦', () {
  292. final d = ExprDoc();
  293. d.makeFunction('Sin');
  294. expect(d.cursor.sameSlotAs(SlotPath.root), isFalse);
  295. expect(
  296. d.cursor.sameSlotAs(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0)),
  297. isTrue,
  298. );
  299. });
  300. });
  301. group('clear', () {
  302. test('清空后回到根槽', () {
  303. final d = ExprDoc();
  304. d.makeFunction('Integrate', prefill: ['', 'x']);
  305. d.clear();
  306. expect(d.toSymja(), 'Null');
  307. expect(d.cursor.steps, isEmpty);
  308. expect(d.root.children, isEmpty);
  309. });
  310. });
  311. }