Files
biz_xfj_cloud/test/sub_screen/reservation_query_widget_test.dart
T
2026-10-09 09:32:08 +08:00

546 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:forui/forui.dart';
import 'package:pj_supermarket_cloud/common/models/page/reservation_menu_page_models.dart';
import 'package:pj_supermarket_cloud/common/widgets/app/reservation_schedule_card.dart';
import 'package:pj_supermarket_cloud/sub_screen/common/models/reservation_query_display_models.dart';
import 'package:pj_supermarket_cloud/sub_screen/common/utils/reservation_query_coordinator.dart';
import 'package:pj_supermarket_cloud/sub_screen/page/reservation_query_display.dart';
void main() {
testWidgets('午餐预约卡只补齐基础三餐,不生成夜间餐占位', (tester) async {
ReservationMenuMealData? verifiedMeal;
const day = ReservationMenuDayData(
date: '2026-07-24',
meals: [
ReservationMenuMealData(
hourType: 'NOONING',
title: '午餐',
items: ['香菇滑鸡'],
orderIds: [1001],
),
],
);
await tester.pumpWidget(
MaterialApp(
home: FTheme(
data: FThemes.zinc.light,
child: Center(
child: SizedBox(
width: 420,
height: 420,
child: ReservationScheduleCard(
day: day,
verificationEnabled: true,
onVerify: (meal) => verifiedMeal = meal,
),
),
),
),
),
);
for (final title in const ['早餐', '午餐', '晚餐']) {
expect(find.text(title), findsOneWidget);
}
expect(find.text('夜间餐'), findsNothing);
expect(find.text('未预定'), findsNWidgets(2));
expect(find.text('核销'), findsOneWidget);
final verificationVisual = tester.getSize(
find.byKey(const ValueKey('reservation-verification-visual-2')),
);
final verificationTarget = tester.getSize(
find.byKey(const ValueKey('reservation-verification-target-2')),
);
expect(verificationVisual.height, 36);
expect(verificationTarget.height, 48);
expect(verificationTarget.width, greaterThan(verificationVisual.width));
await tester.tap(find.text('核销'));
expect(verifiedMeal?.title, '午餐');
});
testWidgets('后端返回夜间餐时正常追加展示', (tester) async {
const day = ReservationMenuDayData(
date: '2026-07-24',
meals: [
ReservationMenuMealData(
hourType: 'LATE_NIGHT',
title: '夜间餐',
items: ['牛肉面'],
),
],
);
await tester.pumpWidget(
MaterialApp(
home: FTheme(
data: FThemes.zinc.light,
child: const Center(
child: SizedBox(
width: 420,
height: 420,
child: ReservationScheduleCard(day: day),
),
),
),
),
);
expect(find.text('夜间餐'), findsOneWidget);
expect(find.text('牛肉面'), findsOneWidget);
final lateNightText = tester.renderObject<RenderParagraph>(
find.byKey(const ValueKey('reservation-meal-title-4')),
);
expect(lateNightText.maxLines, 1);
expect(lateNightText.didExceedMaxLines, isFalse);
});
testWidgets('关闭核销配置时预约餐段仍显示查询详情入口', (tester) async {
ReservationMenuMealData? selectedMeal;
const day = ReservationMenuDayData(
date: '2026-07-24',
meals: [
ReservationMenuMealData(
hourType: '2',
title: '午餐',
items: ['小青菜'],
orderIds: [1001],
),
],
);
await tester.pumpWidget(
MaterialApp(
home: FTheme(
data: FThemes.zinc.light,
child: Center(
child: SizedBox(
width: 420,
height: 420,
child: ReservationScheduleCard(
day: day,
onVerify: (meal) => selectedMeal = meal,
),
),
),
),
),
);
expect(find.text('核销'), findsNothing);
expect(find.text('查询详情'), findsOneWidget);
await tester.tap(find.text('查询详情'));
expect(selectedMeal?.title, '午餐');
});
testWidgets('窄卡片中的查询详情保持紧凑且菜名不再逐字换行', (tester) async {
const day = ReservationMenuDayData(
date: '2026-07-24',
meals: [
ReservationMenuMealData(
hourType: '2',
title: '午餐',
items: ['小青菜'],
orderIds: [1001],
),
],
);
await tester.pumpWidget(
MaterialApp(
home: FTheme(
data: FThemes.zinc.light,
child: const Center(
child: SizedBox(
width: 220,
height: 420,
child: ReservationScheduleCard(
day: day,
),
),
),
),
),
);
expect(tester.takeException(), isNull);
final itemText = tester.renderObject<RenderParagraph>(
find.text('小青菜'),
);
final detailVisual = tester.getSize(
find.byKey(const ValueKey('reservation-verification-visual-2')),
);
expect(itemText.maxLines, 1);
expect(itemText.didExceedMaxLines, isFalse);
expect(itemText.size.height, lessThan(24));
expect(detailVisual.width, lessThanOrEqualTo(70));
expect(detailVisual.height, 30);
});
for (final size in [
const Size(1920, 1080),
const Size(1280, 720),
const Size(1024, 600),
]) {
testWidgets('${size.width.toInt()}x${size.height.toInt()} 各预约状态无布局异常',
(tester) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
for (final scenario in ReservationQueryDisplayScenario.values) {
final coordinator = ReservationQueryCoordinator(
initialState: ReservationQueryDisplayState.demo(
scenario,
now: DateTime(2026, 7, 24, 8),
),
);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
expect(
find.byKey(ValueKey('reservation-scenario-${scenario.name}')),
findsOneWidget,
);
expect(tester.takeException(), isNull);
if (scenario == ReservationQueryDisplayScenario.success) {
expect(find.byType(ReservationScheduleCard), findsNWidgets(10));
expect(find.text('张*明 · 三年级二班'), findsOneWidget);
expect(find.text('张小明'), findsNothing);
expect(find.byKey(const ValueKey('reservation-page-indicator')),
findsOneWidget);
}
await tester.pumpWidget(const SizedBox.shrink());
coordinator.dispose();
}
});
}
testWidgets('小尺寸副屏的文字保底缩放,主标题不随布局缩到不可读', (tester) async {
tester.view.physicalSize = const Size(1024, 600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final coordinator = ReservationQueryCoordinator(
initialState: ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.idle,
now: DateTime(2026, 7, 24, 8),
),
);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
// 1024x600 的布局缩放约为 0.55;文字与首页副屏一样保底 0.9,
// 主标题 38 x 0.9 = 34.2 起步,说明文字没有跟着布局一起缩小。
final title = tester.renderObject<RenderParagraph>(
find.text('请刷饭卡或进行人脸识别'),
);
expect(title.text.style!.fontSize, greaterThanOrEqualTo(34.2));
final description = tester.renderObject<RenderParagraph>(
find.text('识别成功后,这里将展示当前用户本人的预约菜单'),
);
expect(description.text.style!.fontSize, greaterThanOrEqualTo(18.9));
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
coordinator.dispose();
});
testWidgets('键盘提示只在开启开关的等待识别布局展示', (tester) async {
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
Future<void> pumpState(ReservationQueryDisplayState state) async {
final coordinator = ReservationQueryCoordinator(initialState: state);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
addTearDown(coordinator.dispose);
}
await pumpState(
ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.idle,
now: DateTime(2026, 7, 24, 8),
).copyWith(keyboardHintsEnabled: true),
);
expect(find.text('请刷饭卡或进行人脸识别'), findsOneWidget);
expect(find.text('键盘操作提示'), findsOneWidget);
// 键盘模式左缘同时展示竖排“非触屏,按键盘操作”大字路牌。
expect(
find.byKey(const ValueKey('reservation-non-touch-banner')),
findsOneWidget,
);
for (final char in const ['非', '触', '屏', '按', '键', '盘', '操', '作']) {
expect(find.text(char), findsOneWidget);
}
// 竖排路牌为纯文字,顶部不放键盘图标(右侧“键盘操作提示”标题自带图标)。
expect(
find.descendant(
of: find.byKey(const ValueKey('reservation-non-touch-banner')),
matching: find.byType(Icon),
),
findsNothing,
);
// 键盘图形由预渲染 3D 素材图提供:上排 人脸/饭卡/留白键,下排 加宽核销/取消。
final diagram = find.descendant(
of: find.byKey(const ValueKey('reservation-keyboard-hint')),
matching: find.byType(Image),
);
expect(diagram, findsOneWidget);
final image = tester.widget<Image>(diagram);
expect(
(image.image as AssetImage).assetName,
'assets/images/shortcut_keyboard_diagram.png',
);
// 键帽文字已固化在素材图中,页面不再出现独立按键文案与“目前无作用”。
for (final label in const ['人脸', '饭卡', '核销', '取消', '目前无作用']) {
expect(find.text(label), findsNothing);
}
// 键盘下方提供通俗操作说明:两段式按键、核销前提与取消用途。
expect(find.textContaining('再按一下就开始识别'), findsOneWidget);
expect(find.textContaining('当天这一顿有预约才能成功'), findsOneWidget);
expect(find.textContaining('回到开始的画面'), findsOneWidget);
expect(tester.takeException(), isNull);
// 关闭开关或非等待场景时不展示键盘提示与竖排大字路牌。
await pumpState(
ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.idle,
now: DateTime(2026, 7, 24, 8),
),
);
expect(
find.byKey(const ValueKey('reservation-keyboard-hint')),
findsNothing,
);
expect(
find.byKey(const ValueKey('reservation-non-touch-banner')),
findsNothing,
);
await pumpState(
ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.loading,
now: DateTime(2026, 7, 24, 8),
).copyWith(keyboardHintsEnabled: true),
);
expect(
find.byKey(const ValueKey('reservation-keyboard-hint')),
findsNothing,
);
expect(
find.byKey(const ValueKey('reservation-non-touch-banner')),
findsNothing,
);
});
testWidgets('小尺寸副屏开启键盘提示仍保持布局完整', (tester) async {
tester.view.physicalSize = const Size(1024, 600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final coordinator = ReservationQueryCoordinator(
initialState: ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.idle,
now: DateTime(2026, 7, 24, 8),
).copyWith(keyboardHintsEnabled: true),
);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
expect(find.text('键盘操作提示'), findsOneWidget);
expect(
find.byKey(const ValueKey('reservation-keyboard-hint')),
findsOneWidget,
);
// 小尺寸下竖排大字路牌经 scaleDown 微缩后仍完整可读。
expect(
find.byKey(const ValueKey('reservation-non-touch-banner')),
findsOneWidget,
);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
coordinator.dispose();
});
testWidgets('等待放卡场景提示请放置饭卡且不显示键盘提示区', (tester) async {
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final coordinator = ReservationQueryCoordinator(
initialState: ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.waitingCard,
now: DateTime(2026, 7, 24, 8),
keyboardHintsEnabled: true,
),
);
addTearDown(coordinator.dispose);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
// 等待放卡阶段用专属放卡引导替换通用查询文案。
expect(find.text('请放置饭卡'), findsOneWidget);
expect(find.textContaining('将饭卡平放在读卡器上'), findsOneWidget);
expect(find.text('正在查询本人预约'), findsNothing);
// 识别进行中不再展示键盘操作提示与竖排路牌。
expect(
find.byKey(const ValueKey('reservation-keyboard-hint')),
findsNothing,
);
expect(
find.byKey(const ValueKey('reservation-non-touch-banner')),
findsNothing,
);
expect(find.text('键盘操作提示'), findsNothing);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('键盘模式成功页提示按核销键,关闭键盘模式不提示', (tester) async {
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
Future<void> pumpState({required bool keyboardHintsEnabled}) async {
final coordinator = ReservationQueryCoordinator(
initialState: ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.success,
now: DateTime(2026, 7, 24, 8),
keyboardHintsEnabled: keyboardHintsEnabled,
),
);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
// 成功态带隐私倒计时,必须在测试体内释放协调器,避免残留计时器。
coordinator.dispose();
}
await pumpState(keyboardHintsEnabled: true);
final bar = find.byKey(const ValueKey('reservation-redeem-hint'));
expect(bar, findsOneWidget);
// 图标同样由 RichText 承载,按内容匹配校验富文本完整语句。
expect(
find.descendant(
of: bar,
matching: find.textContaining('请在键盘上按「核销」键完成核销',
findRichText: true),
),
findsOneWidget,
);
expect(tester.takeException(), isNull);
// 非键盘模式成功页保持原有布局,不加核销引导条。
await pumpState(keyboardHintsEnabled: false);
expect(
find.byKey(const ValueKey('reservation-redeem-hint')),
findsNothing,
);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('持续人脸副屏同时保留实时预览、预约菜单和十秒倒计时', (tester) async {
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final state = ReservationQueryDisplayState.demo(
ReservationQueryDisplayScenario.success,
now: DateTime(2026, 8, 7, 10),
).copyWith(
remainingSeconds: 10,
continuousFaceRecognition: true,
);
final coordinator = ReservationQueryCoordinator(initialState: state);
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FTheme(
data: FThemes.zinc.light,
child: ReservationQueryDisplayPage(coordinator: coordinator),
),
),
);
await tester.pump();
expect(
find.byKey(const ValueKey('sub-reservation-live-face-preview')),
findsOneWidget,
);
expect(find.byType(ReservationScheduleCard), findsNWidgets(10));
expect(find.text('预约结果展示中'), findsOneWidget);
expect(find.textContaining('10 秒后识别下一位'), findsOneWidget);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
coordinator.dispose();
});
}