import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:marianum_mobile/utils/debouncer.dart'; void main() { // Each test is wrapped in fakeAsync so timers fire deterministically. group('Debouncer.debounce', () { test( 'runs the action once after the delay elapses without further calls', () { fakeAsync((async) { var calls = 0; Debouncer.debounce( 'tag', const Duration(milliseconds: 100), () => calls++, ); async.elapse(const Duration(milliseconds: 99)); expect(calls, 0); async.elapse(const Duration(milliseconds: 1)); expect(calls, 1); }); }, ); test('subsequent calls within the delay reset the timer (coalesce)', () { fakeAsync((async) { var calls = 0; void schedule() => Debouncer.debounce( 'tag', const Duration(milliseconds: 100), () => calls++, ); schedule(); async.elapse(const Duration(milliseconds: 80)); schedule(); // resets async.elapse(const Duration(milliseconds: 80)); schedule(); // resets async.elapse(const Duration(milliseconds: 80)); expect(calls, 0, reason: 'each schedule() resets the timer'); async.elapse(const Duration(milliseconds: 100)); expect(calls, 1); }); }); test('different tags are independent', () { fakeAsync((async) { var aCalls = 0; var bCalls = 0; Debouncer.debounce( 'a', const Duration(milliseconds: 100), () => aCalls++, ); Debouncer.debounce( 'b', const Duration(milliseconds: 100), () => bCalls++, ); async.elapse(const Duration(milliseconds: 100)); expect(aCalls, 1); expect(bCalls, 1); }); }); }); group('Debouncer.throttle', () { test( 'first call runs immediately, subsequent calls within window are dropped', () { fakeAsync((async) { var calls = 0; Debouncer.throttle( 'tag', const Duration(milliseconds: 100), () => calls++, ); expect( calls, 1, reason: 'throttle fires the first call synchronously', ); Debouncer.throttle( 'tag', const Duration(milliseconds: 100), () => calls++, ); Debouncer.throttle( 'tag', const Duration(milliseconds: 100), () => calls++, ); expect( calls, 1, reason: 'subsequent calls within the gate are ignored', ); async.elapse(const Duration(milliseconds: 100)); Debouncer.throttle( 'tag', const Duration(milliseconds: 100), () => calls++, ); expect( calls, 2, reason: 'after the window elapses, throttle fires again', ); }); }, ); test('different tags throttle independently', () { fakeAsync((async) { var aCalls = 0; var bCalls = 0; Debouncer.throttle( 'a', const Duration(milliseconds: 100), () => aCalls++, ); Debouncer.throttle( 'b', const Duration(milliseconds: 100), () => bCalls++, ); expect(aCalls, 1); expect(bCalls, 1); async.elapse(const Duration(milliseconds: 100)); }); }); }); group('Debouncer.cancel', () { test('cancels a pending debounce so the action never runs', () { fakeAsync((async) { var calls = 0; Debouncer.debounce( 'tag', const Duration(milliseconds: 100), () => calls++, ); Debouncer.cancel('tag'); async.elapse(const Duration(milliseconds: 200)); expect(calls, 0); }); }); test( 'cancels an active throttle gate so the next call fires immediately', () { fakeAsync((async) { var calls = 0; Debouncer.throttle( 'tag', const Duration(milliseconds: 100), () => calls++, ); expect(calls, 1); Debouncer.cancel('tag'); Debouncer.throttle( 'tag', const Duration(milliseconds: 100), () => calls++, ); expect( calls, 2, reason: 'cancel removed the gate so the next throttle fires again', ); async.elapse(const Duration(milliseconds: 100)); }); }, ); }); }