import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart'; import 'package:dmc_app/transport/flight_controller_link.dart'; import 'package:dmc_app/ui/providers/home_point_provider.dart'; import 'package:dmc_app/ui/providers/telemetry_provider.dart'; /// Zaehlt readHomePoint()-Aufrufe statt sie tatsaechlich per MSP zu /// beantworten - alle anderen Mitglieder sind fuer diesen Test irrelevant /// und werfen bei Gebrauch bewusst. class _FakeLink implements FlightControllerLink { int readHomePointCalls = 0; HomePoint? nextHomePoint; @override Future readHomePoint() async { readHomePointCalls++; return nextHomePoint; } @override FcCapabilities get capabilities => throw UnimplementedError(); @override Future connect() => throw UnimplementedError(); @override Future disconnect() => throw UnimplementedError(); @override Future uploadMission(FlatWaypointList mission) => throw UnimplementedError(); @override Future verifyMission(FlatWaypointList mission) => throw UnimplementedError(); @override Future setFlightMode(FlightMode mode) => throw UnimplementedError(); @override Future arm() => throw UnimplementedError(); @override Stream subscribeTelemetry() => throw UnimplementedError(); @override Future readActiveWaypointIndex() => throw UnimplementedError(); } TelemetryFrame _frame({bool hasFix = true, bool armed = false}) => TelemetryFrame( lat: 52.5, lon: 13.4, hasFix: hasFix, fixType: hasFix ? 2 : 0, numSat: 10, altitudeM: 100, gpsAltitudeM: 100, speedMs: 15, headingDeg: 0, armed: armed, batteryPercent: 80, batteryVoltage: 22.0, currentA: 10.0, linkQuality: 90, snrDb: 5, hdop: 1.0, navMode: 0, flightMode: 'Idle', sensorStatusBits: 0, temperaturesC: const [null, null, null], verticalSpeedMs: 0, ); void main() { test('fragt den Homepoint beim ersten Telemetrie-Frame ab (Verbindungsaufbau)', () async { final controller = StreamController(); addTearDown(controller.close); final link = _FakeLink() ..nextHomePoint = const HomePoint(lat: 52.5, lon: 13.4, altitudeM: 60); final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => controller.stream), flightControllerLinkProvider.overrideWithValue(link), ]); addTearDown(container.dispose); container.listen(homePointAutoRefreshProvider, (previous, next) {}); controller.add(_frame(hasFix: false)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 1); expect(container.read(homePointProvider)?.lat, closeTo(52.5, 1e-9)); }); test('fragt NICHT bei jedem Frame erneut ab, solange sich Fix-/Arm-Status ' 'nicht aendern (keine kontinuierliche Abfrage)', () async { final controller = StreamController(); addTearDown(controller.close); final link = _FakeLink(); final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => controller.stream), flightControllerLinkProvider.overrideWithValue(link), ]); addTearDown(container.dispose); container.listen(homePointAutoRefreshProvider, (previous, next) {}); controller.add(_frame()); await Future.delayed(Duration.zero); controller.add(_frame()); controller.add(_frame()); controller.add(_frame()); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 1, reason: 'nur der erste Frame (Connect) loest aus'); }); test('fragt beim Uebergang auf einen GPS-Fix erneut ab', () async { final controller = StreamController(); addTearDown(controller.close); final link = _FakeLink(); final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => controller.stream), flightControllerLinkProvider.overrideWithValue(link), ]); addTearDown(container.dispose); container.listen(homePointAutoRefreshProvider, (previous, next) {}); controller.add(_frame(hasFix: false)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 1, reason: 'erster Frame = Connect-Trigger'); controller.add(_frame(hasFix: true)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 2, reason: 'Fix-Erwerb ist ein eigener Trigger'); controller.add(_frame(hasFix: true)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 2, reason: 'weiterhin Fix loest nicht erneut aus'); }); test('fragt beim Uebergang auf armed erneut ab', () async { final controller = StreamController(); addTearDown(controller.close); final link = _FakeLink(); final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => controller.stream), flightControllerLinkProvider.overrideWithValue(link), ]); addTearDown(container.dispose); container.listen(homePointAutoRefreshProvider, (previous, next) {}); controller.add(_frame(armed: false)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 1, reason: 'erster Frame = Connect-Trigger'); controller.add(_frame(armed: true)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 2, reason: 'Armen ist ein eigener Trigger'); controller.add(_frame(armed: true)); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 2, reason: 'weiterhin armed loest nicht erneut aus'); }); test('fragt nach einem Verbindungsverlust bei Wiederverbindung erneut ab', () async { final controller = StreamController(); addTearDown(controller.close); final link = _FakeLink(); final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => controller.stream), flightControllerLinkProvider.overrideWithValue(link), ]); addTearDown(container.dispose); container.listen(homePointAutoRefreshProvider, (previous, next) {}); controller.add(_frame()); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 1); controller.addError(StateError('link down')); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 1, reason: 'ein Fehler alleine fragt nichts ab'); controller.add(_frame()); await Future.delayed(Duration.zero); expect(link.readHomePointCalls, 2, reason: 'erster Frame nach Reconnect ist wieder ein Trigger'); }); test('speichert null, wenn der FC (noch) keinen Homepoint gesetzt hat', () async { final controller = StreamController(); addTearDown(controller.close); final link = _FakeLink()..nextHomePoint = null; final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => controller.stream), flightControllerLinkProvider.overrideWithValue(link), ]); addTearDown(container.dispose); container.listen(homePointAutoRefreshProvider, (previous, next) {}); controller.add(_frame()); await Future.delayed(Duration.zero); expect(container.read(homePointProvider), isNull); }); }