Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ DEPENDENCIES.md
# macOS generated Flutter files
**/macos/Flutter/ephemeral/
**/macos/Flutter/GeneratedPluginRegistrant.swift

build/
314 changes: 304 additions & 10 deletions packages/devtools_app/test/shared/ui/search_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:fake_async/fake_async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

// TODO(https://github.com/flutter/devtools/issues/3514): increase test coverage

void main() {
late TestSearchController searchController;

Expand Down Expand Up @@ -43,6 +43,59 @@ void main() {
}
});

test('nextMatch and previousMatch', () {
searchController.search = 'foo';
expect(searchController.matchIndex.value, equals(1));
expect(searchController.activeSearchMatch.value!.name, equals('Foo'));

searchController.nextMatch();
expect(searchController.matchIndex.value, equals(2));
expect(searchController.activeSearchMatch.value!.name, equals('FooBar'));

searchController.nextMatch();
expect(searchController.matchIndex.value, equals(3));
expect(searchController.activeSearchMatch.value!.name, equals('FooBaz'));

searchController.nextMatch();
expect(searchController.matchIndex.value, equals(1));
expect(searchController.activeSearchMatch.value!.name, equals('Foo'));

searchController.previousMatch();
expect(searchController.matchIndex.value, equals(3));
expect(searchController.activeSearchMatch.value!.name, equals('FooBaz'));

searchController.previousMatch();
expect(searchController.matchIndex.value, equals(2));
expect(searchController.activeSearchMatch.value!.name, equals('FooBar'));
});

test('resetSearch', () {
searchController.search = 'foo';
expect(searchController.search, equals('foo'));
expect(searchController.searchMatches.value.length, equals(3));

searchController.resetSearch();
expect(searchController.search, isEmpty);
expect(searchController.searchMatches.value, isEmpty);
});

test('searchPreviousMatches', () {
searchController.search = 'foo';
expect(searchController.searchMatches.value.length, equals(3));

// Add a new item that matches 'foob' but was not in the previous matches.
searchController.data.add(TestSearchData('FooBarBaz'));

// Since 'foob' contains 'foo', it will search previous matches.
// 'FooBarBaz' was not in the previous matches, so it should not be found.
searchController.search = 'foob';
expect(searchController.searchMatches.value.length, equals(2));
expect(
searchController.searchMatches.value.map((e) => e.name),
equals(['FooBar', 'FooBaz']),
);
});

test('updates values for empty query', () {
searchController.search = 'foo';
expect(searchController.search, equals('foo'));
Expand All @@ -67,6 +120,225 @@ void main() {
expect(data.isSearchMatch, isFalse);
}
});

test('debounce', () {
fakeAsync((async) {
final debounceController = TestDebounceSearchController()
..data.addAll(testData);
expect(debounceController.search, isEmpty);
expect(debounceController.searchMatches.value, isEmpty);

debounceController.search = 'foo';
expect(debounceController.search, equals('foo'));
expect(
debounceController.searchMatches.value,
isEmpty,
); // Has not updated yet
expect(debounceController.isSearchInProgress, isTrue);

async.elapse(debounceController.debounceDelay! * 1.5);

expect(debounceController.isSearchInProgress, isFalse);
expect(debounceController.searchMatches.value.length, equals(3));
});
});
});

group('AutoCompleteMatch', () {
test('transformAutoCompleteMatch without matched segments', () {
final match = AutoCompleteMatch('test');
final result = match.transformAutoCompleteMatch<String>(
transformMatchedSegment: (s) => '[$s]',
transformUnmatchedSegment: (s) => '<$s>',
combineSegments: (segments) => segments.join(),
);
expect(result, equals('<test>'));
});

test('transformAutoCompleteMatch with matched segments', () {
final match = AutoCompleteMatch(
'testSuggestion',
matchedSegments: [
const Range(0, 4), // 'test'
const Range(10, 14), // 'tion'
],
);
final result = match.transformAutoCompleteMatch<String>(
transformMatchedSegment: (s) => '[$s]',
transformUnmatchedSegment: (s) => '<$s>',
combineSegments: (segments) => segments.join(),
);
expect(result, equals('[test]<Sugges>[tion]'));
});
});

