Files
dmc/app/test/services/wind_service_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

146 lines
4.6 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart';
import 'package:dmc_app/services/wind/wind_service.dart';
bool _isPressureRequest(http.Request request) =>
(request.url.queryParameters['hourly'] ?? '').contains('hPa');
http.Response _json(Object body) =>
http.Response(jsonEncode(body), 200, headers: {
'content-type': 'application/json',
});
void main() {
group('WindService.fetchHeaderWind', () {
test('interpoliert auf 120 m AGL ueber dem Referenzort', () async {
final client = MockClient((request) async {
if (_isPressureRequest(request)) {
return http.Response('', 500);
}
return _json({
'elevation': 2.0,
'hourly': {
'time': ['2024-06-01T12:00'],
'wind_speed_10m': [3.0],
'wind_direction_10m': [190.0],
'wind_speed_80m': [5.0],
'wind_direction_80m': [200.0],
'wind_speed_120m': [6.0],
'wind_direction_120m': [205.0],
'wind_speed_180m': [8.0],
'wind_direction_180m': [210.0],
'wind_gusts_10m': [7.5],
},
});
});
final service = WindService(client: client);
final result = await service.fetchHeaderWind(52.0, 4.0);
// Ziel 2 + 120 = 122 m AMSL faellt exakt auf den 120-m-Stuetzpunkt.
expect(result, isNotNull);
expect(result!.speedMs, closeTo(6.0, 0.001));
expect(result.dirFromDeg, closeTo(205.0, 0.001));
});
test('liefert null, wenn beide Requests fehlschlagen', () async {
final client = MockClient((request) async => http.Response('', 500));
final service = WindService(client: client);
final result = await service.fetchHeaderWind(52.0, 4.0);
expect(result, isNull);
});
});
group('WindService.fetchWindForWaypoints', () {
test('interpoliert jeden Wegpunkt an seinem eigenen Ort/seiner eigenen Hoehe',
() async {
final client = MockClient((request) async {
if (_isPressureRequest(request)) {
return http.Response('', 500);
}
return _json([
{
'elevation': 2.0,
'hourly': {
'time': ['2024-06-01T12:00'],
'wind_speed_10m': [3.0],
'wind_direction_10m': [190.0],
'wind_speed_80m': [5.0],
'wind_direction_80m': [200.0],
'wind_speed_120m': [6.0],
'wind_direction_120m': [205.0],
'wind_speed_180m': [8.0],
'wind_direction_180m': [210.0],
'wind_gusts_10m': [7.5],
},
},
{
'elevation': 50.0,
'hourly': {
'time': ['2024-06-01T12:00'],
'wind_speed_10m': [1.0],
'wind_direction_10m': [100.0],
'wind_speed_80m': [9.0],
'wind_direction_80m': [250.0],
'wind_speed_120m': [10.0],
'wind_direction_120m': [260.0],
'wind_speed_180m': [11.0],
'wind_direction_180m': [270.0],
'wind_gusts_10m': [12.0],
},
},
]);
});
final service = WindService(client: client);
final waypoints = [
const Waypoint(
lat: 52.0,
lon: 4.0,
altitudeM: 10, // 2 + 10 = 12 m AMSL: exakt der 10-m-Stuetzpunkt
speedMs: 15,
catchRadiusM: 60,
),
const Waypoint(
lat: 52.1,
lon: 4.1,
altitudeM: 80, // 50 + 80 = 130 m AMSL: exakt der 80-m-Stuetzpunkt
speedMs: 15,
catchRadiusM: 60,
),
];
final result = await service.fetchWindForWaypoints(waypoints);
expect(result[0].windSpeedMs, closeTo(3.0, 0.001));
expect(result[0].windDirFromDeg, closeTo(190.0, 0.001));
expect(result[0].windElevationM, closeTo(2.0, 0.001));
expect(result[1].windSpeedMs, closeTo(9.0, 0.001));
expect(result[1].windDirFromDeg, closeTo(250.0, 0.001));
expect(result[1].windElevationM, closeTo(50.0, 0.001));
});
test('leere Wegpunktliste loest keinen Request aus', () async {
var called = false;
final client = MockClient((request) async {
called = true;
return http.Response('', 500);
});
final service = WindService(client: client);
final result = await service.fetchWindForWaypoints(const []);
expect(result, isEmpty);
expect(called, isFalse);
});
});
}