3273d63b23
- 新增 Home Screen 视觉设计 token (背景、工具栏、对话区、输入框等) - 重构首页布局为浮动式底部输入栈结构 - 新增 HomeBackgroundField、HomeFloatingHeader、HomeAttachmentStrip 组件 - 优化 MessageComposer 视觉样式为悬浮 shell 设计 - 添加相关测试用例
267 lines
7.9 KiB
Dart
267 lines
7.9 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import 'package:social_app/core/theme/design_tokens.dart';
|
|
import 'package:social_app/shared/widgets/message_composer.dart';
|
|
|
|
Widget _buildTestApp({
|
|
required MessageComposerMode mode,
|
|
required MessageComposerProcess process,
|
|
required bool hasMessage,
|
|
required bool isWaitingAgent,
|
|
VoidCallback? onHoldStart,
|
|
VoidCallback? onHoldEnd,
|
|
VoidCallback? onHoldCancel,
|
|
}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: MessageComposer(
|
|
mode: mode,
|
|
process: process,
|
|
hasMessage: hasMessage,
|
|
isWaitingAgent: isWaitingAgent,
|
|
iconSize: 24,
|
|
composerMinHeight: 48,
|
|
onTapPlus: () {},
|
|
onTapRightAction: () {},
|
|
onHoldToSpeakStart: onHoldStart ?? () {},
|
|
onHoldToSpeakEnd: onHoldEnd ?? () {},
|
|
onHoldToSpeakMoveUpdate: (_) {},
|
|
onHoldToSpeakCancel: onHoldCancel ?? () {},
|
|
textInputChild: const SizedBox.shrink(),
|
|
recordingAnimation: const SizedBox.shrink(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
group('MessageComposer', () {
|
|
testWidgets('renders one unified rounded composer container', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.text,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(messageComposerContainerKey), findsOneWidget);
|
|
expect(find.byKey(messageComposerShellKey), findsOneWidget);
|
|
expect(find.byKey(messageComposerInnerKey), findsOneWidget);
|
|
|
|
final containerFinder = find.byKey(messageComposerContainerKey);
|
|
final shellFinder = find.byKey(messageComposerShellKey);
|
|
final plusFinder = find.byKey(messageComposerPlusButtonKey);
|
|
final rightFinder = find.byKey(messageComposerRightButtonKey);
|
|
|
|
expect(
|
|
find.descendant(of: containerFinder, matching: plusFinder),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.descendant(of: containerFinder, matching: rightFinder),
|
|
findsOneWidget,
|
|
);
|
|
|
|
final container = tester.widget<Container>(shellFinder);
|
|
final decoration = container.decoration! as BoxDecoration;
|
|
expect(decoration.color, AppColors.homeComposerShell);
|
|
expect(
|
|
(decoration.border! as Border).top.color,
|
|
AppColors.homeComposerBorder,
|
|
);
|
|
});
|
|
|
|
testWidgets('recording state keeps unified floating shell', (tester) async {
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.recording,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(messageComposerShellKey), findsOneWidget);
|
|
expect(find.byKey(messageComposerInnerKey), findsOneWidget);
|
|
expect(find.text('松开发送'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('right action icon follows state priority', (tester) async {
|
|
Future<IconData> rightIconFor({
|
|
required MessageComposerMode mode,
|
|
required MessageComposerProcess process,
|
|
required bool hasMessage,
|
|
required bool isWaitingAgent,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: mode,
|
|
process: process,
|
|
hasMessage: hasMessage,
|
|
isWaitingAgent: isWaitingAgent,
|
|
),
|
|
);
|
|
|
|
final iconFinder = find.descendant(
|
|
of: find.byKey(messageComposerRightButtonKey),
|
|
matching: find.byType(Icon),
|
|
);
|
|
expect(iconFinder, findsOneWidget);
|
|
final iconWidget = tester.widget<Icon>(iconFinder.first);
|
|
expect(iconWidget.icon, isNotNull);
|
|
return iconWidget.icon!;
|
|
}
|
|
|
|
expect(
|
|
await rightIconFor(
|
|
mode: MessageComposerMode.text,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: true,
|
|
),
|
|
LucideIcons.square,
|
|
);
|
|
|
|
expect(
|
|
await rightIconFor(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: true,
|
|
isWaitingAgent: false,
|
|
),
|
|
LucideIcons.send,
|
|
);
|
|
|
|
expect(
|
|
await rightIconFor(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
LucideIcons.keyboard,
|
|
);
|
|
|
|
expect(
|
|
await rightIconFor(
|
|
mode: MessageComposerMode.text,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
LucideIcons.mic,
|
|
);
|
|
});
|
|
|
|
testWidgets('recording hint appears only while recording', (tester) async {
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
);
|
|
expect(find.byKey(messageComposerRecordingHintKey), findsNothing);
|
|
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.recording,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
);
|
|
expect(find.byKey(messageComposerRecordingHintKey), findsOneWidget);
|
|
expect(find.text('松开发送,上滑取消'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('composer height remains stable across mode switches', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.text,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
);
|
|
final textHeight = tester.getSize(
|
|
find.byKey(messageComposerContainerKey),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
),
|
|
);
|
|
final holdHeight = tester.getSize(
|
|
find.byKey(messageComposerContainerKey),
|
|
);
|
|
|
|
expect(textHeight.height, holdHeight.height);
|
|
});
|
|
|
|
testWidgets('invokes long press start/end callbacks in hold mode', (
|
|
tester,
|
|
) async {
|
|
var started = false;
|
|
var ended = false;
|
|
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
onHoldStart: () => started = true,
|
|
onHoldEnd: () => ended = true,
|
|
),
|
|
);
|
|
|
|
final center = tester.getCenter(find.byKey(messageComposerHoldAreaKey));
|
|
final gesture = await tester.startGesture(center);
|
|
await tester.pump(kLongPressTimeout + const Duration(milliseconds: 10));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(started, isTrue);
|
|
expect(ended, isTrue);
|
|
});
|
|
|
|
testWidgets('invokes long press cancel callback when gesture canceled', (
|
|
tester,
|
|
) async {
|
|
var canceled = false;
|
|
|
|
await tester.pumpWidget(
|
|
_buildTestApp(
|
|
mode: MessageComposerMode.holdToSpeak,
|
|
process: MessageComposerProcess.idle,
|
|
hasMessage: false,
|
|
isWaitingAgent: false,
|
|
onHoldCancel: () => canceled = true,
|
|
),
|
|
);
|
|
|
|
final center = tester.getCenter(find.byKey(messageComposerHoldAreaKey));
|
|
final gesture = await tester.startGesture(center);
|
|
await tester.pump(kLongPressTimeout + const Duration(milliseconds: 10));
|
|
await gesture.cancel();
|
|
await tester.pump();
|
|
|
|
expect(canceled, isTrue);
|
|
});
|
|
});
|
|
}
|