Files
biz_xfj_cloud/test/pages/terminal/menu_query_controller_test.dart
T
2026-07-24 20:41:17 +08:00

302 lines
9.9 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/controller.dart';
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/reservation_menu_repository.dart';
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/reservation_recognition_gateway.dart';
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/reservation_sub_screen_gateway.dart';
void main() {
testWidgets('重复识别只查询一次,旧响应不能覆盖新身份', (tester) async {
final repository = _CompleterRepository();
final recognition = _FakeRecognitionGateway();
final subScreen = _FakeSubScreenGateway();
final controller = ReservationMenuQueryController(
repository: repository,
recognitionGateway: recognition,
subScreenGateway: subScreen,
networkAvailable: () => true,
);
addTearDown(controller.onClose);
controller.onInit();
await tester.pump();
expect(recognition.cardCallbacks, hasLength(1));
final first = recognition.emitCardAt(0, 'old-card');
final duplicate = recognition.emitCardAt(0, 'old-card');
await tester.pump();
expect(repository.calls, hasLength(1));
await duplicate;
await controller.startRecognition();
expect(recognition.cardCallbacks, hasLength(2));
final second = recognition.emitCardAt(1, 'new-card');
await tester.pump();
expect(repository.calls, hasLength(2));
repository.completers[1].complete(_result('李*华', 1));
await second;
expect(controller.result.value?.maskedName, '李*华');
repository.completers[0].complete(_result('张*明', 1));
await first;
expect(controller.result.value?.maskedName, '李*华');
expect(controller.status.value, ReservationMenuQueryStatus.success);
await controller.clearForPrivacy();
});
testWidgets('11 天本地分页、翻页续时并在到期后主副屏清空', (tester) async {
var now = DateTime(2026, 7, 24, 8);
final repository = _ImmediateRepository(_result('张*明', 11));
final recognition = _FakeRecognitionGateway();
final subScreen = _FakeSubScreenGateway();
final controller = ReservationMenuQueryController(
repository: repository,
recognitionGateway: recognition,
subScreenGateway: subScreen,
networkAvailable: () => true,
privacyDuration: const Duration(seconds: 3),
now: () => now,
);
addTearDown(controller.onClose);
controller.onInit();
await tester.pump();
await recognition.emitCardAt(0, 'card-1');
expect(controller.totalPages, 2);
expect(controller.currentDays, hasLength(10));
expect(controller.remainingSeconds.value, 3);
now = now.add(const Duration(seconds: 2));
await tester.pump(const Duration(seconds: 1));
expect(controller.remainingSeconds.value, 1);
await controller.changePage(2);
expect(controller.currentDays, hasLength(1));
expect(controller.remainingSeconds.value, 3);
expect(subScreen.updates.last.currentPage, 2);
expect(subScreen.updates.last.days, hasLength(1));
now = now.add(const Duration(seconds: 3));
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(controller.status.value, ReservationMenuQueryStatus.expired);
expect(controller.result.value, isNull);
expect(subScreen.clearCalls, 1);
});
testWidgets('断网时不调用预约接口,单屏不可用不影响主屏查询', (tester) async {
final offlineRepository = _ImmediateRepository(_result('张*明', 1));
final offlineRecognition = _FakeRecognitionGateway();
final offlineController = ReservationMenuQueryController(
repository: offlineRepository,
recognitionGateway: offlineRecognition,
subScreenGateway: _FakeSubScreenGateway(available: false),
networkAvailable: () => false,
);
addTearDown(offlineController.onClose);
offlineController.onInit();
await tester.pump();
await offlineRecognition.emitCardAt(0, 'card-offline');
expect(offlineRepository.calls, isEmpty);
expect(offlineController.status.value,
ReservationMenuQueryStatus.networkError);
final onlineRepository = _ImmediateRepository(_result('李*华', 1));
final onlineRecognition = _FakeRecognitionGateway();
final onlineController = ReservationMenuQueryController(
repository: onlineRepository,
recognitionGateway: onlineRecognition,
subScreenGateway: _FakeSubScreenGateway(available: false),
networkAvailable: () => true,
);
addTearDown(onlineController.onClose);
onlineController.onInit();
await tester.pump();
await onlineRecognition.emitCardAt(0, 'card-online');
expect(onlineController.status.value, ReservationMenuQueryStatus.success);
expect(onlineController.result.value?.maskedName, '李*华');
await onlineController.clearForPrivacy();
});
testWidgets('人脸方式先显示识别页,识别成功后才发送预约查询状态', (tester) async {
final repository = _ImmediateRepository(_result('张*明', 1));
final recognition = _FakeRecognitionGateway();
final subScreen = _FakeSubScreenGateway();
final controller = ReservationMenuQueryController(
repository: repository,
recognitionGateway: recognition,
subScreenGateway: subScreen,
networkAvailable: () => true,
);
addTearDown(controller.onClose);
controller.onInit();
await tester.pump();
subScreen.updates.clear();
await controller.selectMethod(ReservationRecognitionMethod.face);
expect(recognition.faceCallbacks, hasLength(1));
expect(
subScreen.updates,
isEmpty,
reason: '人脸识别开始后由统一人脸协议接管副屏,不能提前发送预约 loading',
);
await recognition.emitFaceAt(0, '1001');
expect(repository.calls, ['1001']);
expect(
subScreen.updates.map((event) => event.scenario),
['loading', 'success'],
);
await controller.clearForPrivacy();
});
test('预约日期模型按 5、10、11 天正确分页', () {
for (final entry in const {5: 1, 10: 1, 11: 2}.entries) {
final display = ReservationMenuDisplayData.fromService(
_result('张*明', entry.key),
);
expect(display.totalPages, entry.value);
expect(display.pageDays(1), hasLength(entry.key.clamp(0, 10)));
if (entry.key == 11) expect(display.pageDays(2), hasLength(1));
}
});
}
class _CompleterRepository implements ReservationMenuRepository {
final calls = <String>[];
final completers = <Completer<ServiceReservationMenuQuery?>>[];
@override
Future<ServiceReservationMenuQuery?> query({
required ReservationRecognitionMethod method,
required String identityValue,
}) {
calls.add(identityValue);
final completer = Completer<ServiceReservationMenuQuery?>();
completers.add(completer);
return completer.future;
}
}
class _ImmediateRepository implements ReservationMenuRepository {
_ImmediateRepository(this.value);
final ServiceReservationMenuQuery value;
final calls = <String>[];
@override
Future<ServiceReservationMenuQuery?> query({
required ReservationRecognitionMethod method,
required String identityValue,
}) async {
calls.add(identityValue);
return value;
}
}
class _FakeRecognitionGateway implements ReservationRecognitionGateway {
final cardCallbacks = <Future<void> Function(String)>[];
final cardFailures = <void Function(ReservationRecognitionFailure failure)>[];
final faceCallbacks = <Future<void> Function(String)>[];
final faceFailures = <void Function(ReservationRecognitionFailure failure)>[];
int stopAllCalls = 0;
Future<void> emitCardAt(int index, String value) {
return cardCallbacks[index](value);
}
Future<void> emitFaceAt(int index, String value) {
return faceCallbacks[index](value);
}
@override
Future<void> startCard({
required Future<void> Function(String cardNumber) onRecognized,
required void Function(ReservationRecognitionFailure failure) onFailure,
}) async {
cardCallbacks.add(onRecognized);
cardFailures.add(onFailure);
}
@override
Future<void> startFace({
required Future<void> Function(String userId) onRecognized,
required void Function(ReservationRecognitionFailure failure) onFailure,
}) async {
faceCallbacks.add(onRecognized);
faceFailures.add(onFailure);
}
@override
Future<void> stopAll() async {
stopAllCalls++;
}
@override
Future<void> stopCard() async {}
@override
Future<void> stopFace() async {}
}
class _FakeSubScreenGateway implements ReservationSubScreenGateway {
_FakeSubScreenGateway({this.available = true});
final bool available;
final updates = <SubScreenReservationQueryEvent>[];
int showCalls = 0;
int clearCalls = 0;
@override
bool get isAvailable => available;
@override
Future<void> show() async {
showCalls++;
}
@override
Future<void> update(SubScreenReservationQueryEvent event) async {
updates.add(event);
}
@override
Future<void> clear({
required String sessionId,
required int revision,
}) async {
clearCalls++;
}
}
ServiceReservationMenuQuery _result(String maskedName, int dayCount) {
final start = DateTime(2026, 7, 24);
final days = List<ServiceReservationDay>.generate(dayCount, (index) {
final date = start.add(Duration(days: index));
return ServiceReservationDay(
date:
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}',
meals: const [
ServiceReservationMeal(
hourType: 'LUNCH',
mealTitle: '午餐',
items: ['香菇滑鸡', '清炒时蔬'],
),
],
);
});
return ServiceReservationMenuQuery(
maskedName: maskedName,
deptName: '三年级二班',
merchantName: '第一食堂',
totalDayCount: dayCount,
totalMealCount: dayCount,
days: days,
);
}