terminal_search_test.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import 'dart:developer';
  2. import 'package:flutter_test/flutter_test.dart';
  3. import 'package:mockito/annotations.dart';
  4. import 'package:mockito/mockito.dart';
  5. import 'package:xterm/buffer/buffer.dart';
  6. import 'package:xterm/buffer/line/line.dart';
  7. import 'package:xterm/terminal/cursor.dart';
  8. import 'package:xterm/terminal/terminal_search.dart';
  9. import 'package:xterm/terminal/terminal_search_interaction.dart';
  10. import 'package:xterm/util/circular_list.dart';
  11. import 'package:xterm/util/unicode_v11.dart';
  12. import 'terminal_search_test.mocks.dart';
  13. class TerminalSearchTestCircularList extends CircularList<BufferLine> {
  14. TerminalSearchTestCircularList(int maxLines) : super(maxLines);
  15. }
  16. @GenerateMocks([
  17. TerminalSearchInteraction,
  18. Buffer,
  19. TerminalSearchTestCircularList,
  20. BufferLine
  21. ])
  22. void main() {
  23. group('Terminal Search Tests', () {
  24. test('Creation works', () {
  25. _TestFixture();
  26. });
  27. test('Doesn\'t trigger anything when not activated', () {
  28. final fixture = _TestFixture();
  29. verifyNoMoreInteractions(fixture.terminalSearchInteractionMock);
  30. final task = fixture.uut.createSearchTask('testsearch');
  31. task.pattern = "some test";
  32. task.isActive = false;
  33. task.searchResult;
  34. });
  35. test('Basic search works', () {
  36. final fixture = _TestFixture();
  37. fixture.expectTerminalSearchContent(['Simple Content']);
  38. final task = fixture.uut.createSearchTask('testsearch');
  39. task.isActive = true;
  40. task.pattern = 'content';
  41. task.options = TerminalSearchOptions(
  42. caseSensitive: false, matchWholeWord: false, useRegex: false);
  43. final result = task.searchResult;
  44. expect(result.allHits.length, 1);
  45. expect(result.allHits[0].startLineIndex, 0);
  46. expect(result.allHits[0].startIndex, 7);
  47. expect(result.allHits[0].endLineIndex, 0);
  48. expect(result.allHits[0].endIndex, 14);
  49. });
  50. test('Multiline search works', () {
  51. final fixture = _TestFixture();
  52. fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
  53. final task = fixture.uut.createSearchTask('testsearch');
  54. task.isActive = true;
  55. task.pattern = 'line';
  56. task.options = TerminalSearchOptions(
  57. caseSensitive: false, matchWholeWord: false, useRegex: false);
  58. final result = task.searchResult;
  59. expect(result.allHits.length, 1);
  60. expect(result.allHits[0].startLineIndex, 1);
  61. expect(result.allHits[0].startIndex, 7);
  62. expect(result.allHits[0].endLineIndex, 1);
  63. expect(result.allHits[0].endIndex, 11);
  64. });
  65. test('Emoji search works', () {
  66. final fixture = _TestFixture();
  67. fixture.expectBufferContentLine([
  68. '🍏',
  69. '🍎',
  70. '🍐',
  71. '🍊',
  72. '🍋',
  73. '🍌',
  74. '🍉',
  75. '🍇',
  76. '🍓',
  77. '🫐',
  78. '🍈',
  79. '🍒',
  80. '🍑'
  81. ]);
  82. final task = fixture.uut.createSearchTask('testsearch');
  83. task.isActive = true;
  84. task.pattern = '🍋';
  85. task.options = TerminalSearchOptions(
  86. caseSensitive: false, matchWholeWord: false, useRegex: false);
  87. final result = task.searchResult;
  88. expect(result.allHits.length, 1);
  89. expect(result.allHits[0].startLineIndex, 0);
  90. expect(result.allHits[0].startIndex, 8);
  91. expect(result.allHits[0].endLineIndex, 0);
  92. expect(result.allHits[0].endIndex, 10);
  93. });
  94. test('CJK search works', () {
  95. final fixture = _TestFixture();
  96. fixture.expectBufferContentLine(['こ', 'ん', 'に', 'ち', 'は', '世', '界']);
  97. final task = fixture.uut.createSearchTask('testsearch');
  98. task.isActive = true;
  99. task.pattern = 'は';
  100. task.options = TerminalSearchOptions(
  101. caseSensitive: false, matchWholeWord: false, useRegex: false);
  102. final result = task.searchResult;
  103. expect(result.allHits.length, 1);
  104. expect(result.allHits[0].startLineIndex, 0);
  105. expect(result.allHits[0].startIndex, 8);
  106. expect(result.allHits[0].endLineIndex, 0);
  107. expect(result.allHits[0].endIndex, 10);
  108. });
  109. test('Finding strings directly on line break works', () {
  110. final fixture = _TestFixture();
  111. fixture.expectTerminalSearchContent([
  112. 'The search hit is '.padRight(fixture.terminalWidth - 3) + 'spl',
  113. 'it over two lines',
  114. ]);
  115. final task = fixture.uut.createSearchTask('testsearch');
  116. task.isActive = true;
  117. task.pattern = 'split';
  118. task.options = TerminalSearchOptions(
  119. caseSensitive: false, matchWholeWord: false, useRegex: false);
  120. final result = task.searchResult;
  121. expect(result.allHits.length, 1);
  122. expect(result.allHits[0].startLineIndex, 0);
  123. expect(result.allHits[0].startIndex, 77);
  124. expect(result.allHits[0].endLineIndex, 1);
  125. expect(result.allHits[0].endIndex, 2);
  126. });
  127. });
  128. test('Option: case sensitivity works', () {
  129. final fixture = _TestFixture();
  130. fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
  131. final task = fixture.uut.createSearchTask('testsearch');
  132. task.isActive = true;
  133. task.pattern = 'line';
  134. task.options = TerminalSearchOptions(
  135. caseSensitive: true, matchWholeWord: false, useRegex: false);
  136. final result = task.searchResult;
  137. expect(result.allHits.length, 0);
  138. task.pattern = 'Line';
  139. final secondResult = task.searchResult;
  140. expect(secondResult.allHits.length, 1);
  141. expect(secondResult.allHits[0].startLineIndex, 1);
  142. expect(secondResult.allHits[0].startIndex, 7);
  143. expect(secondResult.allHits[0].endLineIndex, 1);
  144. expect(secondResult.allHits[0].endIndex, 11);
  145. });
  146. test('Option: whole word works', () {
  147. final fixture = _TestFixture();
  148. fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
  149. final task = fixture.uut.createSearchTask('testsearch');
  150. task.isActive = true;
  151. task.pattern = 'lin';
  152. task.options = TerminalSearchOptions(
  153. caseSensitive: false, matchWholeWord: true, useRegex: false);
  154. final result = task.searchResult;
  155. expect(result.allHits.length, 0);
  156. task.pattern = 'line';
  157. final secondResult = task.searchResult;
  158. expect(secondResult.allHits.length, 1);
  159. expect(secondResult.allHits[0].startLineIndex, 1);
  160. expect(secondResult.allHits[0].startIndex, 7);
  161. expect(secondResult.allHits[0].endLineIndex, 1);
  162. expect(secondResult.allHits[0].endIndex, 11);
  163. });
  164. test('Option: regex works', () {
  165. final fixture = _TestFixture();
  166. fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
  167. final task = fixture.uut.createSearchTask('testsearch');
  168. task.isActive = true;
  169. task.options = TerminalSearchOptions(
  170. caseSensitive: false, matchWholeWord: false, useRegex: true);
  171. task.pattern =
  172. r'(^|\s)\w{4}($|\s)'; // match exactly 4 characters (and the whitespace before and/or after)
  173. final secondResult = task.searchResult;
  174. expect(secondResult.allHits.length, 1);
  175. expect(secondResult.allHits[0].startLineIndex, 1);
  176. expect(secondResult.allHits[0].startIndex, 6);
  177. expect(secondResult.allHits[0].endLineIndex, 1);
  178. expect(secondResult.allHits[0].endIndex, 12);
  179. });
  180. test('Retrigger search when a BufferLine got dirty works', () {
  181. final fixture = _TestFixture();
  182. fixture.expectTerminalSearchContent(
  183. ['Simple Content', 'Second Line', 'Third row']);
  184. final task = fixture.uut.createSearchTask('testsearch');
  185. task.isActive = true;
  186. task.options = TerminalSearchOptions(
  187. caseSensitive: false, matchWholeWord: false, useRegex: false);
  188. task.pattern = 'line';
  189. final result = task.searchResult;
  190. expect(result.allHits.length, 1);
  191. // overwrite expectations, nothing dirty => no new search
  192. fixture.expectTerminalSearchContent(
  193. ['Simple Content', 'Second Line', 'Third line'],
  194. isSearchStringCached: true);
  195. task.isActive = false;
  196. task.isActive = true;
  197. final secondResult = task.searchResult;
  198. expect(secondResult.allHits.length,
  199. 1); // nothing was dirty => we get the cached search result
  200. // overwrite expectations, one line is dirty => new search
  201. fixture.expectTerminalSearchContent(
  202. ['Simple Content', 'Second Line', 'Third line'],
  203. isSearchStringCached: false,
  204. dirtyIndices: [1]);
  205. final thirdResult = task.searchResult;
  206. expect(thirdResult.allHits.length,
  207. 2); //search has happened again so the new content is found
  208. // overwrite expectations, content has changed => new search
  209. fixture.expectTerminalSearchContent(
  210. ['First line', 'Second Line', 'Third line'],
  211. isSearchStringCached: false,
  212. dirtyIndices: [0]);
  213. final fourthResult = task.searchResult;
  214. expect(fourthResult.allHits.length,
  215. 3); //search has happened again so the new content is found
  216. });
  217. test('Handles regex special characters in non regex mode correctly', () {
  218. final fixture = _TestFixture();
  219. fixture.expectTerminalSearchContent(['Simple Content', 'Second Line.\\{']);
  220. final task = fixture.uut.createSearchTask('testsearch');
  221. task.isActive = true;
  222. task.pattern = 'line.\\{';
  223. task.options = TerminalSearchOptions(
  224. caseSensitive: false, matchWholeWord: false, useRegex: false);
  225. final result = task.searchResult;
  226. expect(result.allHits.length, 1);
  227. expect(result.allHits[0].startLineIndex, 1);
  228. expect(result.allHits[0].startIndex, 7);
  229. expect(result.allHits[0].endLineIndex, 1);
  230. expect(result.allHits[0].endIndex, 14);
  231. });
  232. test('TerminalWidth change leads to retriggering search', () {
  233. final fixture = _TestFixture();
  234. fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
  235. final task = fixture.uut.createSearchTask('testsearch');
  236. task.isActive = true;
  237. task.pattern = 'line';
  238. task.options = TerminalSearchOptions(
  239. caseSensitive: false, matchWholeWord: false, useRegex: false);
  240. final result = task.searchResult;
  241. expect(result.allHits.length, 1);
  242. // change data to detect a search re-run
  243. fixture.expectTerminalSearchContent(
  244. ['First line', 'Second Line']); //has 2 hits
  245. task.isActive = false;
  246. task.isActive = true;
  247. final secondResult = task.searchResult;
  248. expect(
  249. secondResult.allHits.length, 1); //nothing changed so the cache is used
  250. fixture.terminalWidth = 79;
  251. task.isActive = false;
  252. task.isActive = true;
  253. final thirdResult = task.searchResult;
  254. //we changed the terminal width which triggered a re-run of the search
  255. expect(thirdResult.allHits.length, 2);
  256. });
  257. }
  258. class _TestFixture {
  259. _TestFixture({
  260. terminalWidth = 80,
  261. }) : _terminalWidth = terminalWidth {
  262. uut = TerminalSearch(terminalSearchInteractionMock);
  263. when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth);
  264. }
  265. int _terminalWidth;
  266. int get terminalWidth => _terminalWidth;
  267. void set terminalWidth(int terminalWidth) {
  268. _terminalWidth = terminalWidth;
  269. when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth);
  270. }
  271. void expectBufferContentLine(
  272. List<String> cellData, {
  273. isUsingAltBuffer = false,
  274. }) {
  275. final buffer = _getBufferFromCellData(cellData);
  276. when(terminalSearchInteractionMock.buffer).thenReturn(buffer);
  277. when(terminalSearchInteractionMock.isUsingAltBuffer())
  278. .thenReturn(isUsingAltBuffer);
  279. }
  280. void expectTerminalSearchContent(
  281. List<String> lines, {
  282. isUsingAltBuffer = false,
  283. isSearchStringCached = true,
  284. List<int>? dirtyIndices,
  285. }) {
  286. final buffer = _getBuffer(lines,
  287. isCached: isSearchStringCached, dirtyIndices: dirtyIndices);
  288. when(terminalSearchInteractionMock.buffer).thenReturn(buffer);
  289. when(terminalSearchInteractionMock.isUsingAltBuffer())
  290. .thenReturn(isUsingAltBuffer);
  291. }
  292. final terminalSearchInteractionMock = MockTerminalSearchInteraction();
  293. late final TerminalSearch uut;
  294. MockBuffer _getBufferFromCellData(List<String> cellData) {
  295. final result = MockBuffer();
  296. final circularList = MockTerminalSearchTestCircularList();
  297. when(result.lines).thenReturn(circularList);
  298. when(circularList[0]).thenReturn(_getBufferLineFromData(cellData));
  299. when(circularList.length).thenReturn(1);
  300. return result;
  301. }
  302. MockBuffer _getBuffer(
  303. List<String> lines, {
  304. isCached = true,
  305. List<int>? dirtyIndices,
  306. }) {
  307. final result = MockBuffer();
  308. final circularList = MockTerminalSearchTestCircularList();
  309. when(result.lines).thenReturn(circularList);
  310. final bufferLines = _getBufferLinesWithSearchContent(
  311. lines,
  312. isCached: isCached,
  313. dirtyIndices: dirtyIndices,
  314. );
  315. when(circularList[any]).thenAnswer(
  316. (realInvocation) => bufferLines[realInvocation.positionalArguments[0]]);
  317. when(circularList.length).thenReturn(bufferLines.length);
  318. return result;
  319. }
  320. BufferLine _getBufferLineFromData(List<String> cellData) {
  321. final result = BufferLine(length: _terminalWidth);
  322. int currentIndex = 0;
  323. for (var data in cellData) {
  324. final codePoint = data.runes.first;
  325. final width = unicodeV11.wcwidth(codePoint);
  326. result.cellInitialize(
  327. currentIndex,
  328. content: codePoint,
  329. width: width,
  330. cursor: Cursor(bg: 0, fg: 0, flags: 0),
  331. );
  332. currentIndex++;
  333. for (int i = 1; i < width; i++) {
  334. result.cellInitialize(
  335. currentIndex,
  336. content: 0,
  337. width: 0,
  338. cursor: Cursor(bg: 0, fg: 0, flags: 0),
  339. );
  340. currentIndex++;
  341. }
  342. }
  343. return result;
  344. }
  345. List<MockBufferLine> _getBufferLinesWithSearchContent(
  346. List<String> content, {
  347. isCached = true,
  348. List<int>? dirtyIndices,
  349. }) {
  350. final result = List<MockBufferLine>.empty(growable: true);
  351. for (int i = 0; i < content.length; i++) {
  352. final bl = MockBufferLine();
  353. when(bl.hasCachedSearchString).thenReturn(isCached);
  354. when(bl.toSearchString(any)).thenReturn(content[i]);
  355. if (dirtyIndices?.contains(i) ?? false) {
  356. when(bl.isTagDirty(any)).thenReturn(true);
  357. } else {
  358. when(bl.isTagDirty(any)).thenReturn(false);
  359. }
  360. result.add(bl);
  361. }
  362. return result;
  363. }
  364. }