Files
clide/test/widgets/src/clide_toast_test.dart
T
jpmschweitzerandClaude Opus 4.8 1f09abcfd7 add toast notification system (T-50)
Non-modal operation-feedback toasts, bottom-right: a ClideToast card per
severity (success/warning/error/info), auto-dismiss (errors linger), queue
with a visible cap, slide+fade in, manual dismiss, live-region a11y.

ToastService is a MessageBus consumer — components raise a toast by publishing
to the 'toast' channel (publishToast helper), so emitters stay decoupled from
the UI. GitController's push/pull are the first emitters. ToastOverlay mounts
in the app-root Stack.

Also adds comprehensive GitController coverage: importing it for the toast
emitter test first pulled the whole file into the coverage denominator, so the
controller is now tested end to end (status/stage/commit/stash/push/pull).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:14:42 +02:00

86 lines
3.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Widget tests for ClideToast + ToastOverlay (T-50): render per severity,
/// manual dismiss, the live-region a11y contract, and the overlay reflecting
/// the ToastService queue. Toasts are shown sticky (Duration.zero) so no
/// auto-dismiss Timer is left pending at teardown.
library;
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
void main() {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() async => f.dispose());
Widget host(Widget child) => Directionality(
textDirection: TextDirection.ltr,
child: ClideKernel(
services: f.services,
child: ClideTheme(
controller: f.services.theme,
child: Align(alignment: Alignment.topLeft, child: child),
),
),
);
testWidgets('renders the message and calls onDismiss when the × is tapped', (tester) async {
var dismissed = false;
await tester.pumpWidget(host(ClideToast(
entry: const ToastEntry(id: 1, message: 'Pushed to origin/main', severity: ToastSeverity.success),
onDismiss: () => dismissed = true,
)));
await tester.pump(const Duration(milliseconds: 300)); // settle entrance
expect(find.text('Pushed to origin/main'), findsOneWidget);
// The dismiss × is the toast's only ClideTappable.
await tester.tap(find.byType(ClideTappable));
await tester.pump();
expect(dismissed, isTrue);
});
testWidgets('exposes the message as a live region (a11y)', (tester) async {
await tester.pumpWidget(host(ClideToast(
entry: const ToastEntry(id: 1, message: 'Heads up', severity: ToastSeverity.warning),
onDismiss: () {},
)));
await tester.pump(const Duration(milliseconds: 300));
// The message is wrapped in a live-region Semantics so screen readers
// announce it when it appears.
expect(
find.byWidgetPredicate((w) => w is Semantics && w.properties.liveRegion == true && w.properties.label == 'Heads up'),
findsOneWidget,
);
});
testWidgets('overlay renders a card per queued toast and dismiss removes one', (tester) async {
await tester.pumpWidget(host(const SizedBox(
width: 800,
height: 600,
child: Stack(children: [ToastOverlay()]),
)));
await tester.pump();
expect(find.byType(ClideToast), findsNothing);
f.services.toast.show('one', duration: Duration.zero);
f.services.toast.show('two', severity: ToastSeverity.error, duration: Duration.zero);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.byType(ClideToast), findsNWidgets(2));
expect(find.text('one'), findsOneWidget);
expect(find.text('two'), findsOneWidget);
// Dismissing via the service updates the overlay (tap-to-dismiss is
// covered by the ClideToast test above; here we assert reactivity).
f.services.toast.dismiss(f.services.toast.entries.first.id);
await tester.pump();
expect(find.byType(ClideToast), findsNWidgets(1));
expect(find.text('one'), findsNothing);
expect(find.text('two'), findsOneWidget);
});
}