Files
chanora/apps/chanora_flutter/test/widget_test.dart

295 lines
9.2 KiB
Dart

// Smoke test for the Beta UI banners. Verifies the
// non-production-ready banner appears in both supported locales.
//
// Does NOT call into the FRB Rust side; that requires the cdylib at
// runtime and is verified by alpha_e2e_test.dart + beta_e2e_test.dart.
//
// Requirement trace (SWE.4 unit verification — banner widget):
// SRS-165..SRS-169 (localization), SRS-170..SRS-176 (Unicode / server content),
// SAD-014 (design-system + localization integration),
// SDD-031, SDD-032, SDD-033, SDD-034 (localization fallback + product-vs-server text).
// 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/main.dart';
import 'package:chanora_flutter/services/android_permissions_service.dart';
import 'package:chanora_flutter/services/ios_permissions_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chanora_flutter/widgets/permission_state_banner.dart';
import 'package:chanora_flutter/widgets/voice_compact.dart';
void main() {
testWidgets('theme menu emits selected mode from user action', (tester) async {
SharedPreferences.setMockInitialValues({});
var selected = ThemeMode.system;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
actions: [
ChanoraThemeModeMenu(
themeMode: ThemeMode.system,
onThemeModeChanged: (mode) async {
selected = mode;
},
),
],
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.palette_outlined));
await tester.pumpAndSettle();
await tester.tap(find.text('Dark').last);
await tester.pumpAndSettle();
expect(selected, ThemeMode.dark);
});
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.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.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('on-screen PTT can release from pan gestures', (tester) async {
final states = <bool>[];
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: VoicePttButton(
active: false,
listenForPan: true,
onHeldChanged: states.add,
),
),
),
);
final center = tester.getCenter(find.byType(VoicePttButton));
final gesture = await tester.startGesture(center);
await tester.pump();
await gesture.moveBy(const Offset(0, 24));
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);
});
}