// 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 = []; 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 = []; 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 = []; 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); }); }