620 lines
23 KiB
Dart
620 lines
23 KiB
Dart
// 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<SosEvent> 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<SosEvent> _events;
|
|
bool _sending = false;
|
|
String? _sendError;
|
|
bool _sendSuccess = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_events = List.from(widget.events);
|
|
}
|
|
|
|
Future<void> _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);
|
|
});
|
|
});
|
|
});
|
|
}
|