373 lines
14 KiB
Dart
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());
|
|
}
|
|
}
|