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

665 lines
24 KiB
Dart

// test/widget/manual_screen_test.dart
// ignore_for_file: prefer_const_constructors
//
// Widget tests untuk ManualScreen — halaman panduan perintah suara.
// Jalankan: flutter test test/widget/manual_screen_test.dart
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// ---------------------------------------------------------------------------
// Stubs — mereplikasi VoiceCommandKey dan ManualScreen tanpa plugin
// ---------------------------------------------------------------------------
enum VoiceCommandKey {
openWalkguide,
startWalkguide,
stopWalkguide,
callGuardian,
openNotification,
readAllNotif,
openSos,
sendSos,
whereAmI,
openActivity,
openNavigation,
openSettings,
repeatLast,
stopTts,
}
class VoiceCommandEntry {
final VoiceCommandKey key;
final String phrase;
final String description;
final String category;
final bool enabled;
const VoiceCommandEntry({
required this.key,
required this.phrase,
required this.description,
required this.category,
this.enabled = true,
});
}
/// Data perintah suara default (sama seperti di backend)
const List<VoiceCommandEntry> _defaultCommands = [
VoiceCommandEntry(
key: VoiceCommandKey.openWalkguide,
phrase: 'Open Walkguide',
description: 'Membuka layar WalkGuide',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.startWalkguide,
phrase: 'Start Walkguide',
description: 'Memulai sesi navigasi WalkGuide',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.stopWalkguide,
phrase: 'Stop Walkguide',
description: 'Menghentikan sesi navigasi',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.callGuardian,
phrase: 'Call Guardian',
description: 'Menelepon Guardian',
category: 'Komunikasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.openNotification,
phrase: 'Open Notifications',
description: 'Membuka layar notifikasi',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.readAllNotif,
phrase: 'Read All My Notifications',
description: 'TTS membacakan semua notifikasi',
category: 'Aksesibilitas',
),
VoiceCommandEntry(
key: VoiceCommandKey.openSos,
phrase: 'Open SOS',
description: 'Membuka layar SOS',
category: 'Darurat',
),
VoiceCommandEntry(
key: VoiceCommandKey.sendSos,
phrase: 'Send SOS',
description: 'Mengirim sinyal darurat',
category: 'Darurat',
),
VoiceCommandEntry(
key: VoiceCommandKey.whereAmI,
phrase: 'Where Am I',
description: 'TTS membacakan lokasi saat ini',
category: 'Lokasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.openActivity,
phrase: 'Open Activity Log',
description: 'Membuka log aktivitas',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.openNavigation,
phrase: 'Open Navigation',
description: 'Membuka layar navigasi',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.openSettings,
phrase: 'Open Settings',
description: 'Membuka halaman pengaturan',
category: 'Navigasi',
),
VoiceCommandEntry(
key: VoiceCommandKey.repeatLast,
phrase: 'Repeat',
description: 'Mengulangi TTS terakhir',
category: 'Aksesibilitas',
),
VoiceCommandEntry(
key: VoiceCommandKey.stopTts,
phrase: 'Stop',
description: 'Menghentikan TTS',
category: 'Aksesibilitas',
),
];
/// Stub ManualScreen yang lebih kaya dari versi asli untuk keperluan testing
class _StubManualScreen extends StatefulWidget {
final List<VoiceCommandEntry> commands;
final bool showSearch;
const _StubManualScreen({
this.commands = _defaultCommands,
this.showSearch = false,
});
@override
State<_StubManualScreen> createState() => _StubManualScreenState();
}
class _StubManualScreenState extends State<_StubManualScreen> {
String _searchQuery = '';
String? _selectedCategory;
List<VoiceCommandEntry> get _filteredCommands {
return widget.commands.where((cmd) {
final matchesSearch = _searchQuery.isEmpty ||
cmd.phrase.toLowerCase().contains(_searchQuery.toLowerCase()) ||
cmd.description.toLowerCase().contains(_searchQuery.toLowerCase());
final matchesCategory =
_selectedCategory == null || cmd.category == _selectedCategory;
return matchesSearch && matchesCategory;
}).toList();
}
Set<String> get _categories => widget.commands.map((c) => c.category).toSet();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Panduan Suara'),
actions: [
IconButton(
key: const Key('info_button'),
icon: const Icon(Icons.info_outline),
onPressed: () {
showDialog(
context: context,
builder: (_) => AlertDialog(
key: const Key('info_dialog'),
title: const Text('Cara Penggunaan'),
content: const Text(
'Ucapkan perintah suara yang tertera untuk mengontrol aplikasi. '
'Pastikan mikrofon aktif.',
),
actions: [
TextButton(
key: const Key('info_close_button'),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Tutup'),
),
],
),
);
},
),
],
),
body: Column(
children: [
// Header instruksi
Container(
key: const Key('header_banner'),
width: double.infinity,
padding: const EdgeInsets.all(12),
color: Colors.blue.shade50,
child: const Row(
children: [
Icon(Icons.mic, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text(
key: Key('header_text'),
'Ucapkan salah satu perintah di bawah ini untuk mengontrol WalkGuide',
style: TextStyle(color: Colors.blue),
),
),
],
),
),
// Search bar (opsional)
if (widget.showSearch)
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
key: const Key('search_field'),
decoration: const InputDecoration(
hintText: 'Cari perintah...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) => setState(() => _searchQuery = value),
),
),
// Filter kategori
if (_categories.length > 1)
SizedBox(
height: 44,
child: ListView(
key: const Key('category_filter'),
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
key: const Key('filter_all'),
label: const Text('Semua'),
selected: _selectedCategory == null,
onSelected: (_) =>
setState(() => _selectedCategory = null),
),
),
..._categories.map((cat) => Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
key: Key('filter_$cat'),
label: Text(cat),
selected: _selectedCategory == cat,
onSelected: (_) =>
setState(() => _selectedCategory = cat),
),
)),
],
),
),
// Jumlah perintah
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
key: const Key('command_count'),
'${_filteredCommands.length} perintah tersedia',
style: TextStyle(color: Colors.grey.shade600, fontSize: 13),
),
),
),
// Daftar perintah
Expanded(
child: _filteredCommands.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.search_off,
size: 48,
color: Colors.grey,
key: Key('no_result_icon')),
SizedBox(height: 8),
Text(
key: Key('no_result_text'),
'Tidak ada perintah yang cocok',
style: TextStyle(color: Colors.grey),
),
],
),
)
: ListView.separated(
key: const Key('command_list'),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
itemCount: _filteredCommands.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final cmd = _filteredCommands[index];
return ListTile(
key: Key('cmd_tile_${cmd.key.name}'),
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: const Icon(Icons.record_voice_over,
color: Colors.blue),
),
title: Text(
key: Key('cmd_phrase_${cmd.key.name}'),
'"${cmd.phrase}"',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
key: Key('cmd_desc_${cmd.key.name}'),
cmd.description,
style: TextStyle(
fontSize: 12, color: Colors.grey.shade700),
),
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(10),
),
child: Text(
key: Key('cmd_category_${cmd.key.name}'),
cmd.category,
style: const TextStyle(fontSize: 11),
),
),
],
),
trailing: cmd.enabled
? null
: const Icon(Icons.block,
color: Colors.grey,
size: 16,
key: Key('disabled_icon')),
isThreeLine: true,
);
},
),
),
],
),
);
}
}
Widget makeTestable(Widget child) => MaterialApp(home: child);
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
group('ManualScreen Widget Tests', () {
// ── Rendering awal ─────────────────────────────────────────────────────
group('Rendering awal', () {
testWidgets('menampilkan AppBar dengan judul Panduan Suara',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.text('Panduan Suara'), findsOneWidget);
});
testWidgets('menampilkan header banner instruksi', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('header_banner')), findsOneWidget);
expect(find.byKey(const Key('header_text')), findsOneWidget);
});
testWidgets('menampilkan daftar semua perintah default', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('command_list')), findsOneWidget);
});
testWidgets('menampilkan jumlah perintah yang benar (14)',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.text('14 perintah tersedia'), findsOneWidget);
});
testWidgets('menampilkan tombol info di AppBar', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('info_button')), findsOneWidget);
});
testWidgets('menampilkan filter kategori', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('category_filter')), findsOneWidget);
});
testWidgets('filter Semua tersedia', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('filter_all')), findsOneWidget);
});
});
// ── Konten perintah ───────────────────────────────────────────────────
group('Konten perintah suara', () {
testWidgets('menampilkan tile untuk perintah Open Walkguide',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.scrollUntilVisible(
find.byKey(const Key('cmd_tile_openWalkguide')),
200,
);
expect(find.byKey(const Key('cmd_tile_openWalkguide')), findsOneWidget);
});
testWidgets('menampilkan phrase perintah dalam tanda kutip',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.text('"Open Walkguide"'), findsOneWidget);
});
testWidgets('menampilkan deskripsi perintah', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.text('Membuka layar WalkGuide'), findsOneWidget);
});
testWidgets('menampilkan kategori perintah', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('cmd_category_openWalkguide')),
findsOneWidget);
expect(find.text('Navigasi'), findsAtLeastNWidgets(1));
});
testWidgets('menampilkan perintah Call Guardian', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.text('"Call Guardian"'), findsOneWidget);
});
testWidgets('menampilkan perintah Send SOS', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.scrollUntilVisible(find.text('"Send SOS"'), 200);
expect(find.text('"Send SOS"'), findsOneWidget);
});
testWidgets('menampilkan perintah Where Am I', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.scrollUntilVisible(find.text('"Where Am I"'), 200);
expect(find.text('"Where Am I"'), findsOneWidget);
});
testWidgets('menampilkan kategori Darurat untuk Send SOS',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.scrollUntilVisible(
find.byKey(const Key('cmd_category_sendSos')), 200);
expect(find.byKey(const Key('cmd_category_sendSos')), findsOneWidget);
});
});
// ── Dialog info ───────────────────────────────────────────────────────
group('Dialog info penggunaan', () {
testWidgets('tap info button membuka dialog', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('info_button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('info_dialog')), findsOneWidget);
expect(find.text('Cara Penggunaan'), findsOneWidget);
});
testWidgets('dialog berisi teks instruksi', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('info_button')));
await tester.pumpAndSettle();
expect(find.textContaining('mikrofon'), findsOneWidget);
});
testWidgets('tombol Tutup menutup dialog', (tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('info_button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('info_dialog')), findsOneWidget);
await tester.tap(find.byKey(const Key('info_close_button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('info_dialog')), findsNothing);
});
});
// ── Filter kategori ───────────────────────────────────────────────────
group('Filter kategori', () {
testWidgets('tap filter Darurat menampilkan hanya perintah darurat',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('filter_Darurat')));
await tester.pump();
// Hanya Open SOS dan Send SOS
expect(find.byKey(const Key('command_count')), findsOneWidget);
expect(find.text('2 perintah tersedia'), findsOneWidget);
});
testWidgets('tap filter Komunikasi menampilkan hanya Call Guardian',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('filter_Komunikasi')));
await tester.pump();
expect(find.text('1 perintah tersedia'), findsOneWidget);
});
testWidgets('tap filter Semua menampilkan kembali semua perintah',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('filter_Darurat')));
await tester.pump();
await tester.tap(find.byKey(const Key('filter_all')));
await tester.pump();
expect(find.text('14 perintah tersedia'), findsOneWidget);
});
testWidgets('tap filter Aksesibilitas menampilkan 3 perintah',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
await tester.tap(find.byKey(const Key('filter_Aksesibilitas')));
await tester.pump();
expect(find.text('3 perintah tersedia'), findsOneWidget);
});
});
// ── Search ────────────────────────────────────────────────────────────
group('Pencarian perintah (showSearch=true)', () {
testWidgets('menampilkan search field saat showSearch=true',
(tester) async {
await tester.pumpWidget(
makeTestable(const _StubManualScreen(showSearch: true)),
);
expect(find.byKey(const Key('search_field')), findsOneWidget);
});
testWidgets('tidak menampilkan search field secara default',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('search_field')), findsNothing);
});
testWidgets('search "SOS" memfilter perintah SOS', (tester) async {
await tester.pumpWidget(
makeTestable(const _StubManualScreen(showSearch: true)),
);
await tester.enterText(find.byKey(const Key('search_field')), 'SOS');
await tester.pump();
expect(find.text('2 perintah tersedia'), findsOneWidget);
});
testWidgets('search tanpa hasil menampilkan empty state', (tester) async {
await tester.pumpWidget(
makeTestable(const _StubManualScreen(showSearch: true)),
);
await tester.enterText(
find.byKey(const Key('search_field')), 'xyz tidak ada');
await tester.pump();
expect(find.byKey(const Key('no_result_text')), findsOneWidget);
});
testWidgets('search "guardian" menemukan Call Guardian', (tester) async {
await tester.pumpWidget(
makeTestable(const _StubManualScreen(showSearch: true)),
);
await tester.enterText(
find.byKey(const Key('search_field')), 'guardian');
await tester.pump();
expect(find.text('"Call Guardian"'), findsOneWidget);
});
});
// ── Perintah disabled ─────────────────────────────────────────────────
group('Perintah disabled', () {
testWidgets('perintah disabled menampilkan icon block', (tester) async {
final disabledCommands = [
const VoiceCommandEntry(
key: VoiceCommandKey.callGuardian,
phrase: 'Call Guardian',
description: 'Test disabled',
category: 'Komunikasi',
enabled: false,
),
];
await tester.pumpWidget(makeTestable(
_StubManualScreen(commands: disabledCommands),
));
expect(find.byKey(const Key('disabled_icon')), findsOneWidget);
});
testWidgets('perintah enabled tidak menampilkan icon block',
(tester) async {
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(find.byKey(const Key('disabled_icon')), findsNothing);
});
});
// ── 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 _StubManualScreen()));
expect(tester.takeException(), isNull);
});
testWidgets('tidak overflow pada layar 428x926 (iPhone 14 Pro Max)',
(tester) async {
tester.view.physicalSize = const Size(428, 926);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
expect(tester.takeException(), isNull);
});
});
});
}