Files
social-app/apps/test/features/home/ui/widgets/home_composer_test.dart
T
qzl 3273d63b23 feat: 重构 Home Screen 视觉设计与消息输入组件
- 新增 Home Screen 视觉设计 token (背景、工具栏、对话区、输入框等)
- 重构首页布局为浮动式底部输入栈结构
- 新增 HomeBackgroundField、HomeFloatingHeader、HomeAttachmentStrip 组件
- 优化 MessageComposer 视觉样式为悬浮 shell 设计
- 添加相关测试用例
2026-03-13 17:25:29 +08:00

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);
});
});
}