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>
86 lines
3.2 KiB
Dart
86 lines
3.2 KiB
Dart
/// 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);
|
||
});
|
||
}
|