295 lines
9.2 KiB
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);
|
|
});
|
|
}
|