From 8cc58d6a7614d9d6e22212a91803354de4aaec1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCllner?= Date: Wed, 4 Mar 2026 12:25:21 +0100 Subject: [PATCH] test: add unit coverage for scanner parsing and reporting utils --- lib/scanner_screen.dart | 22 ++++----------- lib/screens/my_reports_screen.dart | 24 ++++++---------- lib/utils/reporting_utils.dart | 41 ++++++++++++++++++++++++++++ lib/utils/scanner_utils.dart | 13 +++++++++ test/utils/reporting_utils_test.dart | 40 +++++++++++++++++++++++++++ test/utils/scanner_utils_test.dart | 39 ++++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 lib/utils/reporting_utils.dart create mode 100644 lib/utils/scanner_utils.dart create mode 100644 test/utils/reporting_utils_test.dart create mode 100644 test/utils/scanner_utils_test.dart diff --git a/lib/scanner_screen.dart b/lib/scanner_screen.dart index 0774cf9..6e30a72 100644 --- a/lib/scanner_screen.dart +++ b/lib/scanner_screen.dart @@ -6,6 +6,7 @@ import 'package:camera/camera.dart'; import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'services/collection_service.dart'; import 'theme/app_colors.dart'; +import 'utils/scanner_utils.dart'; /// Screen that uses the camera to scan text (OCR) from a Hot Wheels package /// and extract the hw_id (e.g. "JKF21"). @@ -41,10 +42,6 @@ class _ScannerScreenState extends State { bool _autoScanEnabled = false; Timer? _autoScanTimer; - // Matches typical Hot Wheels model IDs: 2–5 uppercase letters followed by - // 2–4 digits, e.g. JKF21, HCV73, GRX33, FYD83. - final _hwIdPattern = RegExp(r'\b([A-Z]{2,5}\d{2,4})\b'); - @override void initState() { super.initState(); @@ -117,18 +114,11 @@ class _ScannerScreenState extends State { final inputImage = InputImage.fromFilePath(xFile.path); final recognized = await _textRecognizer.processImage(inputImage); - // Search all recognized text blocks for something matching the HW ID pattern. - String? found; - for (final block in recognized.blocks) { - for (final line in block.lines) { - final match = _hwIdPattern.firstMatch(line.text.toUpperCase()); - if (match != null) { - found = match.group(1); - break; - } - } - if (found != null) break; - } + final found = extractHwIdFromLines( + recognized.blocks + .expand((block) => block.lines) + .map((line) => line.text), + ); // Clean up the temp image. try { diff --git a/lib/screens/my_reports_screen.dart b/lib/screens/my_reports_screen.dart index 2cdfc86..171bb5d 100644 --- a/lib/screens/my_reports_screen.dart +++ b/lib/screens/my_reports_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../main.dart'; import '../theme/app_colors.dart'; +import '../utils/reporting_utils.dart'; class MyReportsScreen extends StatefulWidget { const MyReportsScreen({super.key}); @@ -184,7 +185,7 @@ class _MyReportsScreenState extends State { if (createdAt != null) ...[ const SizedBox(height: 10), Text( - _formatDateTime(createdAt), + formatReportDateTime(createdAt), style: const TextStyle(fontSize: 12, color: AppColors.textHint), ), ], @@ -198,14 +199,6 @@ class _MyReportsScreenState extends State { ); } - String _formatDateTime(DateTime date) { - final d = date.toLocal(); - final mm = d.month.toString().padLeft(2, '0'); - final dd = d.day.toString().padLeft(2, '0'); - final hh = d.hour.toString().padLeft(2, '0'); - final min = d.minute.toString().padLeft(2, '0'); - return '$dd.$mm.${d.year} $hh:$min'; - } } class _StatusChip extends StatelessWidget { @@ -215,12 +208,13 @@ class _StatusChip extends StatelessWidget { @override Widget build(BuildContext context) { - final normalized = status.toLowerCase(); - final (label, color) = switch (normalized) { - 'reviewed' => ('Reviewed', Colors.blueGrey), - 'resolved' => ('Resolved', AppColors.success), - 'dismissed' => ('Dismissed', AppColors.error), - _ => ('Open', AppColors.orange), + final normalized = normalizeReportStatus(status); + final label = reportStatusLabel(normalized); + final color = switch (normalized) { + ReportStatus.reviewed => Colors.blueGrey, + ReportStatus.resolved => AppColors.success, + ReportStatus.dismissed => AppColors.error, + ReportStatus.open => AppColors.orange, }; return Container( diff --git a/lib/utils/reporting_utils.dart b/lib/utils/reporting_utils.dart new file mode 100644 index 0000000..05987ef --- /dev/null +++ b/lib/utils/reporting_utils.dart @@ -0,0 +1,41 @@ +enum ReportStatus { + open, + reviewed, + resolved, + dismissed, +} + +ReportStatus normalizeReportStatus(String? status) { + switch ((status ?? '').toLowerCase()) { + case 'reviewed': + return ReportStatus.reviewed; + case 'resolved': + return ReportStatus.resolved; + case 'dismissed': + return ReportStatus.dismissed; + default: + return ReportStatus.open; + } +} + +String reportStatusLabel(ReportStatus status) { + switch (status) { + case ReportStatus.reviewed: + return 'Reviewed'; + case ReportStatus.resolved: + return 'Resolved'; + case ReportStatus.dismissed: + return 'Dismissed'; + case ReportStatus.open: + return 'Open'; + } +} + +String formatReportDateTime(DateTime date) { + final d = date.toLocal(); + final mm = d.month.toString().padLeft(2, '0'); + final dd = d.day.toString().padLeft(2, '0'); + final hh = d.hour.toString().padLeft(2, '0'); + final min = d.minute.toString().padLeft(2, '0'); + return '$dd.$mm.${d.year} $hh:$min'; +} diff --git a/lib/utils/scanner_utils.dart b/lib/utils/scanner_utils.dart new file mode 100644 index 0000000..a1e1e21 --- /dev/null +++ b/lib/utils/scanner_utils.dart @@ -0,0 +1,13 @@ +String? extractHwIdFromLines(Iterable lines) { + final pattern = RegExp(r'\b([A-Z]{2,5}\d{2,4})\b'); + + for (final raw in lines) { + final line = raw.toUpperCase(); + final match = pattern.firstMatch(line); + if (match != null) { + return match.group(1); + } + } + + return null; +} diff --git a/test/utils/reporting_utils_test.dart b/test/utils/reporting_utils_test.dart new file mode 100644 index 0000000..2bc87cb --- /dev/null +++ b/test/utils/reporting_utils_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hwhub/utils/reporting_utils.dart'; + +void main() { + group('normalizeReportStatus', () { + test('maps known statuses', () { + expect(normalizeReportStatus('open'), ReportStatus.open); + expect(normalizeReportStatus('reviewed'), ReportStatus.reviewed); + expect(normalizeReportStatus('resolved'), ReportStatus.resolved); + expect(normalizeReportStatus('dismissed'), ReportStatus.dismissed); + }); + + test('is case-insensitive and defaults unknown to open', () { + expect(normalizeReportStatus('ReSoLvEd'), ReportStatus.resolved); + expect(normalizeReportStatus('other'), ReportStatus.open); + expect(normalizeReportStatus(null), ReportStatus.open); + }); + }); + + group('reportStatusLabel', () { + test('returns user-facing labels', () { + expect(reportStatusLabel(ReportStatus.open), 'Open'); + expect(reportStatusLabel(ReportStatus.reviewed), 'Reviewed'); + expect(reportStatusLabel(ReportStatus.resolved), 'Resolved'); + expect(reportStatusLabel(ReportStatus.dismissed), 'Dismissed'); + }); + }); + + group('formatReportDateTime', () { + test('formats timestamp as dd.mm.yyyy hh:mm', () { + final value = DateTime.utc(2026, 3, 4, 8, 5); + final formatted = formatReportDateTime(value); + final local = value.toLocal(); + final expected = + '${local.day.toString().padLeft(2, '0')}.${local.month.toString().padLeft(2, '0')}.${local.year} ${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}'; + + expect(formatted, expected); + }); + }); +} diff --git a/test/utils/scanner_utils_test.dart b/test/utils/scanner_utils_test.dart new file mode 100644 index 0000000..d5f3edc --- /dev/null +++ b/test/utils/scanner_utils_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hwhub/utils/scanner_utils.dart'; + +void main() { + group('extractHwIdFromLines', () { + test('returns first matching HW ID', () { + final result = extractHwIdFromLines([ + 'some random text', + 'MODEL JKF21 PREMIUM', + 'another HCV73', + ]); + + expect(result, 'JKF21'); + }); + + test('matches lower-case input by normalizing to upper-case', () { + final result = extractHwIdFromLines([ + 'abc', + 'new release grx33', + ]); + + expect(result, 'GRX33'); + }); + + test('returns null when no pattern is present', () { + final result = extractHwIdFromLines([ + 'hot wheels', + 'no sku here', + ]); + + expect(result, isNull); + }); + + test('supports 2 to 5 letters and 2 to 4 digits', () { + expect(extractHwIdFromLines(['AB12']), 'AB12'); + expect(extractHwIdFromLines(['ABCDE1234']), 'ABCDE1234'); + }); + }); +}