Jelajahi Sumber

Adds some tests for the search functionality

devmil 4 tahun lalu
induk
melakukan
f03b6724c5

+ 13 - 6
example/pubspec.lock

@@ -14,7 +14,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.6.1"
+    version: "2.8.2"
   boolean_selector:
     dependency: transitive
     description:
@@ -35,7 +35,7 @@ packages:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.3.1"
   clock:
     dependency: transitive
     description:
@@ -73,6 +73,13 @@ packages:
       url: "https://github.com/TerminalStudio/dartssh"
     source: git
     version: "1.0.4+4"
+  equatable:
+    dependency: transitive
+    description:
+      name: equatable
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
   fake_async:
     dependency: transitive
     description:
@@ -117,14 +124,14 @@ packages:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.10"
+    version: "0.12.11"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.7.0"
   path:
     dependency: transitive
     description:
@@ -206,7 +213,7 @@ packages:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.3.0"
+    version: "0.4.3"
   tweetnacl:
     dependency: transitive
     description:
@@ -243,7 +250,7 @@ packages:
       path: ".."
       relative: true
     source: path
-    version: "2.3.0-pre"
+    version: "2.5.0-pre"
 sdks:
   dart: ">=2.12.0 <3.0.0"
   flutter: ">=2.0.0"

+ 4 - 1
lib/terminal/terminal.dart

@@ -18,6 +18,7 @@ import 'package:xterm/terminal/sbc.dart';
 import 'package:xterm/terminal/tabs.dart';
 import 'package:xterm/terminal/terminal_backend.dart';
 import 'package:xterm/terminal/terminal_search.dart';
+import 'package:xterm/terminal/terminal_search_interaction.dart';
 import 'package:xterm/terminal/terminal_ui_interaction.dart';
 import 'package:xterm/theme/terminal_color.dart';
 import 'package:xterm/theme/terminal_theme.dart';
@@ -35,7 +36,9 @@ void _defaultBellHandler() {}
 void _defaultTitleHandler(String _) {}
 void _defaultIconHandler(String _) {}
 
