// test/widget/walk_guide_screen_test.dart // // Widget tests untuk WalkGuideScreen — layar utama navigasi tunanetra. // Jalankan: flutter test test/widget/walk_guide_screen_test.dart // // Dev dependencies: // flutter_test: sdk: flutter // mockito: ^5.4.4 // build_runner: ^2.4.6 // ignore_for_file: avoid_print import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; // --------------------------------------------------------------------------- // Stubs / Models // --------------------------------------------------------------------------- /// Mereplikasi DetectionResult dari core/ai/obstacle_analyzer.dart class DetectionResult { final String label; final double confidence; final String direction; // 'LEFT' | 'CENTER' | 'RIGHT' final String distance; // 'Very Close' | 'Close' | 'Medium' | 'Far' final bool isAlert; const DetectionResult({ required this.label, required this.confidence, required this.direction, required this.distance, required this.isAlert, }); } /// Stub WalkGuideScreen yang meniru UI asli tanpa plugin dependencies class _StubWalkGuideScreen extends StatefulWidget { final DetectionResult? initialDetection; final bool initialActive; final bool cameraError; const _StubWalkGuideScreen({ this.initialDetection, this.initialActive = false, this.cameraError = false, }); @override State<_StubWalkGuideScreen> createState() => _StubWalkGuideScreenState(); } class _StubWalkGuideScreenState extends State<_StubWalkGuideScreen> { late bool _active; late String _status; DetectionResult? _lastDetection; bool _hasCameraError = false; @override void initState() { super.initState(); _active = widget.initialActive; _status = widget.initialActive ? 'Active' : 'Ready'; _lastDetection = widget.initialDetection; _hasCameraError = widget.cameraError; } void _toggleActive() { setState(() { _active = !_active; _status = _active ? 'Active' : 'Ready'; if (!_active) _lastDetection = null; }); } Color get _statusColor { if (!_active) return Colors.grey; if (_lastDetection == null) return Colors.green; if (_lastDetection!.isAlert) return Colors.red; return Colors.orange; } String get _directionEmoji { if (_lastDetection == null) return ''; return switch (_lastDetection!.direction) { 'LEFT' => '⬅️', 'RIGHT' => '➡️', _ => '⬆️', }; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('WalkGuide'), actions: [ IconButton( key: const Key('settings_button'), icon: const Icon(Icons.settings), onPressed: () {}, ), ], ), body: Column( children: [ // Status bar Container( key: const Key('status_bar'), width: double.infinity, padding: const EdgeInsets.all(16), color: _statusColor, child: Text( key: const Key('status_text'), _status, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), // Camera preview area Expanded( child: _hasCameraError ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.camera_alt, size: 64, color: Colors.grey, key: Key('camera_error_icon')), const SizedBox(height: 8), const Text( key: Key('camera_error_text'), 'Kamera tidak tersedia', style: TextStyle(color: Colors.grey), ), ], ), ) : Container( key: const Key('camera_preview'), color: Colors.black, child: _active ? Stack( children: [ const Center( child: Text( 'Camera Preview', style: TextStyle(color: Colors.white38), ), ), if (_lastDetection != null) Positioned( bottom: 16, left: 0, right: 0, child: _DetectionOverlay(detection: _lastDetection!), ), ], ) : const Center( child: Text( 'Tekan Start untuk memulai', key: Key('inactive_hint'), style: TextStyle(color: Colors.white54), ), ), ), ), // Bottom controls Padding( padding: const EdgeInsets.all(20), child: Column( children: [ // Detection info card if (_lastDetection != null) Card( key: const Key('detection_card'), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Text( _directionEmoji, key: const Key('direction_emoji'), style: const TextStyle(fontSize: 24), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( key: const Key('detection_label'), _lastDetection!.label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), Text( key: const Key('detection_distance'), _lastDetection!.distance, style: TextStyle(color: Colors.grey.shade600), ), ], ), ), Text( key: const Key('detection_confidence'), '${(_lastDetection!.confidence * 100).toStringAsFixed(0)}%', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ), ), const SizedBox(height: 12), // Start/Stop button SizedBox( width: double.infinity, height: 56, child: ElevatedButton.icon( key: const Key('toggle_button'), onPressed: _hasCameraError ? null : _toggleActive, icon: Icon(_active ? Icons.stop : Icons.play_arrow), label: Text(_active ? 'Stop WalkGuide' : 'Start WalkGuide'), style: ElevatedButton.styleFrom( backgroundColor: _active ? Colors.red : Colors.blue, foregroundColor: Colors.white, ), ), ), const SizedBox(height: 8), // SOS button SizedBox( width: double.infinity, height: 44, child: OutlinedButton.icon( key: const Key('sos_button'), onPressed: () {}, icon: const Icon(Icons.sos, color: Colors.red), label: const Text('SOS', style: TextStyle(color: Colors.red)), ), ), ], ), ), ], ), ); } } class _DetectionOverlay extends StatelessWidget { final DetectionResult detection; const _DetectionOverlay({required this.detection}); @override Widget build(BuildContext context) { return Container( key: const Key('detection_overlay'), margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: detection.isAlert ? Colors.red.withOpacity(0.85) : Colors.black87, borderRadius: BorderRadius.circular(8), ), child: Text( '${detection.label} — ${detection.direction} — ${detection.distance}', style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ); } } Widget makeTestable(Widget child) => MaterialApp(home: child); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { group('WalkGuideScreen Widget Tests', () { // ── Rendering awal ───────────────────────────────────────────────────── group('Rendering awal (inactive)', () { testWidgets('menampilkan AppBar dengan judul WalkGuide', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.text('WalkGuide'), findsOneWidget); }); testWidgets('menampilkan status bar dengan status Ready', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.byKey(const Key('status_bar')), findsOneWidget); expect(find.text('Ready'), findsOneWidget); }); testWidgets('menampilkan tombol Start WalkGuide', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.byKey(const Key('toggle_button')), findsOneWidget); expect(find.text('Start WalkGuide'), findsOneWidget); }); testWidgets('menampilkan tombol SOS', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.byKey(const Key('sos_button')), findsOneWidget); }); testWidgets('menampilkan hint teks saat inactive', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.byKey(const Key('inactive_hint')), findsOneWidget); }); testWidgets('tidak menampilkan detection card saat tidak ada deteksi', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.byKey(const Key('detection_card')), findsNothing); }); testWidgets('menampilkan icon settings di AppBar', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(find.byKey(const Key('settings_button')), findsOneWidget); }); }); // ── Toggle aktif/non-aktif ───────────────────────────────────────────── group('Toggle Start/Stop', () { testWidgets('tap Start mengubah status menjadi Active', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); await tester.tap(find.byKey(const Key('toggle_button'))); await tester.pump(); expect(find.text('Active'), findsOneWidget); }); testWidgets('tap Start mengubah label tombol menjadi Stop WalkGuide', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); await tester.tap(find.byKey(const Key('toggle_button'))); await tester.pump(); expect(find.text('Stop WalkGuide'), findsOneWidget); }); testWidgets('tap Stop mengembalikan status ke Ready', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen(initialActive: true))); await tester.tap(find.byKey(const Key('toggle_button'))); await tester.pump(); expect(find.text('Ready'), findsOneWidget); }); testWidgets('tap Stop menghapus detection card', (tester) async { const detection = DetectionResult( label: 'person', confidence: 0.92, direction: 'CENTER', distance: 'Very Close', isAlert: true, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.byKey(const Key('detection_card')), findsOneWidget); await tester.tap(find.byKey(const Key('toggle_button'))); await tester.pump(); expect(find.byKey(const Key('detection_card')), findsNothing); }); }); // ── Detection overlay ───────────────────────────────────────────────── group('Detection display', () { testWidgets('menampilkan detection card saat ada deteksi', (tester) async { const detection = DetectionResult( label: 'car', confidence: 0.87, direction: 'RIGHT', distance: 'Close', isAlert: true, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.byKey(const Key('detection_card')), findsOneWidget); }); testWidgets('menampilkan label obstacle yang benar', (tester) async { const detection = DetectionResult( label: 'motorcycle', confidence: 0.75, direction: 'LEFT', distance: 'Medium', isAlert: false, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.byKey(const Key('detection_label')), findsOneWidget); expect(find.text('motorcycle'), findsOneWidget); }); testWidgets('menampilkan estimasi jarak obstacle', (tester) async { const detection = DetectionResult( label: 'person', confidence: 0.91, direction: 'CENTER', distance: 'Very Close', isAlert: true, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.text('Very Close'), findsOneWidget); }); testWidgets('menampilkan confidence score dalam persen', (tester) async { const detection = DetectionResult( label: 'car', confidence: 0.87, direction: 'CENTER', distance: 'Close', isAlert: true, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.text('87%'), findsOneWidget); }); testWidgets('menampilkan emoji arah LEFT dengan benar', (tester) async { const detection = DetectionResult( label: 'person', confidence: 0.80, direction: 'LEFT', distance: 'Close', isAlert: false, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.byKey(const Key('direction_emoji')), findsOneWidget); expect(find.text('⬅️'), findsOneWidget); }); testWidgets('menampilkan emoji arah RIGHT dengan benar', (tester) async { const detection = DetectionResult( label: 'car', confidence: 0.78, direction: 'RIGHT', distance: 'Medium', isAlert: false, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.text('➡️'), findsOneWidget); }); testWidgets('menampilkan overlay di atas camera preview saat active + ada deteksi', (tester) async { const detection = DetectionResult( label: 'truck', confidence: 0.93, direction: 'CENTER', distance: 'Very Close', isAlert: true, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); expect(find.byKey(const Key('detection_overlay')), findsOneWidget); }); }); // ── Error state kamera ──────────────────────────────────────────────── group('Error state kamera', () { testWidgets('menampilkan pesan error kamera saat kamera tidak tersedia', (tester) async { await tester.pumpWidget( makeTestable(const _StubWalkGuideScreen(cameraError: true)), ); expect(find.byKey(const Key('camera_error_text')), findsOneWidget); expect(find.text('Kamera tidak tersedia'), findsOneWidget); }); testWidgets('menampilkan icon error kamera', (tester) async { await tester.pumpWidget( makeTestable(const _StubWalkGuideScreen(cameraError: true)), ); expect(find.byKey(const Key('camera_error_icon')), findsOneWidget); }); testWidgets('tombol Start di-disable saat kamera error', (tester) async { await tester.pumpWidget( makeTestable(const _StubWalkGuideScreen(cameraError: true)), ); final btn = tester.widget(find.byKey(const Key('toggle_button'))); expect(btn.onPressed, isNull); }); }); // ── Status bar color ────────────────────────────────────────────────── group('Status bar warna', () { testWidgets('status bar berwarna grey saat inactive', (tester) async { await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); final container = tester.widget(find.byKey(const Key('status_bar'))); expect(container.color, Colors.grey); }); testWidgets('status bar berwarna hijau saat active tanpa deteksi', (tester) async { await tester.pumpWidget( makeTestable(const _StubWalkGuideScreen(initialActive: true)), ); final container = tester.widget(find.byKey(const Key('status_bar'))); expect(container.color, Colors.green); }); testWidgets('status bar berwarna merah saat ada obstacle alert', (tester) async { const detection = DetectionResult( label: 'person', confidence: 0.95, direction: 'CENTER', distance: 'Very Close', isAlert: true, ); await tester.pumpWidget(makeTestable( const _StubWalkGuideScreen(initialActive: true, initialDetection: detection), )); final container = tester.widget(find.byKey(const Key('status_bar'))); expect(container.color, Colors.red); }); }); // ── Layout responsif ────────────────────────────────────────────────── group('Responsif', () { testWidgets('tidak overflow pada layar 360x640', (tester) async { tester.view.physicalSize = const Size(360, 640); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(tester.takeException(), isNull); }); testWidgets('tidak overflow pada layar 414x896 (iPhone XR)', (tester) async { tester.view.physicalSize = const Size(414, 896); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(makeTestable(const _StubWalkGuideScreen())); expect(tester.takeException(), isNull); }); }); }); }