// test/widget/sos_screen_test.dart // // Widget tests untuk SosScreen — layar SOS tunanetra. // Jalankan: flutter test test/widget/sos_screen_test.dart // ignore_for_file: avoid_print import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; // --------------------------------------------------------------------------- // Stubs / Models // --------------------------------------------------------------------------- enum SosTriggerType { voiceCommand, button, manual } enum SosStatus { triggered, acknowledged, resolved } class SosEvent { final int id; final SosTriggerType triggerType; final double? lat; final double? lng; final SosStatus status; final DateTime createdAt; final DateTime? acknowledgedAt; const SosEvent({ required this.id, required this.triggerType, this.lat, this.lng, required this.status, required this.createdAt, this.acknowledgedAt, }); } /// Stub SosScreen yang mereplikasi struktur UI asli class _StubSosScreen extends StatefulWidget { final List events; final bool isLoading; final String? errorMessage; final bool hasPairedGuardian; const _StubSosScreen({ this.events = const [], this.isLoading = false, this.errorMessage, this.hasPairedGuardian = true, }); @override State<_StubSosScreen> createState() => _StubSosScreenState(); } class _StubSosScreenState extends State<_StubSosScreen> { late List _events; bool _sending = false; String? _sendError; bool _sendSuccess = false; @override void initState() { super.initState(); _events = List.from(widget.events); } Future _sendSos() async { if (!widget.hasPairedGuardian) { setState(() => _sendError = 'Belum ada Guardian yang terhubung'); return; } setState(() { _sending = true; _sendError = null; _sendSuccess = false; }); await Future.delayed(const Duration(milliseconds: 100)); if (mounted) { setState(() { _sending = false; _sendSuccess = true; _events = [ SosEvent( id: _events.length + 1, triggerType: SosTriggerType.button, lat: -7.2575, lng: 112.7521, status: SosStatus.triggered, createdAt: DateTime.now(), ), ..._events, ]; }); } } String _statusLabel(SosStatus status) => switch (status) { SosStatus.triggered => 'Terkirim', SosStatus.acknowledged => 'Diakui Guardian', SosStatus.resolved => 'Selesai', }; Color _statusColor(SosStatus status) => switch (status) { SosStatus.triggered => Colors.orange, SosStatus.acknowledged => Colors.blue, SosStatus.resolved => Colors.green, }; String _triggerLabel(SosTriggerType type) => switch (type) { SosTriggerType.voiceCommand => 'Voice Command', SosTriggerType.button => 'Tombol SOS', SosTriggerType.manual => 'Manual', }; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('SOS')), body: Column( children: [ // Peringatan tidak ada guardian if (!widget.hasPairedGuardian) Container( key: const Key('no_guardian_banner'), width: double.infinity, padding: const EdgeInsets.all(12), color: Colors.orange.shade100, child: const Row( children: [ Icon(Icons.warning_amber, color: Colors.orange), SizedBox(width: 8), Expanded( child: Text( key: Key('no_guardian_text'), 'Belum ada Guardian terhubung. SOS tetap bisa dikirim.', style: TextStyle(color: Colors.orange), ), ), ], ), ), // SOS button utama Padding( padding: const EdgeInsets.all(24), child: Column( children: [ const Text( 'Tekan tombol di bawah untuk mengirim sinyal darurat', textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), const SizedBox(height: 24), _sending ? const CircularProgressIndicator( key: Key('sending_indicator')) : GestureDetector( onTap: _sendSos, child: Container( key: const Key('sos_button'), width: 140, height: 140, decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.red.withValues(alpha: 0.4), blurRadius: 20, spreadRadius: 4, ), ], ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.sos, color: Colors.white, size: 48), Text( 'SOS', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), ], ), ), ), const SizedBox(height: 16), if (_sendSuccess) Container( key: const Key('send_success_banner'), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, color: Colors.green), SizedBox(width: 8), Text( key: Key('send_success_text'), 'SOS berhasil dikirim!', style: TextStyle( color: Colors.green, fontWeight: FontWeight.bold), ), ], ), ), if (_sendError != null) Container( key: const Key('send_error_banner'), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), ), child: Text( key: const Key('send_error_text'), _sendError!, style: const TextStyle(color: Colors.red), textAlign: TextAlign.center, ), ), ], ), ), // Riwayat SOS const Divider(height: 1), const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Align( alignment: Alignment.centerLeft, child: Text( 'Riwayat SOS', key: Key('history_title'), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ), ), Expanded( child: widget.isLoading ? const Center( child: CircularProgressIndicator( key: Key('loading_indicator'))) : widget.errorMessage != null ? Center( child: Text( key: const Key('error_message'), widget.errorMessage!, ), ) : _events.isEmpty ? const Center( child: Text( key: Key('empty_history_text'), 'Tidak ada riwayat SOS', style: TextStyle(color: Colors.grey), ), ) : ListView.builder( key: const Key('history_list'), padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _events.length, itemBuilder: (context, index) { final event = _events[index]; return Card( key: Key('sos_event_${event.id}'), margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( Icons.sos, key: Key('sos_icon_${event.id}'), color: Colors.red, ), title: Text( key: Key('sos_trigger_${event.id}'), _triggerLabel(event.triggerType), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (event.lat != null && event.lng != null) Text( key: Key('sos_location_${event.id}'), 'Lat: ${event.lat!.toStringAsFixed(4)}, Lng: ${event.lng!.toStringAsFixed(4)}', style: const TextStyle(fontSize: 11), ), ], ), trailing: Container( key: Key('sos_status_${event.id}'), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _statusColor(event.status), borderRadius: BorderRadius.circular(12), ), child: Text( _statusLabel(event.status), style: const TextStyle( color: Colors.white, fontSize: 11), ), ), ), ); }, ), ), ], ), ); } } Widget makeTestable(Widget child) => MaterialApp(home: child); // Fixtures final _triggeredEvent = SosEvent( id: 1, triggerType: SosTriggerType.button, lat: -7.2575, lng: 112.7521, status: SosStatus.triggered, createdAt: DateTime(2025, 5, 10, 14, 0), ); final _acknowledgedEvent = SosEvent( id: 2, triggerType: SosTriggerType.voiceCommand, lat: -7.2601, lng: 112.7510, status: SosStatus.acknowledged, createdAt: DateTime(2025, 5, 9, 10, 0), acknowledgedAt: DateTime(2025, 5, 9, 10, 5), ); final _resolvedEvent = SosEvent( id: 3, triggerType: SosTriggerType.manual, status: SosStatus.resolved, createdAt: DateTime(2025, 5, 8, 8, 0), ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { group('SosScreen Widget Tests', () { // ── Rendering awal ───────────────────────────────────────────────────── group('Rendering awal', () { testWidgets('menampilkan AppBar dengan judul SOS', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.text('SOS'), findsAtLeastNWidgets(1)); }); testWidgets('menampilkan tombol SOS bulat merah besar', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.byKey(const Key('sos_button')), findsOneWidget); }); testWidgets('menampilkan judul riwayat SOS', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.byKey(const Key('history_title')), findsOneWidget); }); testWidgets('menampilkan teks instruksi darurat', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect( find.textContaining('sinyal darurat'), findsOneWidget, ); }); testWidgets('tidak menampilkan success banner saat awal', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.byKey(const Key('send_success_banner')), findsNothing); }); testWidgets('tidak menampilkan error banner saat awal', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.byKey(const Key('send_error_banner')), findsNothing); }); }); // ── Warning no guardian ─────────────────────────────────────────────── group('Warning tidak ada Guardian', () { testWidgets('menampilkan banner warning saat belum ada guardian', (tester) async { await tester.pumpWidget( makeTestable(const _StubSosScreen(hasPairedGuardian: false)), ); expect(find.byKey(const Key('no_guardian_banner')), findsOneWidget); }); testWidgets('menampilkan teks warning dengan benar', (tester) async { await tester.pumpWidget( makeTestable(const _StubSosScreen(hasPairedGuardian: false)), ); expect(find.byKey(const Key('no_guardian_text')), findsOneWidget); }); testWidgets('tidak menampilkan banner warning saat guardian ada', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.byKey(const Key('no_guardian_banner')), findsNothing); }); }); // ── Send SOS ────────────────────────────────────────────────────────── group('Kirim SOS', () { testWidgets('tap tombol SOS menampilkan loading indicator', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); await tester.tap(find.byKey(const Key('sos_button'))); await tester.pump(); expect(find.byKey(const Key('sending_indicator')), findsOneWidget); }); testWidgets('setelah SOS terkirim, tampil success banner', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); await tester.tap(find.byKey(const Key('sos_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('send_success_banner')), findsOneWidget); expect(find.byKey(const Key('send_success_text')), findsOneWidget); }); testWidgets('setelah SOS terkirim, muncul event baru di riwayat', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); await tester.tap(find.byKey(const Key('sos_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('history_list')), findsOneWidget); }); testWidgets('SOS tanpa guardian menampilkan error message', (tester) async { await tester.pumpWidget( makeTestable(const _StubSosScreen(hasPairedGuardian: false)), ); await tester.tap(find.byKey(const Key('sos_button'))); await tester.pump(); expect(find.byKey(const Key('send_error_banner')), findsOneWidget); expect(find.textContaining('Guardian'), findsAtLeastNWidgets(1)); }); }); // ── Riwayat SOS ─────────────────────────────────────────────────────── group('Riwayat SOS', () { testWidgets('menampilkan empty state saat tidak ada riwayat', (tester) async { await tester.pumpWidget(makeTestable(const _StubSosScreen())); expect(find.byKey(const Key('empty_history_text')), findsOneWidget); }); testWidgets('menampilkan event dalam daftar riwayat', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_triggeredEvent, _acknowledgedEvent]), )); expect(find.byKey(const Key('sos_event_1')), findsOneWidget); expect(find.byKey(const Key('sos_event_2')), findsOneWidget); }); testWidgets('menampilkan tipe trigger dengan benar (Button)', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_triggeredEvent]), )); expect(find.text('Tombol SOS'), findsOneWidget); }); testWidgets('menampilkan tipe trigger dengan benar (Voice Command)', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_acknowledgedEvent]), )); expect(find.text('Voice Command'), findsOneWidget); }); testWidgets('menampilkan tipe trigger Manual dengan benar', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_resolvedEvent]), )); expect(find.text('Manual'), findsOneWidget); }); testWidgets('menampilkan status badge TRIGGERED berwarna orange', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_triggeredEvent]), )); expect(find.byKey(const Key('sos_status_1')), findsOneWidget); expect(find.text('Terkirim'), findsOneWidget); }); testWidgets('menampilkan status badge ACKNOWLEDGED berwarna biru', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_acknowledgedEvent]), )); expect(find.text('Diakui Guardian'), findsOneWidget); }); testWidgets('menampilkan status badge RESOLVED berwarna hijau', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_resolvedEvent]), )); expect(find.text('Selesai'), findsOneWidget); }); testWidgets('menampilkan koordinat GPS jika tersedia', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_triggeredEvent]), )); expect(find.byKey(const Key('sos_location_1')), findsOneWidget); expect(find.textContaining('Lat:'), findsOneWidget); expect(find.textContaining('Lng:'), findsOneWidget); }); testWidgets('tidak menampilkan koordinat jika null', (tester) async { await tester.pumpWidget(makeTestable( _StubSosScreen(events: [_resolvedEvent]), )); expect(find.byKey(const Key('sos_location_3')), findsNothing); }); }); // ── Loading & Error ─────────────────────────────────────────────────── group('Loading dan error state', () { testWidgets('menampilkan loading di riwayat saat isLoading=true', (tester) async { await tester .pumpWidget(makeTestable(const _StubSosScreen(isLoading: true))); expect(find.byKey(const Key('loading_indicator')), findsOneWidget); }); testWidgets('menampilkan pesan error di area riwayat', (tester) async { await tester.pumpWidget(makeTestable( const _StubSosScreen(errorMessage: 'Gagal memuat riwayat'), )); expect(find.byKey(const Key('error_message')), findsOneWidget); }); }); // ── 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 _StubSosScreen())); expect(tester.takeException(), isNull); }); testWidgets('tidak overflow saat menampilkan banyak event', (tester) async { final manyEvents = List.generate( 20, (i) => SosEvent( id: i, triggerType: SosTriggerType.button, status: SosStatus.resolved, createdAt: DateTime(2025, 5, i + 1), ), ); await tester.pumpWidget(makeTestable( _StubSosScreen(events: manyEvents), )); expect(tester.takeException(), isNull); }); }); }); }