expr_node_test.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. // Symja 里 xn 是"叫 xn 的变量",不是 x*n;PiE 同理
  37. final d = ExprDoc();
  38. d.insert(LitNode('x'));
  39. d.insert(LitNode('n'));
  40. expect(d.toSymja(), 'x*n');
  41. final p = ExprDoc();
  42. p.insert(LitNode('Pi'));
  43. p.insert(LitNode('E'));
  44. expect(p.toSymja(), 'Pi*E');
  45. });
  46. test('字母后面接数字也要补乘号', () {
  47. final d = ExprDoc();
  48. d.insert(LitNode('x'));
  49. d.insertChar('2');
  50. expect(d.toSymja(), 'x*2');
  51. });
  52. test('数字后面接字母保留 2x 的写法(Mathematica 认它是乘法)', () {
  53. final d = ExprDoc();
  54. d.insertChar('2');
  55. d.insert(LitNode('x'));
  56. expect(d.toSymja(), '2x');
  57. });
  58. test('运算符两侧不补乘号', () {
  59. final d = docWith([LitNode('x'), LitNode('+'), LitNode('1')]);
  60. expect(d.toSymja(), 'x+1');
  61. });
  62. test('函数调用后面接字母不补(Symja 认它是乘法)', () {
  63. final d = ExprDoc();
  64. d.makeFunction('Sin');
  65. d.insertChar('x'); // 光标退回根槽前先把参数填好
  66. d.cursor = const SlotPath(<SlotStep>[], 1);
  67. d.insert(LitNode('y'));
  68. expect(d.toSymja(), 'Sin[x]y');
  69. });
  70. test('数字后面的字母不会被并进去', () {
  71. final d = ExprDoc();
  72. d.insertChar('2');
  73. d.insertChar('x');
  74. expect(d.root.children.length, 2);
  75. expect(d.toSymja(), '2x');
  76. });
  77. test('空槽序列化成 Null(让求值明确报错,而不是静默算错)', () {
  78. final d = ExprDoc();
  79. expect(d.toSymja(), 'Null');
  80. });
  81. });
  82. group('幂', () {
  83. test('先打 x 再按 ^ ,x 成为底、光标落到指数', () {
  84. final d = docWith([LitNode('x')]);
  85. d.makePower();
  86. d.insertChar('2');
  87. expect(d.toSymja(), 'x^2');
  88. });
  89. test('没有左边的节点时造一个空底的幂', () {
  90. final d = ExprDoc();
  91. d.makePower();
  92. d.insertChar('2');
  93. expect(d.toSymja(), 'Null^2');
  94. });
  95. test('底是复合表达式时要加括号', () {
  96. final d = ExprDoc();
  97. d.insert(PowNode(
  98. base: SeqNode([LitNode('x'), LitNode('+'), LitNode('1')]),
  99. exp: SeqNode([LitNode('2')]),
  100. ));
  101. expect(d.toSymja(), '(x+1)^2');
  102. });
  103. test('底是单个字面量时不加多余括号', () {
  104. final d = ExprDoc();
  105. d.insert(PowNode(base: SeqNode([LitNode('x')]), exp: SeqNode([LitNode('2')])));
  106. expect(d.toSymja(), 'x^2');
  107. });
  108. });
  109. group('分式与括号', () {
  110. test('1 再按 / 再打 3 得 1/3', () {
  111. final d = ExprDoc();
  112. d.insertChar('1');
  113. d.makeFraction();
  114. d.insertChar('3');
  115. expect(d.toSymja(), '1/3');
  116. });
  117. test('分母是 x+1 时要加括号', () {
  118. final d = ExprDoc();
  119. d.insertChar('1');
  120. d.makeFraction();
  121. d.insertChar('x');
  122. d.insertChar('+');
  123. d.insertChar('1');
  124. expect(d.toSymja(), '1/(x+1)');
  125. });
  126. test('括号分组', () {
  127. final d = ExprDoc();
  128. d.insertChar('2');
  129. d.insertChar('*');
  130. d.makeGroup();
  131. d.insertChar('x');
  132. d.insertChar('+');
  133. d.insertChar('1');
  134. expect(d.toSymja(), '2*(x+1)');
  135. });
  136. });
  137. group('函数模板', () {
  138. test('空的 prefill 就是空槽,光标落在第一个槽', () {
  139. final d = ExprDoc();
  140. d.makeFunction('Integrate', prefill: ['', 'x']);
  141. expect(d.toSymja(), 'Integrate[Null, x]');
  142. // 光标应当在第一个参数里,所以接着打的字进那个槽
  143. d.insertChar('x');
  144. expect(d.toSymja(), 'Integrate[x, x]');
  145. });
  146. test('填出 Integrate[x^2, x]', () {
  147. final d = ExprDoc();
  148. d.makeFunction('Integrate', prefill: ['', 'x']);
  149. d.insertChar('x');
  150. d.makePower();
  151. d.insertChar('2');
  152. expect(d.toSymja(), 'Integrate[x^2, x]');
  153. });
  154. test('无参模板', () {
  155. final d = ExprDoc();
  156. d.makeFunction('Sin');
  157. expect(d.toSymja(), 'Sin[Null]');
  158. d.insertChar('x');
  159. expect(d.toSymja(), 'Sin[x]');
  160. });
  161. test('逗号切到下一个参数,没有就新建', () {
  162. final d = ExprDoc();
  163. d.makeFunction('f');
  164. d.insertChar('1');
  165. d.nextArgument();
  166. d.insertChar('2');
  167. expect(d.toSymja(), 'f[1, 2]');
  168. });
  169. test('根号键插空模板,往里填即得', () {
  170. final d = ExprDoc();
  171. d.makeFunction('Sqrt');
  172. expect(d.toSymja(), 'Sqrt[Null]');
  173. d.insertChar('2');
  174. expect(d.toSymja(), 'Sqrt[2]');
  175. });
  176. });
  177. group('退格', () {
  178. test('删掉左边那个字面量', () {
  179. final d = docWith([LitNode('1'), LitNode('2')]);
  180. d.backspace();
  181. expect(d.toSymja(), '1');
  182. });
  183. test('光标在最左边时按退格什么都不发生', () {
  184. final d = docWith([LitNode('1')]);
  185. d.cursor = SlotPath.root; // 手动把光标移到最左
  186. d.backspace();
  187. expect(d.toSymja(), '1');
  188. });
  189. test('数字逐位删,不是整个数清掉', () {
  190. final d = ExprDoc();
  191. d.insertChar('1');
  192. d.insertChar('2');
  193. d.insertChar('3');
  194. d.backspace();
  195. expect(d.toSymja(), '12');
  196. d.backspace();
  197. expect(d.toSymja(), '1');
  198. });
  199. test('住进空槽里退格:先退回父槽,顺手把没填完的模板也删掉', () {
  200. final d = ExprDoc();
  201. d.makeFunction('Integrate', prefill: ['', 'x']);
  202. expect(d.toSymja(), 'Integrate[Null, x]');
  203. d.backspace(); // 空槽最左边 → 退回根槽,并把没填完的模板一起删掉
  204. expect(d.toSymja(), 'Null');
  205. });
  206. test('左边是没填完的模板:先进去,再退一次才删掉', () {
  207. final d = ExprDoc();
  208. d.makeFunction('Integrate', prefill: ['', 'x']);
  209. d.cursor = const SlotPath(<SlotStep>[], 1); // 根槽 offset 1 = 模板右边
  210. expect(d.toSymja(), 'Integrate[Null, x]');
  211. d.backspace(); // 钻进那个空槽,内容不变
  212. expect(d.toSymja(), 'Integrate[Null, x]');
  213. d.backspace(); // 退回根槽,顺手把没填完的模板删掉
  214. expect(d.toSymja(), 'Null');
  215. });
  216. test('填完参数的过程里模板不会被误删', () {
  217. final d = ExprDoc();
  218. d.makeFunction('Sin');
  219. d.insertChar('x');
  220. expect(d.toSymja(), 'Sin[x]');
  221. // 回到参数里,且把光标停在 x **之后**(offset 1)
  222. d.cursor = const SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
  223. d.backspace(); // 删参数里的 x
  224. expect(d.toSymja(), 'Sin[Null]');
  225. });
  226. test('左边是填完的模板:整个删掉(否则光标永远出不来)', () {
  227. final d = ExprDoc();
  228. d.makeFunction('Integrate', prefill: ['', 'x']);
  229. d.insertChar('x'); // Integrate[x, x],已经填完
  230. d.cursor = const SlotPath(<SlotStep>[], 1); // 模板右边
  231. expect(d.toSymja(), 'Integrate[x, x]');
  232. d.backspace();
  233. expect(d.toSymja(), 'Null');
  234. });
  235. });
  236. group('光标路径', () {
  237. test('插入带占位槽的节点后光标在正确的槽里', () {
  238. final d = ExprDoc();
  239. d.makeFraction();
  240. expect(d.cursor.steps.length, 1);
  241. expect(d.cursor.steps.first.slot, 1); // 分母
  242. expect(d.cursor.offset, 0);
  243. });
  244. test('parent 回到父槽并停在刚退出的那个孩子之后', () {
  245. final d = ExprDoc();
  246. d.makeFunction('Sin');
  247. d.insertChar('x');
  248. final up = d.cursor.parent;
  249. expect(up, isNotNull);
  250. expect(up!.steps, isEmpty);
  251. expect(up.offset, 1); // Sin 之后
  252. });
  253. });
  254. group('选区与剪切粘贴', () {
  255. ExprDoc twoXPlusOne() {
  256. final d = ExprDoc();
  257. d.insertChar('2');
  258. d.insert(LitNode('x'));
  259. d.insert(LitNode('+'));
  260. d.insertChar('1');
  261. return d;
  262. }
  263. test('选中一个节点', () {
  264. final d = twoXPlusOne();
  265. d.selectNodeAt(SlotPath.root, 0);
  266. expect(d.selectionText, '2');
  267. });
  268. test('扩展选区覆盖连续节点', () {
  269. final d = twoXPlusOne();
  270. d.selectNodeAt(SlotPath.root, 1); // x
  271. d.extendSelectionTo(SlotPath.root, 3); // 1
  272. expect(d.selectionText, 'x+1');
  273. });
  274. test('往左扩展也成立(start 取更小的那个)', () {
  275. final d = twoXPlusOne();
  276. d.selectNodeAt(SlotPath.root, 3);
  277. d.extendSelectionTo(SlotPath.root, 0);
  278. expect(d.selectionText, '2x+1');
  279. });
  280. test('剪切:返回文本、删掉内容、光标落到原位', () {
  281. final d = twoXPlusOne();
  282. d.selectNodeAt(SlotPath.root, 0);
  283. d.extendSelectionTo(SlotPath.root, 3);
  284. expect(d.selectionText, '2x+1');
  285. d.deleteSelection();
  286. expect(d.toSymja(), 'Null');
  287. expect(d.selection, isNull);
  288. expect(d.cursor.offset, 0);
  289. });
  290. test('只删中间一段,两边保留', () {
  291. final d = twoXPlusOne();
  292. d.cursor = const SlotPath(<SlotStep>[], 0);
  293. d.selectNodeAt(SlotPath.root, 1);
  294. // 选中 x 与 +
  295. d.extendSelectionTo(SlotPath.root, 2);
  296. expect(d.selectionText, 'x+');
  297. d.deleteSelection();
  298. expect(d.toSymja(), '21');
  299. });
  300. test('有选区时按退格 = 删掉选中的那段', () {
  301. final d = twoXPlusOne();
  302. d.selectNodeAt(SlotPath.root, 1); // x
  303. d.extendSelectionTo(SlotPath.root, 2); // +
  304. expect(d.selectionText, 'x+');
  305. d.backspace();
  306. expect(d.toSymja(), '21');
  307. expect(d.selection, isNull);
  308. });
  309. test('没有选区时退格还是老行为', () {
  310. final d = twoXPlusOne();
  311. d.backspace();
  312. expect(d.toSymja(), '2x+');
  313. });
  314. test('插入到光标处(粘贴进 Log 的括号里)', () {
  315. final d = ExprDoc();
  316. d.makeFunction('Log'); // Log[□],光标在参数槽里
  317. d.insertNodesAtCursor([LitNode('2'), LitNode('x'), LitNode('+'), LitNode('1')]);
  318. expect(d.toSymja(), 'Log[2x+1]');
  319. expect(d.cursor.offset, 4);
  320. });
  321. test('跨槽扩展会被忽略,不会串到别的槽', () {
  322. final d = ExprDoc();
  323. d.makeFunction('Log'); // 光标在 Log 的参数槽
  324. d.selectNodeAt(SlotPath.root, 0); // 选中根槽里的 Log
  325. d.extendSelectionTo(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0), 0);
  326. expect(d.selectionText, 'Log[Null]'); // 还是原来那个选区
  327. });
  328. });
  329. group('矩阵', () {
  330. test('花括号键造一对括号,光标在里面', () {
  331. final d = ExprDoc();
  332. d.makeBrace();
  333. expect(d.toSymja(), '{Null}');
  334. d.insertChar('1');
  335. expect(d.toSymja(), '{1}');
  336. });
  337. test('花括号里能直接打逗号(矩阵行分隔)', () {
  338. final d = ExprDoc();
  339. d.makeBrace();
  340. d.insertChar('1');
  341. d.insert(LitNode(','));
  342. d.insertChar('2');
  343. expect(d.toSymja(), '{1,2}');
  344. });
  345. });
  346. group('光标槽判定', () {
  347. test('槽内位置不同,仍算同一个槽(渲染光标靠这个)', () {
  348. const a = SlotPath(<SlotStep>[], 0);
  349. const b = SlotPath(<SlotStep>[], 3);
  350. expect(a == b, isFalse); // == 比的是完整位置
  351. expect(a.sameSlotAs(b), isTrue); // sameSlotAs 只比槽
  352. });
  353. test('不同槽不算同一个', () {
  354. const a = SlotPath(<SlotStep>[SlotStep(0, 0)], 1);
  355. const b = SlotPath(<SlotStep>[SlotStep(0, 1)], 1);
  356. expect(a.sameSlotAs(b), isFalse);
  357. });
  358. test('打一个字之后,光标仍被认为在根槽里', () {
  359. final d = ExprDoc();
  360. expect(d.cursor.sameSlotAs(SlotPath.root), isTrue);
  361. d.insertChar('x');
  362. expect(d.cursor.offset, 1);
  363. expect(d.cursor.sameSlotAs(SlotPath.root), isTrue,
  364. reason: 'offset 变了但槽没变,光标必须还画得出来');
  365. });
  366. test('进到函数参数里后,根槽不再算聚焦', () {
  367. final d = ExprDoc();
  368. d.makeFunction('Sin');
  369. expect(d.cursor.sameSlotAs(SlotPath.root), isFalse);
  370. expect(
  371. d.cursor.sameSlotAs(const SlotPath(<SlotStep>[SlotStep(0, 0)], 0)),
  372. isTrue,
  373. );
  374. });
  375. });
  376. group('clear', () {
  377. test('清空后回到根槽', () {
  378. final d = ExprDoc();
  379. d.makeFunction('Integrate', prefill: ['', 'x']);
  380. d.clear();
  381. expect(d.toSymja(), 'Null');
  382. expect(d.cursor.steps, isEmpty);
  383. expect(d.root.children, isEmpty);
  384. });
  385. });
  386. }