302 lines
9.9 KiB
Dart
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,
|
|
);
|
|
}
|