feat: consolidate backend and docker-compose setup

This commit is contained in:
Rafhan Mazaya Fathurrahman committed 2026-06-30 21:09:08 +07:00
commit ff3753a745
306 files changed
+35450

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:app_pfm_ocr_v2/features/camera/blur_detector.dart';
void main() {
test('BlurDetector TDD algorithm test with document borders', () async {
final tempDir = Directory.systemTemp.createTempSync();
final sharpFile = File('${tempDir.path}/sharp_doc.png');
final blurFile = File('${tempDir.path}/blur_doc.png');
// 1. Generate a sharp document with a black background
// Size: 200x200
final sharpImg = img.Image(width: 200, height: 200);
// Fill with black (background table)
img.fill(sharpImg, color: img.ColorRgb8(0, 0, 0));
// Draw white document in the center (from 40 to 160)
for (int y = 40; y < 160; y++) {
for (int x = 40; x < 160; x++) {
// Draw sharp alternating stripes inside the document to simulate sharp text
final isStripe = (x % 6 < 3);
final colorVal = isStripe ? 0 : 255;
sharpImg.setPixelRgba(x, y, colorVal, colorVal, colorVal, 255);
}
}
sharpFile.writeAsBytesSync(img.encodePng(sharpImg));
// 2. Generate a blurry document with the same black background
final blurImg = img.Image(width: 200, height: 200);
img.fill(blurImg, color: img.ColorRgb8(0, 0, 0));
// Draw white document in the center (from 40 to 160)
for (int y = 40; y < 160; y++) {
for (int x = 40; x < 160; x++) {
// Draw solid white or very blurry gray stripes (low contrast)
blurImg.setPixelRgba(x, y, 220, 220, 220, 255);
}
}
// To make it look like a blurred document, we also blur the edges of the white card slightly
final blurredDocImg = img.gaussianBlur(blurImg, radius: 2);
blurFile.writeAsBytesSync(img.encodePng(blurredDocImg));
// 3. Run BlurDetector
final sharpResult = await BlurDetector.checkBlur(sharpFile.path);
final blurResult = await BlurDetector.checkBlur(blurFile.path);
print('TDD Sharp Doc Score: ${sharpResult.score} (Raw: ${sharpResult.rawScore}, IsBlur: ${sharpResult.isBlur})');
print('TDD Blur Doc Score: ${blurResult.score} (Raw: ${blurResult.rawScore}, IsBlur: ${blurResult.isBlur})');
// 4. Assertions
// The sharp document must be detected as sharp (score >= 80.0)
expect(sharpResult.score, greaterThanOrEqualTo(80.0));
expect(sharpResult.isBlur, isFalse);
// The blurry document must be detected as blurry (score < 80.0)
expect(blurResult.score, lessThan(80.0));
expect(blurResult.isBlur, isTrue);
// Clean up
try {
sharpFile.deleteSync();
blurFile.deleteSync();
tempDir.deleteSync();
} catch (_) {}
});
}
+78
View File
@@ -0,0 +1,78 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:app_pfm_ocr_v2/features/camera/camera_screen.dart';
import 'package:app_pfm_ocr_v2/features/camera/image_preview_screen.dart';
import 'package:app_pfm_ocr_v2/core/location/location_service.dart';
void main() {
testWidgets('CameraScreen coordinates capture and preview navigation test', (WidgetTester tester) async {
// Inject mock coordinates
final mockPos = Position(
latitude: -6.2000,
longitude: 106.8000,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 10.0,
altitudeAccuracy: 1.0,
heading: 0.0,
headingAccuracy: 1.0,
speed: 0.0,
speedAccuracy: 1.0,
);
LocationService.mockDeterminator = () async => mockPos;
// Create a dummy image file in temp directory
final tempDir = Directory.systemTemp.createTempSync();
final tempFile = File('${tempDir.path}/test_location_image.png');
final dummyPngBytes = [
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82,
0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137,
0, 0, 0, 10, 73, 68, 65, 84, 120, 156, 99, 0, 1, 0, 0, 5,
0, 1, 13, 10, 45, 180, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
];
tempFile.writeAsBytesSync(dummyPngBytes);
// Build the widget tree
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: Scaffold(
body: CameraScreen(),
),
),
),
);
// Find the state of the CameraScreen
final CameraScreenState state = tester.state(find.byType(CameraScreen));
// Call the navigation method directly with coordinates
state.navigateToPreview(
tempFile.path,
latitude: mockPos.latitude,
longitude: mockPos.longitude,
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500)); // Pushes preview screen
await tester.pump(const Duration(seconds: 1)); // Waits for blur analysis to finish
// Verify that the ImagePreviewScreen is displayed and has coordinates
final previewFinder = find.byType(ImagePreviewScreen);
expect(previewFinder, findsOneWidget);
final ImagePreviewScreen previewWidget = tester.widget(previewFinder);
expect(previewWidget.latitude, equals(-6.2000));
expect(previewWidget.longitude, equals(106.8000));
// Cleanup
try {
tempFile.deleteSync();
tempDir.deleteSync();
} catch (_) {}
LocationService.mockDeterminator = null;
});
}
+52
View File
@@ -0,0 +1,52 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_pfm_ocr_v2/features/camera/camera_screen.dart';
import 'package:app_pfm_ocr_v2/features/camera/image_preview_screen.dart';
void main() {
testWidgets('CameraScreen navigation to ImagePreviewScreen test', (WidgetTester tester) async {
// Generate a valid 1x1 PNG file in temp directory
final tempDir = Directory.systemTemp.createTempSync();
final tempFile = File('${tempDir.path}/test_image.png');
final dummyPngBytes = [
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82,
0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137,
0, 0, 0, 10, 73, 68, 65, 84, 120, 156, 99, 0, 1, 0, 0, 5,
0, 1, 13, 10, 45, 180, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
];
tempFile.writeAsBytesSync(dummyPngBytes);
// Build a mock version of the widget tree with a simple navigator
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: Scaffold(
body: CameraScreen(),
),
),
),
);
// Find the state of the CameraScreen
final CameraScreenState state = tester.state(find.byType(CameraScreen));
// Call the navigation method directly to test the routing
state.navigateToPreview(tempFile.path);
// Pump frames to allow navigation and async processing to complete
await tester.pump();
await tester.pump(const Duration(milliseconds: 500)); // Pushes preview screen
await tester.pump(const Duration(seconds: 1)); // Waits for blur analysis to finish
// Verify that the ImagePreviewScreen is now displayed on top of the navigator
expect(find.byType(ImagePreviewScreen), findsOneWidget);
// Cleanup
try {
tempFile.deleteSync();
tempDir.deleteSync();
} catch (_) {}
});
}
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_pfm_ocr_v2/features/editor/editor_screen.dart';
import 'package:app_pfm_ocr_v2/features/documents/pending_documents_provider.dart';
import 'package:app_pfm_ocr_v2/models/document_model.dart';
import 'package:app_pfm_ocr_v2/main.dart';
import 'package:app_pfm_ocr_v2/core/storage/local_storage.dart';
class MockLocalStorage extends LocalStorage {
@override
Future<void> init() async {}
@override
Future<void> saveDocument(DocumentModel doc) async {}
}
void main() {
testWidgets('EditorScreen input validation tests', (WidgetTester tester) async {
final mockLocalStorage = MockLocalStorage();
final testDoc = DocumentModel(
id: 'pending_123',
tanggal: '29 Juni 2026',
noPo: 'PO/26/1234567890',
noSo: '1234567890',
noDo: 'DO-123',
kepadaYth: 'PT. A',
orderUntuk: 'PRIMA FRESH MART',
alamat: 'Alamat',
platTruk: 'B 1234 A',
namaDriver: 'Driver',
namaPenerima: '',
latitude: -6.2000,
longitude: 106.8000,
items: [
DocumentItem(nomorSku: '12010112', namaBarang: 'FIESTA CHICKEN NUGGET', banyak: '1 BOX', jumlah: '12 PAC'),
],
createdAt: DateTime.now(),
);
// Initialize the Riverpod container with local storage overrides
final container = ProviderContainer(
overrides: [
localStorageProvider.overrideWithValue(mockLocalStorage),
],
);
// Set the pending documents state BEFORE widget tree is built (before initState)
container.read(pendingDocumentsProvider.notifier).state = [
PendingDocument(
id: 'pending_123',
imagePath: 'dummy.png',
status: PendingDocumentStatus.success,
document: testDoc,
latitude: -6.2000,
longitude: 106.8000,
),
];
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const MaterialApp(
home: Scaffold(
body: EditorScreen(pendingId: 'pending_123'),
),
),
),
);
await tester.pumpAndSettle();
// Find all TextFormFields
final textFormFields = find.byType(TextFormField);
expect(textFormFields, findsAtLeastNWidgets(2));
// Enter invalid PO format (Index 0 is Tanggal, Index 1 is No PO)
await tester.enterText(textFormFields.at(1), 'PO-INVALID');
await tester.pumpAndSettle();
// Trigger validation by tapping the button
final reviewButtonFinder = find.text('Review Complete');
expect(reviewButtonFinder, findsOneWidget);
await tester.tap(reviewButtonFinder);
await tester.pumpAndSettle();
// Verify PO format validation error is shown
expect(find.text('Format harus PO/[XX]/[XXXXXXXXXX]'), findsOneWidget);
});
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_pfm_ocr_v2/features/auth/login_screen.dart';
void main() {
testWidgets('LoginScreen validation tests', (WidgetTester tester) async {
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: LoginScreen(),
),
),
);
// Find the login button
final loginButtonFinder = find.byType(ElevatedButton);
expect(loginButtonFinder, findsOneWidget);
// Tap login button with empty inputs
await tester.tap(loginButtonFinder);
await tester.pumpAndSettle();
// Verify inline validation error text is displayed
expect(find.text('Username/ID cannot be empty'), findsOneWidget);
expect(find.text('Password cannot be empty'), findsOneWidget);
// Enter a username, and a short password (e.g. 5 chars)
final textFormFields = find.byType(TextFormField);
expect(textFormFields, findsNWidgets(2));
await tester.enterText(textFormFields.first, 'admin');
await tester.enterText(textFormFields.last, '12345');
// Tap the login button again
await tester.tap(loginButtonFinder);
await tester.pumpAndSettle();
// Verify username warning is gone, but password length warning appears
expect(find.text('Username/ID cannot be empty'), findsNothing);
expect(find.text('Password must be at least 6 characters'), findsOneWidget);
});
}
+100
View File
@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_pfm_ocr_v2/features/documents/documents_screen.dart';
import 'package:app_pfm_ocr_v2/models/document_model.dart';
import 'package:app_pfm_ocr_v2/main.dart';
import 'package:app_pfm_ocr_v2/core/storage/local_storage.dart';
class MockLocalStorage extends LocalStorage {
final List<DocumentModel> docs;
MockLocalStorage(this.docs);
@override
Future<void> init() async {}
@override
List<DocumentModel> getAllDocuments() {
return docs;
}
@override
Future<void> saveDocument(DocumentModel doc) async {}
}
void main() {
testWidgets('DocumentsScreen search and coordinate formatting tests', (WidgetTester tester) async {
final testDocs = [
DocumentModel(
id: '1',
tanggal: '29 Juni 2026',
noPo: 'PO/26/0000111111',
noSo: '1000000001',
noDo: 'DO-111111',
kepadaYth: 'PT. PRIMAFOOD BEKASI',
orderUntuk: 'PRIMA FRESH MART',
alamat: 'Bekasi',
platTruk: 'B 1234 ABC',
namaDriver: 'Driver A',
namaPenerima: 'Penerima A',
latitude: -6.123456,
longitude: 106.567890,
items: [],
createdAt: DateTime.now(),
),
DocumentModel(
id: '2',
tanggal: '30 Juni 2026',
noPo: 'PO/26/0000222222',
noSo: '2000000002',
noDo: 'DO-222222',
kepadaYth: 'PT. PRIMAFOOD TANGERANG',
orderUntuk: 'PRIMA FRESH MART',
alamat: 'Tangerang',
platTruk: 'B 5678 XYZ',
namaDriver: 'Driver B',
namaPenerima: 'Penerima B',
latitude: null,
longitude: null,
items: [],
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
),
];
final mockLocalStorage = MockLocalStorage(testDocs);
await tester.pumpWidget(
ProviderScope(
overrides: [
localStorageProvider.overrideWithValue(mockLocalStorage),
],
child: const MaterialApp(
home: DocumentsScreen(),
),
),
);
await tester.pumpAndSettle();
// Verify both documents are rendered initially
expect(find.text('DO: DO-111111'), findsOneWidget);
expect(find.text('DO: DO-222222'), findsOneWidget);
// Verify coordinate rounding to 4 decimal places
// -6.123456 -> -6.1235
// 106.567890 -> 106.5679
expect(find.text('Lat: -6.1235'), findsOneWidget);
expect(find.text('Lng: 106.5679'), findsOneWidget);
// Type query in search bar to filter only Bekasi
final searchField = find.byType(TextField);
expect(searchField, findsOneWidget);
await tester.enterText(searchField, 'BEKASI');
await tester.pump();
// Verify only the Bekasi document matches
expect(find.text('DO: DO-111111'), findsOneWidget);
expect(find.text('DO: DO-222222'), findsNothing);
});
}
+84
View File
@@ -0,0 +1,84 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_pfm_ocr_v2/features/camera/image_preview_screen.dart';
import 'package:app_pfm_ocr_v2/features/camera/blur_detector.dart';
void main() {
late Directory tempDir;
late File tempFile;
setUpAll(() {
tempDir = Directory.systemTemp.createTempSync();
tempFile = File('${tempDir.path}/test_preview_image.png');
final dummyPngBytes = [
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82,
0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137,
0, 0, 0, 10, 73, 68, 65, 84, 120, 156, 99, 0, 1, 0, 0, 5,
0, 1, 13, 10, 45, 180, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
];
tempFile.writeAsBytesSync(dummyPngBytes);
});
tearDownAll(() {
try {
tempFile.deleteSync();
tempDir.deleteSync();
} catch (_) {}
});
testWidgets('ImagePreviewScreen shows Blur Warning when image is blurry', (WidgetTester tester) async {
// Inject blurry mock result
BlurDetector.mockResult = BlurResult(score: 50.0, isBlur: true, rawScore: 250.0);
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: ImagePreviewScreen(imagePath: tempFile.path),
),
),
);
// Wait for analysis to finish
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Verify warning text is displayed
expect(find.text('Foto Terdeteksi Blur!'), findsOneWidget);
expect(find.text('Kualitas Foto Baik'), findsNothing);
// Verify "Unggah Dokumen" button is disabled (has null onPressed)
final uploadButton = tester.widget<ElevatedButton>(find.byType(ElevatedButton));
expect(uploadButton.onPressed, isNull);
BlurDetector.mockResult = null;
});
testWidgets('ImagePreviewScreen shows Sharp Success when image is clear', (WidgetTester tester) async {
// Inject sharp mock result
BlurDetector.mockResult = BlurResult(score: 95.0, isBlur: false, rawScore: 475.0);
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: ImagePreviewScreen(imagePath: tempFile.path),
),
),
);
// Wait for analysis to finish
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Verify success text is displayed
expect(find.text('Kualitas Foto Baik'), findsOneWidget);
expect(find.text('Foto Terdeteksi Blur!'), findsNothing);
// Verify "Unggah Dokumen" button is enabled (has non-null onPressed)
final uploadButton = tester.widget<ElevatedButton>(find.byType(ElevatedButton));
expect(uploadButton.onPressed, isNotNull);
BlurDetector.mockResult = null;
});
}
+7
View File
@@ -0,0 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
test('Simple passing smoke test', () {
expect(true, isTrue);
});
}