import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tasq/providers/connectivity_provider.dart'; import 'package:tasq/widgets/offline_banner.dart'; // --------------------------------------------------------------------------- // Offline / Connectivity Tests // // Three test tiers: // 1. Unit — pure provider state (no Flutter, no Supabase) // 2. Widget — OfflineBanner visibility // 3. Integration stubs — AppRepository + SQLite (require Supabase setup, // skipped in CI; see comments for manual verification steps) // --------------------------------------------------------------------------- void main() { // ────────────────────────────────────────────────────────────────────────── // Tier 1 — Provider unit tests // ────────────────────────────────────────────────────────────────────────── group('isOnlineProvider', () { test('starts as true (online by default)', () { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(isOnlineProvider), isTrue); }); test('can be set to false (simulate going offline)', () { final container = ProviderContainer(); addTearDown(container.dispose); container.read(isOnlineProvider.notifier).state = false; expect(container.read(isOnlineProvider), isFalse); }); test('can transition false → true (simulate reconnect)', () { final container = ProviderContainer(); addTearDown(container.dispose); container.read(isOnlineProvider.notifier).state = false; expect(container.read(isOnlineProvider), isFalse); container.read(isOnlineProvider.notifier).state = true; expect(container.read(isOnlineProvider), isTrue); }); test('listeners are notified on state change', () { final container = ProviderContainer(); addTearDown(container.dispose); final states = []; container.listen(isOnlineProvider, (_, next) => states.add(next)); container.read(isOnlineProvider.notifier).state = false; container.read(isOnlineProvider.notifier).state = true; container.read(isOnlineProvider.notifier).state = false; expect(states, equals([false, true, false])); }); }); // ────────────────────────────────────────────────────────────────────────── // Tier 2 — Widget tests for OfflineBanner // ────────────────────────────────────────────────────────────────────────── group('OfflineBanner widget', () { /// Builds OfflineBanner inside a ProviderScope that overrides /// [isOnlineProvider] to [online] and [connectivityMonitorProvider] /// to a no-op so no real HTTP calls are made. Widget buildBanner({required bool online}) { return ProviderScope( overrides: [ isOnlineProvider.overrideWith((ref) => online), // Suppress the real connectivity poll — no network in tests. connectivityMonitorProvider.overrideWith((_) {}), ], child: const MaterialApp( home: Scaffold( body: OfflineBanner(child: SizedBox.expand()), ), ), ); } testWidgets('hides banner when online', (tester) async { await tester.pumpWidget(buildBanner(online: true)); await tester.pump(); expect(find.byIcon(Icons.wifi_off), findsNothing); expect( find.text( 'No internet \u2014 changes saved locally, will sync automatically', ), findsNothing, ); }); testWidgets('shows banner when offline', (tester) async { await tester.pumpWidget(buildBanner(online: false)); await tester.pump(); expect(find.byIcon(Icons.wifi_off), findsOneWidget); expect( find.text( 'No internet \u2014 changes saved locally, will sync automatically', ), findsOneWidget, ); }); testWidgets('banner transitions from offline to online', (tester) async { // Use UncontrolledProviderScope so we can update provider state in-place // without replacing the widget tree (which would reset AnimatedSize). final container = ProviderContainer( overrides: [connectivityMonitorProvider.overrideWith((_) {})], ); addTearDown(container.dispose); container.read(isOnlineProvider.notifier).state = false; await tester.pumpWidget( UncontrolledProviderScope( container: container, child: const MaterialApp( home: Scaffold(body: OfflineBanner(child: SizedBox.expand())), ), ), ); await tester.pump(); expect(find.byIcon(Icons.wifi_off), findsOneWidget); // Go online — update provider state, then let AnimatedSize animate. container.read(isOnlineProvider.notifier).state = true; await tester.pumpAndSettle(); expect(find.byIcon(Icons.wifi_off), findsNothing); }); testWidgets('banner transitions from online to offline', (tester) async { final container = ProviderContainer( overrides: [connectivityMonitorProvider.overrideWith((_) {})], ); addTearDown(container.dispose); // Start online (default is true). await tester.pumpWidget( UncontrolledProviderScope( container: container, child: const MaterialApp( home: Scaffold(body: OfflineBanner(child: SizedBox.expand())), ), ), ); await tester.pump(); expect(find.byIcon(Icons.wifi_off), findsNothing); // Go offline — update provider state, then let AnimatedSize animate. container.read(isOnlineProvider.notifier).state = false; await tester.pumpAndSettle(); expect(find.byIcon(Icons.wifi_off), findsOneWidget); }); testWidgets('child widget is always rendered regardless of connectivity', (tester) async { const childKey = Key('test-child'); for (final online in [true, false]) { await tester.pumpWidget( ProviderScope( overrides: [ isOnlineProvider.overrideWith((ref) => online), connectivityMonitorProvider.overrideWith((_) {}), ], child: const MaterialApp( home: Scaffold( body: OfflineBanner( child: SizedBox.expand(key: childKey), ), ), ), ), ); await tester.pump(); expect(find.byKey(childKey), findsOneWidget, reason: 'child must be visible when online=$online'); } }); }); // ────────────────────────────────────────────────────────────────────────── // Tier 3 — Integration stubs (AppRepository + SQLite + Supabase) // // These tests are NOT run automatically because they require: // • a real (or local) Supabase instance // • the full AppRepository.configure() call with a valid client // // To run manually: // 1. Start a local Supabase dev stack: `supabase start` // 2. Set SUPABASE_URL / SUPABASE_ANON_KEY env vars // 3. Run: flutter test test/offline_sync_test.dart --name "integration" // // Scenario A — offline create → sync // 1. Set AppRepository offline (no network / airplane mode mock) // 2. Call AppRepository.instance.upsert(task) // 3. Read back with OfflineFirstGetPolicy.localOnly → expect task present // 4. Restore network // 5. Wait for Brick HTTP queue to replay // 6. Read with OfflineFirstGetPolicy.requireRemote → expect task synced // 7. Verify task_number is now server-assigned (not null) // // Scenario B — online read → go offline → read from cache // 1. Hydrate: AppRepository.instance.get() (pulls from Supabase) // 2. Simulate offline (cut network) // 3. AppRepository.instance.get(policy: localOnly) → expect cached // 4. No exception thrown // // Scenario C — task number placeholder // 1. Create task offline → task.taskNumber == null // 2. UI should display 'OFFLINE-001' (handled by displayTaskNumber helper) // 3. After sync → task.taskNumber != null → UI shows 'YYYY-MM-#####' // ────────────────────────────────────────────────────────────────────────── group('integration (manual only)', () { // Placeholder so the group shows up in test output with a clear note. test('see file comments for manual integration test instructions', () { // This test intentionally passes — it documents the manual steps above. }); }); }