Files
dmc/app/test/ui/providers/wind_provider_test.dart
Constantin Leue eca1c647b3 Auto-fetch wind, drop manual fetch/validate buttons, fix it in fly mode
Redesigns the waypoint list panel to match the drone-status/missions
panels: no title bar, close button floats over the tab row, tabs get
more vertical padding.

Removes the "Fetch wind" and "Validate" header buttons along with the
now-fully-orphaned WindValidatePanel screen and its WindService
support code (fetchValidation, WindValidationResult and friends) -
nothing else referenced them.

Wind is now fetched automatically instead: HeaderWindNotifier gains
ensurePerWaypointWind(), called when the waypoint list panel opens and
when the header pill's per-waypoint toggle switches on. A lat/lon
signature of the waypoint list tracks whether the fetched data is
still current, so a ref.listen(currentMissionProvider) in the same
notifier refetches only when the list itself changes (position added/
removed/moved) - not on every altitude/speed edit. Waypoints that
already carry wind data (e.g. loaded from a saved mission) short-
circuit the check instead of re-fetching pointlessly.

Also wires per-waypoint wind markers into the fly-mode map, which
never had them (plan_screen.dart built windMarkers; fly_screen.dart
just never did) - the reported "doesn't work in fly mode" bug.
windMarkerWidth() moved from a plan_screen.dart-private method to
wind_marker_pill.dart so both screens can share it.

Test containers in widget_test.dart now override windServiceProvider
with a MockClient so the new auto-fetch trigger can't reach the real
network during tests.
2026-08-09 22:26:23 +02:00

158 lines
5.9 KiB
Dart

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/services/wind/wind_service.dart';
import 'package:dmc_app/ui/providers/current_mission_provider.dart';
import 'package:dmc_app/ui/providers/wind_provider.dart';
/// Zaehlt fetchWindForWaypoints()-Aufrufe statt echte HTTP-Requests zu
/// stellen - schreibt synthetische Winddaten zurueck, damit
/// ensurePerWaypointWind()s "bereits vorhanden"-Kurzschluss (siehe dortige
/// Doku) bei einem zweiten Aufruf greifen kann.
class _CountingWindService extends WindService {
int fetchCalls = 0;
@override
Future<List<Waypoint>> fetchWindForWaypoints(List<Waypoint> waypoints) async {
fetchCalls++;
return [
for (final w in waypoints) w.copyWith(windSpeedMs: 5, windDirFromDeg: 200),
];
}
}
Waypoint _wp(double lat, double lon, {double? windSpeedMs}) => Waypoint(
lat: lat,
lon: lon,
altitudeM: 60,
speedMs: 15,
catchRadiusM: 60,
windSpeedMs: windSpeedMs,
windDirFromDeg: windSpeedMs != null ? 180 : null,
);
void main() {
test('ensurePerWaypointWind() fetches once for a list without wind data', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
container.read(currentMissionProvider.notifier).addWaypoint(_wp(0, 0));
await container.read(headerWindProvider.notifier).ensurePerWaypointWind();
expect(service.fetchCalls, 1);
expect(container.read(currentMissionProvider).single.windSpeedMs, 5);
});
test('ensurePerWaypointWind() does not fetch when every waypoint already '
'has wind data (e.g. loaded from a saved mission)', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
container.read(currentMissionProvider.notifier).addWaypoint(_wp(0, 0, windSpeedMs: 3));
await container.read(headerWindProvider.notifier).ensurePerWaypointWind();
expect(service.fetchCalls, 0);
});
test('a second ensurePerWaypointWind() call for the unchanged list does '
'not fetch again', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
container.read(currentMissionProvider.notifier).addWaypoint(_wp(0, 0));
final notifier = container.read(headerWindProvider.notifier);
await notifier.ensurePerWaypointWind();
await notifier.ensurePerWaypointWind();
expect(service.fetchCalls, 1);
});
test('toggleShowPerWaypoint() fetches when turning on, not when turning off', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
container.read(currentMissionProvider.notifier).addWaypoint(_wp(0, 0));
final notifier = container.read(headerWindProvider.notifier);
notifier.toggleShowPerWaypoint(); // an
await Future<void>.delayed(Duration.zero);
expect(container.read(headerWindProvider).showPerWaypoint, isTrue);
expect(service.fetchCalls, 1);
notifier.toggleShowPerWaypoint(); // aus
await Future<void>.delayed(Duration.zero);
expect(container.read(headerWindProvider).showPerWaypoint, isFalse);
expect(service.fetchCalls, 1, reason: 'Ausschalten loest keinen Abruf aus');
});
test('after activation, changing the waypoint list (e.g. adding a point) '
'triggers an automatic refetch', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
final missionNotifier = container.read(currentMissionProvider.notifier);
missionNotifier.addWaypoint(_wp(0, 0));
await container.read(headerWindProvider.notifier).ensurePerWaypointWind();
expect(service.fetchCalls, 1);
missionNotifier.addWaypoint(_wp(0, 0.01));
await Future<void>.delayed(Duration.zero);
expect(service.fetchCalls, 2,
reason: 'die veraenderte Wegpunktliste loest automatisch neu aus');
});
test('after activation, editing only altitude (position unchanged) does '
'NOT trigger a refetch', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
final missionNotifier = container.read(currentMissionProvider.notifier);
missionNotifier.addWaypoint(_wp(0, 0));
await container.read(headerWindProvider.notifier).ensurePerWaypointWind();
expect(service.fetchCalls, 1);
missionNotifier.setAltitude(0, 150);
await Future<void>.delayed(Duration.zero);
expect(service.fetchCalls, 1,
reason: 'nur die Position ist Teil der Aenderungs-Signatur, nicht die Hoehe');
});
test('before activation (menu never opened, pill never toggled), list '
'changes trigger no fetch at all', () async {
final service = _CountingWindService();
final container = ProviderContainer(overrides: [
windServiceProvider.overrideWithValue(service),
]);
addTearDown(container.dispose);
// Beobachtet headerWindProvider, ohne ensurePerWaypointWind() oder
// toggleShowPerWaypoint() aufzurufen - build() registriert den Listener
// zwar, er soll aber inaktiv bleiben.
container.read(headerWindProvider);
container.read(currentMissionProvider.notifier).addWaypoint(_wp(0, 0));
await Future<void>.delayed(Duration.zero);
expect(service.fetchCalls, 0);
});
}