halkyon/packages/halkyon_app/test/speech/device_speech_input_test.dart
Tizian 「ティツィアーン」 2d27451cb8
fix(app,learn,meditate): a device without speech recognition says so, and how to get one
On GrapheneOS, which installs no recogniser, the microphone did nothing
and stayed listening: speech_to_text's initialize throws a
PlatformException (recognizerNotAvailable) there rather than answering
false, and DeviceSpeechInput let it through. A listen now answers how it
went (SpeechStart: listening, noRecognizer, unavailable) and never
throws; Learn's field and Meditate's dream say that no speech
recognition is installed and name two to install (FUTO Voice Input,
Google's speech services), in halkyon_app's texts. It also passes
androidNoBluetooth, as the apps declare no Bluetooth permission the
plugin would otherwise ask for.

Whats-New: Where the device has no speech recognition, the microphone says so and how to get one.
Whats-New-De: Wo das Gerät keine Spracherkennung hat, sagt das Mikrofon das und wie du eine bekommst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:37 +01:00

73 lines
2.5 KiB
Dart

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
/// The device's speech recognition through `speech_to_text`, its platform
/// channel answering as Android does: a phone without a recogniser
/// installed (GrapheneOS has none) fails with a PlatformException, which
/// left the microphone doing nothing and stuck listening (4 Nov 2026).
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('plugin.csdcorp.com/speech_to_text');
late List<MethodCall> calls;
void answer(Object? Function(MethodCall call) answers) {
calls = [];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
calls.add(call);
return answers(call);
});
}
tearDown(
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null),
);
Future<SpeechStart> listen() => DeviceSpeechInput().listen(
language: 'en',
onWords: (_) {},
onDone: () => fail('nothing was heard'),
);
test('without a recogniser installed, it says so, asking for no '
'Bluetooth permission', () async {
answer(
(call) => throw PlatformException(
code: 'recognizerNotAvailable',
message: 'Speech recognition not available on this device',
),
);
expect(await listen(), SpeechStart.noRecognizer);
final initialize = calls.single;
expect(initialize.method, 'initialize');
expect(
(initialize.arguments as Map<Object?, Object?>)['noBluetooth'],
isTrue,
);
});
test('any other failure, it says it cannot listen now', () async {
answer((call) => throw PlatformException(code: 'missingContext'));
expect(await listen(), SpeechStart.unavailable);
});
test('the microphone refused, it says it cannot listen now', () async {
answer((call) => call.method == 'initialize' ? false : null);
expect(await listen(), SpeechStart.unavailable);
});
// Last: `speech_to_text` keeps one instance, initialised from here on.
test('a listen that fails to start says it cannot listen now', () async {
answer(
(call) => switch (call.method) {
'initialize' => true,
'locales' => <Object?>[],
'listen' => throw PlatformException(code: 'listenFailed'),
_ => null,
},
);
expect(await listen(), SpeechStart.unavailable);
});
}