mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
fix: Preserve contacts and sensor updates
This commit is contained in:
@@ -275,10 +275,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Clear runtime contact state before a live device contact sync begins.
|
/// Clear runtime contact state before a live device contact sync begins.
|
||||||
///
|
///
|
||||||
/// This intentionally does not touch persisted storage. It keeps any saved
|
/// This intentionally does not touch persisted storage. It snapshots the
|
||||||
/// contact groups for the active profile, but removes stale in-memory device
|
/// current in-memory contacts so sync updates can still merge against the
|
||||||
/// contacts and discovery state so a newly connected device starts from an
|
/// latest local state, but keeps the visible list intact so reconnects do
|
||||||
/// empty list while sync is in progress.
|
/// not blank the UI while the device is resyncing.
|
||||||
Future<void> prepareForDeviceContactSync({Uint8List? devicePublicKey}) async {
|
Future<void> prepareForDeviceContactSync({Uint8List? devicePublicKey}) async {
|
||||||
_setSelfDevicePublicKey(devicePublicKey);
|
_setSelfDevicePublicKey(devicePublicKey);
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
@@ -292,7 +292,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🧹 [ContactsProvider] Clearing runtime contacts before device sync',
|
'🧹 [ContactsProvider] Preparing retained contact state for device sync',
|
||||||
);
|
);
|
||||||
_retainedContactsForSync
|
_retainedContactsForSync
|
||||||
..clear()
|
..clear()
|
||||||
@@ -301,12 +301,6 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
(contact) => MapEntry(contact.publicKeyHex, contact),
|
(contact) => MapEntry(contact.publicKeyHex, contact),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_contacts.clear();
|
|
||||||
_pendingAdverts.clear();
|
|
||||||
_estimatedLocations.clear();
|
|
||||||
_rssiObservations.clear();
|
|
||||||
_ensurePublicChannelExists();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
||||||
|
|||||||
@@ -29,6 +29,31 @@ class SensorMetricOption {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Set<String> _imperialMeasurementCountries = <String>{
|
||||||
|
'US',
|
||||||
|
'LR',
|
||||||
|
'MM',
|
||||||
|
};
|
||||||
|
|
||||||
|
bool _usesImperialSpeedUnits() {
|
||||||
|
final countryCode =
|
||||||
|
WidgetsBinding.instance.platformDispatcher.locale.countryCode;
|
||||||
|
if (countryCode == null || countryCode.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _imperialMeasurementCountries.contains(countryCode.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatPreviewSpeed(num metersPerSecond) {
|
||||||
|
if (_usesImperialSpeedUnits()) {
|
||||||
|
final milesPerHour = metersPerSecond * 2.2369362920544;
|
||||||
|
return '${_formatPreviewNumber(milesPerHour, maxFractionDigits: 2)} mph';
|
||||||
|
}
|
||||||
|
|
||||||
|
final kilometersPerHour = metersPerSecond * 3.6;
|
||||||
|
return '${_formatPreviewNumber(kilometersPerHour, maxFractionDigits: 2)} km/h';
|
||||||
|
}
|
||||||
|
|
||||||
List<SensorMetricOption> sensorMetricOptionsFor(
|
List<SensorMetricOption> sensorMetricOptionsFor(
|
||||||
Contact? contact, {
|
Contact? contact, {
|
||||||
Map<String, String> labelOverrides = const <String, String>{},
|
Map<String, String> labelOverrides = const <String, String>{},
|
||||||
@@ -795,17 +820,17 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
|||||||
case 'speed':
|
case 'speed':
|
||||||
final metersPerSecond = _previewAsDouble(value);
|
final metersPerSecond = _previewAsDouble(value);
|
||||||
if (metersPerSecond == null) return null;
|
if (metersPerSecond == null) return null;
|
||||||
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
return _formatPreviewSpeed(metersPerSecond);
|
||||||
|
|
||||||
case 'signed_speed':
|
case 'signed_speed':
|
||||||
final metersPerSecond = _previewAsDouble(value);
|
final metersPerSecond = _previewAsDouble(value);
|
||||||
if (metersPerSecond == null) return null;
|
if (metersPerSecond == null) return null;
|
||||||
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
return _formatPreviewSpeed(metersPerSecond);
|
||||||
|
|
||||||
case 'gust':
|
case 'gust':
|
||||||
final metersPerSecond = _previewAsDouble(value);
|
final metersPerSecond = _previewAsDouble(value);
|
||||||
if (metersPerSecond == null) return null;
|
if (metersPerSecond == null) return null;
|
||||||
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
return _formatPreviewSpeed(metersPerSecond);
|
||||||
|
|
||||||
case 'dew':
|
case 'dew':
|
||||||
final degreesCelsius = _previewAsDouble(value);
|
final degreesCelsius = _previewAsDouble(value);
|
||||||
@@ -1066,6 +1091,16 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
this.labelOverrides = const <String, String>{},
|
this.labelOverrides = const <String, String>{},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
String _formatSpeed(num metersPerSecond) {
|
||||||
|
if (_usesImperialSpeedUnits()) {
|
||||||
|
final milesPerHour = metersPerSecond * 2.2369362920544;
|
||||||
|
return '${_formatNumber(milesPerHour, maxFractionDigits: 2)} mph';
|
||||||
|
}
|
||||||
|
|
||||||
|
final kilometersPerHour = metersPerSecond * 3.6;
|
||||||
|
return '${_formatNumber(kilometersPerHour, maxFractionDigits: 2)} km/h';
|
||||||
|
}
|
||||||
|
|
||||||
bool get _showsMenu =>
|
bool get _showsMenu =>
|
||||||
onRefresh != null ||
|
onRefresh != null ||
|
||||||
onCustomize != null ||
|
onCustomize != null ||
|
||||||
@@ -1759,7 +1794,7 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
fieldKey: _extraFieldKey(rawKey),
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
icon: Icons.air,
|
icon: Icons.air,
|
||||||
label: label,
|
label: label,
|
||||||
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
|
value: _formatSpeed(metersPerSecond),
|
||||||
accent: const Color(0xFF2B78A0),
|
accent: const Color(0xFF2B78A0),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
@@ -1771,7 +1806,7 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
fieldKey: _extraFieldKey(rawKey),
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
icon: Icons.air,
|
icon: Icons.air,
|
||||||
label: label,
|
label: label,
|
||||||
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
|
value: _formatSpeed(metersPerSecond),
|
||||||
accent: const Color(0xFF2B78A0),
|
accent: const Color(0xFF2B78A0),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
@@ -1783,7 +1818,7 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
fieldKey: _extraFieldKey(rawKey),
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
icon: Icons.air,
|
icon: Icons.air,
|
||||||
label: label,
|
label: label,
|
||||||
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
|
value: _formatSpeed(metersPerSecond),
|
||||||
accent: const Color(0xFF1E88A8),
|
accent: const Color(0xFF1E88A8),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
@@ -2252,7 +2287,7 @@ class _InlineAlertBadge extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SensorMetricTile extends StatelessWidget {
|
class SensorMetricTile extends StatefulWidget {
|
||||||
final SensorMetricCardData data;
|
final SensorMetricCardData data;
|
||||||
final double width;
|
final double width;
|
||||||
final String keyPrefix;
|
final String keyPrefix;
|
||||||
@@ -2268,8 +2303,43 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SensorMetricTile> createState() => _SensorMetricTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SensorMetricTileState extends State<SensorMetricTile> {
|
||||||
|
final flutter_map.MapController _previewMapController =
|
||||||
|
flutter_map.MapController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant SensorMetricTile oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final previousLocation = oldWidget.data.mapLocation;
|
||||||
|
final nextLocation = widget.data.mapLocation;
|
||||||
|
if (!_sameMapLocation(previousLocation, nextLocation) &&
|
||||||
|
nextLocation != null &&
|
||||||
|
widget.allowMapPreview) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_previewMapController.move(
|
||||||
|
nextLocation,
|
||||||
|
_previewMapController.camera.zoom,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _sameMapLocation(LatLng? a, LatLng? b) {
|
||||||
|
if (a == null || b == null) {
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
return a.latitude == b.latitude && a.longitude == b.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showExpandedMap(BuildContext context) async {
|
Future<void> _showExpandedMap(BuildContext context) async {
|
||||||
final location = data.mapLocation;
|
final location = widget.data.mapLocation;
|
||||||
if (location == null) return;
|
if (location == null) return;
|
||||||
|
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
@@ -2280,9 +2350,9 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(data.label),
|
Text(widget.data.label),
|
||||||
Text(
|
Text(
|
||||||
data.value,
|
widget.data.value,
|
||||||
style: Theme.of(pageContext).textTheme.bodySmall,
|
style: Theme.of(pageContext).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2291,11 +2361,11 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.secondaryValue != null)
|
if (widget.data.secondaryValue != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
child: Text(
|
child: Text(
|
||||||
data.secondaryValue!,
|
widget.data.secondaryValue!,
|
||||||
style: Theme.of(pageContext).textTheme.bodyMedium,
|
style: Theme.of(pageContext).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -2320,7 +2390,7 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
height: 40,
|
height: 40,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.location_on,
|
Icons.location_on,
|
||||||
color: data.accent,
|
color: widget.data.accent,
|
||||||
size: 34,
|
size: 34,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -2340,14 +2410,15 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final data = widget.data;
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
key: ValueKey('${keyPrefix}_${data.fieldKey}'),
|
key: ValueKey('${widget.keyPrefix}_${data.fieldKey}'),
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(22),
|
||||||
onLongPress: onLongPress,
|
onLongPress: widget.onLongPress,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: width,
|
width: widget.width,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: data.accent.withValues(alpha: 0.08),
|
color: data.accent.withValues(alpha: 0.08),
|
||||||
@@ -2368,7 +2439,10 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetricText(data: data, keyPrefix: keyPrefix),
|
child: _MetricText(
|
||||||
|
data: data,
|
||||||
|
keyPrefix: widget.keyPrefix,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -2387,7 +2461,7 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: data.mapLocation == null || !allowMapPreview
|
: data.mapLocation == null || !widget.allowMapPreview
|
||||||
? Stack(
|
? Stack(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -2396,7 +2470,10 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
_MetricIcon(accent: data.accent, icon: data.icon),
|
_MetricIcon(accent: data.accent, icon: data.icon),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetricText(data: data, keyPrefix: keyPrefix),
|
child: _MetricText(
|
||||||
|
data: data,
|
||||||
|
keyPrefix: widget.keyPrefix,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -2424,7 +2501,10 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
_MetricIcon(accent: data.accent, icon: data.icon),
|
_MetricIcon(accent: data.accent, icon: data.icon),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetricText(data: data, keyPrefix: keyPrefix),
|
child: _MetricText(
|
||||||
|
data: data,
|
||||||
|
keyPrefix: widget.keyPrefix,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -2442,6 +2522,7 @@ class SensorMetricTile extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
flutter_map.FlutterMap(
|
flutter_map.FlutterMap(
|
||||||
|
mapController: _previewMapController,
|
||||||
options: flutter_map.MapOptions(
|
options: flutter_map.MapOptions(
|
||||||
initialCenter: data.mapLocation!,
|
initialCenter: data.mapLocation!,
|
||||||
initialZoom: 14,
|
initialZoom: 14,
|
||||||
|
|||||||
@@ -893,7 +893,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'clears runtime contacts before sync without erasing persisted contacts or saved groups',
|
'keeps runtime contacts visible during sync without erasing persisted contacts or saved groups',
|
||||||
() async {
|
() async {
|
||||||
final key = createPublicKey(140);
|
final key = createPublicKey(140);
|
||||||
final pendingKey = createPublicKey(180);
|
final pendingKey = createPublicKey(180);
|
||||||
@@ -906,8 +906,11 @@ void main() {
|
|||||||
|
|
||||||
await provider.prepareForDeviceContactSync();
|
await provider.prepareForDeviceContactSync();
|
||||||
|
|
||||||
expect(provider.chatContacts, isEmpty);
|
expect(
|
||||||
expect(provider.pendingAdverts, isEmpty);
|
provider.chatContacts.map((contact) => contact.advName),
|
||||||
|
contains('Synced Later'),
|
||||||
|
);
|
||||||
|
expect(provider.pendingAdverts, hasLength(1));
|
||||||
expect(provider.savedGroupsForSection('teamMembers'), hasLength(1));
|
expect(provider.savedGroupsForSection('teamMembers'), hasLength(1));
|
||||||
|
|
||||||
final restored = ContactsProvider();
|
final restored = ContactsProvider();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||||
import 'package:meshcore_sar_app/models/contact.dart';
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
||||||
@@ -140,6 +142,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('renders MeshCore custom weather metrics', (tester) async {
|
testWidgets('renders MeshCore custom weather metrics', (tester) async {
|
||||||
|
tester.platformDispatcher.localeTestValue = const Locale('sl', 'SI');
|
||||||
|
addTearDown(tester.platformDispatcher.clearLocaleTestValue);
|
||||||
final publicKey = Uint8List(32);
|
final publicKey = Uint8List(32);
|
||||||
publicKey[0] = 0x46;
|
publicKey[0] = 0x46;
|
||||||
final contact = Contact(
|
final contact = Contact(
|
||||||
@@ -185,11 +189,130 @@ void main() {
|
|||||||
expect(find.text('Wind gust'), findsOneWidget);
|
expect(find.text('Wind gust'), findsOneWidget);
|
||||||
expect(find.text('Dew point'), findsOneWidget);
|
expect(find.text('Dew point'), findsOneWidget);
|
||||||
expect(find.text('Rain'), findsOneWidget);
|
expect(find.text('Rain'), findsOneWidget);
|
||||||
expect(find.text('3.7 m/s'), findsOneWidget);
|
expect(find.textContaining('km/h'), findsOneWidget);
|
||||||
|
expect(find.textContaining('m/s'), findsNothing);
|
||||||
expect(find.text('2°C'), findsOneWidget);
|
expect(find.text('2°C'), findsOneWidget);
|
||||||
expect(find.text('12.3 mm'), findsOneWidget);
|
expect(find.text('12.3 mm'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('renders speed in mph for imperial system locale', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
tester.platformDispatcher.localeTestValue = const Locale('en', 'US');
|
||||||
|
addTearDown(tester.platformDispatcher.clearLocaleTestValue);
|
||||||
|
|
||||||
|
final publicKey = Uint8List(32);
|
||||||
|
publicKey[0] = 0x4A;
|
||||||
|
final contact = Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
type: ContactType.sensor,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: 'WX Station',
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
telemetry: ContactTelemetry(
|
||||||
|
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||||
|
extraSensorData: const {'speed_2': 3.7},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SensorTelemetryCard(
|
||||||
|
contact: contact,
|
||||||
|
state: SensorRefreshState.idle,
|
||||||
|
visibleFields: const {'extra:speed_2'},
|
||||||
|
fieldSpans: sensorFullWidthFieldSpans(const {'extra:speed_2'}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.textContaining('mph'), findsOneWidget);
|
||||||
|
expect(find.textContaining('m/s'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('gps preview map recenters when telemetry location changes', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final publicKey = Uint8List(32);
|
||||||
|
publicKey[0] = 0x4B;
|
||||||
|
|
||||||
|
Contact buildGpsContact(double latitude, double longitude) => Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
type: ContactType.sensor,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: 'GPS Station',
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
telemetry: ContactTelemetry(
|
||||||
|
gpsLocation: LatLng(latitude, longitude),
|
||||||
|
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var contact = buildGpsContact(46.0569, 14.5058);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
contact = buildGpsContact(46.1000, 14.6000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Update'),
|
||||||
|
),
|
||||||
|
SensorTelemetryCard(
|
||||||
|
contact: contact,
|
||||||
|
state: SensorRefreshState.idle,
|
||||||
|
visibleFields: const {'gps'},
|
||||||
|
fieldSpans: sensorFullWidthFieldSpans(const {'gps'}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final initialMap = tester.widget<flutter_map.FlutterMap>(
|
||||||
|
find.byType(flutter_map.FlutterMap).first,
|
||||||
|
);
|
||||||
|
final initialCenter = initialMap.mapController!.camera.center;
|
||||||
|
expect(initialCenter.latitude, closeTo(46.0569, 0.0001));
|
||||||
|
expect(initialCenter.longitude, closeTo(14.5058, 0.0001));
|
||||||
|
|
||||||
|
await tester.tap(find.text('Update'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final updatedMap = tester.widget<flutter_map.FlutterMap>(
|
||||||
|
find.byType(flutter_map.FlutterMap).first,
|
||||||
|
);
|
||||||
|
final updatedCenter = updatedMap.mapController!.camera.center;
|
||||||
|
expect(updatedCenter.latitude, closeTo(46.1000, 0.0001));
|
||||||
|
expect(updatedCenter.longitude, closeTo(14.6000, 0.0001));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('renders generic percentage separately from UV for weather payload', (tester) async {
|
testWidgets('renders generic percentage separately from UV for weather payload', (tester) async {
|
||||||
tester.view.physicalSize = const Size(1600, 2600);
|
tester.view.physicalSize = const Size(1600, 2600);
|
||||||
tester.view.devicePixelRatio = 1.0;
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
|||||||
Reference in New Issue
Block a user