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:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
Reference in New Issue
Block a user