44
55import 'package:devtools_app/devtools_app.dart' ;
66import 'package:devtools_app_shared/utils.dart' ;
7+ import 'package:fake_async/fake_async.dart' ;
8+ import 'package:flutter/material.dart' ;
79import 'package:flutter_test/flutter_test.dart' ;
810
9- // TODO(https://github.com/flutter/devtools/issues/3514): increase test coverage
10-
1111void main () {
1212 late TestSearchController searchController;
1313
@@ -43,6 +43,59 @@ void main() {
4343 }
4444 });
4545
46+ test ('nextMatch and previousMatch' , () {
47+ searchController.search = 'foo' ;
48+ expect (searchController.matchIndex.value, equals (1 ));
49+ expect (searchController.activeSearchMatch.value! .name, equals ('Foo' ));
50+
51+ searchController.nextMatch ();
52+ expect (searchController.matchIndex.value, equals (2 ));
53+ expect (searchController.activeSearchMatch.value! .name, equals ('FooBar' ));
54+
55+ searchController.nextMatch ();
56+ expect (searchController.matchIndex.value, equals (3 ));
57+ expect (searchController.activeSearchMatch.value! .name, equals ('FooBaz' ));
58+
59+ searchController.nextMatch ();
60+ expect (searchController.matchIndex.value, equals (1 ));
61+ expect (searchController.activeSearchMatch.value! .name, equals ('Foo' ));
62+
63+ searchController.previousMatch ();
64+ expect (searchController.matchIndex.value, equals (3 ));
65+ expect (searchController.activeSearchMatch.value! .name, equals ('FooBaz' ));
66+
67+ searchController.previousMatch ();
68+ expect (searchController.matchIndex.value, equals (2 ));
69+ expect (searchController.activeSearchMatch.value! .name, equals ('FooBar' ));
70+ });
71+
72+ test ('resetSearch' , () {
73+ searchController.search = 'foo' ;
74+ expect (searchController.search, equals ('foo' ));
75+ expect (searchController.searchMatches.value.length, equals (3 ));
76+
77+ searchController.resetSearch ();
78+ expect (searchController.search, isEmpty);
79+ expect (searchController.searchMatches.value, isEmpty);
80+ });
81+
82+ test ('searchPreviousMatches' , () {
83+ searchController.search = 'foo' ;
84+ expect (searchController.searchMatches.value.length, equals (3 ));
85+
86+ // Add a new item that matches 'foob' but was not in the previous matches.
87+ searchController.data.add (TestSearchData ('FooBarBaz' ));
88+
89+ // Since 'foob' contains 'foo', it will search previous matches.
90+ // 'FooBarBaz' was not in the previous matches, so it should not be found.
91+ searchController.search = 'foob' ;
92+ expect (searchController.searchMatches.value.length, equals (2 ));
93+ expect (
94+ searchController.searchMatches.value.map ((e) => e.name),
95+ equals (['FooBar' , 'FooBaz' ]),
96+ );
97+ });
98+
4699 test ('updates values for empty query' , () {
47100 searchController.search = 'foo' ;
48101 expect (searchController.search, equals ('foo' ));
@@ -67,6 +120,225 @@ void main() {
67120 expect (data.isSearchMatch, isFalse);
68121 }
69122 });
123+
124+ test ('debounce' , () {
125+ fakeAsync ((async ) {
126+ final debounceController = TestDebounceSearchController ()
127+ ..data.addAll (testData);
128+ expect (debounceController.search, isEmpty);
129+ expect (debounceController.searchMatches.value, isEmpty);
130+
131+ debounceController.search = 'foo' ;
132+ expect (debounceController.search, equals ('foo' ));
133+ expect (
134+ debounceController.searchMatches.value,
135+ isEmpty,
136+ ); // Has not updated yet
137+ expect (debounceController.isSearchInProgress, isTrue);
138+
139+ async .elapse (debounceController.debounceDelay! * 1.5 );
140+
141+ expect (debounceController.isSearchInProgress, isFalse);
142+ expect (debounceController.searchMatches.value.length, equals (3 ));
143+ });
144+ });
145+ });
146+
147+ group ('AutoCompleteMatch' , () {
148+ test ('transformAutoCompleteMatch without matched segments' , () {
149+ final match = AutoCompleteMatch ('test' );
150+ final result = match.transformAutoCompleteMatch <String >(
151+ transformMatchedSegment: (s) => '[$s ]' ,
152+ transformUnmatchedSegment: (s) => '<$s >' ,
153+ combineSegments: (segments) => segments.join (),
154+ );
155+ expect (result, equals ('<test>' ));
156+ });
157+
158+ test ('transformAutoCompleteMatch with matched segments' , () {
159+ final match = AutoCompleteMatch (
160+ 'testSuggestion' ,
161+ matchedSegments: [
162+ const Range (0 , 4 ), // 'test'
163+ const Range (10 , 14 ), // 'tion'
164+ ],
165+ );
166+ final result = match.transformAutoCompleteMatch <String >(
167+ transformMatchedSegment: (s) => '[$s ]' ,
168+ transformUnmatchedSegment: (s) => '<$s >' ,
169+ combineSegments: (segments) => segments.join (),
170+ );
171+ expect (result, equals ('[test]<Sugges>[tion]' ));
172+ });
173+ });
174+
175+ group ('AutoCompleteSearchControllerMixin' , () {
176+ late TestAutoCompleteSearchController autoCompleteController;
177+
178+ setUp (() {
179+ autoCompleteController = TestAutoCompleteSearchController ();
180+ });
181+
182+ tearDown (() {
183+ autoCompleteController.dispose ();
184+ });
185+
186+ test ('clearSearchAutoComplete' , () {
187+ autoCompleteController.searchAutoComplete.value = [
188+ AutoCompleteMatch ('test' ),
189+ ];
190+ autoCompleteController.setCurrentHoveredIndexValue (1 );
191+
192+ autoCompleteController.clearSearchAutoComplete ();
193+
194+ expect (autoCompleteController.searchAutoComplete.value, isEmpty);
195+ expect (autoCompleteController.currentHoveredIndex.value, equals (0 ));
196+ });
197+
198+ test ('updateCurrentSuggestion / clearCurrentSuggestion' , () {
199+ autoCompleteController.searchAutoComplete.value = [
200+ AutoCompleteMatch ('testSuggestion' ),
201+ ];
202+ autoCompleteController.setCurrentHoveredIndexValue (0 );
203+
204+ autoCompleteController.updateCurrentSuggestion ('test' );
205+ expect (
206+ autoCompleteController.currentSuggestion.value,
207+ equals ('Suggestion' ),
208+ );
209+
210+ autoCompleteController.updateCurrentSuggestion ('testSuggest' );
211+ expect (autoCompleteController.currentSuggestion.value, equals ('ion' ));
212+
213+ // Active word is longer than hovered text (should not happen in practice but handled)
214+ autoCompleteController.updateCurrentSuggestion ('testSuggestionWithMore' );
215+ expect (autoCompleteController.currentSuggestion.value, isNull);
216+
217+ autoCompleteController.clearCurrentSuggestion ();
218+ expect (autoCompleteController.currentSuggestion.value, isNull);
219+ });
220+
221+ test ('activeEditingParts' , () {
222+ final parts1 = AutoCompleteSearchControllerMixin .activeEditingParts (
223+ 'addOne.yName + 1000 + myChart.tra' ,
224+ const TextSelection .collapsed (offset: 33 ),
225+ );
226+ expect (parts1.activeWord, equals ('tra' ));
227+ expect (parts1.leftSide, equals ('addOne.yName + 1000 + myChart.' ));
228+ expect (parts1.rightSide, equals ('' ));
229+ expect (parts1.isField, isTrue);
230+
231+ final parts2 = AutoCompleteSearchControllerMixin .activeEditingParts (
232+ 'controller.cl + 1000 + myChart.tra' ,
233+ const TextSelection .collapsed (offset: 13 ),
234+ );
235+ expect (parts2.activeWord, equals ('cl' ));
236+ expect (parts2.leftSide, equals ('controller.' ));
237+ expect (parts2.rightSide, equals (' + 1000 + myChart.tra' ));
238+ expect (parts2.isField, isTrue);
239+
240+ final parts3 = AutoCompleteSearchControllerMixin .activeEditingParts (
241+ 'foo' ,
242+ const TextSelection .collapsed (offset: 3 ),
243+ );
244+ expect (parts3.activeWord, equals ('foo' ));
245+ expect (parts3.leftSide, equals ('' ));
246+ expect (parts3.rightSide, equals ('' ));
247+ expect (parts3.isField, isFalse);
248+ });
249+
250+ test ('clearSearchField' , () {
251+ autoCompleteController.search = 'foo' ;
252+ autoCompleteController.clearSearchField ();
253+ expect (autoCompleteController.search, isEmpty);
254+
255+ autoCompleteController.clearSearchField (force: true );
256+ expect (autoCompleteController.search, isEmpty);
257+ });
258+
259+ test ('updateSearchField' , () {
260+ autoCompleteController.updateSearchField (
261+ newValue: 'foo bar' ,
262+ caretPosition: 3 ,
263+ );
264+ expect (
265+ autoCompleteController.searchTextFieldController.text,
266+ equals ('foo bar' ),
267+ );
268+ expect (
269+ autoCompleteController.searchTextFieldController.selection.baseOffset,
270+ equals (3 ),
271+ );
272+ });
273+ });
274+
275+ group ('StatelessSearchField' , () {
276+ testWidgets ('calls onChanged and onClose' , (WidgetTester tester) async {
277+ final searchController = TestSearchController ()..init ();
278+ bool closeCalled = false ;
279+ String lastChangedValue = '' ;
280+
281+ await tester.pumpWidget (
282+ MaterialApp (
283+ home: Scaffold (
284+ body: StatelessSearchField (
285+ controller: searchController,
286+ searchFieldEnabled: true ,
287+ shouldRequestFocus: false ,
288+ onClose: () {
289+ closeCalled = true ;
290+ },
291+ onChanged: (value) {
292+ lastChangedValue = value;
293+ },
294+ ),
295+ ),
296+ ),
297+ );
298+
299+ final textField = find.byType (TextField );
300+ expect (textField, findsOneWidget);
301+
302+ await tester.enterText (textField, 'test input' );
303+ await tester.pumpAndSettle ();
304+
305+ expect (lastChangedValue, equals ('test input' ));
306+
307+ final closeButton = find.byIcon (Icons .close);
308+ expect (closeButton, findsOneWidget);
309+
310+ await tester.tap (closeButton);
311+ expect (closeCalled, isTrue);
312+ });
313+ });
314+
315+ group ('SearchField' , () {
316+ testWidgets ('calls onClose' , (WidgetTester tester) async {
317+ final searchController = TestSearchController ()..init ();
318+ bool closeCalled = false ;
319+
320+ await tester.pumpWidget (
321+ MaterialApp (
322+ home: Scaffold (
323+ body: SearchField (
324+ searchController: searchController,
325+ onClose: () {
326+ closeCalled = true ;
327+ },
328+ ),
329+ ),
330+ ),
331+ );
332+
333+ await tester.enterText (find.byType (TextField ), 'foo' );
334+ await tester.pumpAndSettle ();
335+ // find the close button
336+ final closeButton = find.byIcon (Icons .close);
337+ expect (closeButton, findsOneWidget);
338+
339+ await tester.tap (closeButton);
340+ expect (closeCalled, isTrue);
341+ });
70342 });
71343}
72344
@@ -75,18 +347,40 @@ class TestSearchController extends DisposableController
75347 final data = < TestSearchData > [];
76348
77349 @override
78- List <TestSearchData > matchesForSearch (
79- String search, {
80- bool searchPreviousMatches = false ,
81- }) {
82- return data
83- .where ((element) => element.name.caseInsensitiveContains (search))
84- .toList ();
85- }
350+ Iterable <TestSearchData > get currentDataToSearchThrough => data;
351+ }
352+
353+ class TestDebounceSearchController extends DisposableController
354+ with SearchControllerMixin <TestSearchData > {
355+ final data = < TestSearchData > [];
356+
357+ @override
358+ Iterable <TestSearchData > get currentDataToSearchThrough => data;
359+
360+ @override
361+ Duration ? get debounceDelay => const Duration (milliseconds: 100 );
86362}
87363
88364class TestSearchData with SearchableDataMixin {
89365 TestSearchData (this .name);
90366
91367 final String name;
368+
369+ @override
370+ bool matchesSearchToken (RegExp regExpSearch) {
371+ return name.caseInsensitiveContains (regExpSearch);
372+ }
373+ }
374+
375+ class TestAutoCompleteSearchController extends DisposableController
376+ with SearchControllerMixin , AutoCompleteSearchControllerMixin {
377+ TestAutoCompleteSearchController () {
378+ init ();
379+ }
380+
381+ @override
382+ final searchFieldKey = GlobalKey ();
383+
384+ @override
385+ Iterable <SearchableDataMixin > get currentDataToSearchThrough => [];
92386}
0 commit comments