Files
pj_supermarket_cloud/test/setting_camera_test.dart
T
2026-10-28 19:11:38 +08:00

373 lines
14 KiB
Dart

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<MethodCall> calls;
late List<Map<String, dynamic>> cameras;
int? savedCameraId;
var failRefresh = false;
var failSave = false;
Completer<Map<String, dynamic>>? pendingPreview;
Map<String, dynamic> snapshot() => {
'cameras': cameras,
'primaryCameraId': savedCameraId,
'refreshedAt': 1792454400000,
};
Map<String, dynamic> 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<CameraSettingController>(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<void> 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<RadioListTile<int?>>(
find.ancestor(
of: find.text('摄像头 #1'), matching: find.byType(RadioListTile<int?>)),
);
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<SettingActionButton>(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<Map<String, dynamic>>();
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<FButton>(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<CameraSettingController>();
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<FaceRecognitionSessionEvent>.broadcast());
/// 仍使用正式模型和错误处理,只替换不依赖实际 Engine 的事件传输。
_GrantedCameraController._(this.events) : super(previewEvents: events.stream);
/// 当前测试拥有的设备错误源,生命周期随控制器结束。
final StreamController<FaceRecognitionSessionEvent> events;
/// 测试当前是否允许打开相机,false 时模拟永久拒绝。
bool granted = true;
/// 将受控权限同步到正式的系统设置入口状态。
@override
Future<bool> requestCameraPermission() async {
permissionCanOpenSettings.value = !granted;
return granted;
}
/// 先取消正式订阅,再结束测试事件源,避免向已销毁的 Obx 回填。
@override
void onClose() {
super.onClose();
unawaited(events.close());
}
}