Files
clide/test/widgets/src/clide_tappable_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

112 lines
3.8 KiB
Dart

/// Widget tests for the keyboard-operable [ClideTappable] (T-100).
///
/// The pre-T-100 widget was mouse-only — these tests assert it now
/// accepts focus, that Enter / Space invoke onTap via the
/// [ActivateIntent] action, and that the focus ring appears when
/// the widget has focus.
library;
import 'package:clide/widgets/widgets.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('ClideTappable — keyboard', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() async => f.dispose());
testWidgets('accepts Tab focus when onTap is provided', (tester) async {
var tapped = 0;
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(
harness(
f,
Center(
child: ClideTappable(focusNode: node, onTap: () => tapped++, builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24)),
),
),
);
expect(node.hasFocus, isFalse);
node.requestFocus();
await tester.pump();
expect(node.hasFocus, isTrue);
// Tap count unchanged — focus alone doesn't invoke.
expect(tapped, 0);
});
testWidgets('Enter on a focused tappable invokes onTap via ActivateIntent', (tester) async {
var tapped = 0;
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(
harness(
f,
Center(
child: ClideTappable(
focusNode: node,
autofocus: true,
onTap: () => tapped++,
builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24),
),
),
),
);
await tester.pump();
expect(node.hasFocus, isTrue);
// Dispatch ActivateIntent directly against the focused context
// — mirrors what KeymapService.resolveEvent → Actions.maybeInvoke
// would do on Enter / Space.
Actions.invoke(node.context!, const ActivateIntent());
await tester.pump();
expect(tapped, 1);
});
testWidgets('disabled tappable (onTap == null) cannot receive focus', (tester) async {
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(
harness(
f,
Center(
child: ClideTappable(focusNode: node, onTap: null, builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24)),
),
),
);
node.requestFocus();
await tester.pump();
expect(node.hasFocus, isFalse, reason: 'canRequestFocus is false when onTap is null');
});
testWidgets('focus ring appears when focused, gone when unfocused', (tester) async {
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(
harness(
f,
Center(
child: ClideTappable(focusNode: node, onTap: () {}, builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24)),
),
),
);
Iterable<Color> ringColors() => tester
.widgetList<DecoratedBox>(find.descendant(of: find.byType(ClideTappable), matching: find.byType(DecoratedBox)))
.map((d) => ((d.decoration as BoxDecoration).border?.top.color ?? const Color(0x00000000)));
// Unfocused: no DecoratedBox descendant carries a non-transparent border.
expect(ringColors().any((c) => c.a > 0), isFalse);
node.requestFocus();
await tester.pumpAndSettle();
expect(node.hasFocus, isTrue);
expect(ringColors().any((c) => c.a > 0), isTrue, reason: 'focus ring border should become opaque on focus');
});
});
}