454 lines
16 KiB
Dart
454 lines
16 KiB
Dart
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:forui/forui.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
const runtimeChannel = MethodChannel('biz_xfj/runtime_environment');
|
|
late int vibrationCount;
|
|
|
|
setUp(() {
|
|
vibrationCount = 0;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(runtimeChannel, (call) async {
|
|
if (call.method == 'vibrate') {
|
|
vibrationCount++;
|
|
return true;
|
|
}
|
|
return null;
|
|
});
|
|
});
|
|
|
|
tearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(runtimeChannel, null);
|
|
});
|
|
|
|
testWidgets('允许关闭时遮罩点击返回空结果并移除弹框', (tester) async {
|
|
String? result = 'pending';
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () async {
|
|
result = await showAppDialog<String>(
|
|
context: context,
|
|
builder: (dialogContext) => AppDialog(
|
|
key: const ValueKey('allowed-dialog'),
|
|
title: const Text('允许关闭'),
|
|
content: const Text('弹框内容'),
|
|
onClose: () => Navigator.of(dialogContext).pop('button'),
|
|
),
|
|
);
|
|
},
|
|
child: const Text('打开允许关闭弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开允许关闭弹框'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const ValueKey('allowed-dialog')), findsOneWidget);
|
|
|
|
await tester.tapAt(const Offset(4, 4));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const ValueKey('allowed-dialog')), findsNothing);
|
|
expect(result, isNull);
|
|
});
|
|
|
|
testWidgets('默认弹框位于顶部安全区下方并保留固定间隔', (tester) async {
|
|
tester.view.devicePixelRatio = 1;
|
|
tester.view.physicalSize = const Size(800, 600);
|
|
addTearDown(tester.view.reset);
|
|
const viewPadding = EdgeInsets.fromLTRB(16, 18, 16, 12);
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
viewPadding: viewPadding,
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AppDialog(
|
|
key: const ValueKey('top-dialog'),
|
|
title: const Text('顶部弹框'),
|
|
width: 360,
|
|
maxWidth: 360,
|
|
content: const Text('默认不再相对整个屏幕居中'),
|
|
onClose: () => Navigator.of(dialogContext).pop(),
|
|
),
|
|
),
|
|
child: const Text('打开顶部弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开顶部弹框'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final rect = tester.getRect(find.byKey(const ValueKey('top-dialog')));
|
|
expect(
|
|
rect.top,
|
|
closeTo(viewPadding.top + appDialogDefaultTopSpacing, 0.01),
|
|
);
|
|
expect(
|
|
rect.left,
|
|
greaterThanOrEqualTo(viewPadding.left + appDialogDefaultEdgeSpacing),
|
|
);
|
|
expect(
|
|
rect.right,
|
|
lessThanOrEqualTo(
|
|
800 - viewPadding.right - appDialogDefaultEdgeSpacing,
|
|
),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('键盘出现后弹框动态缩入键盘上方可用区域', (tester) async {
|
|
tester.view.devicePixelRatio = 1;
|
|
tester.view.physicalSize = const Size(800, 600);
|
|
addTearDown(tester.view.reset);
|
|
final viewInsets = ValueNotifier<EdgeInsets>(EdgeInsets.zero);
|
|
addTearDown(viewInsets.dispose);
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
viewInsetsListenable: viewInsets,
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AppDialog(
|
|
key: const ValueKey('keyboard-dialog'),
|
|
title: const Text('输入信息'),
|
|
width: 420,
|
|
maxWidth: 420,
|
|
height: 400,
|
|
content: const TextField(
|
|
key: ValueKey('keyboard-dialog-field'),
|
|
autofocus: true,
|
|
),
|
|
onClose: () => Navigator.of(dialogContext).pop(),
|
|
),
|
|
),
|
|
child: const Text('打开输入弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开输入弹框'));
|
|
await tester.pumpAndSettle();
|
|
final initialRect = tester.getRect(
|
|
find.byKey(const ValueKey('keyboard-dialog')),
|
|
);
|
|
expect(initialRect.top, closeTo(appDialogDefaultTopSpacing, 0.01));
|
|
|
|
viewInsets.value = const EdgeInsets.only(bottom: 260);
|
|
await tester.pump();
|
|
await tester.pump(appDialogDefaultTransitionDuration);
|
|
|
|
final keyboardRect = tester.getRect(
|
|
find.byKey(const ValueKey('keyboard-dialog')),
|
|
);
|
|
final fieldRect = tester.getRect(
|
|
find.byKey(const ValueKey('keyboard-dialog-field')),
|
|
);
|
|
final keyboardTop = 600 - viewInsets.value.bottom;
|
|
expect(keyboardRect.top, closeTo(appDialogDefaultTopSpacing, 0.01));
|
|
expect(
|
|
keyboardRect.bottom,
|
|
lessThanOrEqualTo(keyboardTop - appDialogDefaultEdgeSpacing),
|
|
);
|
|
expect(fieldRect.bottom, lessThanOrEqualTo(keyboardRect.bottom));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('禁止关闭时节流震动且减少动态模式使用边框高亮', (tester) async {
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
disableAnimations: true,
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
canPop: false,
|
|
builder: (_) => const AppDialog(
|
|
key: ValueKey('blocked-dialog'),
|
|
title: Text('禁止关闭'),
|
|
content: Text('业务处理中'),
|
|
onClose: null,
|
|
),
|
|
),
|
|
child: const Text('打开禁止关闭弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开禁止关闭弹框'));
|
|
await tester.pumpAndSettle();
|
|
await Navigator.of(
|
|
tester.element(find.byKey(const ValueKey('blocked-dialog'))),
|
|
).maybePop();
|
|
await tester.pump();
|
|
|
|
expect(vibrationCount, 1);
|
|
expect(find.byKey(const ValueKey('blocked-dialog')), findsOneWidget);
|
|
expect(
|
|
_dialogBorderColor(tester, 'blocked-dialog'), XfjDesignTokens.warning);
|
|
await tester.tapAt(const Offset(4, 4));
|
|
await tester.pump();
|
|
expect(vibrationCount, 1, reason: '节流窗口内的连续点击不得重复震动');
|
|
expect(find.byKey(const ValueKey('blocked-dialog')), findsOneWidget);
|
|
|
|
await tester.pump(const Duration(milliseconds: 250));
|
|
expect(_dialogBorderColor(tester, 'blocked-dialog'), XfjDesignTokens.line);
|
|
});
|
|
|
|
testWidgets('标题栏拖动后弹框仍完整保留在安全区域', (tester) async {
|
|
tester.view.devicePixelRatio = 1;
|
|
tester.view.physicalSize = const Size(800, 600);
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AppDialog(
|
|
key: const ValueKey('draggable-dialog'),
|
|
title: const Text('拖动标题栏'),
|
|
width: 360,
|
|
maxWidth: 360,
|
|
height: 260,
|
|
draggable: true,
|
|
content: const Text('可拖动内容'),
|
|
onClose: () => Navigator.of(dialogContext).pop(),
|
|
),
|
|
),
|
|
child: const Text('打开可拖动弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开可拖动弹框'));
|
|
await tester.pumpAndSettle();
|
|
await tester.drag(find.text('拖动标题栏'), const Offset(2000, 2000));
|
|
await tester.pump();
|
|
var rect = tester.getRect(find.byKey(const ValueKey('draggable-dialog')));
|
|
expect(rect.right, lessThanOrEqualTo(792));
|
|
expect(rect.bottom, lessThanOrEqualTo(592));
|
|
final bottomRightRect = rect;
|
|
|
|
await tester.drag(find.text('拖动标题栏'), const Offset(2000, 2000));
|
|
await tester.pump();
|
|
rect = tester.getRect(find.byKey(const ValueKey('draggable-dialog')));
|
|
expect(rect, bottomRightRect, reason: '到达边缘后继续同方向拖动不得反复修正位置');
|
|
expect(find.byKey(const ValueKey('draggable-dialog')), findsOneWidget);
|
|
|
|
await tester.drag(find.text('拖动标题栏'), const Offset(-4000, -4000));
|
|
await tester.pump();
|
|
rect = tester.getRect(find.byKey(const ValueKey('draggable-dialog')));
|
|
expect(rect.left, greaterThanOrEqualTo(8));
|
|
expect(rect.top, greaterThanOrEqualTo(8));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('拖动和尺寸约束保留系统安全区', (tester) async {
|
|
tester.view.devicePixelRatio = 1;
|
|
tester.view.physicalSize = const Size(800, 600);
|
|
addTearDown(tester.view.reset);
|
|
const viewPadding = EdgeInsets.fromLTRB(24, 18, 32, 20);
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
viewPadding: viewPadding,
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AppDialog(
|
|
key: const ValueKey('safe-dialog'),
|
|
title: const Text('安全区弹框'),
|
|
width: 790,
|
|
maxWidth: 790,
|
|
height: 560,
|
|
draggable: true,
|
|
content: const Text('安全区内容'),
|
|
onClose: () => Navigator.of(dialogContext).pop(),
|
|
),
|
|
),
|
|
child: const Text('打开安全区弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开安全区弹框'));
|
|
await tester.pumpAndSettle();
|
|
var rect = tester.getRect(find.byKey(const ValueKey('safe-dialog')));
|
|
expect(rect.left, greaterThanOrEqualTo(viewPadding.left + 8));
|
|
expect(rect.right, lessThanOrEqualTo(800 - viewPadding.right - 8));
|
|
expect(rect.top, greaterThanOrEqualTo(viewPadding.top + 8));
|
|
expect(rect.bottom, lessThanOrEqualTo(600 - viewPadding.bottom - 8));
|
|
|
|
await tester.drag(find.text('安全区弹框'), const Offset(3000, -3000));
|
|
await tester.pump();
|
|
rect = tester.getRect(find.byKey(const ValueKey('safe-dialog')));
|
|
expect(rect.left, greaterThanOrEqualTo(viewPadding.left + 8));
|
|
expect(rect.right, lessThanOrEqualTo(800 - viewPadding.right - 8));
|
|
expect(rect.top, greaterThanOrEqualTo(viewPadding.top + 8));
|
|
expect(rect.bottom, lessThanOrEqualTo(600 - viewPadding.bottom - 8));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('ForUI 操作按钮在空间足够时保持横向排列', (tester) async {
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AppDialog(
|
|
title: const Text('操作区布局'),
|
|
width: 680.w,
|
|
maxWidth: 680.w,
|
|
content: const Text('弹框内容'),
|
|
onClose: () => Navigator.of(dialogContext).pop(),
|
|
actions: [
|
|
FButton(
|
|
key: const ValueKey('cancel-action'),
|
|
style: FButtonStyle.outline,
|
|
label: const Text('取消'),
|
|
onPress: () {},
|
|
),
|
|
FButton(
|
|
key: const ValueKey('confirm-action'),
|
|
label: const Text('验证并继续'),
|
|
onPress: () {},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
child: const Text('打开操作区弹框'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开操作区弹框'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final cancelRect =
|
|
tester.getRect(find.byKey(const ValueKey('cancel-action')));
|
|
final confirmRect = tester.getRect(
|
|
find.byKey(const ValueKey('confirm-action')),
|
|
);
|
|
expect(cancelRect.top, closeTo(confirmRect.top, 0.01));
|
|
expect(cancelRect.right, lessThan(confirmRect.left));
|
|
expect(cancelRect.height, greaterThanOrEqualTo(48.h));
|
|
expect(confirmRect.height, greaterThanOrEqualTo(48.h));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('终端操作验证保留上下留白并横向排列操作按钮', (tester) async {
|
|
await tester.pumpWidget(
|
|
_dialogTestApp(
|
|
child: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => terminalAdminPasswordDialog(
|
|
context,
|
|
actionLabel: '进入设置',
|
|
),
|
|
child: const Text('打开终端操作验证'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('打开终端操作验证'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final headerDivider = tester.getRect(find.byType(Divider).first);
|
|
final actionDivider = tester.getRect(find.byType(Divider).last);
|
|
final descriptionRect = tester.getRect(
|
|
find.text('进入设置前需要验证管理密码。'),
|
|
);
|
|
final passwordFieldRect = tester.getRect(find.byType(FTextField));
|
|
final cancelRect = tester.getRect(find.text('取消'));
|
|
final confirmRect = tester.getRect(find.text('验证并继续'));
|
|
expect(
|
|
descriptionRect.top - headerDivider.bottom, greaterThanOrEqualTo(19));
|
|
expect(
|
|
actionDivider.top - passwordFieldRect.bottom, greaterThanOrEqualTo(19));
|
|
expect(cancelRect.top, closeTo(confirmRect.top, 0.01));
|
|
expect(cancelRect.right, lessThan(confirmRect.left));
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.tap(find.text('取消'));
|
|
await tester.pumpAndSettle();
|
|
});
|
|
}
|
|
|
|
/// 构建具备项目 ForUI 主题和 ScreenUtil 设计基准的弹框测试环境。
|
|
Widget _dialogTestApp({
|
|
required Widget child,
|
|
bool disableAnimations = false,
|
|
EdgeInsets viewPadding = EdgeInsets.zero,
|
|
ValueListenable<EdgeInsets>? viewInsetsListenable,
|
|
}) {
|
|
return ScreenUtilInit(
|
|
designSize: const Size(1920, 1080),
|
|
minTextAdapt: true,
|
|
splitScreenMode: true,
|
|
builder: (_, __) => MaterialApp(
|
|
builder: (context, appChild) {
|
|
final mediaData = MediaQuery.of(context).copyWith(
|
|
disableAnimations: disableAnimations,
|
|
padding: viewPadding,
|
|
viewPadding: viewPadding,
|
|
);
|
|
final themedChild = FTheme(
|
|
data: FThemes.zinc.light,
|
|
child: appChild!,
|
|
);
|
|
final viewInsets = viewInsetsListenable;
|
|
if (viewInsets == null) {
|
|
return MediaQuery(data: mediaData, child: themedChild);
|
|
}
|
|
return ValueListenableBuilder<EdgeInsets>(
|
|
valueListenable: viewInsets,
|
|
builder: (_, insets, child) => MediaQuery(
|
|
data: mediaData.copyWith(viewInsets: insets),
|
|
child: child!,
|
|
),
|
|
child: themedChild,
|
|
);
|
|
},
|
|
home: Scaffold(body: Center(child: child)),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 读取指定弹框表面的当前边框颜色,用于验证减少动态效果下的阻止关闭反馈。
|
|
Color _dialogBorderColor(WidgetTester tester, String dialogKey) {
|
|
final materials = tester.widgetList<Material>(
|
|
find.descendant(
|
|
of: find.byKey(ValueKey(dialogKey)),
|
|
matching: find.byType(Material),
|
|
),
|
|
);
|
|
final surface = materials.firstWhere(
|
|
(material) => material.shape is RoundedRectangleBorder,
|
|
);
|
|
return (surface.shape! as RoundedRectangleBorder).side.color;
|
|
}
|