test: add unit coverage for scanner parsing and reporting utils
This commit is contained in:
parent
9daacaf0d8
commit
8cc58d6a76
6 changed files with 148 additions and 31 deletions
|
|
@ -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<ScannerScreen> {
|
|||
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<ScannerScreen> {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -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<MyReportsScreen> {
|
|||
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<MyReportsScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
|||
41
lib/utils/reporting_utils.dart
Normal file
41
lib/utils/reporting_utils.dart
Normal file
|
|
@ -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';
|
||||
}
|
||||
13
lib/utils/scanner_utils.dart
Normal file
13
lib/utils/scanner_utils.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
String? extractHwIdFromLines(Iterable<String> 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;
|
||||
}
|
||||
40
test/utils/reporting_utils_test.dart
Normal file
40
test/utils/reporting_utils_test.dart
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
39
test/utils/scanner_utils_test.dart
Normal file
39
test/utils/scanner_utils_test.dart
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue