import 'package:flutter_test/flutter_test.dart'; import 'package:rippr/src/tiles/map_connectivity.dart'; /// UI-02: exercises the failure-count/threshold/probe logic with a fake probe -- no /// real network -- mirroring V3-11's `tile_downloader_test.dart` pattern of injecting /// fake fetch outcomes rather than hitting a real tile host from a test. void main() { late bool probeResult; late int probeCalls; late MapConnectivityState state; setUp(() { probeResult = false; probeCalls = 0; state = MapConnectivityState( probe: () async { probeCalls++; return probeResult; }, probeInterval: const Duration(milliseconds: 20), ); }); tearDown(() { state.dispose(); }); test('starts in live mode', () { expect(state.skeletonMode, isFalse); }); test('fewer than the threshold of failures stays live', () { state.reportFailure(); state.reportFailure(); expect(state.skeletonMode, isFalse); }); test('reaching the threshold of consecutive failures switches to skeleton mode', () { for (var i = 0; i < skeletonFailureThreshold; i++) { state.reportFailure(); } expect(state.skeletonMode, isTrue); }); test('a success before the threshold resets the failure count', () { state.reportFailure(); state.reportFailure(); state.reportSuccess(); state.reportFailure(); state.reportFailure(); expect(state.skeletonMode, isFalse, reason: 'the success should have reset the run -- two more failures is not ' 'the same as four in a row'); }); test('notifies listeners exactly when skeletonMode actually changes', () { var notifications = 0; state.addListener(() => notifications++); state.reportFailure(); state.reportFailure(); expect(notifications, 0, reason: 'still below threshold -- no state change yet'); state.reportFailure(); expect(notifications, 1); expect(state.skeletonMode, isTrue); }); test('a single success while already in skeleton mode recovers immediately', () { for (var i = 0; i < skeletonFailureThreshold; i++) { state.reportFailure(); } expect(state.skeletonMode, isTrue); state.reportSuccess(); expect(state.skeletonMode, isFalse); }); test('further failures are ignored once already in skeleton mode', () { for (var i = 0; i < skeletonFailureThreshold; i++) { state.reportFailure(); } var notifications = 0; state.addListener(() => notifications++); // Recovery is the probe loop's job from here -- more failure reports (e.g. from a // TileLayer that hadn't yet unmounted) must not do anything further. state.reportFailure(); state.reportFailure(); expect(notifications, 0); expect(state.skeletonMode, isTrue); }); test('the probe loop recovers automatically once it starts succeeding', () async { for (var i = 0; i < skeletonFailureThreshold; i++) { state.reportFailure(); } expect(state.skeletonMode, isTrue); probeResult = true; // Give the periodic probe timer a couple of intervals to fire. await Future.delayed(const Duration(milliseconds: 60)); expect(state.skeletonMode, isFalse); expect(probeCalls, greaterThan(0)); }); test('the probe loop does not run while already live', () async { await Future.delayed(const Duration(milliseconds: 60)); expect(probeCalls, 0, reason: 'a TileLayer that is live and working generates its own fetch ' 'reports -- a periodic probe on top of that would be exactly the ' 'excess-request behaviour this ticket exists to avoid'); }); }