group('AutoCompleteSearchControllerMixin', () {
late TestAutoCompleteSearchController autoCompleteController;

setUp(() {
autoCompleteController = TestAutoCompleteSearchController();
});

tearDown(() {
autoCompleteController.dispose();
});

test('clearSearchAutoComplete', () {
autoCompleteController.searchAutoComplete.value = [
AutoCompleteMatch('test'),
];
autoCompleteController.setCurrentHoveredIndexValue(1);

autoCompleteController.clearSearchAutoComplete();

expect(autoCompleteController.searchAutoComplete.value, isEmpty);
expect(autoCompleteController.currentHoveredIndex.value, equals(0));
});

test('updateCurrentSuggestion / clearCurrentSuggestion', () {
autoCompleteController.searchAutoComplete.value = [
AutoCompleteMatch('testSuggestion'),
];
autoCompleteController.setCurrentHoveredIndexValue(0);

autoCompleteController.updateCurrentSuggestion('test');
expect(
autoCompleteController.currentSuggestion.value,
equals('Suggestion'),
);

autoCompleteController.updateCurrentSuggestion('testSuggest');
expect(autoCompleteController.currentSuggestion.value, equals('ion'));

// Active word is longer than hovered text (should not happen in practice but handled)
autoCompleteController.updateCurrentSuggestion('testSuggestionWithMore');
expect(autoCompleteController.currentSuggestion.value, isNull);

autoCompleteController.clearCurrentSuggestion();
expect(autoCompleteController.currentSuggestion.value, isNull);
});

test('activeEditingParts', () {
final parts1 = AutoCompleteSearchControllerMixin.activeEditingParts(
'addOne.yName + 1000 + myChart.tra',
const TextSelection.collapsed(offset: 33),
);
expect(parts1.activeWord, equals('tra'));
expect(parts1.leftSide, equals('addOne.yName + 1000 + myChart.'));
expect(parts1.rightSide, equals(''));
expect(parts1.isField, isTrue);

final parts2 = AutoCompleteSearchControllerMixin.activeEditingParts(
'controller.cl + 1000 + myChart.tra',
const TextSelection.collapsed(offset: 13),
);
expect(parts2.activeWord, equals('cl'));
expect(parts2.leftSide, equals('controller.'));
expect(parts2.rightSide, equals(' + 1000 + myChart.tra'));
expect(parts2.isField, isTrue);

final parts3 = AutoCompleteSearchControllerMixin.activeEditingParts(
'foo',
const TextSelection.collapsed(offset: 3),
);
expect(parts3.activeWord, equals('foo'));
expect(parts3.leftSide, equals(''));
expect(parts3.rightSide, equals(''));
expect(parts3.isField, isFalse);
});

test('clearSearchField', () {
autoCompleteController.search = 'foo';
autoCompleteController.clearSearchField();
expect(autoCompleteController.search, isEmpty);

autoCompleteController.clearSearchField(force: true);
expect(autoCompleteController.search, isEmpty);
});

test('updateSearchField', () {
autoCompleteController.updateSearchField(
newValue: 'foo bar',
caretPosition: 3,
);
expect(
autoCompleteController.searchTextFieldController.text,
equals('foo bar'),
);
expect(
autoCompleteController.searchTextFieldController.selection.baseOffset,
equals(3),
);
});
});

group('StatelessSearchField', () {
testWidgets('calls onChanged and onClose', (WidgetTester tester) async {
final searchController = TestSearchController()..init();
bool closeCalled = false;
String lastChangedValue = '';

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StatelessSearchField(
controller: searchController,
searchFieldEnabled: true,
shouldRequestFocus: false,
onClose: () {
closeCalled = true;
},
onChanged: (value) {
lastChangedValue = value;
},
),
),
),
);

final textField = find.byType(TextField);
expect(textField, findsOneWidget);

await tester.enterText(textField, 'test input');
await tester.pumpAndSettle();

expect(lastChangedValue, equals('test input'));

final closeButton = find.byIcon(Icons.close);
expect(closeButton, findsOneWidget);

await tester.tap(closeButton);
expect(closeCalled, isTrue);
});
});

group('SearchField', () {
testWidgets('calls onClose', (WidgetTester tester) async {
final searchController = TestSearchController()..init();
bool closeCalled = false;

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SearchField(
searchController: searchController,
onClose: () {
closeCalled = true;
},
),
),
),
);

await tester.enterText(find.byType(TextField), 'foo');
await tester.pumpAndSettle();
// find the close button
final closeButton = find.byIcon(Icons.close);
expect(closeButton, findsOneWidget);

await tester.tap(closeButton);
expect(closeCalled, isTrue);
});
});
}

Expand All @@ -75,18 +347,40 @@ class TestSearchController extends DisposableController
final data = <TestSearchData>[];

@override
List<TestSearchData> matchesForSearch(
String search, {
bool searchPreviousMatches = false,
}) {
return data
.where((element) => element.name.caseInsensitiveContains(search))
.toList();
}
Iterable<TestSearchData> get currentDataToSearchThrough => data;
}

class TestDebounceSearchController extends DisposableController
with SearchControllerMixin<TestSearchData> {
final data = <TestSearchData>[];

@override
Iterable<TestSearchData> get currentDataToSearchThrough => data;

@override
Duration? get debounceDelay => const Duration(milliseconds: 100);
}

class TestSearchData with SearchableDataMixin {
TestSearchData(this.name);

final String name;

@override
bool matchesSearchToken(RegExp regExpSearch) {
return name.caseInsensitiveContains(regExpSearch);
}
Comment thread
kenzieschmoll marked this conversation as resolved.
}

class TestAutoCompleteSearchController extends DisposableController
with SearchControllerMixin, AutoCompleteSearchControllerMixin {
TestAutoCompleteSearchController() {
init();
}

@override
final searchFieldKey = GlobalKey();

@override
Iterable<SearchableDataMixin> get currentDataToSearchThrough => [];
}
15 changes: 10 additions & 5 deletions tool/lib/commands/presubmit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ class PresubmitCommand extends Command {
final pathsToFormat = _getPathsToFormat(p);

final formatProcess = await pm.runProcess(
CliCommand.dart(['format', ...pathsToFormat], throwOnException: false),
CliCommand.dart([
'format',
...pathsToFormat,
], throwOnException: false),
workingDirectory: p.packagePath,
);

Expand Down Expand Up @@ -108,10 +111,12 @@ class PresubmitCommand extends Command {
final pathsToFormat = _getPathsToFormat(p);

final formatProcess = await pm.runProcess(
CliCommand.dart(
['format', '--output=none', '--set-exit-if-changed', ...pathsToFormat],
throwOnException: false,
),
CliCommand.dart([
'format',
'--output=none',
'--set-exit-if-changed',
...pathsToFormat,
], throwOnException: false),
workingDirectory: p.packagePath,
);

Expand Down
Loading
Loading