Files
dmc/app/test/ui/providers/system_message_log_provider_test.dart
T
Constantin LeueandClaude Sonnet 5 b68c16d8e6 Log mission name on send, log mission/drone-profile changes, auto-send on mission switch
Include the mission name in the "Mission sent"/"Mission upload failed"
System Messages (was just the waypoint count before).

Add MissionMeta.switchSeq, bumped only by an actual mission switch
(startNew/startNewFromPlace/loadMission) - not by the id a brand-new
mission gets from its first autosave, and not by restoreLastSession() on
app start. FlyScreen compares it to detect a genuine switch and reacts
two ways: logs "Mission changed to ..." and automatically re-uploads the
new route to the flight controller, reusing the same send path as the
manual send button (same _sending guard, same success/failure snackbar
and log entry).

Mission-change and drone-profile-change logging intentionally live in
FlyScreen's ref.listen callbacks, not in mission_meta_provider.dart /
active_drone_profile_provider.dart themselves - those providers have no
notion of the app mode (Plan vs Fly, tracked separately by AppModeCubit),
and logging there would record a change regardless of mode. Since
FlyScreen only exists while Fly mode is active, scoping the listeners
there means switching missions or drone profiles from Plan mode produces
no System Messages entries, and only a mission switch (not a drone
profile switch) triggers the automatic re-upload, matching what was
asked for.

Also splits systemMessageLogProvider (the plain message list + log(), no
telemetry dependency) from the new systemMessageAutoLogProvider (the
Connected/lost/battery/failsafe/connection-type auto-detection, which
does watch telemetryProvider) - discovered while wiring the mission-change
logging that logging a plain message from Plan-mode code was forcing the
entire telemetry/transport stack to spin up as a side effect, which broke
an unrelated Plan-mode test (UdpTransport threw on a double-disconnect
during teardown). Keeping the two concerns apart means calling log() for
a one-off message never has that side effect.

Verified end to end on the Pixel_10a emulator: switching to an empty
mission while in Fly mode correctly showed "No waypoints to send" and
logged "Mission changed to ..." automatically, without touching the send
button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:04:35 +02:00

143 lines
5.2 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dmc_app/domain/telemetry/system_message.dart';
import 'package:dmc_app/transport/connection_type.dart';
import 'package:dmc_app/transport/flight_controller_link.dart';
import 'package:dmc_app/ui/providers/connection_settings_provider.dart';
import 'package:dmc_app/ui/providers/system_message_log_provider.dart';
import 'package:dmc_app/ui/providers/telemetry_provider.dart';
TelemetryFrame _frame() => const TelemetryFrame(
lat: 52.5,
lon: 13.4,
hasFix: true,
numSat: 10,
altitudeM: 100,
speedMs: 15,
headingDeg: 0,
armed: false,
batteryPercent: 80,
batteryVoltage: 22.0,
currentA: 10.0,
linkQuality: 90,
snrDb: 5,
hdop: 1.0,
navMode: 0,
flightMode: 'Idle',
);
typedef _ConnectionSettings = ({
int? activeDroneProfileId,
int? currentMissionId,
String? connectionDeviceAddress,
bool connectionAutoConnect,
ConnectionType connectionType,
String? connectionWifiHost,
int connectionWifiPort,
String connectionWifiSsidPrefix,
String? connectionWifiLastSsid,
});
_ConnectionSettings _settings(ConnectionType type) => (
activeDroneProfileId: null,
currentMissionId: null,
connectionDeviceAddress: null,
connectionAutoConnect: true,
connectionType: type,
connectionWifiHost: null,
connectionWifiPort: 14550,
connectionWifiSsidPrefix: 'mLRS-',
connectionWifiLastSsid: null,
);
void main() {
test('loggt "Connected" beim ersten Telemetrie-Frame und "Connection '
'lost" bei einem anschliessenden Stream-Fehler', () async {
final controller = StreamController<TelemetryFrame>();
addTearDown(controller.close);
final container = ProviderContainer(overrides: [
telemetryProvider.overrideWith((ref) => controller.stream),
connectionSettingsProvider
.overrideWith((ref) async => _settings(ConnectionType.mock)),
]);
addTearDown(container.dispose);
container.listen(systemMessageAutoLogProvider, (previous, next) {});
controller.add(_frame());
await Future<void>.delayed(Duration.zero);
final afterConnect = container.read(systemMessageLogProvider);
expect(afterConnect, hasLength(1));
expect(afterConnect.single.message, 'Connected');
expect(afterConnect.single.severity, SystemMessageSeverity.info);
controller.addError(StateError('link down'));
await Future<void>.delayed(Duration.zero);
final afterLost = container.read(systemMessageLogProvider);
expect(afterLost.first.message, 'Connection lost');
expect(afterLost.first.severity, SystemMessageSeverity.critical);
});
test('loggt keine "Connected"-Meldung, solange nie ein Frame ankommt', () async {
final container = ProviderContainer(overrides: [
telemetryProvider.overrideWith((ref) => const Stream<TelemetryFrame>.empty()),
connectionSettingsProvider
.overrideWith((ref) async => _settings(ConnectionType.mock)),
]);
addTearDown(container.dispose);
container.listen(systemMessageAutoLogProvider, (previous, next) {});
await Future<void>.delayed(Duration.zero);
expect(container.read(systemMessageLogProvider), isEmpty);
});
test('loggt "Connection type changed" bei einer Aenderung, nicht beim '
'initialen Laden der Einstellungen', () async {
var type = ConnectionType.wifi;
final container = ProviderContainer(overrides: [
telemetryProvider.overrideWith((ref) => const Stream<TelemetryFrame>.empty()),
connectionSettingsProvider.overrideWith((ref) async => _settings(type)),
]);
addTearDown(container.dispose);
container.listen(systemMessageAutoLogProvider, (previous, next) {});
await container.read(connectionSettingsProvider.future);
expect(container.read(systemMessageLogProvider), isEmpty);
type = ConnectionType.mock;
container.invalidate(connectionSettingsProvider);
await container.read(connectionSettingsProvider.future);
final log = container.read(systemMessageLogProvider);
expect(log.single.message, 'Connection type changed to Mock (Test)');
expect(log.single.severity, SystemMessageSeverity.info);
});
test('log() fuegt einen Eintrag manuell hinzu (Doku: Wegpunkte uebertragen)', () async {
final container = ProviderContainer(overrides: [
telemetryProvider.overrideWith((ref) => const Stream<TelemetryFrame>.empty()),
connectionSettingsProvider
.overrideWith((ref) async => _settings(ConnectionType.mock)),
]);
addTearDown(container.dispose);
container.read(systemMessageLogProvider.notifier).log('Mission sent (3 waypoints)');
var log = container.read(systemMessageLogProvider);
expect(log.single.message, 'Mission sent (3 waypoints)');
expect(log.single.severity, SystemMessageSeverity.info);
container.read(systemMessageLogProvider.notifier).log(
'Mission upload failed: timeout',
severity: SystemMessageSeverity.critical,
);
log = container.read(systemMessageLogProvider);
expect(log.first.message, 'Mission upload failed: timeout');
expect(log.first.severity, SystemMessageSeverity.critical);
});
}