Files
clide/test/widgets/src/clide_tooltip_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

157 lines
5.7 KiB
Dart

/// 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);
});
});
}