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>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/// 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user