// test/unit/voice_command_handler_test.dart // // Unit test untuk VoiceCommandHandler — matching & dispatching command. // Jalankan: flutter test test/unit/voice_command_handler_test.dart // // Pure Dart test, tidak butuh platform plugin. import 'package:flutter_test/flutter_test.dart'; // ---------- Stubs (mirror dari project asli) ---------- enum VoiceCommandKey { openWalkguide, startWalkguide, stopWalkguide, callGuardian, openNotification, readAllNotif, openSos, sendSos, whereAmI, openActivity, openNavigation, openSettings, repeatLast, stopTts, } class VoiceCommand { final VoiceCommandKey key; final String phrase; final bool enabled; const VoiceCommand( {required this.key, required this.phrase, required this.enabled}); } // Minimal TtsService stub class _FakeTts { String? lastSpeech; bool stopped = false; void repeatLast() => lastSpeech = 'repeated'; void stop() => stopped = true; } // Minimal SttService stub class _FakeStt { void Function(String)? onResult; void simulateUtterance(String text) => onResult?.call(text); } // VoiceCommandHandler (mirror dari project) typedef CommandCallback = void Function(VoiceCommandKey key); class _VoiceCommandHandler { final _FakeStt _stt; final _FakeTts _tts; List _commands = []; CommandCallback? onCommand; _VoiceCommandHandler(this._stt, this._tts); void loadCommands(List commands) { _commands = commands; _stt.onResult = _processText; } void loadDefaultCommands() { _commands = const [ VoiceCommand( key: VoiceCommandKey.openWalkguide, phrase: 'open walkguide', enabled: true), VoiceCommand( key: VoiceCommandKey.startWalkguide, phrase: 'start walkguide', enabled: true), VoiceCommand( key: VoiceCommandKey.stopWalkguide, phrase: 'stop walkguide', enabled: true), VoiceCommand( key: VoiceCommandKey.callGuardian, phrase: 'call guardian', enabled: true), VoiceCommand( key: VoiceCommandKey.openNotification, phrase: 'open notifications', enabled: true), VoiceCommand( key: VoiceCommandKey.readAllNotif, phrase: 'read all my notifications', enabled: true), VoiceCommand( key: VoiceCommandKey.openSos, phrase: 'open sos', enabled: true), VoiceCommand( key: VoiceCommandKey.sendSos, phrase: 'send sos', enabled: true), VoiceCommand( key: VoiceCommandKey.whereAmI, phrase: 'where am i', enabled: true), VoiceCommand( key: VoiceCommandKey.openActivity, phrase: 'open activity log', enabled: true), VoiceCommand( key: VoiceCommandKey.openNavigation, phrase: 'open navigation', enabled: true), VoiceCommand( key: VoiceCommandKey.openSettings, phrase: 'open settings', enabled: true), VoiceCommand( key: VoiceCommandKey.repeatLast, phrase: 'repeat', enabled: true), VoiceCommand(key: VoiceCommandKey.stopTts, phrase: 'stop', enabled: true), ]; _stt.onResult = _processText; } void _processText(String text) { final lower = text.toLowerCase().trim(); for (final cmd in _commands) { if (!cmd.enabled) continue; if (lower.contains(cmd.phrase.toLowerCase())) { _handleCommand(cmd.key); return; } } } void _handleCommand(VoiceCommandKey key) { onCommand?.call(key); if (key == VoiceCommandKey.repeatLast) _tts.repeatLast(); if (key == VoiceCommandKey.stopTts) _tts.stop(); } } // ---------- Tests ---------- void main() { late _VoiceCommandHandler handler; late _FakeStt fakeStt; late _FakeTts fakeTts; final List dispatchedCommands = []; setUp(() { fakeStt = _FakeStt(); fakeTts = _FakeTts(); handler = _VoiceCommandHandler(fakeStt, fakeTts); dispatchedCommands.clear(); handler.loadDefaultCommands(); handler.onCommand = dispatchedCommands.add; }); // ── BASIC COMMAND MATCHING ──────────────────────────────────────── group('default commands — exact phrase match', () { test('"start walkguide" → startWalkguide', () { fakeStt.simulateUtterance('start walkguide'); expect(dispatchedCommands, contains(VoiceCommandKey.startWalkguide)); }); test('"stop walkguide" → stopWalkguide', () { fakeStt.simulateUtterance('stop walkguide'); expect(dispatchedCommands, contains(VoiceCommandKey.stopWalkguide)); }); test('"call guardian" → callGuardian', () { fakeStt.simulateUtterance('call guardian'); expect(dispatchedCommands, contains(VoiceCommandKey.callGuardian)); }); test('"open notifications" → openNotification', () { fakeStt.simulateUtterance('open notifications'); expect(dispatchedCommands, contains(VoiceCommandKey.openNotification)); }); test('"send sos" → sendSos', () { fakeStt.simulateUtterance('send sos'); expect(dispatchedCommands, contains(VoiceCommandKey.sendSos)); }); test('"where am i" → whereAmI', () { fakeStt.simulateUtterance('where am i'); expect(dispatchedCommands, contains(VoiceCommandKey.whereAmI)); }); test('"open activity log" → openActivity', () { fakeStt.simulateUtterance('open activity log'); expect(dispatchedCommands, contains(VoiceCommandKey.openActivity)); }); test('"open navigation" → openNavigation', () { fakeStt.simulateUtterance('open navigation'); expect(dispatchedCommands, contains(VoiceCommandKey.openNavigation)); }); test('"open settings" → openSettings', () { fakeStt.simulateUtterance('open settings'); expect(dispatchedCommands, contains(VoiceCommandKey.openSettings)); }); test('"read all my notifications" → readAllNotif', () { fakeStt.simulateUtterance('read all my notifications'); expect(dispatchedCommands, contains(VoiceCommandKey.readAllNotif)); }); }); // ── CASE INSENSITIVITY ──────────────────────────────────────────── group('case insensitive matching', () { test('"START WALKGUIDE" → startWalkguide', () { fakeStt.simulateUtterance('START WALKGUIDE'); expect(dispatchedCommands, contains(VoiceCommandKey.startWalkguide)); }); test('"Start Walkguide" → startWalkguide', () { fakeStt.simulateUtterance('Start Walkguide'); expect(dispatchedCommands, contains(VoiceCommandKey.startWalkguide)); }); test('"CALL GUARDIAN" → callGuardian', () { fakeStt.simulateUtterance('CALL GUARDIAN'); expect(dispatchedCommands, contains(VoiceCommandKey.callGuardian)); }); test('"Send SOS" → sendSos', () { fakeStt.simulateUtterance('Send SOS'); expect(dispatchedCommands, contains(VoiceCommandKey.sendSos)); }); }); // ── PHRASE CONTAINS MATCH (dari kalimat lebih panjang) ─────────── group('phrase contains match', () { test('kalimat panjang mengandung "start walkguide" → startWalkguide', () { fakeStt.simulateUtterance('tolong start walkguide sekarang'); expect(dispatchedCommands, contains(VoiceCommandKey.startWalkguide)); }); test('kalimat panjang mengandung "send sos" → sendSos', () { fakeStt.simulateUtterance('saya mau send sos ke guardian'); expect(dispatchedCommands, contains(VoiceCommandKey.sendSos)); }); test('kalimat panjang mengandung "call guardian" → callGuardian', () { fakeStt.simulateUtterance('bisa tolong call guardian untuk aku'); expect(dispatchedCommands, contains(VoiceCommandKey.callGuardian)); }); }); // ── NO MATCH ────────────────────────────────────────────────────── group('unrecognized input tidak memicu command', () { test('kata random tidak memicu command', () { fakeStt.simulateUtterance('halo apa kabar'); expect(dispatchedCommands, isEmpty); }); test('string kosong tidak memicu command', () { fakeStt.simulateUtterance(''); expect(dispatchedCommands, isEmpty); }); test('kata mirip tapi bukan phrase yang valid tidak match', () { fakeStt.simulateUtterance('starting guide'); expect(dispatchedCommands, isEmpty); }); test('spasi saja tidak memicu command', () { fakeStt.simulateUtterance(' '); expect(dispatchedCommands, isEmpty); }); }); // ── BUILT-IN TTS ACTIONS ───────────────────────────────────────── group('built-in TTS actions', () { test('"repeat" → memanggil tts.repeatLast()', () { fakeStt.simulateUtterance('repeat'); expect(fakeTts.lastSpeech, 'repeated'); expect(dispatchedCommands, contains(VoiceCommandKey.repeatLast)); }); test('"stop" → memanggil tts.stop()', () { fakeStt.simulateUtterance('stop'); expect(fakeTts.stopped, true); expect(dispatchedCommands, contains(VoiceCommandKey.stopTts)); }); }); // ── DISABLED COMMAND ───────────────────────────────────────────── group('disabled command tidak memicu callback', () { test('command disabled tidak di-dispatch', () { handler.loadCommands([ const VoiceCommand( key: VoiceCommandKey.sendSos, phrase: 'send sos', enabled: false), const VoiceCommand( key: VoiceCommandKey.callGuardian, phrase: 'call guardian', enabled: true), ]); fakeStt.simulateUtterance('send sos'); expect(dispatchedCommands, isEmpty); }); test('command enabled di-dispatch, disabled tidak', () { handler.loadCommands([ const VoiceCommand( key: VoiceCommandKey.sendSos, phrase: 'send sos', enabled: false), const VoiceCommand( key: VoiceCommandKey.callGuardian, phrase: 'call guardian', enabled: true), ]); fakeStt.simulateUtterance('call guardian'); expect(dispatchedCommands, contains(VoiceCommandKey.callGuardian)); expect(dispatchedCommands, isNot(contains(VoiceCommandKey.sendSos))); }); }); // ── CUSTOM COMMANDS ─────────────────────────────────────────────── group('loadCommands dengan custom phrase', () { test('custom phrase "mulai jalan" → startWalkguide', () { handler.loadCommands([ const VoiceCommand( key: VoiceCommandKey.startWalkguide, phrase: 'mulai jalan', enabled: true), ]); fakeStt.simulateUtterance('mulai jalan'); expect(dispatchedCommands, contains(VoiceCommandKey.startWalkguide)); }); test('custom phrase "darurat" → sendSos', () { handler.loadCommands([ const VoiceCommand( key: VoiceCommandKey.sendSos, phrase: 'darurat', enabled: true), ]); fakeStt.simulateUtterance('ini darurat tolong'); expect(dispatchedCommands, contains(VoiceCommandKey.sendSos)); }); }); // ── FIRST MATCH WINS ───────────────────────────────────────────── group('hanya satu command yang dipicu per utterance', () { test('hanya command pertama yang match yang dipicu', () { fakeStt.simulateUtterance('stop walkguide now'); // "stop walkguide" match stopWalkguide, tapi "stop" juga ada di list // Urutan di default list: stopWalkguide (index 2) sebelum stopTts (index 13) expect(dispatchedCommands.length, 1); }); }); }