// test/widget/manual_screen_test.dart // // 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 _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 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 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 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); }); }); }); }