test sweep: kernel commands + widgets coverage, ratchet floor to 93

Add tests for `keybindings.dart` (KeyEvent → Keybinding mapping,
parse-error edges, resolver entries view), `toolchain_paths.dart`
(the Flutter-free `ToolchainView.resolved` static view), and several
`widgets/src/` primitives: tooltip hover/overlay, palette filter +
submit, multitab controller `copyWith` + size getters, and additional
markdown branches (h3–h6 headings, tables, strikethrough, default
block fallback, record-link tap).

Unfreezes the pre-push coverage floor that was held at 90 on
2026-05-14 by mistake and ratchets to 93. Tidies eight test files
that had accumulated unused imports flagged by `unnecessary_import`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 20:20:37 +02:00
co-authored by Claude Opus 4.7
parent 9030e564e5
commit e430a87569
18 changed files with 557 additions and 12 deletions
@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:clide/builtin/theme_picker/theme_picker.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
-1
View File
@@ -2,7 +2,6 @@
library;
import 'package:clide/clide.dart';
import 'package:clide/src/daemon/dispatcher.dart';
import 'package:test/test.dart';
IpcRequest _req(String cmd, {String id = '1', Map<String, Object?> args = const {}}) {
@@ -10,7 +10,6 @@ import 'dart:io';
import 'package:clide/clide.dart';
import 'package:clide/kernel/src/toolchain_paths.dart';
import 'package:clide/src/daemon/pql_commands.dart';
import 'package:clide/src/pql/client.dart';
import 'package:test/test.dart';
void main() {
@@ -1,7 +1,10 @@
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Keybinding.parse + equality', () {
test('parses single-key bindings', () {
final k = Keybinding.parse('g');
@@ -20,10 +23,30 @@ void main() {
expect(() => Keybinding.parse(''), throwsA(isA<ArgumentError>()));
});
test('rejects spec ending in `+` (missing key)', () {
expect(() => Keybinding.parse('ctrl+'), throwsA(isA<ArgumentError>()));
});
test('canonical modifier order is deterministic', () {
final k = Keybinding.parse('alt+ctrl+shift+x');
expect(k.modifiers, ['alt', 'ctrl', 'shift']);
});
test('canonical of modifier-free binding is just the key', () {
expect(Keybinding.parse('escape').canonical, 'escape');
});
test('hashCode matches for equal bindings, differs for distinct', () {
final a = Keybinding.parse('ctrl+shift+g');
final b = Keybinding.parse('Shift+Ctrl+G');
final c = Keybinding.parse('ctrl+g');
expect(a.hashCode, b.hashCode);
expect(a.hashCode, isNot(c.hashCode));
});
test('toString embeds canonical form', () {
expect(Keybinding.parse('ctrl+k').toString(), 'Keybinding(ctrl+k)');
});
});
group('KeybindingResolver', () {
@@ -41,5 +64,74 @@ void main() {
r.unbind(k);
expect(r.commandFor(k), isNull);
});
test('entries exposes registered bindings', () {
final r = KeybindingResolver();
r.bind(Keybinding.parse('ctrl+p'), 'palette.open');
r.bind(Keybinding.parse('ctrl+shift+p'), 'palette.commands');
final commands = r.entries.map((e) => e.value).toSet();
expect(commands, {'palette.open', 'palette.commands'});
});
});
group('KeybindingResolver.fromKeyEvent', () {
late HardwareKeyboard kb;
setUp(() => kb = HardwareKeyboard.instance);
tearDown(() => kb.clearState());
test('returns null for KeyUpEvent', () {
final up = KeyUpEvent(
physicalKey: PhysicalKeyboardKey.keyG,
logicalKey: LogicalKeyboardKey.keyG,
timeStamp: Duration.zero,
);
expect(KeybindingResolver.fromKeyEvent(up, kb), isNull);
});
test('returns null when logicalKey has no keyLabel', () {
// A synthetic logical key with an unassigned id has an empty label.
final unlabeled = LogicalKeyboardKey(0x1000fffff);
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.controlLeft,
logicalKey: unlabeled,
timeStamp: Duration.zero,
);
expect(KeybindingResolver.fromKeyEvent(down, kb), isNull);
});
test('maps a plain KeyDownEvent to a modifier-free Keybinding', () {
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.keyG,
logicalKey: LogicalKeyboardKey.keyG,
timeStamp: Duration.zero,
);
final b = KeybindingResolver.fromKeyEvent(down, kb);
expect(b, isNotNull);
expect(b!.key, 'g');
expect(b.modifiers, isEmpty);
});
test('includes every held modifier in the resulting Keybinding', () {
// Simulate ctrl+shift+alt+meta held, then a keyG down.
_holdModifier(PhysicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlLeft);
_holdModifier(PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft);
_holdModifier(PhysicalKeyboardKey.altLeft, LogicalKeyboardKey.altLeft);
_holdModifier(PhysicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaLeft);
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.keyG,
logicalKey: LogicalKeyboardKey.keyG,
timeStamp: Duration.zero,
);
final b = KeybindingResolver.fromKeyEvent(down, kb)!;
expect(b.key, 'g');
expect(b.modifiers.toSet(), {'ctrl', 'shift', 'alt', 'cmd'});
});
});
}
void _holdModifier(PhysicalKeyboardKey physical, LogicalKeyboardKey logical) {
HardwareKeyboard.instance.handleKeyEvent(
KeyDownEvent(physicalKey: physical, logicalKey: logical, timeStamp: Duration.zero),
);
}
-2
View File
@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -6,7 +6,6 @@ library;
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:clide/kernel/src/i18n/catalog_loader.dart';
@@ -3,7 +3,6 @@ library;
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/kernel/src/panels/drag_resize.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
Binary file not shown.
-2
View File
@@ -6,10 +6,8 @@
library;
import 'dart:io';
import 'dart:ui';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/kernel/src/theme/contrast.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
+50
View File
@@ -0,0 +1,50 @@
/// Unit tests for `ToolchainView.resolved` (the Flutter-free `_StaticToolchain`)
/// in `lib/kernel/src/toolchain_paths.dart`. The listenable `Toolchain` and the
/// top-level `resolveToolchainPaths` are covered by `toolchain_test.dart`.
library;
import 'package:clide/kernel/src/toolchain_paths.dart';
import 'package:test/test.dart';
void main() {
group('ToolchainView.resolved', () {
test('exposes the supplied paths verbatim', () {
final v = ToolchainView.resolved(const ResolvedPaths(
git: '/opt/git',
pql: '/opt/pql',
tmux: '/opt/tmux',
shell: '/usr/bin/zsh',
gitEnv: {'GIT_EXEC_PATH': '/opt/git-core'},
));
expect(v.git, '/opt/git');
expect(v.pql, '/opt/pql');
expect(v.tmux, '/opt/tmux');
expect(v.shell, '/usr/bin/zsh');
expect(v.gitEnv, {'GIT_EXEC_PATH': '/opt/git-core'});
expect(v.resolved, isTrue);
expect(v.allOk, isTrue);
expect(v.missing, isEmpty);
});
test('falls back to bare command names when paths are null', () {
final v = ToolchainView.resolved(const ResolvedPaths());
expect(v.git, 'git');
expect(v.pql, 'pql');
expect(v.tmux, 'tmux');
expect(v.shell, '/bin/bash');
expect(v.gitEnv, isNull);
expect(v.resolved, isTrue);
expect(v.allOk, isFalse);
expect(v.missing, ['git', 'pql', 'tmux']);
});
test('missing reports only the unresolved tools', () {
final v = ToolchainView.resolved(const ResolvedPaths(
git: '/opt/git',
// pql + tmux null → missing.
));
expect(v.missing, ['pql', 'tmux']);
expect(v.allOk, isFalse);
});
});
}
+47
View File
@@ -84,6 +84,53 @@ After.
expect(find.byType(ClideMarkdown), findsOneWidget);
expect(tapped, isEmpty); // not tapped yet — no crash is the point
});
testWidgets('record-id link tap actually invokes onRecordTap', (tester) async {
var tapped = '';
const src = '[D-1](#anchor)';
await tester.pumpWidget(
harness(f, ClideMarkdown(src, onRecordTap: (id) => tapped = id)),
);
await tester.pumpAndSettle();
// The link renders as a ClideTappable embedded in a WidgetSpan.
await tester.tap(find.text('D-1'));
await tester.pumpAndSettle();
expect(tapped, 'D-1');
});
testWidgets('h3 / h4 / h5 / h6 headings render with the right padding tier', (tester) async {
const src = '### h3\n\n#### h4\n\n##### h5\n\n###### h6\n';
await tester.pumpWidget(harness(f, const ClideMarkdown(src)));
await tester.pumpAndSettle();
expect(find.byType(ClideMarkdown), findsOneWidget);
// Each heading contributes a Padding parent — at minimum the document
// must render without throwing and include RichText spans for each.
expect(find.byType(RichText), findsWidgets);
});
testWidgets('renders pipe-style tables (thead + tbody)', (tester) async {
const src = '| col a | col b |\n|-------|-------|\n| a1 | b1 |\n| a2 | b2 |\n';
await tester.pumpWidget(harness(f, const ClideMarkdown(src)));
await tester.pumpAndSettle();
// The Flutter `Table` widget appears for every rendered markdown table.
expect(find.byType(Table), findsOneWidget);
});
testWidgets('renders ~~strikethrough~~ as a del span', (tester) async {
const src = 'this is ~~gone~~ now';
await tester.pumpWidget(harness(f, const ClideMarkdown(src)));
await tester.pumpAndSettle();
expect(find.byType(ClideMarkdown), findsOneWidget);
});
testWidgets('unknown block tags fall through to the default branch without throwing', (tester) async {
// Raw HTML the markdown parser leaves as a passthrough element with an
// unrecognized tag — exercises the `default:` arm of `_buildBlock`.
const src = '<aside>side note</aside>';
await tester.pumpWidget(harness(f, const ClideMarkdown(src)));
await tester.pumpAndSettle();
expect(find.byType(ClideMarkdown), findsOneWidget);
});
});
group('ClideCodeBlock', () {
@@ -162,5 +162,47 @@ void main() {
c.remove('b');
expect(calls, 4);
});
test('length / isEmpty / isNotEmpty mirror the entries list', () {
final c = MultitabController<String>();
expect(c.length, 0);
expect(c.isEmpty, isTrue);
expect(c.isNotEmpty, isFalse);
c.add(entry('a'));
expect(c.length, 1);
expect(c.isEmpty, isFalse);
expect(c.isNotEmpty, isTrue);
});
});
group('MultitabEntry.copyWith', () {
test('overrides each field independently and keeps id', () {
const base = MultitabEntry<int>(id: 'x', title: 't', payload: 1);
final renamed = base.copyWith(title: 'T');
expect(renamed.id, 'x');
expect(renamed.title, 'T');
expect(renamed.payload, 1);
expect(renamed.closeable, isTrue);
expect(renamed.reorderable, isTrue);
final repayloaded = base.copyWith(payload: 99);
expect(repayloaded.payload, 99);
final pinned = base.copyWith(closeable: false, reorderable: false);
expect(pinned.closeable, isFalse);
expect(pinned.reorderable, isFalse);
});
test('omitting all overrides yields an equivalent entry', () {
const base = MultitabEntry<String>(id: 'a', title: 'A', payload: 'p');
final clone = base.copyWith();
expect(clone.id, base.id);
expect(clone.title, base.title);
expect(clone.payload, base.payload);
expect(clone.closeable, base.closeable);
expect(clone.reorderable, base.reorderable);
});
});
}
+161
View File
@@ -0,0 +1,161 @@
/// Widget tests for `lib/widgets/src/clide_tooltip.dart` — hover-driven
/// OverlayEntry that respects showDelay, places itself below the target by
/// default, and flips above when the screen is short on space below.
library;
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
import '../../helpers/widget_harness.dart';
void main() {
group('ClideTooltip', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() async => f.dispose());
testWidgets('tooltip appears after showDelay on hover and hides on exit', (tester) async {
await tester.pumpWidget(
harness(
f,
const Align(
alignment: Alignment.topLeft,
child: ClideTooltip(
message: 'hello',
showDelay: Duration(milliseconds: 10),
child: SizedBox(width: 40, height: 20, key: ValueKey('target')),
),
),
),
);
// Not yet hovering — tooltip text is not in the tree.
expect(find.text('hello'), findsNothing);
// Move a mouse pointer over the target.
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
await gesture.moveTo(tester.getCenter(find.byKey(const ValueKey('target'))));
await tester.pump();
// Let the showDelay elapse and the overlay insert.
await tester.pump(const Duration(milliseconds: 20));
expect(find.text('hello'), findsOneWidget);
// Hover out — overlay entry is removed synchronously.
await gesture.moveTo(const Offset(2000, 2000));
await tester.pump();
expect(find.text('hello'), findsNothing);
});
testWidgets('mouse-exit before showDelay elapses suppresses the overlay', (tester) async {
await tester.pumpWidget(
harness(
f,
const Align(
alignment: Alignment.topLeft,
child: ClideTooltip(
message: 'late',
showDelay: Duration(milliseconds: 50),
child: SizedBox(width: 40, height: 20, key: ValueKey('target')),
),
),
),
);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
await gesture.moveTo(tester.getCenter(find.byKey(const ValueKey('target'))));
await tester.pump(const Duration(milliseconds: 10));
// Exit before the delay completes.
await gesture.moveTo(const Offset(2000, 2000));
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('late'), findsNothing);
});
testWidgets('places tooltip above the target when little space below', (tester) async {
// Shrink the test view so the target sits near the bottom edge.
tester.view.physicalSize = const Size(400, 100);
tester.view.devicePixelRatio = 1.0;
addTearDown(() {
tester.view.resetPhysicalSize();
tester.view.resetDevicePixelRatio();
});
await tester.pumpWidget(
harness(
f,
const Align(
alignment: Alignment.bottomLeft,
child: ClideTooltip(
message: 'above',
showDelay: Duration(milliseconds: 1),
child: SizedBox(width: 40, height: 20, key: ValueKey('target')),
),
),
),
);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
await gesture.moveTo(tester.getCenter(find.byKey(const ValueKey('target'))));
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('above'), findsOneWidget);
// The Positioned ancestor of the tooltip uses `bottom:` (above-mode),
// not `top:`, when there isn't enough room below.
final positioned = tester.widget<Positioned>(
find.ancestor(
of: find.text('above'),
matching: find.byType(Positioned),
),
);
expect(positioned.bottom, isNotNull);
expect(positioned.top, isNull);
});
testWidgets('re-entering after exit shows the tooltip again (replaces overlay entry)', (tester) async {
await tester.pumpWidget(
harness(
f,
const Align(
alignment: Alignment.topLeft,
child: ClideTooltip(
message: 'again',
showDelay: Duration(milliseconds: 5),
child: SizedBox(width: 40, height: 20, key: ValueKey('target')),
),
),
),
);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
// First hover cycle.
await gesture.moveTo(tester.getCenter(find.byKey(const ValueKey('target'))));
await tester.pump(const Duration(milliseconds: 20));
expect(find.text('again'), findsOneWidget);
// Exit.
await gesture.moveTo(const Offset(2000, 2000));
await tester.pump();
expect(find.text('again'), findsNothing);
// Re-enter — _show takes the `_entry?.remove()` branch (entry is null
// now, but the rebuild proves the overlay path is re-traversed).
await gesture.moveTo(tester.getCenter(find.byKey(const ValueKey('target'))));
await tester.pump(const Duration(milliseconds: 20));
expect(find.text('again'), findsOneWidget);
});
});
}
@@ -13,6 +13,8 @@ import 'package:clide/widgets/src/clide_resize_border.dart';
import 'package:clide/widgets/src/clide_spine.dart';
import 'package:clide/widgets/src/icons/phosphor.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -70,6 +72,79 @@ void main() {
await tester.pumpAndSettle();
expect(invocations, 1);
});
testWidgets('typing narrows the visible commands via palette.setFilter', (tester) async {
f.services.commands.register(CommandContribution(
id: 'c1',
command: 'alpha.cmd',
title: 'Alpha',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
));
f.services.commands.register(CommandContribution(
id: 'c2',
command: 'beta.cmd',
title: 'Beta',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
));
f.services.palette.open();
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
await tester.pumpAndSettle();
await tester.enterText(find.byType(EditableText), 'alpha');
await tester.pumpAndSettle();
expect(find.text('Alpha'), findsOneWidget);
expect(find.text('Beta'), findsNothing);
});
testWidgets('submitting the input invokes the first filtered command', (tester) async {
var invocations = 0;
f.services.commands.register(CommandContribution(
id: 'c1',
command: 'submit.target',
title: 'Submit Target',
run: (_) async {
invocations++;
return IpcResponse.ok(id: '', data: const {});
},
));
f.services.commands.register(CommandContribution(
id: 'c2',
command: 'other.cmd',
title: 'Other',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
));
f.services.palette.open();
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
await tester.pumpAndSettle();
await tester.enterText(find.byType(EditableText), 'submit');
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(invocations, 1);
});
testWidgets('hovering a palette row updates its hover state', (tester) async {
f.services.commands.register(CommandContribution(
id: 'c1',
command: 'hover.cmd',
title: 'Hoverable',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
));
f.services.palette.open();
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
await tester.pumpAndSettle();
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
await gesture.moveTo(tester.getCenter(find.text('Hoverable')));
await tester.pumpAndSettle();
// Exit again to also exercise the onExit branch.
await gesture.moveTo(const Offset(2000, 2000));
await tester.pumpAndSettle();
expect(find.text('Hoverable'), findsOneWidget);
});
});
group('ClideFilterBox', () {