Files
biz_xfj_cloud/test/widgets/app_soft_keyboard_test.dart
T
dzc fe88259cf2 feat:键盘颜色渐变
feat:键盘长按删除
feat:查询菜单姓名加粗颜色为主题颜色
2026-10-08 11:46:33 +08:00

387 lines
12 KiB
Dart

import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:forui/forui.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
/// 自研软键盘组件的渲染与输入联动测试。
void main() {
Widget wrapHost(Widget Function(BuildContext context) child) =>
ScreenUtilInit(
designSize: const Size(1920, 1080),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, __) => MaterialApp(
home: FTheme(
data: FThemes.zinc.light,
child: Builder(builder: child),
),
),
);
testWidgets('弹出后渲染面板并可输入目标控制器', (tester) async {
final controller = TextEditingController();
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: FButton(
onPress: () => AppSoftKeyboard.show(
context,
controller: controller,
mode: AppSoftKeyboardMode.digits,
),
label: const Text('打开键盘'),
),
),
),
),
);
await tester.tap(find.text('打开键盘'));
await tester.pumpAndSettle();
// 面板、tab 与确认键都应渲染。
expect(find.text('确认'), findsOneWidget);
// 键盘应停靠在屏幕下半部(底部 16h + 面板高度内)。
final confirmRect = tester.getRect(find.text('确认'));
expect(confirmRect.bottom, lessThan(1080));
expect(confirmRect.bottom, greaterThan(1080 * 0.5));
// 默认停靠位为底部水平居中。
expect(confirmRect.center.dx, closeTo(960, 1));
// 点击数字 1 与 5 后字符进入目标控制器。
await tester.tap(find.text('1'));
await tester.pump();
await tester.tap(find.text('5'));
await tester.pump();
expect(controller.text, '15');
// 切换字母与符号面板均不产生布局溢出。
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
await tester.tap(find.text('#+='));
await tester.pumpAndSettle();
await tester.tap(find.text('123'));
await tester.pumpAndSettle();
});
testWidgets('在 AppDialog 弹框内弹出键盘仍然渲染', (tester) async {
final passwordController = TextEditingController();
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: FButton(
onPress: () => showAppDialog<void>(
context: context,
barrierDismissible: false,
canPop: false,
builder: (dialogContext) => AppDialog(
title: const Text('终端操作验证'),
width: 680,
maxWidth: 680,
content: FTextField(
controller: passwordController,
keyboardType: TextInputType.none,
onTap: () => AppSoftKeyboard.show(
dialogContext,
controller: passwordController,
mode: AppSoftKeyboardMode.digits,
),
),
actions: [
FButton(
style: FButtonStyle.outline,
label: const Text('取消'),
onPress: () => Navigator.of(dialogContext).pop(),
),
],
),
),
label: const Text('打开验证弹框'),
),
),
),
),
);
await tester.tap(find.text('打开验证弹框'));
await tester.pumpAndSettle();
// 弹框内的输入框应可见。
expect(find.text('终端操作验证'), findsOneWidget);
await tester.tap(find.byType(TextField).first);
await tester.pumpAndSettle();
// 键盘面板在弹框之上仍应渲染。
expect(find.text('确认'), findsOneWidget);
});
testWidgets('内嵌面板在弹框内渲染并输入', (tester) async {
final controller = TextEditingController();
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: AppSoftKeyboardPanel(
controller: controller,
mode: AppSoftKeyboardMode.digits,
allowPattern: RegExp('[0-9]'),
maxLength: 12,
).tight(width: 624),
),
),
),
);
// 面板切换 tab 与数字键应渲染;内嵌模式没有确认键。
expect(find.text('123'), findsOneWidget);
expect(find.text('确认'), findsNothing);
await tester.tap(find.text('2'));
await tester.pump();
await tester.tap(find.text('5'));
await tester.pump();
await tester.tap(find.text('0'));
await tester.pump();
await tester.tap(find.text('3'));
await tester.pump();
expect(controller.text, '2503');
// 清空键一键清空。
await tester.tap(find.text('清空'));
await tester.pump();
expect(controller.text, '');
// 内嵌宽度(624)下切换字母与符号面板同样不溢出。
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
await tester.tap(find.text('#+='));
await tester.pumpAndSettle();
});
testWidgets('左侧停靠模式下键盘位于左半屏且垂直居中', (tester) async {
final controller = TextEditingController();
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: FButton(
onPress: () => AppSoftKeyboard.show(
context,
controller: controller,
mode: AppSoftKeyboardMode.digits,
dock: AppSoftKeyboardDock.centerLeft,
),
label: const Text('打开键盘'),
),
),
),
),
);
await tester.tap(find.text('打开键盘'));
await tester.pumpAndSettle();
final confirmRect = tester.getRect(find.text('确认'));
// 面板贴左缘停靠,整体落在左半屏。
expect(confirmRect.left, greaterThanOrEqualTo(0));
expect(confirmRect.right, lessThan(1920 * 0.5));
// 垂直居中:面板整体跨在屏幕纵向中线附近。
expect(
confirmRect.center.dy,
inExclusiveRange(1080 * 0.25, 1080 * 0.75),
);
// 输入与确认收起流程不受停靠位影响。
await tester.tap(find.text('7'));
await tester.pump();
expect(controller.text, '7');
await tester.tap(find.text('确认'));
await tester.pumpAndSettle();
expect(find.text('确认'), findsNothing);
});
testWidgets('浮层键盘不抢走输入框焦点', (tester) async {
final controller = TextEditingController();
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: SizedBox(
width: 480,
child: FTextField(
controller: controller,
keyboardType: TextInputType.none,
onTap: () => AppSoftKeyboard.show(
context,
controller: controller,
mode: AppSoftKeyboardMode.digits,
),
),
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.pumpAndSettle();
// 键盘弹出后输入框必须保持聚焦:主焦点不落入弹层,聚焦描边不消失,
// 实体键盘与扫码枪输入仍进入该输入框。
final editable = tester.widget<EditableText>(find.byType(EditableText));
expect(editable.focusNode.hasFocus, isTrue);
expect(find.text('确认'), findsOneWidget);
// 聚焦保持的同时虚拟按键照常写入。
await tester.tap(find.text('3'));
await tester.pump();
expect(controller.text, '3');
// 点击键盘外收起后输入框仍保持聚焦。
await tester.tapAt(const Offset(1800, 60));
await tester.pumpAndSettle();
expect(find.text('确认'), findsNothing);
expect(editable.focusNode.hasFocus, isTrue);
});
testWidgets('按键按压时整键缩放、抬起复原且完成输入', (tester) async {
final controller = TextEditingController();
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: FButton(
onPress: () => AppSoftKeyboard.show(
context,
controller: controller,
mode: AppSoftKeyboardMode.digits,
),
label: const Text('打开键盘'),
),
),
),
),
);
await tester.tap(find.text('打开键盘'));
await tester.pumpAndSettle();
final keyScale = find
.ancestor(of: find.text('5'), matching: find.byType(AnimatedScale))
.first;
expect(tester.widget<AnimatedScale>(keyScale).scale, 1.0);
// 抬起态为纯色底,无渐变。
final keyContainer = find
.ancestor(of: find.text('5'), matching: find.byType(AnimatedContainer))
.first;
var keyDecoration =
tester.widget<AnimatedContainer>(keyContainer).decoration
as BoxDecoration;
expect(keyDecoration.gradient, isNull);
// 指针按下未抬起:键帽立即进入按压缩放态并铺上主题色渐变。
final gesture = await tester.startGesture(
tester.getCenter(find.text('5')),
);
await tester.pump();
expect(tester.widget<AnimatedScale>(keyScale).scale, closeTo(0.92, 0.001));
keyDecoration = tester.widget<AnimatedContainer>(keyContainer).decoration
as BoxDecoration;
expect(keyDecoration.gradient, isA<LinearGradient>());
// 抬起后键帽复原、渐变退回纯色并完成一次输入。
await gesture.up();
await tester.pumpAndSettle();
expect(tester.widget<AnimatedScale>(keyScale).scale, 1.0);
keyDecoration = tester.widget<AnimatedContainer>(keyContainer).decoration
as BoxDecoration;
expect(keyDecoration.gradient, isNull);
expect(controller.text, '5');
});
testWidgets('长按退格键持续删除,抬起即停', (tester) async {
final semanticsHandle = tester.ensureSemantics();
final controller = TextEditingController(text: '12345678');
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrapHost(
(context) => Scaffold(
body: Center(
child: FButton(
onPress: () => AppSoftKeyboard.show(
context,
controller: controller,
mode: AppSoftKeyboardMode.digits,
),
label: const Text('打开键盘'),
),
),
),
),
);
await tester.tap(find.text('打开键盘'));
await tester.pumpAndSettle();
final deleteFinder = find.bySemanticsLabel('退格删除,长按连续删除');
final gesture = await tester.startGesture(
tester.getCenter(deleteFinder),
);
// 长按阈值(500ms)到达时触发第一次删除,连发尚未开始。
await tester.pump(const Duration(milliseconds: 550));
expect(controller.text, '1234567');
// 连发周期 100ms:推进 340ms 触发 600/700/800 三个周期。
await tester.pump(const Duration(milliseconds: 340));
expect(controller.text, '1234');
// 抬起后连发立即停止,文本不再变化。
await gesture.up();
await tester.pump(const Duration(seconds: 1));
expect(controller.text, '1234');
await tester.pumpAndSettle();
// 单击退格仍然只删除一位。
await tester.tap(deleteFinder);
await tester.pump();
expect(controller.text, '123');
semanticsHandle.dispose();
});
}