mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
fix: Tighten device settings layout
This commit is contained in:
25
test/providers/channels_provider_test.dart
Normal file
25
test/providers/channels_provider_test.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/providers/channels_provider.dart';
|
||||
|
||||
void main() {
|
||||
group('ChannelsProvider device sync preparation', () {
|
||||
test('clears runtime channel state before sync', () {
|
||||
final provider = ChannelsProvider();
|
||||
|
||||
provider.addOrUpdateChannel(
|
||||
index: 2,
|
||||
name: 'Ops',
|
||||
secret: Uint8List.fromList(List<int>.filled(16, 7)),
|
||||
);
|
||||
provider.selectChannel(2);
|
||||
|
||||
provider.prepareForDeviceSync();
|
||||
|
||||
expect(provider.channels, isEmpty);
|
||||
expect(provider.selectedChannelIndex, 0);
|
||||
expect(provider.selectedChannel, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -10,10 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Contact createContact({
|
||||
required Uint8List key,
|
||||
required String name,
|
||||
}) {
|
||||
Contact createContact({required Uint8List key, required String name}) {
|
||||
return Contact(
|
||||
publicKey: key,
|
||||
type: ContactType.chat,
|
||||
@@ -36,34 +33,37 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('initializeEarly respects the active profile storage namespace', () async {
|
||||
final storage = ContactStorageService();
|
||||
final defaultContact = createContact(
|
||||
key: Uint8List.fromList(List<int>.filled(32, 1)),
|
||||
name: 'Default Contact',
|
||||
);
|
||||
final alphaContact = createContact(
|
||||
key: Uint8List.fromList(List<int>.filled(32, 2)),
|
||||
name: 'Alpha Contact',
|
||||
);
|
||||
test(
|
||||
'initializeEarly respects the active profile storage namespace',
|
||||
() async {
|
||||
final storage = ContactStorageService();
|
||||
final defaultContact = createContact(
|
||||
key: Uint8List.fromList(List<int>.filled(32, 1)),
|
||||
name: 'Default Contact',
|
||||
);
|
||||
final alphaContact = createContact(
|
||||
key: Uint8List.fromList(List<int>.filled(32, 2)),
|
||||
name: 'Alpha Contact',
|
||||
);
|
||||
|
||||
await storage.saveContacts([defaultContact]);
|
||||
await storage.saveContacts([alphaContact], namespace: 'alpha');
|
||||
await storage.saveContacts([defaultContact]);
|
||||
await storage.saveContacts([alphaContact], namespace: 'alpha');
|
||||
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'alpha',
|
||||
);
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'alpha',
|
||||
);
|
||||
|
||||
final provider = ContactsProvider();
|
||||
await provider.initializeEarly();
|
||||
final provider = ContactsProvider();
|
||||
await provider.initializeEarly();
|
||||
|
||||
final names = provider.contacts
|
||||
.where((contact) => !contact.isChannel)
|
||||
.map((contact) => contact.advName)
|
||||
.toList();
|
||||
final names = provider.contacts
|
||||
.where((contact) => !contact.isChannel)
|
||||
.map((contact) => contact.advName)
|
||||
.toList();
|
||||
|
||||
expect(names, <String>['Alpha Contact']);
|
||||
expect(provider.storageNamespace, 'alpha');
|
||||
});
|
||||
expect(names, <String>['Alpha Contact']);
|
||||
expect(provider.storageNamespace, 'alpha');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,14 +173,8 @@ void main() {
|
||||
var updated = scopedProvider.findContactByKey(scopedKey)!;
|
||||
expect(updated.telemetry, isNotNull);
|
||||
expect(updated.telemetry!.gpsLocation, isNull);
|
||||
expect(
|
||||
updated.displayLocation!.latitude,
|
||||
closeTo(45.1234, 0.0001),
|
||||
);
|
||||
expect(
|
||||
updated.displayLocation!.longitude,
|
||||
closeTo(13.8765, 0.0001),
|
||||
);
|
||||
expect(updated.displayLocation!.latitude, closeTo(45.1234, 0.0001));
|
||||
expect(updated.displayLocation!.longitude, closeTo(13.8765, 0.0001));
|
||||
|
||||
// Invalid 0,0 GPS frame should behave the same way.
|
||||
final invalidGps = CayenneLppParser.createGpsData(
|
||||
@@ -192,14 +186,8 @@ void main() {
|
||||
updated = scopedProvider.findContactByKey(scopedKey)!;
|
||||
expect(updated.telemetry, isNotNull);
|
||||
expect(updated.telemetry!.gpsLocation, isNull);
|
||||
expect(
|
||||
updated.displayLocation!.latitude,
|
||||
closeTo(45.1234, 0.0001),
|
||||
);
|
||||
expect(
|
||||
updated.displayLocation!.longitude,
|
||||
closeTo(13.8765, 0.0001),
|
||||
);
|
||||
expect(updated.displayLocation!.latitude, closeTo(45.1234, 0.0001));
|
||||
expect(updated.displayLocation!.longitude, closeTo(13.8765, 0.0001));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -337,95 +325,107 @@ void main() {
|
||||
expect(updated.telemetry!.extraSensorData, containsPair('co2', 415.0));
|
||||
});
|
||||
|
||||
test('retains scalar telemetry but wipes stale extra sensor fields on refresh', () {
|
||||
final fullTelemetry = ContactTelemetry(
|
||||
gpsLocation: const LatLng(46.0569, 14.5058),
|
||||
batteryPercentage: 54.0,
|
||||
batteryMilliVolts: 3780,
|
||||
temperature: 19.5,
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 2)),
|
||||
humidity: 58.0,
|
||||
pressure: 1011.2,
|
||||
extraSensorData: const {'pm25': 8.0},
|
||||
);
|
||||
test(
|
||||
'retains scalar telemetry but wipes stale extra sensor fields on refresh',
|
||||
() {
|
||||
final fullTelemetry = ContactTelemetry(
|
||||
gpsLocation: const LatLng(46.0569, 14.5058),
|
||||
batteryPercentage: 54.0,
|
||||
batteryMilliVolts: 3780,
|
||||
temperature: 19.5,
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 2)),
|
||||
humidity: 58.0,
|
||||
pressure: 1011.2,
|
||||
extraSensorData: const {'pm25': 8.0},
|
||||
);
|
||||
|
||||
provider.addOrUpdateContact(
|
||||
createContact(
|
||||
key: publicKey,
|
||||
type: ContactType.chat,
|
||||
).copyWith(telemetry: fullTelemetry),
|
||||
);
|
||||
provider.addOrUpdateContact(
|
||||
createContact(
|
||||
key: publicKey,
|
||||
type: ContactType.chat,
|
||||
).copyWith(telemetry: fullTelemetry),
|
||||
);
|
||||
|
||||
final batteryOnly = CayenneLppParser.createBatteryData(3.95);
|
||||
provider.updateTelemetry(publicKey.sublist(0, 6), batteryOnly);
|
||||
final batteryOnly = CayenneLppParser.createBatteryData(3.95);
|
||||
provider.updateTelemetry(publicKey.sublist(0, 6), batteryOnly);
|
||||
|
||||
final updated = provider.findContactByKey(publicKey)!;
|
||||
expect(updated.telemetry, isNotNull);
|
||||
expect(updated.telemetry!.gpsLocation, isNull);
|
||||
expect(updated.displayLocation, const LatLng(46.0569, 14.5058));
|
||||
expect(updated.telemetry!.batteryMilliVolts, isNotNull);
|
||||
expect(updated.telemetry!.batteryPercentage, isNotNull);
|
||||
expect(updated.telemetry!.temperature, equals(19.5));
|
||||
expect(updated.telemetry!.humidity, equals(58.0));
|
||||
expect(updated.telemetry!.pressure, equals(1011.2));
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('__source_channel:battery', 0),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('__source_channel:voltage', 0),
|
||||
);
|
||||
expect(updated.telemetry!.extraSensorData, isNot(contains('pm25')));
|
||||
});
|
||||
final updated = provider.findContactByKey(publicKey)!;
|
||||
expect(updated.telemetry, isNotNull);
|
||||
expect(updated.telemetry!.gpsLocation, isNull);
|
||||
expect(updated.displayLocation, const LatLng(46.0569, 14.5058));
|
||||
expect(updated.telemetry!.batteryMilliVolts, isNotNull);
|
||||
expect(updated.telemetry!.batteryPercentage, isNotNull);
|
||||
expect(updated.telemetry!.temperature, equals(19.5));
|
||||
expect(updated.telemetry!.humidity, equals(58.0));
|
||||
expect(updated.telemetry!.pressure, equals(1011.2));
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('__source_channel:battery', 0),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('__source_channel:voltage', 0),
|
||||
);
|
||||
expect(updated.telemetry!.extraSensorData, isNot(contains('pm25')));
|
||||
},
|
||||
);
|
||||
|
||||
test('replaces old source-channel mappings when a metric moves channels', () {
|
||||
final initialTelemetry = ContactTelemetry(
|
||||
gpsLocation: null,
|
||||
batteryPercentage: null,
|
||||
batteryMilliVolts: null,
|
||||
temperature: 21.5,
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 2)),
|
||||
humidity: null,
|
||||
pressure: null,
|
||||
extraSensorData: const {
|
||||
'__source_channel:temperature': 2,
|
||||
'temperature_2': 21.5,
|
||||
'humidity_4': 66.0,
|
||||
},
|
||||
);
|
||||
test(
|
||||
'replaces old source-channel mappings when a metric moves channels',
|
||||
() {
|
||||
final initialTelemetry = ContactTelemetry(
|
||||
gpsLocation: null,
|
||||
batteryPercentage: null,
|
||||
batteryMilliVolts: null,
|
||||
temperature: 21.5,
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 2)),
|
||||
humidity: null,
|
||||
pressure: null,
|
||||
extraSensorData: const {
|
||||
'__source_channel:temperature': 2,
|
||||
'temperature_2': 21.5,
|
||||
'humidity_4': 66.0,
|
||||
},
|
||||
);
|
||||
|
||||
provider.addOrUpdateContact(
|
||||
createContact(
|
||||
key: publicKey,
|
||||
type: ContactType.chat,
|
||||
).copyWith(telemetry: initialTelemetry),
|
||||
);
|
||||
provider.addOrUpdateContact(
|
||||
createContact(
|
||||
key: publicKey,
|
||||
type: ContactType.chat,
|
||||
).copyWith(telemetry: initialTelemetry),
|
||||
);
|
||||
|
||||
final movedChannelTelemetry = CayenneLppParser.createTemperatureData(
|
||||
23.5,
|
||||
channel: 3,
|
||||
);
|
||||
final movedChannelTelemetry = CayenneLppParser.createTemperatureData(
|
||||
23.5,
|
||||
channel: 3,
|
||||
);
|
||||
|
||||
provider.updateTelemetry(publicKey.sublist(0, 6), movedChannelTelemetry);
|
||||
provider.updateTelemetry(
|
||||
publicKey.sublist(0, 6),
|
||||
movedChannelTelemetry,
|
||||
);
|
||||
|
||||
final updated = provider.findContactByKey(publicKey)!;
|
||||
expect(updated.telemetry, isNotNull);
|
||||
expect(updated.telemetry!.temperature, closeTo(23.5, 0.1));
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('__source_channel:temperature', 3),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('temperature_3', closeTo(23.5, 0.1)),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
isNot(contains('temperature_2')),
|
||||
);
|
||||
expect(updated.telemetry!.extraSensorData, isNot(contains('humidity_4')));
|
||||
});
|
||||
final updated = provider.findContactByKey(publicKey)!;
|
||||
expect(updated.telemetry, isNotNull);
|
||||
expect(updated.telemetry!.temperature, closeTo(23.5, 0.1));
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('__source_channel:temperature', 3),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
containsPair('temperature_3', closeTo(23.5, 0.1)),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
isNot(contains('temperature_2')),
|
||||
);
|
||||
expect(
|
||||
updated.telemetry!.extraSensorData,
|
||||
isNot(contains('humidity_4')),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('builds message snapshot from latest valid telemetry', () {
|
||||
final telemetryData = CayenneLppParser.createGpsData(
|
||||
@@ -870,6 +870,71 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ContactsProvider device sync preparation', () {
|
||||
late ContactsProvider provider;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
provider = ContactsProvider();
|
||||
});
|
||||
|
||||
test(
|
||||
'clears runtime contacts before sync without erasing persisted contacts or saved groups',
|
||||
() async {
|
||||
final key = createPublicKey(140);
|
||||
final pendingKey = createPublicKey(180);
|
||||
|
||||
provider.addOrUpdateContact(
|
||||
createContact(key: key, type: ContactType.chat, name: 'Synced Later'),
|
||||
);
|
||||
provider.addPendingAdvert(pendingKey);
|
||||
await provider.addSavedGroupForFilter('teamMembers', 'alpha');
|
||||
|
||||
await provider.prepareForDeviceContactSync();
|
||||
|
||||
expect(provider.chatContacts, isEmpty);
|
||||
expect(provider.pendingAdverts, isEmpty);
|
||||
expect(provider.savedGroupsForSection('teamMembers'), hasLength(1));
|
||||
|
||||
final restored = ContactsProvider();
|
||||
await restored.initializeEarly();
|
||||
|
||||
expect(
|
||||
restored.chatContacts.map((contact) => contact.advName),
|
||||
contains('Synced Later'),
|
||||
);
|
||||
expect(restored.savedGroupsForSection('teamMembers'), hasLength(1));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('ContactsProvider self telemetry', () {
|
||||
test(
|
||||
'stores self telemetry without re-adding the device as a contact',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final provider = ContactsProvider();
|
||||
final selfKey = createPublicKey(200);
|
||||
|
||||
await provider.initialize(devicePublicKey: selfKey);
|
||||
provider.updateTelemetry(
|
||||
selfKey.sublist(0, 6),
|
||||
CayenneLppParser.createTemperatureData(23.5, channel: 1),
|
||||
);
|
||||
|
||||
expect(provider.selfTelemetry, isNotNull);
|
||||
expect(provider.selfTelemetry!.temperature, closeTo(23.5, 0.1));
|
||||
expect(provider.findContactByKey(selfKey), isNull);
|
||||
expect(
|
||||
provider.contacts.any(
|
||||
(contact) => contact.publicKeyHex == publicKeyHex(selfKey),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String publicKeyHex(Uint8List publicKey) {
|
||||
|
||||
134
test/providers/media_provider_profile_scope_test.dart
Normal file
134
test/providers/media_provider_profile_scope_test.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/providers/image_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/voice_provider.dart';
|
||||
import 'package:meshcore_sar_app/services/profiles_feature_service.dart';
|
||||
import 'package:meshcore_sar_app/services/voice_codec_service.dart';
|
||||
import 'package:meshcore_sar_app/services/voice_player_service.dart';
|
||||
import 'package:meshcore_sar_app/utils/image_message_parser.dart';
|
||||
import 'package:meshcore_sar_app/utils/voice_message_parser.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'alpha',
|
||||
);
|
||||
});
|
||||
|
||||
test('VoiceProvider reloads profile-scoped sessions', () async {
|
||||
final player = _FakeVoicePlayerService();
|
||||
final provider = VoiceProvider(codec: VoiceCodecService(), player: player);
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await provider.reloadProfileScopedState();
|
||||
provider.registerEnvelope(
|
||||
const VoiceEnvelope(
|
||||
sessionId: 'a1b2c3d4',
|
||||
mode: VoicePacketMode.mode1200,
|
||||
total: 2,
|
||||
durationMs: 1600,
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'beta',
|
||||
);
|
||||
await provider.reloadProfileScopedState();
|
||||
expect(provider.session('a1b2c3d4'), isNull);
|
||||
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'alpha',
|
||||
);
|
||||
await provider.reloadProfileScopedState();
|
||||
expect(provider.session('a1b2c3d4'), isNotNull);
|
||||
});
|
||||
|
||||
test('ImageProvider reloads profile-scoped sessions', () async {
|
||||
final provider = ImageProvider();
|
||||
|
||||
await provider.reloadProfileScopedState();
|
||||
provider.registerEnvelope(
|
||||
const ImageEnvelope(
|
||||
sessionId: 'a1b2c3d4',
|
||||
format: ImageFormat.avif,
|
||||
total: 2,
|
||||
width: 32,
|
||||
height: 32,
|
||||
sizeBytes: 8,
|
||||
),
|
||||
);
|
||||
provider.addFragment(
|
||||
ImagePacket(
|
||||
sessionId: 'a1b2c3d4',
|
||||
format: ImageFormat.avif,
|
||||
index: 0,
|
||||
total: 2,
|
||||
data: Uint8List.fromList([1, 2, 3, 4]),
|
||||
),
|
||||
width: 32,
|
||||
height: 32,
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'beta',
|
||||
);
|
||||
await provider.reloadProfileScopedState();
|
||||
expect(provider.session('a1b2c3d4'), isNull);
|
||||
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: true,
|
||||
activeProfileId: 'alpha',
|
||||
);
|
||||
await provider.reloadProfileScopedState();
|
||||
expect(provider.session('a1b2c3d4'), isNotNull);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeVoicePlayerService implements VoicePlayerService {
|
||||
final StreamController<void> _events = StreamController<void>.broadcast();
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
bool get isPlaying => _isPlaying;
|
||||
|
||||
@override
|
||||
Duration get position => Duration.zero;
|
||||
|
||||
@override
|
||||
Duration get duration => Duration.zero;
|
||||
|
||||
@override
|
||||
Stream<void> get events => _events.stream;
|
||||
|
||||
@override
|
||||
Future<void> play(Int16List pcmSamples, {required int sampleRateHz}) async {
|
||||
_isPlaying = true;
|
||||
_events.add(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_isPlaying = false;
|
||||
_events.add(null);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_events.close();
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:meshcore_sar_app/models/device_info.dart';
|
||||
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
||||
import 'package:meshcore_sar_app/services/cayenne_lpp_parser.dart';
|
||||
import 'package:meshcore_sar_app/services/profiles_feature_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -19,10 +20,17 @@ class _FakeContactsProvider extends ContactsProvider {
|
||||
}
|
||||
|
||||
class _FakeConnectionProvider extends ConnectionProvider {
|
||||
_FakeConnectionProvider({required bool isConnected})
|
||||
: _isConnected = isConnected;
|
||||
_FakeConnectionProvider({
|
||||
required bool isConnected,
|
||||
Uint8List? publicKey,
|
||||
String? selfName,
|
||||
}) : _isConnected = isConnected,
|
||||
_publicKey = publicKey,
|
||||
_selfName = selfName;
|
||||
|
||||
final bool _isConnected;
|
||||
final Uint8List? _publicKey;
|
||||
final String? _selfName;
|
||||
|
||||
int pingCalls = 0;
|
||||
|
||||
@@ -31,6 +39,8 @@ class _FakeConnectionProvider extends ConnectionProvider {
|
||||
connectionState: _isConnected
|
||||
? ConnectionState.connected
|
||||
: ConnectionState.disconnected,
|
||||
publicKey: _publicKey,
|
||||
selfName: _selfName,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -65,9 +75,12 @@ void main() {
|
||||
expect(provider.isLoaded, isTrue);
|
||||
}
|
||||
|
||||
Contact buildSensorContact() {
|
||||
Contact buildSensorContact({
|
||||
int firstByte = 0x44,
|
||||
String name = 'WX Station',
|
||||
}) {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[0] = 0x44;
|
||||
publicKey[0] = firstByte;
|
||||
|
||||
return Contact(
|
||||
publicKey: publicKey,
|
||||
@@ -75,7 +88,7 @@ void main() {
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(64),
|
||||
advName: 'WX Station',
|
||||
advName: name,
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
@@ -264,6 +277,36 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('watched sensor order persists across reloads', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final first = buildSensorContact(firstByte: 0x44, name: 'First');
|
||||
final second = buildSensorContact(firstByte: 0x45, name: 'Second');
|
||||
final third = buildSensorContact(firstByte: 0x46, name: 'Third');
|
||||
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
await provider.addSensor(first);
|
||||
await provider.addSensor(second);
|
||||
await provider.addSensor(third);
|
||||
|
||||
await provider.reorderSensors(2, 0);
|
||||
|
||||
expect(provider.watchedSensorKeys, <String>[
|
||||
third.publicKeyHex,
|
||||
first.publicKeyHex,
|
||||
second.publicKeyHex,
|
||||
]);
|
||||
|
||||
final reloadedProvider = SensorsProvider();
|
||||
await waitUntilLoaded(reloadedProvider);
|
||||
|
||||
expect(reloadedProvider.watchedSensorKeys, <String>[
|
||||
third.publicKeyHex,
|
||||
first.publicKeyHex,
|
||||
second.publicKeyHex,
|
||||
]);
|
||||
});
|
||||
|
||||
test(
|
||||
'unsupported auto refresh minutes normalize to nearest option',
|
||||
() async {
|
||||
@@ -340,4 +383,32 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('selfContact includes stored self telemetry', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final selfKey = Uint8List(32)..[0] = 0x66;
|
||||
final contactsProvider = ContactsProvider();
|
||||
await contactsProvider.initialize(devicePublicKey: selfKey);
|
||||
contactsProvider.updateTelemetry(
|
||||
selfKey.sublist(0, 6),
|
||||
CayenneLppParser.createTemperatureData(19.5, channel: 1),
|
||||
);
|
||||
|
||||
final connectionProvider = _FakeConnectionProvider(
|
||||
isConnected: true,
|
||||
publicKey: selfKey,
|
||||
selfName: 'My Device',
|
||||
);
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
|
||||
final selfContact = provider.selfContact(
|
||||
contactsProvider,
|
||||
connectionProvider,
|
||||
);
|
||||
|
||||
expect(selfContact, isNotNull);
|
||||
expect(selfContact!.telemetry, isNotNull);
|
||||
expect(selfContact.telemetry!.temperature, closeTo(19.5, 0.1));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import 'package:meshcore_sar_app/providers/drawing_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/map_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/voice_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/image_provider.dart' as ip;
|
||||
import 'package:meshcore_sar_app/services/app_config_snapshot_service.dart';
|
||||
import 'package:meshcore_sar_app/services/contact_storage_service.dart';
|
||||
import 'package:meshcore_sar_app/services/device_config_applicator.dart';
|
||||
@@ -207,15 +209,21 @@ void main() {
|
||||
final connectionProvider = _FakeConnectionProvider(
|
||||
deviceInfo: DeviceInfo(publicKey: Uint8List.fromList([1, 2, 3, 4])),
|
||||
);
|
||||
final voiceProvider = _FakeVoiceProvider();
|
||||
final imageProvider = _FakeImageProvider();
|
||||
final coordinator = _buildCoordinator(
|
||||
profileManager: manager,
|
||||
connectionProvider: connectionProvider,
|
||||
voiceProvider: voiceProvider,
|
||||
imageProvider: imageProvider,
|
||||
);
|
||||
|
||||
await coordinator.syncActiveProfileForCurrentDevice();
|
||||
|
||||
expect(manager.activeProfileId, alpha.id);
|
||||
expect(connectionProvider.disconnectCallCount, 0);
|
||||
expect(voiceProvider.reloadCallCount, 1);
|
||||
expect(imageProvider.reloadCallCount, 1);
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -257,6 +265,8 @@ void main() {
|
||||
ProfileWorkspaceCoordinator _buildCoordinator({
|
||||
required ProfileManager profileManager,
|
||||
_FakeConnectionProvider? connectionProvider,
|
||||
_FakeVoiceProvider? voiceProvider,
|
||||
_FakeImageProvider? imageProvider,
|
||||
}) {
|
||||
return ProfileWorkspaceCoordinator(
|
||||
profileManager: profileManager,
|
||||
@@ -267,6 +277,8 @@ ProfileWorkspaceCoordinator _buildCoordinator({
|
||||
mapProvider: _FakeMapProvider(),
|
||||
drawingProvider: _FakeDrawingProvider(),
|
||||
channelsProvider: _FakeChannelsProvider(),
|
||||
voiceProvider: voiceProvider ?? _FakeVoiceProvider(),
|
||||
imageProvider: imageProvider ?? _FakeImageProvider(),
|
||||
appProvider: _FakeAppProvider(),
|
||||
appConfigSnapshotService: _FakeAppConfigSnapshotService(),
|
||||
mapWorkspaceSnapshotService: _FakeMapWorkspaceSnapshotService(),
|
||||
@@ -332,9 +344,8 @@ class _FakeDeviceConfigApplicator extends DeviceConfigApplicator {
|
||||
}
|
||||
|
||||
class _FakeConnectionProvider implements ConnectionProvider {
|
||||
_FakeConnectionProvider({
|
||||
DeviceInfo? deviceInfo,
|
||||
}) : deviceInfo = deviceInfo ?? DeviceInfo();
|
||||
_FakeConnectionProvider({DeviceInfo? deviceInfo})
|
||||
: deviceInfo = deviceInfo ?? DeviceInfo();
|
||||
|
||||
int disconnectCallCount = 0;
|
||||
|
||||
@@ -407,6 +418,30 @@ class _FakeChannelsProvider implements ChannelsProvider {
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeVoiceProvider implements VoiceProvider {
|
||||
int reloadCallCount = 0;
|
||||
|
||||
@override
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
reloadCallCount += 1;
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeImageProvider implements ip.ImageProvider {
|
||||
int reloadCallCount = 0;
|
||||
|
||||
@override
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
reloadCallCount += 1;
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeAppProvider implements AppProvider {
|
||||
@override
|
||||
Future<void> reloadProfileScopedSettings() async {}
|
||||
|
||||
@@ -189,4 +189,36 @@ void main() {
|
||||
expect(find.text('2°C'), findsOneWidget);
|
||||
expect(find.text('12.3 mm'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('long pressing a telemetry bubble triggers refresh', (
|
||||
tester,
|
||||
) async {
|
||||
final contact = buildContact();
|
||||
var refreshCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: SensorTelemetryCard(
|
||||
contact: contact,
|
||||
state: SensorRefreshState.idle,
|
||||
visibleFields: const {'temperature'},
|
||||
fieldSpans: sensorFullWidthFieldSpans(const {'temperature'}),
|
||||
onRefresh: () async {
|
||||
refreshCount += 1;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.longPress(
|
||||
find.byKey(const ValueKey('sensor_metric_temperature')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(refreshCount, 1);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user