expr_node_test.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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 = ExprDoc();
  136. d.makeFunction('Sqrt');
  137. expect(d.toSymja(), 'Sqrt[Null]');
  138. d.insertChar('2');
  139. expect(d.toSymja(), 'Sqrt[2]');
  140. });
  141. });
  142. group('退格', () {
  143. test('删掉左边那个字面量', () {
  144. final d = docWith([LitNode('1'), LitNode('2')]);
  145. d.backspace();
  146. expect(d.toSymja(), '1');
  147. });
  148. test('光标在最左边时按退格什么都不发生', () {
  149. final d = docWith([LitNode('1')]);
  150. d.cursor = SlotPath.root; // 手动把光标移到最左
  151. d.backspace();
  152. expect(d.toSymja(), '1');
  153. });
  154. test('数字逐位删,不是整个数清掉', () {
  155. final d = ExprDoc();
  156. d.insertChar('1');
  157. d.insertChar('2');
  158. d.insertChar('3');
  159. d.backspace();
  160. expect(d.toSymja(), '12');
  161. d.backspace();
  162. expect(d.toSymja(), '1');
  163. });
  164. test('住进空槽里退格:先退回父槽,顺手把没填完的模板也删掉', () {
  165. final d = ExprDoc();
  166. d.makeFunction('Integrate', prefill: ['', 'x']);
  167. expect(d.toSymja(), 'Integrate[Null, x]');
  168. d.backspace(); // 空槽最左边 → 退回根槽,并把没填完的模板一起删掉
  169. expect(d.toSymja(), 'Null');
  170. });
  171. test('左边是没填完的模板:先进去,再退一次才删掉', () {
  172. final d = ExprDoc();
  173. d.makeFunction('Integrate', prefill: ['', 'x']);
  174. d.cursor = const SlotPath(<SlotStep>[], 1); // 根槽 offset 1 = 模板右边
  175. expect(d.toSymja(), 'Integrate[Null, x]');
  176. d.backspace(); // 钻进那个空槽,内容不变
  177. expect(d.toSymja(), 'Integrate[Null, x]');
  178. d.backspace(); // 退回根槽,顺手把没填完的模板删掉
  179. expect(d.toSymja(), 'Null');
  180. });
  181. test('填完参数的过程里模板不会被误删', () {
  182. final d = ExprDoc();
  183. d.makeFunction('Sin');
  184. d.insertChar('x');
  185. expect(d.toSymja(), 'Sin[x]');
  186. // 回到参数里,且把光标停在 x **之后**(offset 1)
  187. d.cursor = const SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
  188. d.backspace(); // 删参数里的 x
  189. expect(d.toSymja(), 'Sin[Null]');
  190. });
  191. test('左边是填完的模板:整个删掉(否则光标永远出不来)', () {
  192. final d = ExprDoc();
  193. d.makeFunction('Integrate', prefill: ['', 'x']);
  194. d.insertChar('x'); // Integrate[x, x],已经填完
  195. d.cursor = const SlotPath(<SlotStep>[], 1); // 模板右边
  196. expect(d.toSymja(), 'Integrate[x, x]');
  197. d.backspace();
  198. expect(d.toSymja(), 'Null');
  199. });
  200. });
  201. group('光标路径', () {
  202. test('插入带占位槽的节点后光标在正确的槽里', () {
  203. final d = ExprDoc();
  204. d.makeFraction();
  205. expect(d.cursor.steps.length, 1);
  206. expect(d.cursor.steps.first.slot, 1); // 分母
  207. expect(d.cursor.offset, 0);
  208. });
  209. test('parent 回到父槽并停在刚退出的那个孩子之后', () {
  210. final d = ExprDoc();
  211. d.makeFunction('Sin');
  212. d.insertChar('x');
  213. final up = d.cursor.parent;
  214. expect(up, isNotNull);
  215. expect(up!.steps, isEmpty);
  216. expect(up.offset, 1); // Sin 之后
  217. });
  218. });
  219. group('选区与剪切粘贴', () {
  220. ExprDoc twoXPlusOne() {
  221. final d = ExprDoc();
  222. d.insertChar('2');
  223. d.insert(LitNode('x'));
  224. d.insert(LitNode('+'));
  225. d.insertChar('1');
  226. return d;
  227. }
  228. test('选中一个节点', () {
  229. final d = twoXPlusOne();
  230. d.selectNodeAt(SlotPath.root, 0);
  231. expect(d.selectionText, '2');
  232. });
  233. test('扩展选区覆盖连续节点', () {
  234. final d = twoXPlusOne();
  235. d.selectNodeAt(SlotPath.root, 1); // x
  236. d.extendSelectionTo(SlotPath.root, 3); // 1
  237. expect(d.selectionText, 'x+1');
  238. });
  239. test('往左扩展也成立(start 取更小的那个)', () {
  240. final d = twoXPlusOne();
  241. d.selectNodeAt(SlotPath.root, 3);
  242. d.extendSelectionTo(SlotPath.root, 0);
  243. expect(d.selectionText, '2x+1');
  244. });
  245. test('剪切:返回文本、删掉内容、光标落到原位', () {
  246. final d = twoXPlusOne();
  247. d.selectNodeAt(SlotPath.root, 0);
  248. d.extendSelectionTo(SlotPath.root, 3);
  249. expect(d.selectionText, '2x+1');
  250. d.deleteSelection();
  251. expect(d.toSymja(), 'Null');
  252. expect(d.selection, isNull);
  253. expect(d.cursor.offset, 0);
  254. });
  255. test('只删中间一段,两边保留', () {
  256. final d = twoXPlusOne();
  257. d.cursor = const SlotPath(<SlotStep>[], 0);
  258. d.selectNodeAt(SlotPath.root, 1);
  259. // 选中 x 与 +
  260. d.extendSelectionTo(SlotPath.root, 2);
  261. expect(d.selectionText, 'x+');
  262. d.deleteSelection();
  263. expect(d.toSymja(), '21');
  264. });
  265. test('有选区时按退格 = 删掉选中的那段', () {
  266. final d = twoXPlusOne();
  267. d.selectNodeAt(SlotPath.root, 1); // x
  268. d.extendSelectionTo(SlotPath.root, 2); // +
  269. expect(d.selectionText, 'x+');
  270. d.backspace();
  271. expect(d.toSymja(), '21');
  272. expect(d.selection, isNull);
  273. });
  274. test('没有选区时退格还是老行为', () {
  275. final d = twoXPlusOne();
  276. d.backspace();
  277. expect(d.toSymja(), '2x+');
  278. });
  279. test('插入到光标处(粘贴进 Log 的括号里)', () {
  280. final d = ExprDoc();
  281. d.makeFunction('Log'); // Log[□],光标在参数槽里
  282. d.insertNodesAtCursor([LitNode('2'), LitNode('x'), LitNode('+'), LitNode('1')]);
  283. expect(d.toSymja(), 'Log[2x+1]');
  284. expect(d.cursor.offset, 4);
  285. });
  286. test('跨槽扩展会被忽略,不会串到别的槽', () {
  287. final d = ExprDoc();
  288. d.makeFunction('Log'); // 光标在 Log 的参数槽
  289. d.selectNodeAt(SlotPath.root, 0); // 选中根槽里的 Log
  290. d.extendSelectionTo(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0), 0);
  291. expect(d.selectionText, 'Log[Null]'); // 还是原来那个选区
  292. });
  293. });
  294. group('矩阵', () {
  295. test('花括号键造一对括号,光标在里面', () {
  296. final d = ExprDoc();
  297. d.makeBrace();
  298. expect(d.toSymja(), '{Null}');
  299. d.insertChar('1');
  300. expect(d.toSymja(), '{1}');
  301. });
  302. test('拼出一个 2x2 矩阵', () {
  303. final d = ExprDoc();
  304. d.makeBrace(); // 外层
  305. d.makeBrace(); // 第一行
  306. d.insertChar('1');
  307. d.insert(LitNode(','));
  308. d.insertChar('2');
  309. d.addMatrixRow(); // 换行
  310. d.insertChar('3');
  311. d.insert(LitNode(','));
  312. d.insertChar('4');
  313. expect(d.toSymja(), '{{1,2}, {3,4}}');
  314. });
  315. test('换行是结构操作:新行是花括号,不是两个字符', () {
  316. final d = ExprDoc();
  317. d.makeBrace();
  318. d.makeBrace();
  319. d.addMatrixRow();
  320. final outer = d.root.children.single as BraceNode;
  321. // 外层里应当是「行花括号、逗号、新行花括号」
  322. expect(outer.inner.children.whereType<BraceNode>().length, 2);
  323. expect(outer.inner.children.whereType<LitNode>().length, 1);
  324. });
  325. test('不在花括号里按换行键什么都不做', () {
  326. final d = ExprDoc();
  327. d.insertChar('1');
  328. d.addMatrixRow();
  329. expect(d.toSymja(), '1');
  330. });
  331. test('三层嵌套时换行加在**最外层**', () {
  332. final d = ExprDoc();
  333. d.makeBrace(); // 外层
  334. d.makeBrace(); // 行
  335. d.makeBrace(); // 行里再嵌一层
  336. d.addMatrixRow();
  337. final outer = d.root.children.single as BraceNode;
  338. expect(outer.inner.children.whereType<BraceNode>().length, 2,
  339. reason: '新行应该加到最外层,不是嵌套的那一层');
  340. });
  341. });
  342. group('光标槽判定', () {
  343. test('槽内位置不同,仍算同一个槽(渲染光标靠这个)', () {
  344. const a = SlotPath(<SlotStep>[], 0);
  345. const b = SlotPath(<SlotStep>[], 3);
  346. expect(a == b, isFalse); // == 比的是完整位置
  347. expect(a.sameSlotAs(b), isTrue); // sameSlotAs 只比槽
  348. });
  349. test('不同槽不算同一个', () {
  350. const a = SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
  351. const b = SlotPath(<SlotStep>[SlotStep(0, 1)], 1);
  352. expect(a.sameSlotAs(b), isFalse);
  353. });
  354. test('打一个字之后,光标仍被认为在根槽里', () {
  355. final d = ExprDoc();
  356. expect(d.cursor.sameSlotAs(SlotPath.root), isTrue);
  357. d.insertChar('x');
  358. expect(d.cursor.offset, 1);
  359. expect(d.cursor.sameSlotAs(SlotPath.root), isTrue,
  360. reason: 'offset 变了但槽没变,光标必须还画得出来');
  361. });
  362. test('进到函数参数里后,根槽不再算聚焦', () {
  363. final d = ExprDoc();
  364. d.makeFunction('Sin');
  365. expect(d.cursor.sameSlotAs(SlotPath.root), isFalse);
  366. expect(
  367. d.cursor.sameSlotAs(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0)),
  368. isTrue,
  369. );
  370. });
  371. });
  372. group('clear', () {
  373. test('清空后回到根槽', () {
  374. final d = ExprDoc();
  375. d.makeFunction('Integrate', prefill: ['', 'x']);
  376. d.clear();
  377. expect(d.toSymja(), 'Null');
  378. expect(d.cursor.steps, isEmpty);
  379. expect(d.root.children, isEmpty);
  380. });
  381. });
  382. }