mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: MeshCore SAR - Flutter BLE mesh radio companion app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
328
test/utils/drawing_message_parser_test.dart
Normal file
328
test/utils/drawing_message_parser_test.dart
Normal file
@@ -0,0 +1,328 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_sar_app/models/map_drawing.dart';
|
||||
import 'package:meshcore_sar_app/utils/drawing_message_parser.dart';
|
||||
|
||||
void main() {
|
||||
group('DrawingMessageParser - Critical String Formatting Tests', () {
|
||||
test('createDrawingMessage returns raw string, not Object', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'test-123',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [
|
||||
LatLng(37.7749, -122.4194),
|
||||
LatLng(37.7750, -122.4195),
|
||||
],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
// CRITICAL: Must be a String type
|
||||
expect(message, isA<String>());
|
||||
|
||||
// CRITICAL: Must NOT contain "Instance of" or "Object"
|
||||
expect(message, isNot(contains('Instance of')));
|
||||
expect(message, isNot(contains('Object')));
|
||||
|
||||
// CRITICAL: Must start with D: prefix
|
||||
expect(message, startsWith('D:'));
|
||||
|
||||
// CRITICAL: Must be valid JSON after prefix
|
||||
final jsonPart = message.substring(2);
|
||||
expect(() => jsonPart, returnsNormally);
|
||||
});
|
||||
|
||||
test('createDrawingMessage produces parseable JSON string', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'test-456',
|
||||
color: DrawingColors.palette[2], // green
|
||||
createdAt: DateTime.now(),
|
||||
points: [
|
||||
LatLng(40.7128, -74.0060),
|
||||
LatLng(40.7129, -74.0061),
|
||||
LatLng(40.7130, -74.0062),
|
||||
],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
// Should be parseable back
|
||||
final parsed = DrawingMessageParser.parseDrawingMessage(
|
||||
message,
|
||||
senderName: 'Test Sender',
|
||||
messageId: 'msg-123',
|
||||
);
|
||||
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed, isA<LineDrawing>());
|
||||
expect((parsed as LineDrawing).points.length, equals(3));
|
||||
});
|
||||
|
||||
test('createDrawingMessage handles rectangle with proper string format', () {
|
||||
final drawing = RectangleDrawing(
|
||||
id: 'rect-789',
|
||||
color: DrawingColors.palette[4], // orange
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: LatLng(45.5231, -122.6765),
|
||||
bottomRight: LatLng(45.5100, -122.6600),
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
// CRITICAL: Must be pure string
|
||||
expect(message, isA<String>());
|
||||
expect(message, startsWith('D:'));
|
||||
|
||||
// Should contain compact JSON format
|
||||
expect(message, contains('"t":'));
|
||||
expect(message, contains('"c":'));
|
||||
expect(message, contains('"b":'));
|
||||
|
||||
// Must NOT contain any object representations
|
||||
expect(message, isNot(contains('RectangleDrawing')));
|
||||
expect(message, isNot(contains('Instance')));
|
||||
});
|
||||
|
||||
test('JSON encoding produces string with coordinates as numbers', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'coord-test',
|
||||
color: DrawingColors.palette[1], // blue
|
||||
createdAt: DateTime.now(),
|
||||
points: [
|
||||
LatLng(37.77490, -122.41940),
|
||||
],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
// Extract JSON part
|
||||
final jsonStr = message.substring(2);
|
||||
|
||||
// CRITICAL: Coordinates must be numbers, not strings
|
||||
// Format: {"t":0,"c":1,"p":[37.7749,-122.4194]}
|
||||
expect(jsonStr, contains('37.7749'));
|
||||
expect(jsonStr, contains('-122.4194'));
|
||||
|
||||
// Should NOT have coordinates as quoted strings
|
||||
expect(jsonStr, isNot(contains('"37.7749"')));
|
||||
expect(jsonStr, isNot(contains('"-122.4194"')));
|
||||
});
|
||||
|
||||
test('isDrawingMessage correctly identifies valid drawing messages', () {
|
||||
expect(DrawingMessageParser.isDrawingMessage('D:{"t":0,"c":1,"p":[1,2]}'), isTrue);
|
||||
expect(DrawingMessageParser.isDrawingMessage('S:🧑:37,-122'), isFalse);
|
||||
expect(DrawingMessageParser.isDrawingMessage('Plain text'), isFalse);
|
||||
expect(DrawingMessageParser.isDrawingMessage('D:'), isTrue);
|
||||
});
|
||||
|
||||
test('parseDrawingMessage returns null for malformed messages', () {
|
||||
expect(
|
||||
DrawingMessageParser.parseDrawingMessage('Not a drawing'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
DrawingMessageParser.parseDrawingMessage('D:invalid json'),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('round-trip: create -> parse -> create produces consistent output', () {
|
||||
final original = LineDrawing(
|
||||
id: 'roundtrip-1',
|
||||
color: DrawingColors.palette[3], // yellow
|
||||
createdAt: DateTime.now(),
|
||||
points: [
|
||||
LatLng(51.5074, -0.1278),
|
||||
LatLng(51.5075, -0.1279),
|
||||
],
|
||||
);
|
||||
|
||||
// Create message
|
||||
final message1 = DrawingMessageParser.createDrawingMessage(original);
|
||||
|
||||
// Parse it
|
||||
final parsed = DrawingMessageParser.parseDrawingMessage(
|
||||
message1,
|
||||
senderName: 'TestUser',
|
||||
messageId: 'msg-rt1',
|
||||
);
|
||||
|
||||
expect(parsed, isNotNull);
|
||||
|
||||
// Create message again from parsed
|
||||
final message2 = DrawingMessageParser.createDrawingMessage(parsed!);
|
||||
|
||||
// Both messages should have same structure (excluding metadata like IDs)
|
||||
expect(message1.substring(0, 10), equals(message2.substring(0, 10)));
|
||||
});
|
||||
|
||||
test('color indices are preserved as integers in JSON', () {
|
||||
for (int i = 0; i < DrawingColors.palette.length; i++) {
|
||||
final drawing = LineDrawing(
|
||||
id: 'color-$i',
|
||||
color: DrawingColors.palette[i],
|
||||
createdAt: DateTime.now(),
|
||||
points: [LatLng(0, 0)],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
final jsonStr = message.substring(2);
|
||||
|
||||
// Color index should be integer in JSON
|
||||
expect(jsonStr, contains('"c":$i'));
|
||||
}
|
||||
});
|
||||
|
||||
test('type indices are preserved correctly', () {
|
||||
// Line = type 0
|
||||
final line = LineDrawing(
|
||||
id: 'line-type',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [LatLng(1, 1), LatLng(2, 2)],
|
||||
);
|
||||
|
||||
final lineMsg = DrawingMessageParser.createDrawingMessage(line);
|
||||
expect(lineMsg, contains('"t":0'));
|
||||
|
||||
// Rectangle = type 1
|
||||
final rect = RectangleDrawing(
|
||||
id: 'rect-type',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: LatLng(1, 1),
|
||||
bottomRight: LatLng(2, 2),
|
||||
);
|
||||
|
||||
final rectMsg = DrawingMessageParser.createDrawingMessage(rect);
|
||||
expect(rectMsg, contains('"t":1'));
|
||||
});
|
||||
|
||||
test('coordinates are rounded to 5 decimal places', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'precision-test',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [
|
||||
LatLng(37.774901234567, -122.419401234567),
|
||||
],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
final jsonStr = message.substring(2);
|
||||
|
||||
// Should be rounded to 5 decimals
|
||||
expect(jsonStr, contains('37.7749'));
|
||||
expect(jsonStr, contains('-122.4194'));
|
||||
|
||||
// Should NOT contain extra precision
|
||||
expect(jsonStr, isNot(contains('37.774901234567')));
|
||||
});
|
||||
|
||||
test('empty points array produces valid message', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'empty-line',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
expect(message, isA<String>());
|
||||
expect(message, startsWith('D:'));
|
||||
expect(message, contains('"p":[]'));
|
||||
});
|
||||
|
||||
test('large coordinate values are handled correctly', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'large-coords',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [
|
||||
LatLng(89.99999, 179.99999), // Near max valid coords
|
||||
LatLng(-89.99999, -179.99999), // Near min valid coords
|
||||
],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
expect(message, isA<String>());
|
||||
expect(message, contains('89.99999'));
|
||||
expect(message, contains('179.99999'));
|
||||
expect(message, contains('-89.99999'));
|
||||
expect(message, contains('-179.99999'));
|
||||
});
|
||||
|
||||
test('getDrawingTypeDisplay returns correct type names', () {
|
||||
final lineMsg = 'D:{"t":0,"c":1,"p":[1,2,3,4]}';
|
||||
final rectMsg = 'D:{"t":1,"c":2,"b":[1,2,3,4]}';
|
||||
|
||||
expect(DrawingMessageParser.getDrawingTypeDisplay(lineMsg), equals('Line'));
|
||||
expect(DrawingMessageParser.getDrawingTypeDisplay(rectMsg), equals('Rectangle'));
|
||||
expect(DrawingMessageParser.getDrawingTypeDisplay('Invalid'), isNull);
|
||||
});
|
||||
|
||||
test('getColorName returns correct color names', () {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
final msg = 'D:{"t":0,"c":$i,"p":[0,0]}';
|
||||
final colorName = DrawingMessageParser.getColorName(msg);
|
||||
expect(colorName, isNotNull);
|
||||
expect(colorName, isA<String>());
|
||||
}
|
||||
|
||||
expect(DrawingMessageParser.getColorName('Invalid'), isNull);
|
||||
});
|
||||
|
||||
test('getDrawingMetadata extracts correct information', () {
|
||||
final lineMsg = 'D:{"t":0,"c":2,"p":[1,2,3,4,5,6]}';
|
||||
final metadata = DrawingMessageParser.getDrawingMetadata(lineMsg);
|
||||
|
||||
expect(metadata, isNotNull);
|
||||
expect(metadata!['type'], equals('Line'));
|
||||
expect(metadata['color'], equals('Green'));
|
||||
expect(metadata['pointCount'], equals(3)); // 6 values = 3 points
|
||||
});
|
||||
|
||||
test('message does not contain sender metadata in JSON', () {
|
||||
final drawing = LineDrawing(
|
||||
id: 'no-sender',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [LatLng(1, 2)],
|
||||
senderName: 'TestSender', // Should NOT be in network JSON
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
// CRITICAL: Sender name should NOT be in the message
|
||||
expect(message, isNot(contains('sender')));
|
||||
expect(message, isNot(contains('TestSender')));
|
||||
|
||||
// Should only contain compact fields: t, c, p (or b)
|
||||
final jsonStr = message.substring(2);
|
||||
expect(jsonStr, contains('"t":'));
|
||||
expect(jsonStr, contains('"c":'));
|
||||
expect(jsonStr, matches(RegExp(r'"p":|"b":')));
|
||||
});
|
||||
|
||||
test('special characters in metadata do not break format', () {
|
||||
// Even though sender name isn't included in network JSON,
|
||||
// test that it doesn't affect the message creation
|
||||
final drawing = LineDrawing(
|
||||
id: 'special-chars-"quotes"',
|
||||
color: DrawingColors.palette[0],
|
||||
createdAt: DateTime.now(),
|
||||
points: [LatLng(1, 2)],
|
||||
);
|
||||
|
||||
final message = DrawingMessageParser.createDrawingMessage(drawing);
|
||||
|
||||
expect(message, isA<String>());
|
||||
expect(message, startsWith('D:'));
|
||||
// Should still be parseable
|
||||
expect(() => DrawingMessageParser.parseDrawingMessage(message), returnsNormally);
|
||||
});
|
||||
});
|
||||
}
|
||||
426
test/utils/sar_message_parser_test.dart
Normal file
426
test/utils/sar_message_parser_test.dart
Normal file
@@ -0,0 +1,426 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_sar_app/models/sar_marker.dart';
|
||||
import 'package:meshcore_sar_app/utils/sar_message_parser.dart';
|
||||
|
||||
void main() {
|
||||
group('SarMessageParser - Critical String Formatting Tests', () {
|
||||
test('createSarMessage returns raw string, not Object', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(37.7749, -122.4194),
|
||||
colorIndex: 2,
|
||||
);
|
||||
|
||||
// CRITICAL: Must be a String type
|
||||
expect(message, isA<String>());
|
||||
|
||||
// CRITICAL: Must NOT contain "Instance of" or "Object"
|
||||
expect(message, isNot(contains('Instance of')));
|
||||
expect(message, isNot(contains('Object')));
|
||||
expect(message, isNot(contains('LatLng')));
|
||||
|
||||
// CRITICAL: Must start with S: prefix
|
||||
expect(message, startsWith('S:'));
|
||||
});
|
||||
|
||||
test('createSarMessage produces correct format with all components', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(37.7749, -122.4194),
|
||||
notes: 'Found alive',
|
||||
colorIndex: 2,
|
||||
);
|
||||
|
||||
// New format: S:<emoji>:<colorIndex>:<lat>,<lon>:<notes>
|
||||
expect(message, startsWith('S:'));
|
||||
expect(message, contains('🧑')); // or 👤
|
||||
expect(message, contains(':2:')); // color index
|
||||
expect(message, contains('37.7749'));
|
||||
expect(message, contains('-122.4194'));
|
||||
expect(message, contains('Found alive'));
|
||||
});
|
||||
|
||||
test('coordinates are converted to strings properly', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.fire,
|
||||
location: LatLng(40.7128, -74.0060),
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
// CRITICAL: Coordinates must be in string form, not Object
|
||||
expect(message, contains('40.7128'));
|
||||
expect(message, contains('-74.006')); // Trailing zero trimmed by toString()
|
||||
|
||||
// Must NOT contain object representation
|
||||
expect(message, isNot(contains('LatLng')));
|
||||
expect(message, isNot(contains('latitude:')));
|
||||
expect(message, isNot(contains('longitude:')));
|
||||
});
|
||||
|
||||
test('colorIndex is converted to string in format', () {
|
||||
for (int i = 0; i <= 7; i++) {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.stagingArea,
|
||||
location: LatLng(0, 0),
|
||||
colorIndex: i,
|
||||
);
|
||||
|
||||
// Color index should be in string
|
||||
expect(message, contains(':$i:'));
|
||||
expect(message, isA<String>());
|
||||
}
|
||||
});
|
||||
|
||||
test('null colorIndex defaults to 0', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(1, 1),
|
||||
colorIndex: null,
|
||||
);
|
||||
|
||||
// Should default to color index 0
|
||||
expect(message, contains(':0:'));
|
||||
});
|
||||
|
||||
test('isSarMessage correctly identifies SAR messages', () {
|
||||
expect(SarMessageParser.isSarMessage('S:🧑:2:37.7,-122.4'), isTrue);
|
||||
expect(SarMessageParser.isSarMessage('S:🔥:0:40.7,-74.0:Fire'), isTrue);
|
||||
expect(SarMessageParser.isSarMessage('D:{"t":0}'), isFalse);
|
||||
expect(SarMessageParser.isSarMessage('Plain text'), isFalse);
|
||||
});
|
||||
|
||||
test('parse correctly extracts all components', () {
|
||||
final message = 'S:🧑:2:37.7749,-122.4194:Found alive';
|
||||
final info = SarMessageParser.parse(message);
|
||||
|
||||
expect(info, isNotNull);
|
||||
expect(info!.type, equals(SarMarkerType.foundPerson));
|
||||
expect(info.location.latitude, equals(37.7749));
|
||||
expect(info.location.longitude, equals(-122.4194));
|
||||
expect(info.colorIndex, equals(2));
|
||||
expect(info.notes, contains('Found alive'));
|
||||
});
|
||||
|
||||
test('parse handles old format without color index', () {
|
||||
// Old format: S:<emoji>:<lat>,<lon>:<notes>
|
||||
final message = 'S:🔥:40.7128,-74.0060:Large fire';
|
||||
final info = SarMessageParser.parse(message);
|
||||
|
||||
expect(info, isNotNull);
|
||||
expect(info!.type, equals(SarMarkerType.fire));
|
||||
expect(info.location.latitude, equals(40.7128));
|
||||
expect(info.location.longitude, equals(-74.0060));
|
||||
expect(info.colorIndex, isNull); // Old format has no color index
|
||||
expect(info.notes, equals('Large fire'));
|
||||
});
|
||||
|
||||
test('round-trip: create -> parse -> create preserves format', () {
|
||||
final original = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.stagingArea,
|
||||
location: LatLng(51.5074, -0.1278),
|
||||
notes: 'Command center',
|
||||
colorIndex: 4,
|
||||
);
|
||||
|
||||
// Parse it
|
||||
final parsed = SarMessageParser.parse(original);
|
||||
expect(parsed, isNotNull);
|
||||
|
||||
// Create again
|
||||
final recreated = SarMessageParser.createSarMessage(
|
||||
type: parsed!.type,
|
||||
location: parsed.location,
|
||||
notes: parsed.notes,
|
||||
colorIndex: parsed.colorIndex,
|
||||
);
|
||||
|
||||
// Should be identical
|
||||
expect(recreated, equals(original));
|
||||
});
|
||||
|
||||
test('negative coordinates are handled correctly', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(-33.8688, -151.2093), // Sydney (negative coords)
|
||||
colorIndex: 1,
|
||||
);
|
||||
|
||||
expect(message, contains('-33.8688'));
|
||||
expect(message, contains('-151.2093'));
|
||||
|
||||
// Should be parseable
|
||||
final parsed = SarMessageParser.parse(message);
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.location.latitude, equals(-33.8688));
|
||||
expect(parsed.location.longitude, equals(-151.2093));
|
||||
});
|
||||
|
||||
test('extreme valid coordinates work correctly', () {
|
||||
// Test near max valid latitude/longitude
|
||||
final message1 = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(89.99999, 179.99999),
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
expect(message1, contains('89.99999'));
|
||||
expect(message1, contains('179.99999'));
|
||||
|
||||
// Test near min valid latitude/longitude
|
||||
final message2 = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(-89.99999, -179.99999),
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
expect(message2, contains('-89.99999'));
|
||||
expect(message2, contains('-179.99999'));
|
||||
});
|
||||
|
||||
test('parse rejects invalid coordinates', () {
|
||||
expect(SarMessageParser.parse('S:🧑:0:91.0,0'), isNull); // lat > 90
|
||||
expect(SarMessageParser.parse('S:🧑:0:-91.0,0'), isNull); // lat < -90
|
||||
expect(SarMessageParser.parse('S:🧑:0:0,181.0'), isNull); // lon > 180
|
||||
expect(SarMessageParser.parse('S:🧑:0:0,-181.0'), isNull); // lon < -180
|
||||
});
|
||||
|
||||
test('parse rejects invalid color indices', () {
|
||||
// Color index should be 0-7
|
||||
final message = 'S:🧑:9:37.7,-122.4'; // Invalid index 9
|
||||
final info = SarMessageParser.parse(message);
|
||||
|
||||
expect(info, isNotNull);
|
||||
expect(info!.colorIndex, isNull); // Should be ignored if invalid
|
||||
});
|
||||
|
||||
test('notes with special characters are preserved', () {
|
||||
final notes = 'Multi-line\nwith: colons, commas';
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(1, 2),
|
||||
notes: notes,
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
final parsed = SarMessageParser.parse(message);
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.notes, contains('Multi-line'));
|
||||
expect(parsed.notes, contains('colons, commas'));
|
||||
});
|
||||
|
||||
test('empty notes produce valid message', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.fire,
|
||||
location: LatLng(1, 2),
|
||||
notes: '',
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
// Should not have trailing colon
|
||||
expect(message, isNot(endsWith(':')));
|
||||
|
||||
// Should be parseable
|
||||
final parsed = SarMessageParser.parse(message);
|
||||
expect(parsed, isNotNull);
|
||||
});
|
||||
|
||||
test('null notes produce valid message', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.stagingArea,
|
||||
location: LatLng(1, 2),
|
||||
notes: null,
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
// Should not have notes section
|
||||
expect(message, isNot(endsWith(':')));
|
||||
|
||||
// Should be parseable
|
||||
final parsed = SarMessageParser.parse(message);
|
||||
expect(parsed, isNotNull);
|
||||
});
|
||||
|
||||
test('all emoji types produce valid strings', () {
|
||||
final types = [
|
||||
SarMarkerType.foundPerson,
|
||||
SarMarkerType.fire,
|
||||
SarMarkerType.stagingArea,
|
||||
];
|
||||
|
||||
for (final type in types) {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: type,
|
||||
location: LatLng(1, 2),
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
expect(message, isA<String>());
|
||||
expect(message, startsWith('S:'));
|
||||
expect(message, contains(type.emoji));
|
||||
|
||||
// Must not contain type name or object representation
|
||||
expect(message, isNot(contains('SarMarkerType')));
|
||||
expect(message, isNot(contains('Instance')));
|
||||
}
|
||||
});
|
||||
|
||||
test('isValidFormat correctly validates messages', () {
|
||||
expect(
|
||||
SarMessageParser.isValidFormat('S:🧑:2:37.7749,-122.4194'),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
SarMessageParser.isValidFormat('S:invalid:format'),
|
||||
isFalse,
|
||||
);
|
||||
|
||||
expect(
|
||||
SarMessageParser.isValidFormat('Not SAR message'),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('getFormatError provides helpful error messages', () {
|
||||
expect(
|
||||
SarMessageParser.getFormatError('Not SAR'),
|
||||
contains('must start with "S:"'),
|
||||
);
|
||||
|
||||
expect(
|
||||
SarMessageParser.getFormatError('S:'),
|
||||
contains('Invalid format'),
|
||||
);
|
||||
|
||||
expect(
|
||||
SarMessageParser.getFormatError('S::37.7,-122.4'),
|
||||
contains('Missing emoji'),
|
||||
);
|
||||
});
|
||||
|
||||
test('extractNotes correctly handles multi-line messages', () {
|
||||
final message = 'S:🧑:0:37.7,-122.4:First line\nSecond line\nThird line';
|
||||
final notes = SarMessageParser.extractNotes(message);
|
||||
|
||||
expect(notes, isNotNull);
|
||||
expect(notes, contains('Second line'));
|
||||
expect(notes, contains('Third line'));
|
||||
});
|
||||
|
||||
test('message format is compact and efficient', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(37.7749, -122.4194),
|
||||
colorIndex: 2,
|
||||
);
|
||||
|
||||
// Should be very compact: S:🧑:2:37.7749,-122.4194
|
||||
expect(message.length, lessThan(50));
|
||||
|
||||
// Should not have extra whitespace
|
||||
expect(message, isNot(contains(' ')));
|
||||
expect(message, isNot(contains('\n')));
|
||||
});
|
||||
|
||||
test('coordinates maintain precision', () {
|
||||
final precise = LatLng(37.774901234, -122.419401234);
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: precise,
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
final parsed = SarMessageParser.parse(message);
|
||||
expect(parsed, isNotNull);
|
||||
|
||||
// Should maintain reasonable precision (Dart's toString default)
|
||||
expect(parsed!.location.latitude, closeTo(37.774901234, 0.000001));
|
||||
expect(parsed.location.longitude, closeTo(-122.419401234, 0.000001));
|
||||
});
|
||||
|
||||
test('zero coordinates work correctly', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.stagingArea,
|
||||
location: LatLng(0.0, 0.0),
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
expect(message, contains('0.0,0.0'));
|
||||
|
||||
final parsed = SarMessageParser.parse(message);
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.location.latitude, equals(0.0));
|
||||
expect(parsed.location.longitude, equals(0.0));
|
||||
});
|
||||
|
||||
test('emoji is preserved as string, not code points', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(1, 2),
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
// Should contain actual emoji character
|
||||
expect(message, contains('🧑'));
|
||||
|
||||
// Should NOT contain unicode code points or escape sequences
|
||||
expect(message, isNot(contains(r'\u')));
|
||||
expect(message, isNot(contains('U+')));
|
||||
});
|
||||
|
||||
test('format is compatible with CLAUDE.md specification', () {
|
||||
// According to CLAUDE.md:
|
||||
// New format: S:<emoji>:<colorIndex>:<latitude>,<longitude>:<optional_message>
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.fire,
|
||||
location: LatLng(40.7128, -74.0060),
|
||||
notes: 'Large wildfire spreading rapidly',
|
||||
colorIndex: 0,
|
||||
);
|
||||
|
||||
// Should match: S:🔥:0:40.7128,-74.006:Large wildfire spreading rapidly
|
||||
expect(message, startsWith('S:🔥:0:'));
|
||||
expect(message, contains('40.7128,-74.006')); // Trailing zero trimmed
|
||||
expect(message, endsWith('Large wildfire spreading rapidly'));
|
||||
});
|
||||
|
||||
test('backward compatibility with old format', () {
|
||||
// Old format without color index: S:<emoji>:<lat>,<lon>:<notes>
|
||||
final oldMessage = 'S:🧑:37.7749,-122.4194:Found person';
|
||||
final parsed = SarMessageParser.parse(oldMessage);
|
||||
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.type, equals(SarMarkerType.foundPerson));
|
||||
expect(parsed.colorIndex, isNull); // Old format has no color index
|
||||
expect(parsed.notes, equals('Found person'));
|
||||
});
|
||||
|
||||
test('new format with color index is preferred', () {
|
||||
final message = SarMessageParser.createSarMessage(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(1, 2),
|
||||
colorIndex: 3,
|
||||
);
|
||||
|
||||
// Should use new format with color index
|
||||
expect(message, contains(':3:'));
|
||||
expect(message, contains('🧑'));
|
||||
});
|
||||
|
||||
test('toString produces human-readable output for SarMarkerInfo', () {
|
||||
final info = SarMarkerInfo(
|
||||
type: SarMarkerType.foundPerson,
|
||||
location: LatLng(37.7749, -122.4194),
|
||||
emoji: '🧑',
|
||||
notes: 'Test notes',
|
||||
colorIndex: 2,
|
||||
);
|
||||
|
||||
final str = info.toString();
|
||||
expect(str, contains('SarMarkerInfo'));
|
||||
expect(str, contains('Found Person')); // Display name, not enum name
|
||||
expect(str, contains('colorIndex: 2'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user