mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
Enhance sensor telemetry cards
This commit is contained in:
194
test/providers/sensors_provider_test.dart
Normal file
194
test/providers/sensors_provider_test.dart
Normal file
@@ -0,0 +1,194 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
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:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class _FakeContactsProvider extends ContactsProvider {
|
||||
_FakeContactsProvider(this._contacts);
|
||||
|
||||
final List<Contact> _contacts;
|
||||
|
||||
@override
|
||||
List<Contact> get contacts => _contacts;
|
||||
}
|
||||
|
||||
class _FakeConnectionProvider extends ConnectionProvider {
|
||||
_FakeConnectionProvider({required bool isConnected})
|
||||
: _isConnected = isConnected;
|
||||
|
||||
final bool _isConnected;
|
||||
|
||||
int pingCalls = 0;
|
||||
|
||||
@override
|
||||
DeviceInfo get deviceInfo => DeviceInfo(
|
||||
connectionState: _isConnected
|
||||
? ConnectionState.connected
|
||||
: ConnectionState.disconnected,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<PingResult> smartPing({
|
||||
required Uint8List contactPublicKey,
|
||||
required bool hasPath,
|
||||
Function()? onRetryWithFlooding,
|
||||
}) async {
|
||||
pingCalls += 1;
|
||||
return const PingResult(
|
||||
success: true,
|
||||
usedFlooding: false,
|
||||
timedOut: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> waitUntilLoaded(SensorsProvider provider) async {
|
||||
for (var i = 0; i < 20 && !provider.isLoaded; i++) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
expect(provider.isLoaded, isTrue);
|
||||
}
|
||||
|
||||
Contact buildSensorContact() {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[0] = 0x44;
|
||||
|
||||
return 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,
|
||||
);
|
||||
}
|
||||
|
||||
test('metric label overrides persist across reloads', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final contact = buildSensorContact();
|
||||
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
await provider.addSensor(contact);
|
||||
await provider.setMetricLabel(
|
||||
contact.publicKeyHex,
|
||||
'extra:illuminance_2',
|
||||
'Solar',
|
||||
);
|
||||
|
||||
expect(
|
||||
provider.labelOverrideFor(contact.publicKeyHex, 'extra:illuminance_2'),
|
||||
'Solar',
|
||||
);
|
||||
|
||||
final reloadedProvider = SensorsProvider();
|
||||
await waitUntilLoaded(reloadedProvider);
|
||||
|
||||
expect(
|
||||
reloadedProvider.labelOverrideFor(
|
||||
contact.publicKeyHex,
|
||||
'extra:illuminance_2',
|
||||
),
|
||||
'Solar',
|
||||
);
|
||||
});
|
||||
|
||||
test('metric order persists across reloads', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final contact = buildSensorContact();
|
||||
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
await provider.addSensor(contact);
|
||||
await provider.moveMetric(
|
||||
contact.publicKeyHex,
|
||||
availableFieldKeys: const ['voltage', 'battery', 'temperature'],
|
||||
oldIndex: 2,
|
||||
newIndex: 0,
|
||||
);
|
||||
|
||||
expect(
|
||||
provider.metricOrderFor(contact.publicKeyHex, const [
|
||||
'voltage',
|
||||
'battery',
|
||||
'temperature',
|
||||
]),
|
||||
const ['temperature', 'voltage', 'battery'],
|
||||
);
|
||||
|
||||
final reloadedProvider = SensorsProvider();
|
||||
await waitUntilLoaded(reloadedProvider);
|
||||
|
||||
expect(
|
||||
reloadedProvider.metricOrderFor(contact.publicKeyHex, const [
|
||||
'voltage',
|
||||
'battery',
|
||||
'temperature',
|
||||
]),
|
||||
const ['temperature', 'voltage', 'battery'],
|
||||
);
|
||||
});
|
||||
|
||||
test('auto refresh minutes persist across reloads', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final contact = buildSensorContact();
|
||||
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
await provider.addSensor(contact);
|
||||
await provider.setAutoRefreshMinutes(contact.publicKeyHex, 5);
|
||||
|
||||
expect(provider.autoRefreshMinutesFor(contact.publicKeyHex), 5);
|
||||
|
||||
final reloadedProvider = SensorsProvider();
|
||||
await waitUntilLoaded(reloadedProvider);
|
||||
|
||||
expect(reloadedProvider.autoRefreshMinutesFor(contact.publicKeyHex), 5);
|
||||
});
|
||||
|
||||
test('refreshDueSensors respects per-contact interval', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final contact = buildSensorContact();
|
||||
final contactsProvider = _FakeContactsProvider(<Contact>[contact]);
|
||||
final connectionProvider = _FakeConnectionProvider(isConnected: true);
|
||||
final start = DateTime(2026, 3, 15, 9, 0);
|
||||
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
await provider.addSensor(contact);
|
||||
await provider.setAutoRefreshMinutes(contact.publicKeyHex, 5);
|
||||
|
||||
await provider.refreshDueSensors(
|
||||
now: start,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
expect(connectionProvider.pingCalls, 1);
|
||||
|
||||
await provider.refreshDueSensors(
|
||||
now: start.add(const Duration(minutes: 4)),
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
expect(connectionProvider.pingCalls, 1);
|
||||
|
||||
await provider.refreshDueSensors(
|
||||
now: start.add(const Duration(minutes: 5)),
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
expect(connectionProvider.pingCalls, 2);
|
||||
});
|
||||
}
|
||||
100
test/screens/sensors_tab_test.dart
Normal file
100
test/screens/sensors_tab_test.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
||||
import 'package:meshcore_sar_app/screens/sensors_tab.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
Future<void> waitUntilLoaded(SensorsProvider provider) async {
|
||||
for (var i = 0; i < 20 && !provider.isLoaded; i++) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
expect(provider.isLoaded, isTrue);
|
||||
}
|
||||
|
||||
Contact buildSensorContact() {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[0] = 0x44;
|
||||
|
||||
return 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(
|
||||
batteryPercentage: 84,
|
||||
temperature: 21.5,
|
||||
extraSensorData: const {
|
||||
'__source_channel:battery': 1,
|
||||
'__source_channel:temperature': 1,
|
||||
'illuminance_2': 500.0,
|
||||
},
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('customize sheet shows metric value previews and channels', (
|
||||
tester,
|
||||
) async {
|
||||
final contact = buildSensorContact();
|
||||
final sensorsProvider = SensorsProvider();
|
||||
final contactsProvider = ContactsProvider();
|
||||
|
||||
await waitUntilLoaded(sensorsProvider);
|
||||
contactsProvider.addOrUpdateContact(contact);
|
||||
await sensorsProvider.addSensor(contact);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ContactsProvider>.value(
|
||||
value: contactsProvider,
|
||||
),
|
||||
ChangeNotifierProvider<SensorsProvider>.value(value: sensorsProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const SensorsTab(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.tap(find.text('Customize fields'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_selector_value_extra:illuminance_2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('500 lx'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_selector_channel_extra:illuminance_2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('ch2'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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/widgets/contacts/contact_tile.dart';
|
||||
import 'package:meshcore_sar_app/widgets/sensors/sensor_telemetry_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -40,14 +41,21 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pumpTile(WidgetTester tester, Contact contact) async {
|
||||
Future<void> pumpTile(
|
||||
WidgetTester tester,
|
||||
Contact contact, {
|
||||
SensorsProvider? sensorsProvider,
|
||||
}) async {
|
||||
final resolvedSensorsProvider = sensorsProvider ?? SensorsProvider();
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => ConnectionProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ContactsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => MessagesProvider()),
|
||||
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
||||
ChangeNotifierProvider<SensorsProvider>.value(
|
||||
value: resolvedSensorsProvider,
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => MapProvider()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
@@ -125,6 +133,9 @@ void main() {
|
||||
temperature: 21.5,
|
||||
humidity: 58.0,
|
||||
extraSensorData: const {
|
||||
'__source_channel:battery': 1,
|
||||
'__source_channel:temperature': 1,
|
||||
'__source_channel:humidity': 1,
|
||||
'co2': 415.0,
|
||||
'illuminance_2': 500.0,
|
||||
'current_2': 0.015,
|
||||
@@ -151,11 +162,29 @@ void main() {
|
||||
expect(find.text('21.5°C'), findsOneWidget);
|
||||
expect(find.text('CO2'), findsOneWidget);
|
||||
expect(find.text('415 ppm'), findsOneWidget);
|
||||
expect(find.text('Illuminance (ch 2)'), findsOneWidget);
|
||||
expect(find.text('Illuminance'), findsOneWidget);
|
||||
expect(find.text('~4.2 W/m2 daylight'), findsOneWidget);
|
||||
expect(find.text('Current (ch 2)'), findsOneWidget);
|
||||
expect(find.text('Current'), findsOneWidget);
|
||||
expect(find.text('15 mA'), findsOneWidget);
|
||||
expect(find.text('Power (ch 2)'), findsOneWidget);
|
||||
expect(find.text('Distance (ch 2)'), findsOneWidget);
|
||||
expect(find.text('Power'), findsOneWidget);
|
||||
expect(find.text('Distance'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_channel_battery')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text('ch1'),
|
||||
findsWidgets,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_channel_extra:illuminance_2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
final sensorCardSize = tester.getSize(find.byType(SensorTelemetryCard));
|
||||
final batteryTileSize = tester.getSize(
|
||||
find.byKey(const ValueKey('sensor_metric_battery')),
|
||||
);
|
||||
expect(batteryTileSize.width, greaterThan(sensorCardSize.width * 0.8));
|
||||
});
|
||||
}
|
||||
|
||||
144
test/widgets/sensor_telemetry_card_test.dart
Normal file
144
test/widgets/sensor_telemetry_card_test.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
||||
import 'package:meshcore_sar_app/widgets/sensors/sensor_telemetry_card.dart';
|
||||
|
||||
void main() {
|
||||
Contact buildContact() {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[0] = 0x44;
|
||||
|
||||
return 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(
|
||||
temperature: 21.5,
|
||||
extraSensorData: const {
|
||||
'__source_channel:temperature': 1,
|
||||
'illuminance_2': 500.0,
|
||||
},
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('renders custom labels and channel badges', (tester) async {
|
||||
final contact = buildContact();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: SensorTelemetryCard(
|
||||
contact: contact,
|
||||
state: SensorRefreshState.idle,
|
||||
visibleFields: const {'temperature', 'extra:illuminance_2'},
|
||||
labelOverrides: const {
|
||||
'temperature': 'Ambient',
|
||||
'extra:illuminance_2': 'Light',
|
||||
},
|
||||
fieldSpans: sensorFullWidthFieldSpans(
|
||||
const {'temperature', 'extra:illuminance_2'},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Ambient'), findsOneWidget);
|
||||
expect(find.text('Light'), findsOneWidget);
|
||||
expect(find.text('Temperature'), findsNothing);
|
||||
expect(find.text('Illuminance'), findsNothing);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_channel_temperature')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_channel_extra:illuminance_2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('renders metrics in the provided order', (tester) async {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[0] = 0x45;
|
||||
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(
|
||||
batteryPercentage: 84,
|
||||
temperature: 21.5,
|
||||
extraSensorData: const {
|
||||
'__source_channel:battery': 1,
|
||||
'__source_channel:temperature': 1,
|
||||
'illuminance_2': 500.0,
|
||||
},
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: SensorTelemetryCard(
|
||||
contact: contact,
|
||||
state: SensorRefreshState.idle,
|
||||
visibleFields: const {
|
||||
'battery',
|
||||
'temperature',
|
||||
'extra:illuminance_2',
|
||||
},
|
||||
fieldOrder: const [
|
||||
'extra:illuminance_2',
|
||||
'temperature',
|
||||
'battery',
|
||||
],
|
||||
fieldSpans: sensorFullWidthFieldSpans(
|
||||
const {
|
||||
'battery',
|
||||
'temperature',
|
||||
'extra:illuminance_2',
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final illuminanceTop = tester.getTopLeft(
|
||||
find.byKey(const ValueKey('sensor_metric_extra:illuminance_2')),
|
||||
);
|
||||
final temperatureTop = tester.getTopLeft(
|
||||
find.byKey(const ValueKey('sensor_metric_temperature')),
|
||||
);
|
||||
final batteryTop = tester.getTopLeft(
|
||||
find.byKey(const ValueKey('sensor_metric_battery')),
|
||||
);
|
||||
|
||||
expect(illuminanceTop.dy, lessThan(temperatureTop.dy));
|
||||
expect(temperatureTop.dy, lessThan(batteryTop.dy));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user