Files
clide/test/kernel/src/toast_test.dart
T
jpmschweitzerandClaude Opus 4.8 e55b4a9173 test: deflake the suite — pumpEventQueue instead of Future.delayed(Duration.zero)
~173 `await Future<void>.delayed(Duration.zero)` async-settle waits across 25
test files yield the microtask queue exactly once; when an event→handler chain
needs more than one hop they lose the race under CI's parallel load, so the
failing set varied run to run. Replace with `await pumpEventQueue()` (the
deterministic drain already used elsewhere in the suite); rewired the shared
settle()/tick() helpers in one shot. menu_bar's toggle-close test gets a bounded
extra pump. Verified green under CI=true.

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

128 lines
4.9 KiB
Dart

/// Unit tests for ToastService (T-50): MessageBus consumption, queueing,
/// the visible cap, manual dismiss, clear, and per-severity auto-dismiss.
///
/// Timer-driven cases run under testWidgets so flutter_test's fake clock
/// fires the Timers on `tester.pump(duration)` — no `fake_async` dependency.
library;
import 'package:clide/kernel/src/events/message_bus.dart';
import 'package:clide/kernel/src/toast.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// A service wired to a fresh bus; both torn down.
(ToastService, MessageBus) make({int maxVisible = 4}) {
final bus = MessageBus();
final t = ToastService(messages: bus, maxVisible: maxVisible);
addTearDown(t.dispose);
addTearDown(bus.dispose);
return (t, bus);
}
group('ToastService — MessageBus consumption', () {
test('shows a toast for each message published to the toast channel', () async {
final (t, bus) = make();
publishToast(bus, 'builtin.git', 'Pushed to origin/main', severity: ToastSeverity.success, duration: Duration.zero);
await pumpEventQueue(); // let the broadcast stream deliver
expect(t.entries.single.message, 'Pushed to origin/main');
expect(t.entries.single.severity, ToastSeverity.success);
});
test('ignores messages without a string "message" payload', () async {
final (t, bus) = make();
bus.publish('x', toastChannel, {'severity': 'error'}); // no message
bus.publish('x', 'other-channel', {'message': 'nope'}); // wrong channel
await pumpEventQueue();
expect(t.entries, isEmpty);
});
test('parses severity by name and defaults unknown to info', () async {
final (t, bus) = make();
bus.publish('x', toastChannel, {'message': 'a', 'severity': 'warning', 'durationMs': 0});
bus.publish('x', toastChannel, {'message': 'b', 'severity': 'bogus', 'durationMs': 0});
await pumpEventQueue();
expect(t.entries.map((e) => e.severity), [ToastSeverity.warning, ToastSeverity.info]);
});
});
group('ToastService — queue', () {
test('show appends entries with monotonic ids and the given severity', () {
final (t, _) = make();
final a = t.show('hello', duration: Duration.zero);
final b = t.show('there', severity: ToastSeverity.error, duration: Duration.zero);
expect(a, isNot(b));
expect(t.entries.map((e) => e.message), ['hello', 'there']);
expect(t.entries.first.severity, ToastSeverity.info); // default
expect(t.entries.last.severity, ToastSeverity.error);
});
test('caps the visible count, dropping the oldest', () {
final (t, _) = make(maxVisible: 2);
t.show('1', duration: Duration.zero);
t.show('2', duration: Duration.zero);
t.show('3', duration: Duration.zero);
expect(t.entries.map((e) => e.message), ['2', '3']);
});
test('dismiss removes by id and is a no-op for unknown ids', () {
final (t, _) = make();
final id = t.show('x', duration: Duration.zero);
var fired = 0;
t.addListener(() => fired++);
t.dismiss(99999); // unknown → no notify
expect(fired, 0);
t.dismiss(id);
expect(t.entries, isEmpty);
expect(fired, 1);
});
test('clear drops everything (and only notifies when non-empty)', () {
final (t, _) = make();
var fired = 0;
t.addListener(() => fired++);
t.clear(); // already empty → no notify
expect(fired, 0);
t.show('a', duration: Duration.zero);
t.show('b', duration: Duration.zero);
fired = 0;
t.clear();
expect(t.entries, isEmpty);
expect(fired, 1);
});
});
group('ToastService — auto-dismiss timers', () {
testWidgets('fires after the default duration; errors linger longer', (tester) async {
final (t, _) = make();
await tester.pumpWidget(const SizedBox());
t.show('info');
t.show('boom', severity: ToastSeverity.error);
await tester.pump(const Duration(seconds: 3, milliseconds: 900));
expect(t.entries.length, 2);
await tester.pump(const Duration(milliseconds: 200)); // past 4s
expect(t.entries.map((e) => e.message), ['boom']);
await tester.pump(const Duration(seconds: 4)); // past 8s
expect(t.entries, isEmpty);
});
testWidgets('a zero/sticky duration never auto-dismisses', (tester) async {
final (t, _) = make();
await tester.pumpWidget(const SizedBox());
t.show('sticky', duration: Duration.zero);
await tester.pump(const Duration(minutes: 5));
expect(t.entries.length, 1);
});
testWidgets('dispose cancels pending timers (no lingering callbacks)', (tester) async {
final bus = MessageBus();
addTearDown(bus.dispose);
final t = ToastService(messages: bus);
await tester.pumpWidget(const SizedBox());
t.show('x');
t.dispose();
await tester.pump(const Duration(seconds: 10)); // cancelled timer must not fire
});
});
}