import 'dart:async'; import 'package:ducafe_ui_core/ducafe_ui_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:forui/forui.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; import 'package:pj_supermarket_cloud/pages/setting/camera_setting_controller.dart'; import 'package:pj_supermarket_cloud/pages/setting/widgets/setting_camera.dart'; import 'package:pj_supermarket_cloud/pages/setting/widgets/setting_components.dart'; /// 通过真实点击路径核对候选草稿、保存/取消、权限及异步清理,不连接相机或虹软。 void main() { TestWidgetsFlutterBinding.ensureInitialized(); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; const eventChannel = MethodChannel('local/face_discern_session_event'); late List calls; late List> cameras; int? savedCameraId; var failRefresh = false; var failSave = false; Completer>? pendingPreview; Map snapshot() => { 'cameras': cameras, 'primaryCameraId': savedCameraId, 'refreshedAt': 1792454400000, }; Map previewMetadata(MethodCall call) => { 'previewSessionId': call.arguments['previewSessionId'], 'textureId': calls.length, 'cameraId': call.arguments['cameraId'] ?? 0, 'width': 1280, 'height': 720, 'rotation': 0, 'mirror': false, }; setUp(() { Get.testMode = true; calls = []; cameras = [ {'cameraId': 0, 'facing': 0, 'orientation': 0}, {'cameraId': 1, 'facing': 1, 'orientation': 0}, ]; savedCameraId = 0; failRefresh = false; failSave = false; pendingPreview = null; messenger.setMockMethodCallHandler(eventChannel, (_) async => null); messenger.setMockMethodCallHandler(FaceDiscernPlugs.getChannel, (call) async { calls.add(call); switch (call.method) { case 'getCameraConfiguration': return snapshot(); case 'refreshCameraList': if (failRefresh) { throw PlatformException( code: 'CAMERA_LIST_FAILED', message: '刷新失败,保留原目录'); } return snapshot(); case 'startCameraSelectionPreview': return pendingPreview == null ? previewMetadata(call) : await pendingPreview!.future; case 'stopCameraSelectionPreview': return null; case 'setPrimaryCamera': if (failSave) { throw PlatformException( code: 'SAVE_PRIMARY_CAMERA_FAILED', message: '主摄保存失败,请重试'); } savedCameraId = call.arguments['cameraId'] as int?; return snapshot(); default: throw PlatformException( code: 'UNEXPECTED_CALL', message: call.method); } }); }); tearDown(() { FToast().removeCustomToast(); FToast().removeQueuedCustomToasts(); Get.reset(); messenger.setMockMethodCallHandler(FaceDiscernPlugs.getChannel, null); messenger.setMockMethodCallHandler(eventChannel, null); }); Future<_GrantedCameraController> mount( WidgetTester tester, { Size size = const Size(1920, 1080), double textScale = 1, }) async { await tester.binding.setSurfaceSize(size); addTearDown(() => tester.binding.setSurfaceSize(null)); final controller = _GrantedCameraController(); Get.put(controller); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(1920, 1080), builder: (_, __) => GetMaterialApp( builder: (context, child) => FTheme( data: FThemes.zinc.light, child: FToastBuilder()( context, MediaQuery( data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear(textScale), ), child: child!, ), ), ), home: const Scaffold( body: SingleChildScrollView(child: CameraSettingGroup()), ), ), ), ); await tester.pumpAndSettle(); return controller; } Future openSelection(WidgetTester tester) async { await tester.tap(find.text('选择摄像头')); await tester.pumpAndSettle(); } testWidgets('切换更新选中态及画面编号,取消保留旧主摄且不启动识别', (tester) async { final controller = await mount(tester); await openSelection(tester); await tester.tap(find.text('摄像头 #1')); await tester.pumpAndSettle(); final tile = tester.widget>( find.ancestor( of: find.text('摄像头 #1'), matching: find.byType(RadioListTile)), ); expect(tile.groupValue, 1); expect(controller.preview.value?.cameraId, 1); expect(savedCameraId, 0); expect(calls.where((call) => call.method == 'startRecognition'), isEmpty); expect(calls.where((call) => call.method == 'setPrimaryCamera'), isEmpty); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); expect(controller.selectionOpen.value, isFalse); expect(controller.preview.value, isNull); expect(savedCameraId, 0); expect( calls .where((call) => call.method == 'stopCameraSelectionPreview') .length, greaterThanOrEqualTo(2)); expect(tester.takeException(), isNull); }); testWidgets('保存失败保留画面和原选择,重试保存后重新读取能恢复', (tester) async { final controller = await mount(tester); await openSelection(tester); await tester.tap(find.text('摄像头 #1')); await tester.pumpAndSettle(); failSave = true; await tester.tap(find.text('保存选择')); await tester.pumpAndSettle(); expect(find.text('主摄保存失败,请重试'), findsOneWidget); expect(controller.preview.value?.cameraId, 1); expect(savedCameraId, 0); failSave = false; await tester.tap(find.text('保存选择')); await tester.pumpAndSettle(); await tester.pump(const Duration(seconds: 4)); await tester.pumpAndSettle(); expect(savedCameraId, 1); expect(find.byType(CameraSelectionDialog), findsNothing); controller.configuration.value = null; await controller.loadConfiguration(); await tester.pump(); expect( find.byWidgetPredicate((widget) => widget is SettingInfoRow && widget.label == '识别摄像头' && widget.value == '摄像头 #1'), findsOneWidget, ); expect(tester.takeException(), isNull); }); testWidgets('刷新失败保留缓存,成功的空列表禁用识别摄像头并保留失联偏好', (tester) async { final controller = await mount(tester); failRefresh = true; await tester.tap(find.text('刷新列表')); await tester.pumpAndSettle(); expect(find.text('刷新失败,保留原目录'), findsOneWidget); expect(controller.configuration.value?.cameras.length, 2); failRefresh = false; cameras = []; await tester.tap(find.text('刷新列表')); await tester.pumpAndSettle(); expect(find.text('未发现摄像头,请检查设备后刷新列表'), findsOneWidget); final select = tester .widgetList(find.byType(SettingActionButton)) .singleWhere((button) => button.label == '选择摄像头'); expect(select.enabled, isFalse); expect(controller.configuration.value?.primaryMissing, isTrue); expect(savedCameraId, 0); }); testWidgets('候选创建期间取消会释放迟到纹理且不改变主摄', (tester) async { final controller = await mount(tester); await openSelection(tester); pendingPreview = Completer>(); await tester.tap(find.text('摄像头 #1')); await tester.pump(); final pendingCall = calls.lastWhere((call) => call.method == 'startCameraSelectionPreview'); expect(controller.isPreviewing.value, isTrue); expect(controller.canSave, isFalse); expect( tester.widget(find.widgetWithText(FButton, '保存选择')).onPress, isNull, ); await tester.tap(find.text('取消')); await tester.pump(const Duration(milliseconds: 300)); pendingPreview!.complete(previewMetadata(pendingCall)); await tester.pumpAndSettle(); expect(controller.preview.value, isNull); expect(controller.selectionOpen.value, isFalse); expect(savedCameraId, 0); expect( calls.where((call) => call.method == 'stopCameraSelectionPreview' && call.arguments['previewSessionId'] == pendingCall.arguments['previewSessionId']), isNotEmpty); }); testWidgets('后台释放候选,前台重试后重新获得可保存的画面', (tester) async { final controller = await mount(tester); await openSelection(tester); controller.didChangeAppLifecycleState(AppLifecycleState.paused); await tester.pump(); expect(controller.preview.value, isNull); expect(controller.canSave, isFalse); controller.didChangeAppLifecycleState(AppLifecycleState.resumed); await tester.ensureVisible(find.text('重新预览')); await tester.pumpAndSettle(); await tester.tap(find.text('重新预览')); await tester.pumpAndSettle(); expect(controller.preview.value, isNotNull); expect(controller.canSave, isTrue); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); }); testWidgets('权限永久拒绝展示设置入口,授权重试之前不能保存', (tester) async { final controller = await mount(tester); controller.granted = false; await openSelection(tester); expect(find.text('打开系统设置'), findsOneWidget); expect(controller.canSave, isFalse); expect(calls.where((call) => call.method == 'startCameraSelectionPreview'), isEmpty); controller.granted = true; await tester.ensureVisible(find.text('重新预览')); await tester.pumpAndSettle(); await tester.tap(find.text('重新预览')); await tester.pumpAndSettle(); expect(controller.canSave, isTrue); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); }); testWidgets('自动模式先预览默认设备,保存后重新创建控制器仍回显自动', (tester) async { final controller = await mount(tester); await openSelection(tester); await tester.tap(find.text('自动(默认规则)')); await tester.pumpAndSettle(); expect(controller.preview.value?.cameraId, 0); expect(savedCameraId, 0); await tester.tap(find.text('保存选择')); await tester.pumpAndSettle(); expect(savedCameraId, isNull); await tester.pump(const Duration(seconds: 4)); await tester.pumpAndSettle(); await tester.pumpWidget(const SizedBox.shrink()); Get.delete(); final restored = await mount(tester); expect(restored.configuration.value?.primaryCameraId, isNull); expect(find.text('自动(默认规则)'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('迟到的旧设备错误不关闭新候选,当前设备断开则禁止保存', (tester) async { final controller = await mount(tester); await openSelection(tester); final previous = controller.preview.value!.previewSessionId; await tester.tap(find.text('摄像头 #1')); await tester.pumpAndSettle(); final current = controller.preview.value!.previewSessionId; void emitFailure(String sessionId) => controller.events.add( FaceRecognitionSessionEvent.fromMap({ 'cameraPreviewSessionId': sessionId, 'message': '摄像头已断开,请重新预览', }), ); emitFailure(previous); await tester.pump(); expect(controller.preview.value?.previewSessionId, current); expect(controller.canSave, isTrue); emitFailure(current); await tester.pump(); expect(controller.preview.value, isNull); expect(controller.canSave, isFalse); expect(savedCameraId, 0); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); }); for (final size in [const Size(800, 600), const Size(1280, 720)]) { testWidgets('较小窗口 $size 及放大字体仍能切换、取消', (tester) async { final controller = await mount(tester, size: size, textScale: 1.5); await openSelection(tester); await tester.ensureVisible(find.text('摄像头 #1')); await tester.tap(find.text('摄像头 #1')); await tester.pumpAndSettle(); expect(controller.preview.value?.cameraId, 1); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); expect(savedCameraId, 0); expect(tester.takeException(), isNull); }); } } /// 用受控权限代替真实 Android 授权,其余通道和页面逻辑均使用正式实现。 class _GrantedCameraController extends CameraSettingController { /// 为每个测试实例创建独立的事件源,避免全局原生流跨测试异步区域存活。 _GrantedCameraController() : this._(StreamController.broadcast()); /// 仍使用正式模型和错误处理,只替换不依赖实际 Engine 的事件传输。 _GrantedCameraController._(this.events) : super(previewEvents: events.stream); /// 当前测试拥有的设备错误源,生命周期随控制器结束。 final StreamController events; /// 测试当前是否允许打开相机,false 时模拟永久拒绝。 bool granted = true; /// 将受控权限同步到正式的系统设置入口状态。 @override Future requestCameraPermission() async { permissionCanOpenSettings.value = !granted; return granted; } /// 先取消正式订阅,再结束测试事件源,避免向已销毁的 Obx 回填。 @override void onClose() { super.onClose(); unawaited(events.close()); } }