mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
feat: Open per-metric sensor history
This commit is contained in:
@@ -215,6 +215,70 @@ void main() {
|
||||
expect(reloadedProvider.autoRefreshMinutesFor(contact.publicKeyHex), 1440);
|
||||
});
|
||||
|
||||
test('tracked auto refresh history is captured and persisted', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final timestamp = DateTime(2026, 3, 15, 9, 0);
|
||||
final contacts = <Contact>[
|
||||
buildSensorContact().copyWith(
|
||||
telemetry: ContactTelemetry(
|
||||
temperature: 12.5,
|
||||
humidity: 54,
|
||||
pressure: 918.2,
|
||||
timestamp: timestamp,
|
||||
extraSensorData: const {'illuminance_2': 150.0},
|
||||
),
|
||||
),
|
||||
];
|
||||
final contactsProvider = _FakeContactsProvider(contacts);
|
||||
final connectionProvider = _FakeConnectionProvider(isConnected: true);
|
||||
|
||||
final provider = SensorsProvider();
|
||||
await waitUntilLoaded(provider);
|
||||
await provider.addSensor(contacts.first);
|
||||
await provider.setAutoRefreshMinutes(contacts.first.publicKeyHex, 5);
|
||||
|
||||
await provider.captureTrackedTelemetryHistory(
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
|
||||
final firstHistory = provider.historyFor(contacts.first.publicKeyHex);
|
||||
expect(firstHistory, hasLength(1));
|
||||
expect(firstHistory.first.values['temperature'], 12.5);
|
||||
expect(firstHistory.first.values['humidity'], 54);
|
||||
expect(firstHistory.first.values['pressure'], 918.2);
|
||||
expect(firstHistory.first.values['extra:illuminance_2'], 150.0);
|
||||
|
||||
await provider.captureTrackedTelemetryHistory(
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
expect(provider.historyFor(contacts.first.publicKeyHex), hasLength(1));
|
||||
|
||||
contacts[0] = contacts[0].copyWith(
|
||||
telemetry: ContactTelemetry(
|
||||
temperature: 13.1,
|
||||
humidity: 52,
|
||||
pressure: 919.0,
|
||||
timestamp: timestamp.add(const Duration(minutes: 5)),
|
||||
extraSensorData: const {'illuminance_2': 160.0},
|
||||
),
|
||||
);
|
||||
|
||||
await provider.captureTrackedTelemetryHistory(
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
|
||||
final updatedHistory = provider.historyFor(contacts.first.publicKeyHex);
|
||||
expect(updatedHistory, hasLength(2));
|
||||
expect(updatedHistory.last.values['temperature'], 13.1);
|
||||
|
||||
final reloadedProvider = SensorsProvider();
|
||||
await waitUntilLoaded(reloadedProvider);
|
||||
expect(reloadedProvider.historyFor(contacts.first.publicKeyHex), hasLength(2));
|
||||
});
|
||||
|
||||
test('watched sensors and preferences are isolated per profile', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final contact = buildSensorContact();
|
||||
|
||||
@@ -69,7 +69,7 @@ void main() {
|
||||
expect(await MeshMapNodesService.hasFreshCache(), isFalse);
|
||||
});
|
||||
|
||||
test('clearCache removes persisted online trace database', () async {
|
||||
test('clearCache removes persisted online node cache', () async {
|
||||
final client = MockClient(
|
||||
(_) async => http.Response(
|
||||
jsonEncode({
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_sar_app/services/mesh_map_nodes_service.dart';
|
||||
import 'package:meshcore_sar_app/utils/trace_node_resolver.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'prefers closest local repeater over online fallback for shared prefix',
|
||||
() {
|
||||
final localNear = _node(
|
||||
name: 'Near Local',
|
||||
publicKey: 'aa1100',
|
||||
latitude: 46.05,
|
||||
longitude: 14.50,
|
||||
);
|
||||
final localFar = _node(
|
||||
name: 'Far Local',
|
||||
publicKey: 'aa2200',
|
||||
latitude: 46.40,
|
||||
longitude: 14.90,
|
||||
);
|
||||
final online = _node(
|
||||
name: 'Online',
|
||||
publicKey: 'aa3300',
|
||||
latitude: 46.06,
|
||||
longitude: 14.51,
|
||||
);
|
||||
|
||||
final resolved = TraceNodeResolver.resolveBest(
|
||||
nodes: [localNear, localFar, online],
|
||||
localPublicKeys: {localNear.publicKey, localFar.publicKey},
|
||||
prefixHex: 'aa',
|
||||
referenceA: const LatLng(46.0, 14.5),
|
||||
referenceB: const LatLng(46.1, 14.5),
|
||||
);
|
||||
|
||||
expect(resolved.node?.name, 'Near Local');
|
||||
expect(resolved.usedOnlineFallback, isFalse);
|
||||
expect(resolved.matchCount, 2);
|
||||
expect(resolved.matchSummary, '2 local matches');
|
||||
},
|
||||
);
|
||||
|
||||
test('falls back to online node only when local match is missing', () {
|
||||
final online = _node(
|
||||
name: 'Online Only',
|
||||
publicKey: 'bb1100',
|
||||
latitude: 46.06,
|
||||
longitude: 14.51,
|
||||
);
|
||||
|
||||
final resolved = TraceNodeResolver.resolveBest(
|
||||
nodes: [online],
|
||||
localPublicKeys: const {},
|
||||
prefixHex: 'bb',
|
||||
referenceA: const LatLng(46.0, 14.5),
|
||||
referenceB: const LatLng(46.1, 14.5),
|
||||
);
|
||||
|
||||
expect(resolved.node?.name, 'Online Only');
|
||||
expect(resolved.usedOnlineFallback, isTrue);
|
||||
expect(resolved.matchCount, 1);
|
||||
});
|
||||
|
||||
test('cycles through ambiguous local prefix matches', () {
|
||||
final first = _node(
|
||||
name: 'First Match',
|
||||
publicKey: 'cc1100',
|
||||
latitude: 46.08,
|
||||
longitude: 14.52,
|
||||
);
|
||||
final second = _node(
|
||||
name: 'Second Match',
|
||||
publicKey: 'cc11ff',
|
||||
latitude: 46.09,
|
||||
longitude: 14.53,
|
||||
);
|
||||
|
||||
final resolved = TraceNodeResolver.resolveBest(
|
||||
nodes: [second, first],
|
||||
localPublicKeys: {first.publicKey, second.publicKey},
|
||||
prefixHex: 'cc11',
|
||||
);
|
||||
|
||||
expect(resolved.matchCount, 2);
|
||||
expect(resolved.canCycle, isTrue);
|
||||
expect(resolved.node?.name, 'Second Match');
|
||||
expect(resolved.cycle().node?.name, 'First Match');
|
||||
expect(resolved.cycle().cycle().node?.name, 'Second Match');
|
||||
});
|
||||
|
||||
test('aligns ambiguous hops to the closest continuous path', () {
|
||||
final start = _node(
|
||||
name: 'Start',
|
||||
publicKey: 'start00',
|
||||
latitude: 46.000,
|
||||
longitude: 14.000,
|
||||
);
|
||||
final end = _node(
|
||||
name: 'End',
|
||||
publicKey: 'end000',
|
||||
latitude: 46.300,
|
||||
longitude: 14.300,
|
||||
);
|
||||
final hop1Near = _node(
|
||||
name: 'Hop 1 Near',
|
||||
publicKey: 'aa1100',
|
||||
latitude: 46.100,
|
||||
longitude: 14.100,
|
||||
);
|
||||
final hop1Far = _node(
|
||||
name: 'Hop 1 Far',
|
||||
publicKey: 'aa11ff',
|
||||
latitude: 46.250,
|
||||
longitude: 14.000,
|
||||
);
|
||||
final hop2Near = _node(
|
||||
name: 'Hop 2 Near',
|
||||
publicKey: 'bb2200',
|
||||
latitude: 46.200,
|
||||
longitude: 14.200,
|
||||
);
|
||||
final hop2Far = _node(
|
||||
name: 'Hop 2 Far',
|
||||
publicKey: 'bb22ff',
|
||||
latitude: 46.050,
|
||||
longitude: 14.280,
|
||||
);
|
||||
|
||||
final aligned = TraceNodeResolver.alignPathSelections(
|
||||
nodes: [
|
||||
TraceNodeResolver.resolveBest(
|
||||
nodes: [hop1Far, hop1Near],
|
||||
localPublicKeys: {hop1Near.publicKey, hop1Far.publicKey},
|
||||
prefixHex: 'aa11',
|
||||
),
|
||||
TraceNodeResolver.resolveBest(
|
||||
nodes: [hop2Far, hop2Near],
|
||||
localPublicKeys: {hop2Near.publicKey, hop2Far.publicKey},
|
||||
prefixHex: 'bb22',
|
||||
),
|
||||
],
|
||||
startNode: start,
|
||||
endNode: end,
|
||||
);
|
||||
|
||||
expect(aligned[0].node?.name, 'Hop 1 Near');
|
||||
expect(aligned[1].node?.name, 'Hop 2 Near');
|
||||
});
|
||||
}
|
||||
|
||||
MeshMapNode _node({
|
||||
required String name,
|
||||
required String publicKey,
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
}) {
|
||||
return MeshMapNode(
|
||||
type: 1,
|
||||
name: name,
|
||||
publicKey: publicKey,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
updatedAtMs: 1,
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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/app_provider.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/map_provider.dart';
|
||||
@@ -14,6 +15,32 @@ import 'package:meshcore_sar_app/widgets/sensors/sensor_telemetry_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class _FakeAppProvider extends ChangeNotifier implements AppProvider {
|
||||
@override
|
||||
ChannelLocationSharingMode? channelLocationSharingModeForChannel(
|
||||
int channelIdx,
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ChannelLocationSharingState> getChannelLocationSharingState(
|
||||
int channelIdx,
|
||||
) async {
|
||||
return const ChannelLocationSharingState(
|
||||
mode: ChannelLocationSharingMode.appFallback,
|
||||
isSharing: false,
|
||||
hardwareSupported: false,
|
||||
isConnected: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
@@ -41,22 +68,31 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pumpTile(
|
||||
Future<Future<void> Function()> pumpTile(
|
||||
WidgetTester tester,
|
||||
Contact contact, {
|
||||
SensorsProvider? sensorsProvider,
|
||||
}) async {
|
||||
final connectionProvider = ConnectionProvider();
|
||||
final contactsProvider = ContactsProvider();
|
||||
final messagesProvider = MessagesProvider();
|
||||
final mapProvider = MapProvider();
|
||||
final appProvider = _FakeAppProvider();
|
||||
final resolvedSensorsProvider = sensorsProvider ?? SensorsProvider();
|
||||
final ownsSensorsProvider = sensorsProvider == null;
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => ConnectionProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ContactsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => MessagesProvider()),
|
||||
ChangeNotifierProvider<ConnectionProvider>.value(
|
||||
value: connectionProvider,
|
||||
),
|
||||
ChangeNotifierProvider<ContactsProvider>.value(value: contactsProvider),
|
||||
ChangeNotifierProvider<MessagesProvider>.value(value: messagesProvider),
|
||||
ChangeNotifierProvider<SensorsProvider>.value(
|
||||
value: resolvedSensorsProvider,
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => MapProvider()),
|
||||
ChangeNotifierProvider<MapProvider>.value(value: mapProvider),
|
||||
ChangeNotifierProvider<AppProvider>.value(value: appProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
@@ -65,64 +101,97 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return () async {
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
connectionProvider.dispose();
|
||||
if (ownsSensorsProvider) {
|
||||
resolvedSensorsProvider.dispose();
|
||||
}
|
||||
await tester.pump();
|
||||
};
|
||||
}
|
||||
|
||||
testWidgets('shows trace action for non-channel contacts', (tester) async {
|
||||
await pumpTile(
|
||||
Future<void> withPumpedTile(
|
||||
WidgetTester tester,
|
||||
Contact contact,
|
||||
Future<void> Function() body, {
|
||||
SensorsProvider? sensorsProvider,
|
||||
}) async {
|
||||
final dispose = await pumpTile(
|
||||
tester,
|
||||
contact,
|
||||
sensorsProvider: sensorsProvider,
|
||||
);
|
||||
try {
|
||||
await body();
|
||||
} finally {
|
||||
await dispose();
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('does not show diagnostic action for non-channel contacts', (
|
||||
tester,
|
||||
) async {
|
||||
await withPumpedTile(
|
||||
tester,
|
||||
buildContact(name: 'John Smith', type: ContactType.chat),
|
||||
() async {
|
||||
await tester.tap(find.text('John Smith'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Trace'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
await tester.tap(find.text('John Smith'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Trace'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('does not show trace action for channels', (tester) async {
|
||||
await pumpTile(
|
||||
testWidgets('does not show diagnostic action for channels', (tester) async {
|
||||
await withPumpedTile(
|
||||
tester,
|
||||
buildContact(name: 'Ops', type: ContactType.channel, secondByte: 3),
|
||||
() async {
|
||||
await tester.tap(find.text('Ops'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Trace'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Ops'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Trace'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows overridden contact name as primary label', (tester) async {
|
||||
await pumpTile(
|
||||
await withPumpedTile(
|
||||
tester,
|
||||
buildContact(
|
||||
name: 'John Smith',
|
||||
type: ContactType.chat,
|
||||
).copyWith(nameOverride: 'Rescue One'),
|
||||
() async {
|
||||
expect(find.text('Rescue One'), findsOneWidget);
|
||||
expect(find.text('John Smith'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
expect(find.text('Rescue One'), findsOneWidget);
|
||||
expect(find.text('John Smith'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hides public key in contact tile', (tester) async {
|
||||
final contact = buildContact(name: 'John Smith', type: ContactType.chat);
|
||||
|
||||
await pumpTile(tester, contact);
|
||||
|
||||
expect(find.text(contact.publicKeyShort), findsNothing);
|
||||
expect(find.byIcon(Icons.key_outlined), findsNothing);
|
||||
await withPumpedTile(tester, contact, () async {
|
||||
expect(find.text(contact.publicKeyShort), findsNothing);
|
||||
expect(find.byIcon(Icons.key_outlined), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('sensor contacts can be added to sensors', (tester) async {
|
||||
await pumpTile(
|
||||
await withPumpedTile(
|
||||
tester,
|
||||
buildContact(name: 'WX Station', type: ContactType.sensor),
|
||||
() async {
|
||||
await tester.tap(find.text('WX Station'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Add to Sensors'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
await tester.tap(find.text('WX Station'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Add to Sensors'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('sensor preview shows telemetry card', (tester) async {
|
||||
@@ -146,46 +215,49 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await pumpTile(tester, contact);
|
||||
await withPumpedTile(tester, contact, () async {
|
||||
await tester.tap(find.text('WX Station'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('WX Station'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Preview'), findsOneWidget);
|
||||
|
||||
expect(find.text('Preview'), findsOneWidget);
|
||||
await tester.tap(find.text('Preview'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Preview'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
||||
expect(find.text('Battery'), findsOneWidget);
|
||||
expect(find.text('84%'), findsOneWidget);
|
||||
expect(find.text('Temperature'), findsOneWidget);
|
||||
expect(find.text('21.5°C'), findsOneWidget);
|
||||
expect(find.text('CO2'), findsOneWidget);
|
||||
expect(find.text('415 ppm'), findsOneWidget);
|
||||
expect(find.text('Illuminance'), findsOneWidget);
|
||||
expect(find.text('~4.2 W/m2'), findsOneWidget);
|
||||
expect(find.textContaining('lx'), findsNothing);
|
||||
expect(find.text('Current'), findsOneWidget);
|
||||
expect(find.text('15 mA'), findsOneWidget);
|
||||
expect(find.text('Power'), findsOneWidget);
|
||||
expect(find.text('Distance'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_battery')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('ch1'), findsWidgets);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_extra:illuminance_2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
||||
expect(find.text('Battery'), findsOneWidget);
|
||||
expect(find.text('84%'), findsOneWidget);
|
||||
expect(find.text('Temperature'), findsOneWidget);
|
||||
expect(find.text('21.5°C'), findsOneWidget);
|
||||
expect(find.text('CO2'), findsOneWidget);
|
||||
expect(find.text('415 ppm'), findsOneWidget);
|
||||
expect(find.text('Illuminance'), findsOneWidget);
|
||||
expect(find.text('~4.2 W/m2'), findsOneWidget);
|
||||
expect(find.textContaining('lx'), findsNothing);
|
||||
expect(find.text('Current'), findsOneWidget);
|
||||
expect(find.text('15 mA'), findsOneWidget);
|
||||
expect(find.text('Power'), findsOneWidget);
|
||||
expect(find.text('Distance'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('sensor_metric_battery')), findsOneWidget);
|
||||
expect(find.text('ch1'), findsWidgets);
|
||||
expect(
|
||||
find.byKey(const ValueKey('sensor_metric_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));
|
||||
|
||||
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));
|
||||
await tester.tap(find.byIcon(Icons.close));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.close));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SensorTelemetryCard), findsNothing);
|
||||
expect(find.byType(SensorTelemetryCard), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
37
test/widgets/sensor_history_sheet_test.dart
Normal file
37
test/widgets/sensor_history_sheet_test.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/widgets/sensors/sensor_history_sheet.dart';
|
||||
|
||||
void main() {
|
||||
test('history sheet honors initial field key when available', () {
|
||||
final selectedFieldKey = resolveInitialSensorHistoryField(
|
||||
requestedFieldKey: 'extra:illuminance_2',
|
||||
availableFieldKeys: const <String>[
|
||||
'temperature',
|
||||
'extra:illuminance_2',
|
||||
],
|
||||
);
|
||||
|
||||
expect(selectedFieldKey, 'extra:illuminance_2');
|
||||
});
|
||||
|
||||
test('history sheet falls back to first available field', () {
|
||||
final selectedFieldKey = resolveInitialSensorHistoryField(
|
||||
requestedFieldKey: 'extra:missing',
|
||||
availableFieldKeys: const <String>[
|
||||
'temperature',
|
||||
'extra:illuminance_2',
|
||||
],
|
||||
);
|
||||
|
||||
expect(selectedFieldKey, 'temperature');
|
||||
});
|
||||
|
||||
test('history sheet returns null when no fields are available', () {
|
||||
final selectedFieldKey = resolveInitialSensorHistoryField(
|
||||
requestedFieldKey: 'temperature',
|
||||
availableFieldKeys: const <String>[],
|
||||
);
|
||||
|
||||
expect(selectedFieldKey, isNull);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.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:latlong2/latlong.dart';
|
||||
@@ -517,7 +518,69 @@ void main() {
|
||||
expect(moveDownCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('overflow menu copies raw response', (tester) async {
|
||||
testWidgets('tapping a measurement tile triggers metric callback', (
|
||||
tester,
|
||||
) async {
|
||||
final contact = buildContact();
|
||||
String? tappedFieldKey;
|
||||
|
||||
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'}),
|
||||
onMetricTap: (fieldKey) async {
|
||||
tappedFieldKey = fieldKey;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('sensor_metric_temperature')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tappedFieldKey, 'temperature');
|
||||
});
|
||||
|
||||
testWidgets('tapping the card body does not trigger metric callback', (
|
||||
tester,
|
||||
) async {
|
||||
final contact = buildContact();
|
||||
var tapCount = 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'}),
|
||||
onMetricTap: (_) async {
|
||||
tapCount += 1;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('WX Station'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tapCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('raw response metadata alone does not show an overflow menu', (
|
||||
tester,
|
||||
) async {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[0] = 0x48;
|
||||
final contact = Contact(
|
||||
@@ -538,11 +601,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
final scaffoldKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
scaffoldMessengerKey: scaffoldKey,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
@@ -556,44 +616,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.text('Copy raw response'), findsOneWidget);
|
||||
|
||||
// Capture clipboard writes via the test platform channel mock.
|
||||
String? clipboardText;
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(MethodCall call) async {
|
||||
if (call.method == 'Clipboard.setData') {
|
||||
final args = call.arguments as Map<dynamic, dynamic>;
|
||||
clipboardText = args['text'] as String?;
|
||||
}
|
||||
if (call.method == 'Clipboard.getData') {
|
||||
return <String, dynamic>{'text': clipboardText};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
addTearDown(() {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
await tester.tap(find.text('Copy raw response'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(clipboardText, '01 67 00 d7');
|
||||
expect(find.text('Raw response copied'), findsOneWidget);
|
||||
|
||||
// Clear the SnackBar to prevent its timer from blocking teardown.
|
||||
scaffoldKey.currentState?.clearSnackBars();
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
expect(find.byIcon(Icons.more_vert), findsNothing);
|
||||
expect(find.byIcon(Icons.chevron_right_rounded), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user