-class Terminal with Observable implements TerminalUiInteraction {
+class Terminal
+    with Observable
+    implements TerminalUiInteraction, TerminalSearchInteraction {
   Terminal({
     this.backend,
     this.onBell = _defaultBellHandler,

+ 4 - 5
lib/terminal/terminal_search.dart

@@ -1,6 +1,6 @@
 import 'package:equatable/equatable.dart';
 import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/terminal/terminal.dart';
+import 'package:xterm/terminal/terminal_search_interaction.dart';
 import 'package:xterm/util/constants.dart';
 
 /// Represents a search result.
@@ -129,7 +129,7 @@ class TerminalSearchTask {
       this._terminalSearchOptions);
 
   final TerminalSearch _search;
-  final Terminal _terminal;
+  final TerminalSearchInteraction _terminal;
   String? _pattern = null;
   bool _isPatternDirty = true;
   RegExp? _searchRegexp = null;
@@ -257,8 +257,7 @@ class TerminalSearchTask {
       if (!_terminalSearchOptions.useRegex) {
         pattern = RegExp.escape(_pattern!);
       }
-      final regex = '(?<hit>$pattern)';
-      _searchRegexp = RegExp(regex,
+      _searchRegexp = RegExp(pattern,
           caseSensitive: _terminalSearchOptions.caseSensitive,
           multiLine: false);
     }
@@ -315,7 +314,7 @@ class TerminalSearchTask {
 class TerminalSearch {
   TerminalSearch(this._terminal);
 
-  final Terminal _terminal;
+  final TerminalSearchInteraction _terminal;
   String? _cachedSearchString;
   int? _lastTerminalWidth;
 

+ 14 - 0
lib/terminal/terminal_search_interaction.dart

@@ -0,0 +1,14 @@
+import 'package:xterm/buffer/buffer.dart';
+
+/// This interface defines the functionality of a Terminal that is needed
+/// by the search functionality
+abstract class TerminalSearchInteraction {
+  /// the current buffer
+  Buffer get buffer;
+
+  /// indication if the alternative buffer is currently used
+  bool isUsingAltBuffer();
+
+  /// the terminal width
+  int get terminalWidth;
+}

+ 299 - 5
pubspec.lock

@@ -1,13 +1,34 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "24.0.0"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
   async:
     dependency: transitive
     description:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.7.0"
+    version: "2.8.2"
   boolean_selector:
     dependency: transitive
     description:
@@ -15,6 +36,62 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  build:
+    dependency: transitive
+    description:
+      name: build
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  build_config:
+    dependency: transitive
+    description:
+      name: build_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  build_daemon:
+    dependency: transitive
+    description:
+      name: build_daemon
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  build_resolvers:
+    dependency: transitive
+    description:
+      name: build_resolvers
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  build_runner:
+    dependency: "direct dev"
+    description:
+      name: build_runner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  build_runner_core:
+    dependency: transitive
+    description:
+      name: build_runner_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "7.1.0"
+  built_collection:
+    dependency: transitive
+    description:
+      name: built_collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
+  built_value:
+    dependency: transitive
+    description:
+      name: built_value
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.1.2"
   characters:
     dependency: transitive
     description:
@@ -28,7 +105,21 @@ packages:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.3.1"
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  cli_util:
+    dependency: transitive
+    description:
+      name: cli_util
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.3"
   clock:
     dependency: transitive
     description:
@@ -36,6 +127,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.1.0"
+  code_builder:
+    dependency: transitive
+    description:
+      name: code_builder
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.0"
   collection:
     dependency: transitive
     description:
@@ -50,6 +148,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
   equatable:
     dependency: "direct main"
     description:
@@ -64,6 +176,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.2.0"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.2"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -74,20 +200,104 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  graphs:
+    dependency: transitive
+    description:
+      name: graphs
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.0"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.3"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.10"
+    version: "0.12.11"
   meta:
     dependency: "direct main"
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.4.0"
+    version: "1.7.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  mockito:
+    dependency: "direct dev"
+    description:
+      name: mockito
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.0.15"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   path:
     dependency: transitive
     description:
@@ -95,6 +305,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.0"
+  pedantic:
+    dependency: transitive
+    description:
+      name: pedantic
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.11.1"
   platform_info:
     dependency: "direct main"
     description:
@@ -102,6 +319,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.0-nullsafety.1"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.0"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   quiver:
     dependency: "direct main"
     description:
@@ -109,11 +347,32 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.0"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   sky_engine:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.99"
+  source_gen:
+    dependency: transitive
+    description:
+      name: source_gen
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
   source_span:
     dependency: transitive
     description:
@@ -135,6 +394,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   string_scanner:
     dependency: transitive
     description:
@@ -155,7 +421,14 @@ packages:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.0"
+    version: "0.4.3"
+  timing:
+    dependency: transitive
+    description:
+      name: timing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   typed_data:
     dependency: transitive
     description:
@@ -170,6 +443,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
 sdks:
   dart: ">=2.12.0 <3.0.0"
   flutter: ">=2.0.0"

+ 2 - 0
pubspec.yaml

@@ -19,6 +19,8 @@ dependencies:
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  mockito: ^5.0.15
+  build_runner: ^2.1.1
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec

+ 260 - 0
test/terminal/terminal_search_test.dart

@@ -0,0 +1,260 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/line/line.dart';
+import 'package:xterm/terminal/terminal_search.dart';
+import 'package:xterm/terminal/terminal_search_interaction.dart';
+import 'package:xterm/util/circular_list.dart';
+
+import 'terminal_search_test.mocks.dart';
+
+class TerminalSearchTestCircularList extends CircularList<BufferLine> {
+  TerminalSearchTestCircularList(int maxLines) : super(maxLines);
+}
+
+@GenerateMocks([
+  TerminalSearchInteraction,
+  Buffer,
+  TerminalSearchTestCircularList,
+  BufferLine
+])
+void main() {
+  group('Terminal Search Tests', () {
+    test('Creation works', () {});
+
+    test('Doesn\'t trigger anything when not activated', () {
+      final fixture = _TestFixture();
+      verifyNoMoreInteractions(fixture.terminalSearchInteractionMock);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.pattern = "some test";
+      task.isActive = false;
+      task.searchResult;
+    });
+
+    test('Basic search works', () {
+      final fixture = _TestFixture();
+      fixture.expectTerminalSearchContent(['Simple Content']);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'content';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 0);
+      expect(result.allHits[0].startIndex, 7);
+      expect(result.allHits[0].endLineIndex, 0);
+      expect(result.allHits[0].endIndex, 14);
+    });
+
+    test('Multiline search works', () {
+      final fixture = _TestFixture();
+      fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'line';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 1);
+      expect(result.allHits[0].startIndex, 7);
+      expect(result.allHits[0].endLineIndex, 1);
+      expect(result.allHits[0].endIndex, 11);
+    });
+
+    test('Finding strings directly on line break works', () {
+      final fixture = _TestFixture();
+      fixture.expectTerminalSearchContent([
+        'The search hit is '.padRight(fixture.terminalWidth - 3) + 'spl',
+        'it over two lines',
+      ]);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'split';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 0);
+      expect(result.allHits[0].startIndex, 77);
+      expect(result.allHits[0].endLineIndex, 1);
+      expect(result.allHits[0].endIndex, 2);
+    });
+  });
+
+  test('Option: case sensitivity works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.pattern = 'line';
+    task.options = TerminalSearchOptions(
+        caseSensitive: true, matchWholeWord: false, useRegex: false);
+
+    final result = task.searchResult;
+    expect(result.allHits.length, 0);
+
+    task.pattern = 'Line';
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length, 1);
+    expect(secondResult.allHits[0].startLineIndex, 1);
+    expect(secondResult.allHits[0].startIndex, 7);
+    expect(secondResult.allHits[0].endLineIndex, 1);
+    expect(secondResult.allHits[0].endIndex, 11);
+  });
+
+  test('Option: whole word works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.pattern = 'lin';
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: true, useRegex: false);
+
+    final result = task.searchResult;
+    expect(result.allHits.length, 0);
+
+    task.pattern = 'line';
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length, 1);
+    expect(secondResult.allHits[0].startLineIndex, 1);
+    expect(secondResult.allHits[0].startIndex, 7);
+    expect(secondResult.allHits[0].endLineIndex, 1);
+    expect(secondResult.allHits[0].endIndex, 11);
+  });
+
+  test('Option: regex works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: false, useRegex: true);
+
+    task.pattern =
+        r'(^|\s)\w{4}($|\s)'; // match exactly 4 characters (and the whitespace before and/or after)
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length, 1);
+    expect(secondResult.allHits[0].startLineIndex, 1);
+    expect(secondResult.allHits[0].startIndex, 6);
+    expect(secondResult.allHits[0].endLineIndex, 1);
+    expect(secondResult.allHits[0].endIndex, 12);
+  });
+
+  test('Retrigger search when a BufferLine got dirty works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(
+        ['Simple Content', 'Second Line', 'Third row']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: false, useRegex: false);
+
+    task.pattern = 'line';
+    final result = task.searchResult;
+    expect(result.allHits.length, 1);
+
+    // overwrite expectations, nothing dirty => no new search
+    fixture.expectTerminalSearchContent(
+        ['Simple Content', 'Second Line', 'Third line'],
+        isSearchStringCached: false);
+
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length,
+        1); // nothing was dirty => we get the cached search result
+
+    // overwrite expectations, one line is dirty => new search
+    fixture.expectTerminalSearchContent(
+        ['Simple Content', 'Second Line', 'Third line'],
+        isSearchStringCached: false,
+        dirtyIndices: [1]);
+
+    final thirdResult = task.searchResult;
+    expect(thirdResult.allHits.length,
+        2); //search has happened again so the new content is found
+
+    // overwrite expectations, content has changed => new search
+    fixture.expectTerminalSearchContent(
+        ['First line', 'Second Line', 'Third line'],
+        isSearchStringCached: false,
+        dirtyIndices: [0]);
+
+    final fourthResult = task.searchResult;
+    expect(fourthResult.allHits.length,
+        3); //search has happened again so the new content is found
+  });
+}
+
+class _TestFixture {
+  _TestFixture({
+    this.terminalWidth = 80,
+  }) {
+    uut = TerminalSearch(terminalSearchInteractionMock);
+    when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth);
+  }
+
+  final int terminalWidth;
+
+  void expectTerminalSearchContent(
+    List<String> lines, {
+    isUsingAltBuffer = false,
+    isSearchStringCached = true,
+    List<int>? dirtyIndices,
+  }) {
+    final buffer = _getBuffer(lines,
+        isCached: isSearchStringCached, dirtyIndices: dirtyIndices);
+
+    when(terminalSearchInteractionMock.buffer).thenReturn(buffer);
+    when(terminalSearchInteractionMock.isUsingAltBuffer())
+        .thenReturn(isUsingAltBuffer);
+  }
+
+  final terminalSearchInteractionMock = MockTerminalSearchInteraction();
+  late final TerminalSearch uut;
+
+  MockBuffer _getBuffer(
+    List<String> lines, {
+    isCached = true,
+    List<int>? dirtyIndices,
+  }) {
+    final result = MockBuffer();
+    final circularList = MockTerminalSearchTestCircularList();
+    when(result.lines).thenReturn(circularList);
+
+    final bufferLines = _getBufferLinesWithSearchContent(
+      lines,
+      isCached: isCached,
+      dirtyIndices: dirtyIndices,
+    );
+
+    when(circularList[any]).thenAnswer(
+        (realInvocation) => bufferLines[realInvocation.positionalArguments[0]]);
+    when(circularList.length).thenReturn(bufferLines.length);
+
+    return result;
+  }
+
+  List<MockBufferLine> _getBufferLinesWithSearchContent(
+    List<String> content, {
+    isCached = true,
+    List<int>? dirtyIndices,
+  }) {
+    final result = List<MockBufferLine>.empty(growable: true);
+    for (int i = 0; i < content.length; i++) {
+      final bl = MockBufferLine();
+      when(bl.hasCachedSearchString).thenReturn(isCached);
+      when(bl.toSearchString(any)).thenReturn(content[i]);
+      if (dirtyIndices?.contains(i) ?? false) {
+        when(bl.isTagDirty(any)).thenReturn(true);
+      } else {
+        when(bl.isTagDirty(any)).thenReturn(false);
+      }
+      result.add(bl);
+    }
+
+    return result;
+  }
+}

