feat: 重构 Home Screen 视觉设计与消息输入组件
- 新增 Home Screen 视觉设计 token (背景、工具栏、对话区、输入框等) - 重构首页布局为浮动式底部输入栈结构 - 新增 HomeBackgroundField、HomeFloatingHeader、HomeAttachmentStrip 组件 - 优化 MessageComposer 视觉样式为悬浮 shell 设计 - 添加相关测试用例
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/features/home/ui/widgets/home_background_field.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('home background field renders layered glow surfaces', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: HomeBackgroundField())),
|
||||
);
|
||||
|
||||
expect(find.byKey(homeBackgroundFieldKey), findsOneWidget);
|
||||
expect(find.byKey(homeTopGlowKey), findsOneWidget);
|
||||
expect(find.byKey(homeBottomGlowKey), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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({
|
||||
@@ -50,8 +51,11 @@ void main() {
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
@@ -63,6 +67,29 @@ void main() {
|
||||
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 {
|
||||
@@ -85,7 +112,9 @@ void main() {
|
||||
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!;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:social_app/core/api/i_api_client.dart';
|
||||
import 'package:social_app/core/di/injection.dart';
|
||||
import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
|
||||
import 'package:social_app/features/home/ui/screens/home_screen.dart';
|
||||
import 'package:social_app/features/home/ui/widgets/home_attachment_strip.dart';
|
||||
import 'package:social_app/features/home/ui/widgets/home_floating_header.dart';
|
||||
import 'package:social_app/features/messages/data/inbox_api.dart';
|
||||
|
||||
class _TestApiClient implements IApiClient {
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) async {
|
||||
return Response<T>(requestOptions: RequestOptions(path: path));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) async {
|
||||
return Response<T>(
|
||||
requestOptions: RequestOptions(path: path),
|
||||
data: <dynamic>[] as T,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<String>> getSseLines(
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
return const Stream<String>.empty();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> patch<T>(String path, {data, Options? options}) async {
|
||||
return Response<T>(requestOptions: RequestOptions(path: path));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) async {
|
||||
return Response<T>(requestOptions: RequestOptions(path: path));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late ChatBloc chatBloc;
|
||||
|
||||
setUp(() {
|
||||
final apiClient = _TestApiClient();
|
||||
if (sl.isRegistered<InboxApi>()) {
|
||||
sl.unregister<InboxApi>();
|
||||
}
|
||||
sl.registerSingleton<InboxApi>(InboxApi(apiClient));
|
||||
chatBloc = ChatBloc(apiClient: apiClient);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await chatBloc.close();
|
||||
if (sl.isRegistered<InboxApi>()) {
|
||||
await sl.unregister<InboxApi>();
|
||||
}
|
||||
});
|
||||
|
||||
Future<void> pumpHomeScreen(
|
||||
WidgetTester tester, {
|
||||
List<XFile> initialSelectedImages = const [],
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HomeScreen(
|
||||
chatBloc: chatBloc,
|
||||
autoLoadHistory: false,
|
||||
initialSelectedImages: initialSelectedImages,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'home screen shows floating header, conversation stage, and bottom input stack',
|
||||
(tester) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
expect(find.byKey(homeFloatingHeaderKey), findsOneWidget);
|
||||
expect(find.byKey(homeConversationStageKey), findsOneWidget);
|
||||
expect(find.byKey(homeBottomInputStackKey), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('empty state keeps clean stage without helper copy', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
expect(find.byKey(homeConversationStageKey), findsOneWidget);
|
||||
expect(find.byKey(homeEmptyStateAmbientKey), findsOneWidget);
|
||||
expect(find.text('开始对话吧'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('selected images render in attachment strip above composer', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHomeScreen(
|
||||
tester,
|
||||
initialSelectedImages: [XFile('/tmp/mock-image-a.png')],
|
||||
);
|
||||
|
||||
expect(find.byKey(homeAttachmentStripKey), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user