198 lines
7.7 KiB
Dart
198 lines
7.7 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tasq/providers/realtime_controller.dart';
|
|
import 'package:tasq/providers/stream_recovery.dart';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RealtimeController only accesses `_client.auth.onAuthStateChange` during
|
|
// `_init()`, which wraps the subscription in a try-catch. We use a minimal
|
|
// SupabaseClient pointed at localhost — the auth stream subscription will
|
|
// either return an empty stream or throw (caught internally), so no network
|
|
// activity occurs during tests.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void main() {
|
|
group('RealtimeController', () {
|
|
late RealtimeController controller;
|
|
|
|
setUp(() {
|
|
// SupabaseClient constructor does not connect eagerly; _init() catches
|
|
// any exception thrown when accessing auth.onAuthStateChange.
|
|
controller = RealtimeController(
|
|
SupabaseClient('http://localhost', 'test-anon-key',
|
|
authOptions: const AuthClientOptions(autoRefreshToken: false)),
|
|
);
|
|
});
|
|
|
|
tearDown(() {
|
|
controller.dispose();
|
|
});
|
|
|
|
// ── Initial state ───────────────────────────────────────────────────
|
|
|
|
test('starts with no recovering channels', () {
|
|
expect(controller.recoveringChannels, isEmpty);
|
|
expect(controller.isAnyStreamRecovering, isFalse);
|
|
});
|
|
|
|
test('isChannelRecovering returns false for unknown channel', () {
|
|
expect(controller.isChannelRecovering('tasks'), isFalse);
|
|
});
|
|
|
|
// ── markChannelRecovering ───────────────────────────────────────────
|
|
|
|
test('markChannelRecovering adds channel to the set', () {
|
|
controller.markChannelRecovering('tasks');
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isTrue);
|
|
expect(controller.isAnyStreamRecovering, isTrue);
|
|
});
|
|
|
|
test('markChannelRecovering with multiple channels tracks all of them', () {
|
|
controller.markChannelRecovering('tasks');
|
|
controller.markChannelRecovering('tickets');
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isTrue);
|
|
expect(controller.isChannelRecovering('tickets'), isTrue);
|
|
expect(controller.recoveringChannels.length, 2);
|
|
});
|
|
|
|
test('markChannelRecovering notifies listeners exactly once per new channel',
|
|
() {
|
|
int notifyCount = 0;
|
|
controller.addListener(() => notifyCount++);
|
|
|
|
controller.markChannelRecovering('tasks');
|
|
expect(notifyCount, 1);
|
|
|
|
// Adding the same channel again must NOT trigger a second notification.
|
|
controller.markChannelRecovering('tasks');
|
|
expect(notifyCount, 1);
|
|
});
|
|
|
|
// ── markChannelRecovered ────────────────────────────────────────────
|
|
|
|
test('markChannelRecovered removes channel from the set', () {
|
|
controller.markChannelRecovering('tasks');
|
|
controller.markChannelRecovered('tasks');
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isFalse);
|
|
expect(controller.isAnyStreamRecovering, isFalse);
|
|
});
|
|
|
|
test('markChannelRecovered on unknown channel does not notify', () {
|
|
int notifyCount = 0;
|
|
controller.addListener(() => notifyCount++);
|
|
|
|
// Recovering a channel that was never marked recovering is a no-op.
|
|
controller.markChannelRecovered('nonexistent');
|
|
expect(notifyCount, 0);
|
|
});
|
|
|
|
test('markChannelRecovered notifies listeners when channel is removed', () {
|
|
controller.markChannelRecovering('tasks');
|
|
|
|
int notifyCount = 0;
|
|
controller.addListener(() => notifyCount++);
|
|
|
|
controller.markChannelRecovered('tasks');
|
|
expect(notifyCount, 1);
|
|
});
|
|
|
|
test('recovering two channels and recovering one leaves the other active',
|
|
() {
|
|
controller.markChannelRecovering('tasks');
|
|
controller.markChannelRecovering('tickets');
|
|
controller.markChannelRecovered('tasks');
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isFalse);
|
|
expect(controller.isChannelRecovering('tickets'), isTrue);
|
|
expect(controller.isAnyStreamRecovering, isTrue);
|
|
});
|
|
|
|
// ── recoveringChannels unmodifiable view ────────────────────────────
|
|
|
|
test('recoveringChannels returns an unmodifiable set', () {
|
|
controller.markChannelRecovering('tasks');
|
|
final view = controller.recoveringChannels;
|
|
|
|
expect(() => (view as dynamic).add('other'), throwsUnsupportedError);
|
|
});
|
|
|
|
// ── handleChannelStatus ─────────────────────────────────────────────
|
|
|
|
test('handleChannelStatus with connected marks channel as recovered', () {
|
|
controller.markChannelRecovering('tasks');
|
|
controller.handleChannelStatus('tasks', StreamConnectionStatus.connected);
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isFalse);
|
|
});
|
|
|
|
test('handleChannelStatus with polling marks channel as recovered', () {
|
|
controller.markChannelRecovering('tasks');
|
|
controller.handleChannelStatus('tasks', StreamConnectionStatus.polling);
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isFalse);
|
|
});
|
|
|
|
test('handleChannelStatus with recovering marks channel as recovering', () {
|
|
controller.handleChannelStatus(
|
|
'tasks', StreamConnectionStatus.recovering);
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isTrue);
|
|
});
|
|
|
|
test('handleChannelStatus with stale marks channel as recovering', () {
|
|
controller.handleChannelStatus('tasks', StreamConnectionStatus.stale);
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isTrue);
|
|
});
|
|
|
|
test('handleChannelStatus with failed marks channel as recovering', () {
|
|
controller.handleChannelStatus('tasks', StreamConnectionStatus.failed);
|
|
|
|
expect(controller.isChannelRecovering('tasks'), isTrue);
|
|
});
|
|
|
|
test('handleChannelStatus connected after recovering clears it', () {
|
|
controller.handleChannelStatus(
|
|
'tasks', StreamConnectionStatus.recovering);
|
|
expect(controller.isAnyStreamRecovering, isTrue);
|
|
|
|
controller.handleChannelStatus('tasks', StreamConnectionStatus.connected);
|
|
expect(controller.isAnyStreamRecovering, isFalse);
|
|
});
|
|
|
|
// ── Legacy compat ───────────────────────────────────────────────────
|
|
|
|
test('markStreamRecovering maps to _global channel', () {
|
|
controller.markStreamRecovering();
|
|
|
|
expect(controller.isChannelRecovering('_global'), isTrue);
|
|
expect(controller.isAnyStreamRecovering, isTrue);
|
|
});
|
|
|
|
test('markStreamRecovered clears _global channel', () {
|
|
controller.markStreamRecovering();
|
|
controller.markStreamRecovered();
|
|
|
|
expect(controller.isChannelRecovering('_global'), isFalse);
|
|
expect(controller.isAnyStreamRecovering, isFalse);
|
|
});
|
|
|
|
// ── isAnyStreamRecovering aggregate ────────────────────────────────
|
|
|
|
test('isAnyStreamRecovering is false only when all channels are recovered',
|
|
() {
|
|
controller.markChannelRecovering('a');
|
|
controller.markChannelRecovering('b');
|
|
controller.markChannelRecovered('a');
|
|
|
|
expect(controller.isAnyStreamRecovering, isTrue);
|
|
|
|
controller.markChannelRecovered('b');
|
|
expect(controller.isAnyStreamRecovering, isFalse);
|
|
});
|
|
});
|
|
}
|