2026-05-17 18:40:03 +07:00

583 lines
21 KiB
Dart

// test/widget/walk_guide_screen_test.dart
// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables
//
// 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.withValues(alpha: 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<ElevatedButton>(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<Container>(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<Container>(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<Container>(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);
});
});
});
}