cover quick-open + find-in-files edge cases
Add tests for the quick-open overlay's keymap-intent handlers (nav, accept, dismiss), the no-match / truncated / walk-failure hints, the search panel's error + no-results states and toggle re-run, the controller's failed-grep and exclude paths, and engine glob/regex-group cases. Restores the coverage floor (95.20%). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -148,4 +148,29 @@ void main() {
|
||||
c.cancel();
|
||||
expect(c.running, isFalse);
|
||||
});
|
||||
|
||||
test('a failed search.grep surfaces the error', () async {
|
||||
f.ipc.stub(
|
||||
'search.grep',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'nope'),
|
||||
));
|
||||
final c = make();
|
||||
await c.run('foo');
|
||||
expect(c.error, 'nope');
|
||||
expect(c.running, isFalse);
|
||||
});
|
||||
|
||||
test('exclude setter feeds the next search', () async {
|
||||
Map<String, Object?>? sent;
|
||||
f.ipc.stub('search.grep', (args) async {
|
||||
sent = args;
|
||||
return _ok({'searchId': 's1'});
|
||||
});
|
||||
final c = make();
|
||||
c.exclude = 'build/** , *.g.dart';
|
||||
await c.run('foo');
|
||||
expect(sent!['exclude'], ['build/**', '*.g.dart']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,4 +69,50 @@ void main() {
|
||||
expect(opened!['path'], 'lib/a.dart');
|
||||
expect(opened!['line'], 12);
|
||||
});
|
||||
|
||||
testWidgets('no results shows the No results status', (tester) async {
|
||||
await tester.pumpWidget(harness(f, const SearchPanelView()));
|
||||
await tester.enterText(find.byType(EditableText).first, 'foo');
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
await pumpAsync(tester);
|
||||
f.services.events.emit(DaemonEvent(
|
||||
subsystem: 'search',
|
||||
kind: 'search.done',
|
||||
data: const {'searchId': 's1', 'cancelled': false},
|
||||
ts: DateTime.now().toUtc(),
|
||||
));
|
||||
await pumpAsync(tester);
|
||||
expect(find.text('No results'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a search error is shown in the panel', (tester) async {
|
||||
await tester.pumpWidget(harness(f, const SearchPanelView()));
|
||||
await tester.enterText(find.byType(EditableText).first, '(bad');
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
await pumpAsync(tester);
|
||||
f.services.events.emit(DaemonEvent(
|
||||
subsystem: 'search',
|
||||
kind: 'search.error',
|
||||
data: const {'searchId': 's1', 'message': 'invalid regex: boom'},
|
||||
ts: DateTime.now().toUtc(),
|
||||
));
|
||||
await pumpAsync(tester);
|
||||
expect(find.textContaining('invalid regex'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('toggling regex re-runs the search', (tester) async {
|
||||
var grepCalls = 0;
|
||||
f.ipc.stub('search.grep', (_) async {
|
||||
grepCalls++;
|
||||
return _ok({'searchId': 's1'});
|
||||
});
|
||||
await tester.pumpWidget(harness(f, const SearchPanelView()));
|
||||
await tester.enterText(find.byType(EditableText).first, 'foo');
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
await pumpAsync(tester);
|
||||
final before = grepCalls;
|
||||
await tester.tap(find.text('.*')); // regex toggle
|
||||
await pumpAsync(tester);
|
||||
expect(grepCalls, greaterThan(before));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,6 +144,24 @@ void main() {
|
||||
|
||||
// Spawns real worker isolates — runs in the --concurrency=1 serial
|
||||
// pass to avoid competing with the parallel flutter pool (T-193).
|
||||
test('? and ** glob metacharacters match as expected', () async {
|
||||
File('${root.path}/a1.dart').writeAsStringSync('main\n');
|
||||
// '?' matches a single char: a?.dart → a1.dart (and a.dart).
|
||||
final q = await run(const SearchQuery(pattern: 'main', include: ['a?.dart']));
|
||||
expect(q.any((m) => m.path == 'a1.dart'), isTrue);
|
||||
expect(q.any((m) => m.path == 'sub/c.txt'), isFalse);
|
||||
// '**' spans directories.
|
||||
final r = await run(const SearchQuery(pattern: 'main', include: ['sub/**']));
|
||||
expect(r.any((m) => m.path == 'sub/c.txt'), isTrue);
|
||||
});
|
||||
|
||||
test('regex capture groups are available on the match line', () async {
|
||||
// The engine surfaces spans; group expansion is the replacer's job
|
||||
// (T-53), but the regex itself must match with groups.
|
||||
final r = await run(const SearchQuery(pattern: r'final (\w+)', regex: true));
|
||||
expect(r.any((m) => m.path == 'a.dart'), isTrue);
|
||||
});
|
||||
|
||||
test('runs across isolates without error (smoke)', tags: ['serial'], () async {
|
||||
final out = <SearchMatch>[];
|
||||
await for (final batch in grepWorkspace(
|
||||
|
||||
@@ -90,4 +90,77 @@ void main() {
|
||||
// Empty query → recents listed.
|
||||
expect(find.text('app.dart'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a non-matching query shows the no-match hint', (tester) async {
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
await tester.enterText(find.byType(EditableText), 'zzzzz-nope');
|
||||
await pumpAsync(tester);
|
||||
expect(find.text('No matching files'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('truncated walk surfaces the limited-results hint', (tester) async {
|
||||
f.ipc.stub(
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.ok(id: '1', data: const {
|
||||
'files': ['lib/main.dart'],
|
||||
'truncated': true,
|
||||
}));
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
await tester.enterText(find.byType(EditableText), 'main');
|
||||
await pumpAsync(tester);
|
||||
expect(find.text('Results limited — large workspace'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('keymap intents drive nav, accept and dismiss', (tester) async {
|
||||
String? opened;
|
||||
f.ipc.stub('editor.open', (args) async {
|
||||
opened = args['path'] as String?;
|
||||
return IpcResponse.ok(id: '1', data: {'path': args['path']});
|
||||
});
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
await tester.enterText(find.byType(EditableText), 'lib');
|
||||
await pumpAsync(tester);
|
||||
|
||||
final ctx = tester.element(find.byType(EditableText));
|
||||
Actions.invoke(ctx, const QuickOpenSelectNextIntent());
|
||||
await pumpAsync(tester);
|
||||
Actions.invoke(ctx, const QuickOpenSelectPreviousIntent());
|
||||
await pumpAsync(tester);
|
||||
// Accept opens the highlighted result in the editor.
|
||||
Actions.invoke(ctx, const QuickOpenAcceptIntent());
|
||||
await pumpAsync(tester);
|
||||
expect(opened, isNotNull);
|
||||
expect(f.services.quickOpen.isOpen, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('dismiss intent closes the overlay', (tester) async {
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
final ctx = tester.element(find.byType(EditableText));
|
||||
Actions.invoke(ctx, const DismissIntent());
|
||||
await pumpAsync(tester);
|
||||
expect(f.services.quickOpen.isOpen, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('files.walk failure leaves the list empty (no crash)', (tester) async {
|
||||
f.ipc.stub(
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '1',
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'boom'),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
await tester.enterText(find.byType(EditableText), 'main');
|
||||
await pumpAsync(tester);
|
||||
expect(find.text('No matching files'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user