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

360 lines
12 KiB
Dart

// 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<VoiceCommand> _commands = [];
CommandCallback? onCommand;
_VoiceCommandHandler(this._stt, this._tts);
void loadCommands(List<VoiceCommand> 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<VoiceCommandKey> 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);
});
});
}