import 'package:flutter_test/flutter_test.dart'; import 'package:votianlt_app/models/job.dart'; import 'package:votianlt_app/models/cargo_item.dart'; import 'package:votianlt_app/models/task.dart'; import 'package:votianlt_app/models/tasks/confirmation_task.dart'; import 'package:votianlt_app/models/tasks/photo_task.dart'; import 'package:votianlt_app/models/tasks/signature_task.dart'; import 'package:votianlt_app/models/tasks/barcode_task.dart'; import 'package:votianlt_app/models/tasks/todolist_task.dart'; import 'package:votianlt_app/models/tasks/comment_task.dart'; import 'package:votianlt_app/models/tasks/generic_task.dart'; /// Test data based on job_json.md documentation from /// https://www.appcreation.de/download/job_json.md void main() { // Complete job JSON according to documentation final Map completeJobJson = { 'job': { 'id': {'timestamp': 1705312200, '\$oid': '65a4b5c8d4e5f6a7b8c9d0e1'}, 'jobNumber': 'JOB-2024-001', 'status': 'ASSIGNED', 'createdAt': '2024-01-15T10:30:00.000Z', 'updatedAt': '2024-01-15T14:45:00.000Z', 'createdBy': 'admin@example.com', 'customerSelection': 'Kunde ABC GmbH', 'pickupCompany': 'Absender GmbH', 'pickupSalutation': 'Herr', 'pickupFirstName': 'Max', 'pickupLastName': 'Mustermann', 'pickupPhone': '+49 123 456789', 'pickupStreet': 'Hauptstraße', 'pickupHouseNumber': '42', 'pickupAddressAddition': 'Hinterhaus', 'pickupZip': '10115', 'pickupCity': 'Berlin', 'deliveryCompany': 'Empfänger AG', 'deliverySalutation': 'Frau', 'deliveryFirstName': 'Erika', 'deliveryLastName': 'Musterfrau', 'deliveryPhone': '+49 987 654321', 'deliveryStreet': 'Nebenstraße', 'deliveryHouseNumber': '7a', 'deliveryAddressAddition': null, 'deliveryZip': '80331', 'deliveryCity': 'München', 'digitalProcessing': true, 'appUser': 'driver@example.com', 'pickupDate': '2024-01-16', 'deliveryDate': '2024-01-17', 'remark': 'Bitte vorsichtig behandeln', 'price': 149.99, 'draft': false, }, 'cargoItems': [ { 'id': {'timestamp': 1705312201}, 'jobId': {'timestamp': 1705312200}, 'description': 'Palette mit Elektronik', 'quantity': 2, 'weightKg': 150.5, 'lengthMm': 1200.0, 'widthMm': 800.0, 'heightMm': 1000.0, }, { 'id': {'timestamp': 1705312202}, 'jobId': {'timestamp': 1705312200}, 'description': 'Karton mit Dokumenten', 'quantity': 5, 'weightKg': 12.0, 'lengthMm': 400.0, 'widthMm': 300.0, 'heightMm': 200.0, }, ], 'tasks': [ { 'id': {'timestamp': 1705312210}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'completedAt': null, 'completedBy': null, 'taskOrder': 1, 'taskSpecificData': { 'taskType': 'CONFIRMATION', 'buttonText': 'Abholung bestätigen', }, }, { 'id': {'timestamp': 1705312211}, 'jobId': {'timestamp': 1705312200}, 'completed': true, 'completedAt': '2024-01-16T09:15:00.000Z', 'completedBy': 'driver@example.com', 'taskOrder': 2, 'taskSpecificData': {'taskType': 'SIGNATURE'}, }, { 'id': {'timestamp': 1705312212}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'completedAt': null, 'completedBy': null, 'taskOrder': 3, 'taskSpecificData': { 'taskType': 'PHOTO', 'minPhotoCount': 2, 'maxPhotoCount': 10, }, }, { 'id': {'timestamp': 1705312213}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'completedAt': null, 'completedBy': null, 'taskOrder': 4, 'taskSpecificData': { 'taskType': 'BARCODE', 'minBarcodeCount': 1, 'maxBarcodeCount': 5, }, }, { 'id': {'timestamp': 1705312214}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'completedAt': null, 'completedBy': null, 'taskOrder': 5, 'taskSpecificData': { 'taskType': 'TODOLIST', 'todoItems': [ 'Ladung sichern', 'Dokumente prüfen', 'Unterschrift einholen', ], }, }, { 'id': {'timestamp': 1705312215}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'completedAt': null, 'completedBy': null, 'taskOrder': 6, 'taskSpecificData': { 'taskType': 'COMMENT', 'commentText': '', 'required': true, }, }, ], }; group('Job Parsing', () { late Job job; setUp(() { job = Job.fromJson(completeJobJson); }); test('parses job basic fields correctly', () { expect(job.id, '1705312200'); expect(job.jobNumber, 'JOB-2024-001'); expect(job.status, 'ASSIGNED'); expect(job.createdBy, 'admin@example.com'); expect(job.customerSelection, 'Kunde ABC GmbH'); expect(job.appUser, 'driver@example.com'); expect(job.remark, 'Bitte vorsichtig behandeln'); }); test('parses pickup address correctly', () { expect(job.pickupCompany, 'Absender GmbH'); expect(job.pickupSalutation, 'Herr'); expect(job.pickupFirstName, 'Max'); expect(job.pickupLastName, 'Mustermann'); expect(job.pickupPhone, '+49 123 456789'); expect(job.pickupStreet, 'Hauptstraße'); expect(job.pickupHouseNumber, '42'); expect(job.pickupAddressAddition, 'Hinterhaus'); expect(job.pickupZip, '10115'); expect(job.pickupCity, 'Berlin'); }); test('parses delivery address correctly', () { expect(job.deliveryCompany, 'Empfänger AG'); expect(job.deliverySalutation, 'Frau'); expect(job.deliveryFirstName, 'Erika'); expect(job.deliveryLastName, 'Musterfrau'); expect(job.deliveryPhone, '+49 987 654321'); expect(job.deliveryStreet, 'Nebenstraße'); expect(job.deliveryHouseNumber, '7a'); expect(job.deliveryAddressAddition, ''); expect(job.deliveryZip, '80331'); expect(job.deliveryCity, 'München'); }); test('parses date strings correctly', () { expect(job.pickupDate, '2024-01-16'); expect(job.deliveryDate, '2024-01-17'); }); test('parses DateTime from ISO string', () { expect(job.createdAt, DateTime.utc(2024, 1, 15, 10, 30, 0)); expect(job.updatedAt, DateTime.utc(2024, 1, 15, 14, 45, 0)); }); test('parses DateTime from array format', () { final jsonWithArrayDate = Map.from(completeJobJson); final jobData = Map.from(jsonWithArrayDate['job']); jobData['createdAt'] = [2024, 1, 15, 10, 30, 0, 0]; jobData['updatedAt'] = [2024, 1, 15, 14, 45, 0, 500000000]; jsonWithArrayDate['job'] = jobData; final jobWithArrayDate = Job.fromJson(jsonWithArrayDate); expect(jobWithArrayDate.createdAt, DateTime(2024, 1, 15, 10, 30, 0, 0)); expect(jobWithArrayDate.updatedAt.year, 2024); expect(jobWithArrayDate.updatedAt.month, 1); expect(jobWithArrayDate.updatedAt.day, 15); }); test('parses price as double', () { expect(job.price, 149.99); }); test('parses boolean fields correctly', () { expect(job.digitalProcessing, true); expect(job.draft, false); }); test('parses cargoItems array', () { expect(job.cargoItems.length, 2); }); test('parses tasks array', () { expect(job.tasks.length, 6); }); test('parses delivery stations and flattens station tasks', () { final jsonWithStations = { 'job': { 'id': 'station-job-1', 'jobNumber': 'JOB-STATION-001', 'status': 'CREATED', 'deliveryCompany': 'Legacy Delivery', 'deliveryStreet': 'Legacy Street', 'deliveryHouseNumber': '1', 'deliveryZip': '12345', 'deliveryCity': 'Legacy City', 'deliveryCitiesDisplay': 'Boostedt -> Geesthacht', 'firstDeliveryCity': 'Boostedt', 'lastDeliveryCity': 'Geesthacht', 'deliveryStations': [ { 'stationOrder': 0, 'company': 'Volker Hinst', 'street': 'Vossbarg', 'houseNumber': '24', 'zip': '24893', 'city': 'Boostedt', 'tasks': [ { 'id': 'station-task-1', 'jobId': 'station-job-1', 'taskOrder': 0, 'description': 'Erste Station', 'displayName': 'Bestätigung', 'taskSpecificData': { 'taskType': 'CONFIRMATION', 'buttonText': 'Blubb', }, }, ], }, { 'stationOrder': 1, 'company': 'Timm GmbH', 'street': 'Gerhart-Hauptmann-Weg', 'houseNumber': '14', 'zip': '21502', 'city': 'Geesthacht', 'tasks': [ { 'id': 'station-task-2', 'jobId': 'station-job-1', 'taskOrder': 0, 'description': 'Zweite Station', 'displayName': 'Bestätigung', 'taskSpecificData': { 'taskType': 'CONFIRMATION', 'buttonText': 'Blubb', }, }, ], }, ], }, 'cargoItems': [], 'tasks': [ { 'id': 'legacy-task', 'jobId': 'station-job-1', 'taskOrder': 0, 'description': 'Legacy', 'taskSpecificData': { 'taskType': 'CONFIRMATION', 'buttonText': 'Legacy', }, }, ], }; final jobWithStations = Job.fromJson(jsonWithStations); expect(jobWithStations.deliveryStations.length, 2); expect(jobWithStations.tasks.length, 2); expect(jobWithStations.tasks[0].stationOrder, 0); expect(jobWithStations.tasks[1].stationOrder, 1); expect( jobWithStations.deliveryStations[0].tasks.first.id, 'station-task-1', ); expect(jobWithStations.deliveryCitiesDisplay, 'Boostedt -> Geesthacht'); }); test('extracts ID from Map object with timestamp', () { expect(job.id, '1705312200'); }); test('extracts ID from Map object with \$oid fallback', () { final jsonWithOidOnly = { 'job': { 'id': {'\$oid': '65a4b5c8d4e5f6a7b8c9d0e1'}, 'jobNumber': 'JOB-TEST', 'status': 'CREATED', }, 'cargoItems': [], 'tasks': [], }; final jobWithOid = Job.fromJson(jsonWithOidOnly); expect(jobWithOid.id, '65a4b5c8d4e5f6a7b8c9d0e1'); }); test('handles flat JSON structure (without nested job object)', () { final flatJson = { 'id': 'flat-job-id-123', 'jobNumber': 'JOB-FLAT-001', 'status': 'CREATED', 'createdAt': '2024-01-15T10:30:00.000Z', 'updatedAt': '2024-01-15T10:30:00.000Z', 'createdBy': 'test@test.de', 'customerSelection': 'Test Kunde', 'pickupCompany': 'Test Firma', 'pickupFirstName': 'Test', 'pickupLastName': 'User', 'pickupPhone': '12345', 'pickupStreet': 'Teststr', 'pickupHouseNumber': '1', 'pickupAddressAddition': '', 'pickupZip': '12345', 'pickupCity': 'Teststadt', 'deliveryCompany': 'Ziel Firma', 'deliveryFirstName': 'Ziel', 'deliveryLastName': 'Person', 'deliveryPhone': '54321', 'deliveryStreet': 'Zielstr', 'deliveryHouseNumber': '2', 'deliveryAddressAddition': '', 'deliveryZip': '54321', 'deliveryCity': 'Zielstadt', 'digitalProcessing': false, 'appUser': 'user@test.de', 'pickupDate': '2024-01-20', 'deliveryDate': '2024-01-21', 'remark': '', 'price': 0.0, 'draft': true, }; final flatJob = Job.fromJson(flatJson); expect(flatJob.id, 'flat-job-id-123'); expect(flatJob.jobNumber, 'JOB-FLAT-001'); expect(flatJob.draft, true); }); }); group('CargoItem Parsing', () { late Job job; setUp(() { job = Job.fromJson(completeJobJson); }); test('parses CargoItem fields correctly', () { final cargoItem = job.cargoItems[0]; expect(cargoItem.id, '1705312201'); expect(cargoItem.jobId, '1705312200'); expect(cargoItem.description, 'Palette mit Elektronik'); expect(cargoItem.quantity, 2); expect(cargoItem.weightKg, 150.5); expect(cargoItem.lengthCm, 1200.0); expect(cargoItem.widthCm, 800.0); expect(cargoItem.heightCm, 1000.0); }); test('parses multiple CargoItems', () { expect(job.cargoItems.length, 2); expect(job.cargoItems[0].description, 'Palette mit Elektronik'); expect(job.cargoItems[1].description, 'Karton mit Dokumenten'); }); test('extracts CargoItem ID from Map object', () { expect(job.cargoItems[0].id, '1705312201'); expect(job.cargoItems[1].id, '1705312202'); }); test('handles CargoItem with simple string ID', () { final cargoJson = { 'id': 'simple-string-id', 'jobId': 'simple-job-id', 'description': 'Test Item', 'quantity': 1, 'weightKg': 10.0, 'lengthMm': 100.0, 'widthMm': 100.0, 'heightMm': 100.0, }; final cargoItem = CargoItem.fromJson(cargoJson); expect(cargoItem.id, 'simple-string-id'); expect(cargoItem.jobId, 'simple-job-id'); }); }); group('Task Parsing', () { late Job job; setUp(() { job = Job.fromJson(completeJobJson); }); test('creates ConfirmationTask with buttonText', () { final task = job.tasks[0]; expect(task, isA()); final confirmationTask = task as ConfirmationTask; expect(confirmationTask.taskOrder, 1); expect(confirmationTask.buttonText, 'Abholung bestätigen'); expect(confirmationTask.completed, false); }); test('creates SignatureTask', () { final task = job.tasks[1]; expect(task, isA()); expect(task.taskOrder, 2); expect(task.completed, true); expect(task.completedBy, 'driver@example.com'); }); test('creates PhotoTask with min/maxPhotoCount', () { final task = job.tasks[2]; expect(task, isA()); final photoTask = task as PhotoTask; expect(photoTask.taskOrder, 3); expect(photoTask.minPhotoCount, 2); expect(photoTask.maxPhotoCount, 10); }); test('creates BarcodeTask with min/maxBarcodeCount', () { final task = job.tasks[3]; expect(task, isA()); final barcodeTask = task as BarcodeTask; expect(barcodeTask.taskOrder, 4); expect(barcodeTask.minBarcodeCount, 1); expect(barcodeTask.maxBarcodeCount, 5); }); test('creates TodoListTask with todoItems', () { final task = job.tasks[4]; expect(task, isA()); final todoListTask = task as TodoListTask; expect(todoListTask.taskOrder, 5); expect(todoListTask.todoItems.length, 3); expect(todoListTask.todoItems[0], 'Ladung sichern'); expect(todoListTask.todoItems[1], 'Dokumente prüfen'); expect(todoListTask.todoItems[2], 'Unterschrift einholen'); }); test('creates CommentTask with commentText and required', () { final task = job.tasks[5]; expect(task, isA()); final commentTask = task as CommentTask; expect(commentTask.taskOrder, 6); expect(commentTask.commentText, ''); expect(commentTask.required, true); }); test('falls back to GenericTask for unknown task type', () { final unknownTaskJson = { 'id': {'timestamp': 1705312299}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'taskOrder': 99, 'taskSpecificData': {'taskType': 'UNKNOWN_TYPE'}, }; final task = Task.fromJson(unknownTaskJson); expect(task, isA()); }); test('falls back to GenericTask when taskType is missing', () { final noTypeTaskJson = { 'id': {'timestamp': 1705312298}, 'jobId': {'timestamp': 1705312200}, 'completed': false, 'taskOrder': 98, 'taskSpecificData': {}, }; final task = Task.fromJson(noTypeTaskJson); expect(task, isA()); }); test('parses completedAt from ISO string', () { final task = job.tasks[1]; expect(task.completedAt, DateTime.utc(2024, 1, 16, 9, 15, 0)); }); test('parses completedAt from array format', () { final taskJsonWithArrayDate = { 'id': {'timestamp': 1705312220}, 'jobId': {'timestamp': 1705312200}, 'completed': true, 'completedAt': [2024, 1, 16, 9, 15, 0, 0], 'completedBy': 'driver@example.com', 'taskOrder': 10, 'taskSpecificData': {'taskType': 'SIGNATURE'}, }; final task = Task.fromJson(taskJsonWithArrayDate); expect(task.completedAt, DateTime(2024, 1, 16, 9, 15, 0, 0)); }); test('extracts task ID from Map object', () { final task = job.tasks[0]; expect(task.id, '1705312210'); }); test('extracts task jobId from Map object', () { final task = job.tasks[0]; expect(task.jobId, '1705312200'); }); }); group('Task Defaults', () { test('ConfirmationTask uses default buttonText', () { final taskJson = { 'id': 'task-1', 'jobId': 'job-1', 'taskOrder': 1, 'taskSpecificData': {'taskType': 'CONFIRMATION'}, }; final task = Task.fromJson(taskJson) as ConfirmationTask; expect(task.buttonText, 'Bestätigen'); }); test('PhotoTask uses default min/max counts', () { final taskJson = { 'id': 'task-2', 'jobId': 'job-1', 'taskOrder': 2, 'taskSpecificData': {'taskType': 'PHOTO'}, }; final task = Task.fromJson(taskJson) as PhotoTask; expect(task.minPhotoCount, 1); expect(task.maxPhotoCount, 5); }); test('BarcodeTask uses default min/max counts', () { final taskJson = { 'id': 'task-3', 'jobId': 'job-1', 'taskOrder': 3, 'taskSpecificData': {'taskType': 'BARCODE'}, }; final task = Task.fromJson(taskJson) as BarcodeTask; expect(task.minBarcodeCount, 1); expect(task.maxBarcodeCount, 10); }); test('CommentTask uses default values', () { final taskJson = { 'id': 'task-4', 'jobId': 'job-1', 'taskOrder': 4, 'taskSpecificData': {'taskType': 'COMMENT'}, }; final task = Task.fromJson(taskJson) as CommentTask; expect(task.commentText, ''); expect(task.required, false); }); test('TodoListTask handles empty todoItems', () { final taskJson = { 'id': 'task-5', 'jobId': 'job-1', 'taskOrder': 5, 'taskSpecificData': {'taskType': 'TODOLIST'}, }; final task = Task.fromJson(taskJson) as TodoListTask; expect(task.todoItems, isEmpty); }); }); group('Edge Cases', () { test('handles empty cargoItems array', () { final jsonWithEmptyCargoItems = Map.from( completeJobJson, ); jsonWithEmptyCargoItems['cargoItems'] = []; final job = Job.fromJson(jsonWithEmptyCargoItems); expect(job.cargoItems, isEmpty); }); test('handles empty tasks array', () { final jsonWithEmptyTasks = Map.from(completeJobJson); jsonWithEmptyTasks['tasks'] = []; final job = Job.fromJson(jsonWithEmptyTasks); expect(job.tasks, isEmpty); }); test('handles missing optional fields with defaults', () { final minimalJson = { 'job': {'jobNumber': 'JOB-MIN-001'}, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(minimalJson); expect(job.jobNumber, 'JOB-MIN-001'); expect(job.status, 'UNKNOWN'); expect(job.pickupCompany, ''); expect(job.deliveryCompany, ''); expect(job.digitalProcessing, false); expect(job.price, 0.0); expect(job.draft, false); }); test('handles null values in optional fields', () { final jsonWithNulls = { 'job': { 'id': 'null-test-id', 'jobNumber': 'JOB-NULL-001', 'status': 'CREATED', 'pickupSalutation': null, 'deliverySalutation': null, 'pickupAddressAddition': null, 'deliveryAddressAddition': null, 'remark': null, }, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(jsonWithNulls); expect(job.pickupSalutation, isNull); expect(job.deliverySalutation, isNull); expect(job.pickupAddressAddition, ''); expect(job.deliveryAddressAddition, ''); expect(job.remark, ''); }); test('generates ID from jobNumber when ID is missing', () { final jsonWithoutId = { 'job': {'jobNumber': 'JOB-NOID-001'}, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(jsonWithoutId); expect(job.id, 'jobnum:JOB-NOID-001'); }); }); group('Roundtrip (fromJson -> toJson -> fromJson)', () { test('Job preserves data through serialization', () { final original = Job.fromJson(completeJobJson); final json = original.toJson(); final restored = Job.fromJson(json); expect(restored.id, original.id); expect(restored.jobNumber, original.jobNumber); expect(restored.status, original.status); expect(restored.pickupCompany, original.pickupCompany); expect(restored.pickupCity, original.pickupCity); expect(restored.deliveryCompany, original.deliveryCompany); expect(restored.deliveryCity, original.deliveryCity); expect(restored.price, original.price); expect(restored.digitalProcessing, original.digitalProcessing); }); test('CargoItem preserves data through serialization', () { final original = Job.fromJson(completeJobJson).cargoItems[0]; final json = original.toJson(); final restored = CargoItem.fromJson(json); expect(restored.id, original.id); expect(restored.jobId, original.jobId); expect(restored.description, original.description); expect(restored.quantity, original.quantity); expect(restored.weightKg, original.weightKg); expect(restored.lengthCm, original.lengthCm); }); test('ConfirmationTask preserves data through serialization', () { final original = Job.fromJson(completeJobJson).tasks[0] as ConfirmationTask; final json = original.toJson(); final restored = Task.fromJson(json) as ConfirmationTask; expect(restored.id, original.id); expect(restored.jobId, original.jobId); expect(restored.buttonText, original.buttonText); expect(restored.taskOrder, original.taskOrder); }); test('PhotoTask preserves data through serialization', () { final original = Job.fromJson(completeJobJson).tasks[2] as PhotoTask; final json = original.toJson(); final restored = Task.fromJson(json) as PhotoTask; expect(restored.minPhotoCount, original.minPhotoCount); expect(restored.maxPhotoCount, original.maxPhotoCount); }); test('TodoListTask preserves data through serialization', () { final original = Job.fromJson(completeJobJson).tasks[4] as TodoListTask; final json = original.toJson(); final restored = Task.fromJson(json) as TodoListTask; expect(restored.todoItems, original.todoItems); }); test('CommentTask preserves data through serialization', () { final original = Job.fromJson(completeJobJson).tasks[5] as CommentTask; final json = original.toJson(); final restored = Task.fromJson(json) as CommentTask; expect(restored.commentText, original.commentText); expect(restored.required, original.required); }); }); group('Job Status', () { test('statusDisplayText returns German text for known statuses', () { final statuses = { 'CREATED': 'Erstellt', 'PENDING': 'Wartend', 'ASSIGNED': 'Zugewiesen', 'IN_PROGRESS': 'In Bearbeitung', 'STARTED': 'In Bearbeitung', 'COMPLETED': 'Abgeschlossen', 'DONE': 'Abgeschlossen', 'CANCELLED': 'Abgebrochen', 'FAILED': 'Fehlgeschlagen', }; for (final entry in statuses.entries) { final json = { 'job': { 'id': 'status-test', 'jobNumber': 'JOB-STATUS', 'status': entry.key, }, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(json); expect( job.statusDisplayText, entry.value, reason: 'Status ${entry.key} should display as ${entry.value}', ); } }); test('statusColor returns correct color for statuses', () { final statusColors = { 'CREATED': 'orange', 'ASSIGNED': 'orange', 'IN_PROGRESS': 'blue', 'COMPLETED': 'green', 'CANCELLED': 'red', }; for (final entry in statusColors.entries) { final json = { 'job': { 'id': 'color-test', 'jobNumber': 'JOB-COLOR', 'status': entry.key, }, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(json); expect( job.statusColor, entry.value, reason: 'Status ${entry.key} should have color ${entry.value}', ); } }); }); group('Job normalized()', () { test('trims string fields', () { final json = { 'job': { 'id': 'normalize-test', 'jobNumber': ' JOB-TRIM ', 'status': ' ASSIGNED ', 'pickupCompany': ' Test Company ', 'pickupCity': ' Berlin ', }, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(json).normalized(); expect(job.jobNumber, 'JOB-TRIM'); expect(job.status, 'ASSIGNED'); expect(job.pickupCompany, 'Test Company'); expect(job.pickupCity, 'Berlin'); }); test('converts null strings to empty strings', () { final json = { 'job': { 'id': 'null-normalize-test', 'jobNumber': 'JOB-NULL', 'pickupSalutation': null, }, 'cargoItems': [], 'tasks': [], }; final job = Job.fromJson(json).normalized(); expect(job.pickupSalutation, ''); }); }); }