feat(voice): add iOS VAD runtime support
This commit is contained in:
@@ -11,35 +11,224 @@
|
||||
// Verification-plan rows: SWE4-UV-014, SWE4-UV-019, SWE4-UV-020 (swe4-unit-verification-plan.md).
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
|
||||
import 'package:chanora_flutter/services/android_permissions_service.dart';
|
||||
import 'package:chanora_flutter/services/ios_permissions_service.dart';
|
||||
import 'package:chanora_flutter/widgets/permission_state_banner.dart';
|
||||
import 'package:chanora_flutter/widgets/voice_compact.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders English banner', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Builder(builder: (ctx) {
|
||||
final l10n = AppL10n.of(ctx);
|
||||
return Scaffold(body: Text(l10n.homeNotProductionReadyBanner));
|
||||
}),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (ctx) {
|
||||
final l10n = AppL10n.of(ctx);
|
||||
return Scaffold(body: Text(l10n.homeNotProductionReadyBanner));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Beta build'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders Simplified Chinese banner', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
locale: const Locale('zh'),
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Builder(builder: (ctx) {
|
||||
final l10n = AppL10n.of(ctx);
|
||||
return Scaffold(body: Text(l10n.homeNotProductionReadyBanner));
|
||||
}),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('zh'),
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (ctx) {
|
||||
final l10n = AppL10n.of(ctx);
|
||||
return Scaffold(body: Text(l10n.homeNotProductionReadyBanner));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Beta 版本'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('localizes diagnostic export save action', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (ctx) {
|
||||
final l10n = AppL10n.of(ctx);
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
Text(l10n.diagnosticsSaveAction),
|
||||
Text(l10n.diagnosticsSaved('/tmp/chanora-diagnostics.txt')),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Save export'), findsOneWidget);
|
||||
expect(find.textContaining('chanora-diagnostics.txt'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('on-screen PTT exposes hold-to-talk semantics', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: VoicePttButton(active: false, onHeldChanged: (_) {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final node = tester.getSemantics(find.byType(VoicePttButton));
|
||||
expect(node.label, 'Hold to talk');
|
||||
expect(node.hint, 'Press and hold to transmit voice; release to stop.');
|
||||
expect(node.hasFlag(SemanticsFlag.isButton), isTrue);
|
||||
expect(node.hasFlag(SemanticsFlag.isLiveRegion), isTrue);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('permission banner grants denied microphone access', (
|
||||
tester,
|
||||
) async {
|
||||
final state = ValueNotifier(AndroidRecordAudioPermissionState.denied);
|
||||
var grantCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: PermissionStateBanner.fromCallbacks(
|
||||
recordAudioState: state,
|
||||
ensureRecordAudio: () async {
|
||||
grantCount += 1;
|
||||
return AndroidRecordAudioPermissionState.granted;
|
||||
},
|
||||
openAppSettings: () async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Grant'), findsOneWidget);
|
||||
await tester.tap(find.text('Grant'));
|
||||
expect(grantCount, 1);
|
||||
state.dispose();
|
||||
});
|
||||
|
||||
testWidgets('permission banner opens settings after permanent denial', (
|
||||
tester,
|
||||
) async {
|
||||
final state = ValueNotifier(
|
||||
AndroidRecordAudioPermissionState.permanentlyDenied,
|
||||
);
|
||||
var settingsCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: PermissionStateBanner.fromCallbacks(
|
||||
recordAudioState: state,
|
||||
ensureRecordAudio: () async =>
|
||||
AndroidRecordAudioPermissionState.permanentlyDenied,
|
||||
openAppSettings: () async {
|
||||
settingsCount += 1;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Open System Settings'), findsOneWidget);
|
||||
await tester.tap(find.text('Open System Settings'));
|
||||
expect(settingsCount, 1);
|
||||
state.dispose();
|
||||
});
|
||||
|
||||
testWidgets('on-screen PTT reports press and release gestures', (
|
||||
tester,
|
||||
) async {
|
||||
final states = <bool>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: VoicePttButton(active: false, onHeldChanged: states.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final center = tester.getCenter(find.byType(VoicePttButton));
|
||||
final gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(states, [true, false]);
|
||||
});
|
||||
|
||||
testWidgets('iOS permission service maps channel states and settings', (
|
||||
tester,
|
||||
) async {
|
||||
const channel = MethodChannel(iosPlatformChannelName);
|
||||
final methods = <String>[];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
methods.add(call.method);
|
||||
switch (call.method) {
|
||||
case methodGetMicrophonePermissionState:
|
||||
return 'NotDetermined';
|
||||
case methodRequestMicrophonePermission:
|
||||
return 'Granted';
|
||||
case methodIosOpenAppSettings:
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
final service = IosPermissionsService(channel: channel);
|
||||
await service.start();
|
||||
expect(
|
||||
service.recordAudioState.value,
|
||||
AndroidRecordAudioPermissionState.denied,
|
||||
);
|
||||
expect(
|
||||
await service.ensureRecordAudio(),
|
||||
AndroidRecordAudioPermissionState.granted,
|
||||
);
|
||||
await service.openAppSettings();
|
||||
|
||||
expect(methods, [
|
||||
methodGetMicrophonePermissionState,
|
||||
methodRequestMicrophonePermission,
|
||||
methodIosOpenAppSettings,
|
||||
]);
|
||||
|
||||
service.dispose();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user