+ 552 - 0
test/terminal/terminal_search_test.mocks.dart

@@ -0,0 +1,552 @@
+// Mocks generated by Mockito 5.0.15 from annotations
+// in xterm/test/terminal/terminal_search_test.dart.
+// Do not manually edit this file.
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:xterm/buffer/buffer.dart' as _i2;
+import 'package:xterm/buffer/line/line.dart' as _i6;
+import 'package:xterm/terminal/charset.dart' as _i4;
+import 'package:xterm/terminal/cursor.dart' as _i10;
+import 'package:xterm/terminal/terminal.dart' as _i3;
+import 'package:xterm/terminal/terminal_search_interaction.dart' as _i8;
+import 'package:xterm/util/circular_list.dart' as _i5;
+import 'package:xterm/util/scroll_range.dart' as _i7;
+
+import 'terminal_search_test.dart' as _i9;
+
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+
+class _FakeBuffer_0 extends _i1.Fake implements _i2.Buffer {}
+
+class _FakeTerminal_1 extends _i1.Fake implements _i3.Terminal {}
+
+class _FakeCharset_2 extends _i1.Fake implements _i4.Charset {}
+
+class _FakeCircularList_3<T> extends _i1.Fake implements _i5.CircularList<T> {}
+
+class _FakeBufferLine_4 extends _i1.Fake implements _i6.BufferLine {}
+
+class _FakeScrollRange_5 extends _i1.Fake implements _i7.ScrollRange {}
+
+class _FakeBufferLineData_6 extends _i1.Fake implements _i6.BufferLineData {}
+
+/// A class which mocks [TerminalSearchInteraction].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTerminalSearchInteraction extends _i1.Mock
+    implements _i8.TerminalSearchInteraction {
+  MockTerminalSearchInteraction() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.Buffer get buffer => (super.noSuchMethod(Invocation.getter(#buffer),
+      returnValue: _FakeBuffer_0()) as _i2.Buffer);
+  @override
+  int get terminalWidth =>
+      (super.noSuchMethod(Invocation.getter(#terminalWidth), returnValue: 0)
+          as int);
+  @override
+  bool isUsingAltBuffer() =>
+      (super.noSuchMethod(Invocation.method(#isUsingAltBuffer, []),
+          returnValue: false) as bool);
+  @override
+  String toString() => super.toString();
+}
+
+/// A class which mocks [Buffer].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBuffer extends _i1.Mock implements _i2.Buffer {
+  MockBuffer() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i3.Terminal get terminal => (super.noSuchMethod(Invocation.getter(#terminal),
+      returnValue: _FakeTerminal_1()) as _i3.Terminal);
+  @override
+  bool get isAltBuffer =>
+      (super.noSuchMethod(Invocation.getter(#isAltBuffer), returnValue: false)
+          as bool);
+  @override
+  _i4.Charset get charset => (super.noSuchMethod(Invocation.getter(#charset),
+      returnValue: _FakeCharset_2()) as _i4.Charset);
+  @override
+  _i5.CircularList<_i6.BufferLine> get lines =>
+      (super.noSuchMethod(Invocation.getter(#lines),
+              returnValue: _FakeCircularList_3<_i6.BufferLine>())
+          as _i5.CircularList<_i6.BufferLine>);
+  @override
+  set lines(_i5.CircularList<_i6.BufferLine>? _lines) =>
+      super.noSuchMethod(Invocation.setter(#lines, _lines),
+          returnValueForMissingStub: null);
+  @override
+  int get scrollOffsetFromBottom =>
+      (super.noSuchMethod(Invocation.getter(#scrollOffsetFromBottom),
+          returnValue: 0) as int);
+  @override
+  int get scrollOffsetFromTop => (super
+          .noSuchMethod(Invocation.getter(#scrollOffsetFromTop), returnValue: 0)
+      as int);
+  @override
+  bool get isUserScrolling => (super
+          .noSuchMethod(Invocation.getter(#isUserScrolling), returnValue: false)
+      as bool);
+  @override
+  int get cursorX =>
+      (super.noSuchMethod(Invocation.getter(#cursorX), returnValue: 0) as int);
+  @override
+  int get cursorY =>
+      (super.noSuchMethod(Invocation.getter(#cursorY), returnValue: 0) as int);
+  @override
+  int get marginTop =>
+      (super.noSuchMethod(Invocation.getter(#marginTop), returnValue: 0)
+          as int);
+  @override
+  int get marginBottom =>
+      (super.noSuchMethod(Invocation.getter(#marginBottom), returnValue: 0)
+          as int);
+  @override
+  _i6.BufferLine get currentLine =>
+      (super.noSuchMethod(Invocation.getter(#currentLine),
+          returnValue: _FakeBufferLine_4()) as _i6.BufferLine);
+  @override
+  int get height =>
+      (super.noSuchMethod(Invocation.getter(#height), returnValue: 0) as int);
+  @override
+  bool get hasScrollableRegion =>
+      (super.noSuchMethod(Invocation.getter(#hasScrollableRegion),
+          returnValue: false) as bool);
+  @override
+  bool get isInScrollableRegion =>
+      (super.noSuchMethod(Invocation.getter(#isInScrollableRegion),
+          returnValue: false) as bool);
+  @override
+  void write(String? text) =>
+      super.noSuchMethod(Invocation.method(#write, [text]),
+          returnValueForMissingStub: null);
+  @override
+  void writeChar(int? codePoint) =>
+      super.noSuchMethod(Invocation.method(#writeChar, [codePoint]),
+          returnValueForMissingStub: null);
+  @override
+  _i6.BufferLine getViewLine(int? index) =>
+      (super.noSuchMethod(Invocation.method(#getViewLine, [index]),
+          returnValue: _FakeBufferLine_4()) as _i6.BufferLine);
+  @override
+  int convertViewLineToRawLine(int? viewLine) => (super.noSuchMethod(
+      Invocation.method(#convertViewLineToRawLine, [viewLine]),
+      returnValue: 0) as int);
+  @override
+  int convertRawLineToViewLine(int? rawLine) => (super.noSuchMethod(
+      Invocation.method(#convertRawLineToViewLine, [rawLine]),
+      returnValue: 0) as int);
+  @override
+  void newLine() => super.noSuchMethod(Invocation.method(#newLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void carriageReturn() =>
+      super.noSuchMethod(Invocation.method(#carriageReturn, []),
+          returnValueForMissingStub: null);
+  @override
+  void backspace() => super.noSuchMethod(Invocation.method(#backspace, []),
+      returnValueForMissingStub: null);
+  @override
+  List<_i6.BufferLine> getVisibleLines() =>
+      (super.noSuchMethod(Invocation.method(#getVisibleLines, []),
+          returnValue: <_i6.BufferLine>[]) as List<_i6.BufferLine>);
+  @override
+  void eraseDisplayFromCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseDisplayFromCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseDisplayToCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseDisplayToCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseDisplay() =>
+      super.noSuchMethod(Invocation.method(#eraseDisplay, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseLineFromCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseLineFromCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseLineToCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseLineToCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseLine() => super.noSuchMethod(Invocation.method(#eraseLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void eraseCharacters(int? count) =>
+      super.noSuchMethod(Invocation.method(#eraseCharacters, [count]),
+          returnValueForMissingStub: null);
+  @override
+  _i7.ScrollRange getAreaScrollRange() =>
+      (super.noSuchMethod(Invocation.method(#getAreaScrollRange, []),
+          returnValue: _FakeScrollRange_5()) as _i7.ScrollRange);
+  @override
+  void areaScrollDown(int? lines) =>
+      super.noSuchMethod(Invocation.method(#areaScrollDown, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void areaScrollUp(int? lines) =>
+      super.noSuchMethod(Invocation.method(#areaScrollUp, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void index() => super.noSuchMethod(Invocation.method(#index, []),
+      returnValueForMissingStub: null);
+  @override
+  void reverseIndex() =>
+      super.noSuchMethod(Invocation.method(#reverseIndex, []),
+          returnValueForMissingStub: null);
+  @override
+  void cursorGoForward() =>
+      super.noSuchMethod(Invocation.method(#cursorGoForward, []),
+          returnValueForMissingStub: null);
+  @override
+  void setCursorX(int? cursorX) =>
+      super.noSuchMethod(Invocation.method(#setCursorX, [cursorX]),
+          returnValueForMissingStub: null);
+  @override
+  void setCursorY(int? cursorY) =>
+      super.noSuchMethod(Invocation.method(#setCursorY, [cursorY]),
+          returnValueForMissingStub: null);
+  @override
+  void moveCursorX(int? offset) =>
+      super.noSuchMethod(Invocation.method(#moveCursorX, [offset]),
+          returnValueForMissingStub: null);
+  @override
+  void moveCursorY(int? offset) =>
+      super.noSuchMethod(Invocation.method(#moveCursorY, [offset]),
+          returnValueForMissingStub: null);
+  @override
+  void setPosition(int? cursorX, int? cursorY) =>
+      super.noSuchMethod(Invocation.method(#setPosition, [cursorX, cursorY]),
+          returnValueForMissingStub: null);
+  @override
+  void movePosition(int? offsetX, int? offsetY) =>
+      super.noSuchMethod(Invocation.method(#movePosition, [offsetX, offsetY]),
+          returnValueForMissingStub: null);
+  @override
+  void setScrollOffsetFromBottom(int? offsetFromBottom) => super.noSuchMethod(
+      Invocation.method(#setScrollOffsetFromBottom, [offsetFromBottom]),
+      returnValueForMissingStub: null);
+  @override
+  void setScrollOffsetFromTop(int? offsetFromTop) => super.noSuchMethod(
+      Invocation.method(#setScrollOffsetFromTop, [offsetFromTop]),
+      returnValueForMissingStub: null);
+  @override
+  void screenScrollUp(int? lines) =>
+      super.noSuchMethod(Invocation.method(#screenScrollUp, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void screenScrollDown(int? lines) =>
+      super.noSuchMethod(Invocation.method(#screenScrollDown, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void saveCursor() => super.noSuchMethod(Invocation.method(#saveCursor, []),
+      returnValueForMissingStub: null);
+  @override
+  void restoreCursor() =>
+      super.noSuchMethod(Invocation.method(#restoreCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void setVerticalMargins(int? top, int? bottom) =>
+      super.noSuchMethod(Invocation.method(#setVerticalMargins, [top, bottom]),
+          returnValueForMissingStub: null);
+  @override
+  void resetVerticalMargins() =>
+      super.noSuchMethod(Invocation.method(#resetVerticalMargins, []),
+          returnValueForMissingStub: null);
+  @override
+  void deleteChars(int? count) =>
+      super.noSuchMethod(Invocation.method(#deleteChars, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void clearScrollback() =>
+      super.noSuchMethod(Invocation.method(#clearScrollback, []),
+          returnValueForMissingStub: null);
+  @override
+  void clear() => super.noSuchMethod(Invocation.method(#clear, []),
+      returnValueForMissingStub: null);
+  @override
+  void insertBlankCharacters(int? count) =>
+      super.noSuchMethod(Invocation.method(#insertBlankCharacters, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void insertLines(int? count) =>
+      super.noSuchMethod(Invocation.method(#insertLines, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void insertLine() => super.noSuchMethod(Invocation.method(#insertLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void deleteLines(int? count) =>
+      super.noSuchMethod(Invocation.method(#deleteLines, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void deleteLine() => super.noSuchMethod(Invocation.method(#deleteLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void resize(int? oldWidth, int? oldHeight, int? newWidth, int? newHeight) =>
+      super.noSuchMethod(
+          Invocation.method(
+              #resize, [oldWidth, oldHeight, newWidth, newHeight]),
+          returnValueForMissingStub: null);
+  @override
+  dynamic adjustSavedCursor(int? dx, int? dy) =>
+      super.noSuchMethod(Invocation.method(#adjustSavedCursor, [dx, dy]));
+  @override
+  String toString() => super.toString();
+}
+
+/// A class which mocks [TerminalSearchTestCircularList].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTerminalSearchTestCircularList extends _i1.Mock
+    implements _i9.TerminalSearchTestCircularList {
+  MockTerminalSearchTestCircularList() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  int get maxLength =>
+      (super.noSuchMethod(Invocation.getter(#maxLength), returnValue: 0)
+          as int);
+  @override
+  set maxLength(int? value) =>
+      super.noSuchMethod(Invocation.setter(#maxLength, value),
+          returnValueForMissingStub: null);
+  @override
+  int get length =>
+      (super.noSuchMethod(Invocation.getter(#length), returnValue: 0) as int);
+  @override
+  set length(int? value) =>
+      super.noSuchMethod(Invocation.setter(#length, value),
+          returnValueForMissingStub: null);
+  @override
+  bool get isFull =>
+      (super.noSuchMethod(Invocation.getter(#isFull), returnValue: false)
+          as bool);
+  @override
+  void forEach(void Function(_i6.BufferLine)? callback) =>
+      super.noSuchMethod(Invocation.method(#forEach, [callback]),
+          returnValueForMissingStub: null);
+  @override
+  _i6.BufferLine operator [](int? index) =>
+      (super.noSuchMethod(Invocation.method(#[], [index]),
+          returnValue: _FakeBufferLine_4()) as _i6.BufferLine);
+  @override
+  void operator []=(int? index, _i6.BufferLine? value) =>
+      super.noSuchMethod(Invocation.method(#[]=, [index, value]),
+          returnValueForMissingStub: null);
+  @override
+  void clear() => super.noSuchMethod(Invocation.method(#clear, []),
+      returnValueForMissingStub: null);
+  @override
+  void pushAll(Iterable<_i6.BufferLine>? items) =>
+      super.noSuchMethod(Invocation.method(#pushAll, [items]),
+          returnValueForMissingStub: null);
+  @override
+  void push(_i6.BufferLine? value) =>
+      super.noSuchMethod(Invocation.method(#push, [value]),
+          returnValueForMissingStub: null);
+  @override
+  _i6.BufferLine pop() => (super.noSuchMethod(Invocation.method(#pop, []),
+      returnValue: _FakeBufferLine_4()) as _i6.BufferLine);
+  @override
+  void remove(int? index, [int? count = 1]) =>
+      super.noSuchMethod(Invocation.method(#remove, [index, count]),
+          returnValueForMissingStub: null);
+  @override
+  void insert(int? index, _i6.BufferLine? item) =>
+      super.noSuchMethod(Invocation.method(#insert, [index, item]),
+          returnValueForMissingStub: null);
+  @override
+  void insertAll(int? index, List<_i6.BufferLine>? items) =>
+      super.noSuchMethod(Invocation.method(#insertAll, [index, items]),
+          returnValueForMissingStub: null);
+  @override
+  void trimStart(int? count) =>
+      super.noSuchMethod(Invocation.method(#trimStart, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void shiftElements(int? start, int? count, int? offset) => super.noSuchMethod(
+      Invocation.method(#shiftElements, [start, count, offset]),
+      returnValueForMissingStub: null);
+  @override
+  void replaceWith(List<_i6.BufferLine>? replacement) =>
+      super.noSuchMethod(Invocation.method(#replaceWith, [replacement]),
+          returnValueForMissingStub: null);
+  @override
+  List<_i6.BufferLine> toList() =>
+      (super.noSuchMethod(Invocation.method(#toList, []),
+          returnValue: <_i6.BufferLine>[]) as List<_i6.BufferLine>);
+  @override
+  String toString() => super.toString();
+}
+
+/// A class which mocks [BufferLine].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBufferLine extends _i1.Mock implements _i6.BufferLine {
+  MockBufferLine() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i6.BufferLineData get data => (super.noSuchMethod(Invocation.getter(#data),
+      returnValue: _FakeBufferLineData_6()) as _i6.BufferLineData);
+  @override
+  bool get isWrapped =>
+      (super.noSuchMethod(Invocation.getter(#isWrapped), returnValue: false)
+          as bool);
+  @override
+  set isWrapped(bool? value) =>
+      super.noSuchMethod(Invocation.setter(#isWrapped, value),
+          returnValueForMissingStub: null);
+  @override
+  bool get hasCachedSearchString =>
+      (super.noSuchMethod(Invocation.getter(#hasCachedSearchString),
+          returnValue: false) as bool);
+  @override
+  void markTagAsNonDirty(String? tag) =>
+      super.noSuchMethod(Invocation.method(#markTagAsNonDirty, [tag]),
+          returnValueForMissingStub: null);
+  @override
+  bool isTagDirty(String? tag) =>
+      (super.noSuchMethod(Invocation.method(#isTagDirty, [tag]),
+          returnValue: false) as bool);
+  @override
+  void ensure(int? length) =>
+      super.noSuchMethod(Invocation.method(#ensure, [length]),
+          returnValueForMissingStub: null);
+  @override
+  void insert(int? index) =>
+      super.noSuchMethod(Invocation.method(#insert, [index]),
+          returnValueForMissingStub: null);
+  @override
+  void insertN(int? index, int? count) =>
+      super.noSuchMethod(Invocation.method(#insertN, [index, count]),
+          returnValueForMissingStub: null);
+  @override
+  void removeN(int? index, int? count) =>
+      super.noSuchMethod(Invocation.method(#removeN, [index, count]),
+          returnValueForMissingStub: null);
+  @override
+  void clear() => super.noSuchMethod(Invocation.method(#clear, []),
+      returnValueForMissingStub: null);
+  @override
+  void erase(_i10.Cursor? cursor, int? start, int? end,
+          [bool? resetIsWrapped = false]) =>
+      super.noSuchMethod(
+          Invocation.method(#erase, [cursor, start, end, resetIsWrapped]),
+          returnValueForMissingStub: null);
+  @override
+  void cellClear(int? index) =>
+      super.noSuchMethod(Invocation.method(#cellClear, [index]),
+          returnValueForMissingStub: null);
+  @override
+  void cellInitialize(int? index,
+          {int? content, int? width, _i10.Cursor? cursor}) =>
+      super.noSuchMethod(
+          Invocation.method(#cellInitialize, [index],
+              {#content: content, #width: width, #cursor: cursor}),
+          returnValueForMissingStub: null);
+  @override
+  bool cellHasContent(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellHasContent, [index]),
+          returnValue: false) as bool);
+  @override
+  int cellGetContent(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetContent, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetContent(int? index, int? content) =>
+      super.noSuchMethod(Invocation.method(#cellSetContent, [index, content]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetFgColor(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetFgColor, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetFgColor(int? index, int? color) =>
+      super.noSuchMethod(Invocation.method(#cellSetFgColor, [index, color]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetBgColor(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetBgColor, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetBgColor(int? index, int? color) =>
+      super.noSuchMethod(Invocation.method(#cellSetBgColor, [index, color]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetFlags(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetFlags, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetFlags(int? index, int? flags) =>
+      super.noSuchMethod(Invocation.method(#cellSetFlags, [index, flags]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetWidth(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetWidth, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetWidth(int? index, int? width) =>
+      super.noSuchMethod(Invocation.method(#cellSetWidth, [index, width]),
+          returnValueForMissingStub: null);
+  @override
+  void cellClearFlags(int? index) =>
+      super.noSuchMethod(Invocation.method(#cellClearFlags, [index]),
+          returnValueForMissingStub: null);
+  @override
+  bool cellHasFlag(int? index, int? flag) =>
+      (super.noSuchMethod(Invocation.method(#cellHasFlag, [index, flag]),
+          returnValue: false) as bool);
+  @override
+  void cellSetFlag(int? index, int? flag) =>
+      super.noSuchMethod(Invocation.method(#cellSetFlag, [index, flag]),
+          returnValueForMissingStub: null);
+  @override
+  void cellErase(int? index, _i10.Cursor? cursor) =>
+      super.noSuchMethod(Invocation.method(#cellErase, [index, cursor]),
+          returnValueForMissingStub: null);
+  @override
+  int getTrimmedLength([int? cols]) =>
+      (super.noSuchMethod(Invocation.method(#getTrimmedLength, [cols]),
+          returnValue: 0) as int);
+  @override
+  void copyCellsFrom(_i6.BufferLine? src, int? srcCol, int? dstCol, int? len) =>
+      super.noSuchMethod(
+          Invocation.method(#copyCellsFrom, [src, srcCol, dstCol, len]),
+          returnValueForMissingStub: null);
+  @override
+  void removeRange(int? start, int? end) =>
+      super.noSuchMethod(Invocation.method(#removeRange, [start, end]),
+          returnValueForMissingStub: null);
+  @override
+  void clearRange(int? start, int? end) =>
+      super.noSuchMethod(Invocation.method(#clearRange, [start, end]),
+          returnValueForMissingStub: null);
+  @override
+  String toDebugString(int? cols) =>
+      (super.noSuchMethod(Invocation.method(#toDebugString, [cols]),
+          returnValue: '') as String);
+  @override
+  String toSearchString(int? cols) =>
+      (super.noSuchMethod(Invocation.method(#toSearchString, [cols]),
+          returnValue: '') as String);
+  @override
+  String toString() => super.toString();
+}