360 lines
12 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|