Files
Client/test/utils/debouncer_test.dart
T
2026-05-08 20:12:40 +02:00

188 lines
4.7 KiB
Dart

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));
});
},
);
});
}