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