import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:social_app/core/api/api_exception.dart'; import 'package:social_app/core/api/mock_api_client.dart'; import 'package:social_app/core/di/injection.dart'; import 'package:social_app/features/chat/data/models/ag_ui_event.dart'; import 'package:social_app/features/chat/data/services/ag_ui_service.dart'; import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart'; import 'package:social_app/features/home/data/voice_recorder.dart'; import 'package:social_app/features/home/ui/screens/home_screen.dart'; import 'package:social_app/features/messages/data/inbox_api.dart'; class _FakeVoiceRecorder implements VoiceRecorder { bool started = false; String? stoppedPath; @override Future start() async { started = true; } @override Future stop() async { started = false; stoppedPath ??= '${Directory.systemTemp.path}/test-audio-${DateTime.now().microsecondsSinceEpoch}.wav'; return stoppedPath; } @override Future dispose() async {} } class _WaitingAgUiService extends AgUiService { _WaitingAgUiService() : super(onEvent: (_) {}); final Completer _pending = Completer(); @override Future sendMessage(String content, {List? images}) async { onEvent(RunStartedEvent(threadId: 't1', runId: 'r1')); return _pending.future; } void emitStepStarted(String stepName) { onEvent(StepStartedEvent(stepName: stepName)); } } void main() { setUpAll(() { if (!sl.isRegistered()) { sl.registerSingleton(InboxApi(MockApiClient())); } }); IconData _inputActionIcon(WidgetTester tester) { final icon = tester.widget( find.byKey(const ValueKey('home_input_action_icon')), ); return icon.icon!; } group('HomeScreen Widget Tests', () { testWidgets('displays input field', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: HomeScreen(autoLoadHistory: false)), ); await tester.pumpAndSettle(); expect(find.byType(TextField), findsOneWidget); expect(find.text('输入消息...'), findsOneWidget); }); testWidgets('displays header icons', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: HomeScreen(autoLoadHistory: false)), ); await tester.pumpAndSettle(); expect(find.byIcon(LucideIcons.settings), findsOneWidget); expect(find.byIcon(LucideIcons.calendar), findsOneWidget); expect(find.byIcon(LucideIcons.messageSquare), findsOneWidget); }); testWidgets('displays send or mic icon based on input', ( WidgetTester tester, ) async { await tester.pumpWidget( const MaterialApp(home: HomeScreen(autoLoadHistory: false)), ); await tester.pumpAndSettle(); expect(find.byIcon(LucideIcons.mic), findsOneWidget); }); testWidgets('tap mic starts recording and shows listening state', ( WidgetTester tester, ) async { final fakeRecorder = _FakeVoiceRecorder(); await tester.pumpWidget( MaterialApp( home: HomeScreen(voiceRecorder: fakeRecorder, autoLoadHistory: false), ), ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(LucideIcons.mic)); await tester.pump(); expect(fakeRecorder.started, true); expect(find.text('正在聆听...'), findsOneWidget); expect(_inputActionIcon(tester), LucideIcons.send); }); testWidgets('tap send while recording transcribes and auto sends message', ( WidgetTester tester, ) async { final fakeRecorder = _FakeVoiceRecorder(); String? sentTranscript; await tester.pumpWidget( MaterialApp( home: HomeScreen( voiceRecorder: fakeRecorder, autoLoadHistory: false, onTranscribeAudio: (filePath) async { expect(filePath.endsWith('.wav'), true); return '语音自动发送'; }, onAutoSendTranscript: (transcript) async { sentTranscript = transcript; }, ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(const Duration(milliseconds: 300)); expect(sentTranscript, '语音自动发送'); expect(find.byIcon(LucideIcons.plus), findsOneWidget); }); testWidgets('tap stop enters transcribing state', ( WidgetTester tester, ) async { final fakeRecorder = _FakeVoiceRecorder(); await tester.pumpWidget( MaterialApp( home: HomeScreen( voiceRecorder: fakeRecorder, autoLoadHistory: false, onTranscribeAudio: (filePath) async { expect(filePath.endsWith('.wav'), true); return '语音转文字结果'; }, onAutoSendTranscript: (_) async {}, ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); expect(find.text('语音识别中...'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsAtLeastNWidgets(1)); }); testWidgets('tap stop shows readable unauthorized message', ( WidgetTester tester, ) async { final fakeRecorder = _FakeVoiceRecorder(); await tester.pumpWidget( MaterialApp( home: HomeScreen( voiceRecorder: fakeRecorder, autoLoadHistory: false, onTranscribeAudio: (_) async { throw const UnauthorizedException(); }, ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(const Duration(milliseconds: 300)); expect(find.text('请重新登录'), findsOneWidget); await tester.pump(const Duration(seconds: 3)); }); testWidgets('tap stop shows message when transcript is empty', ( WidgetTester tester, ) async { final fakeRecorder = _FakeVoiceRecorder(); await tester.pumpWidget( MaterialApp( home: HomeScreen( voiceRecorder: fakeRecorder, autoLoadHistory: false, onTranscribeAudio: (_) async => '', ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(const Duration(milliseconds: 300)); expect(find.text('未识别到有效语音,请靠近麦克风并连续说话后重试'), findsOneWidget); await tester.pump(const Duration(seconds: 3)); }); testWidgets('shows transcribing indicator while waiting ASR result', ( WidgetTester tester, ) async { final fakeRecorder = _FakeVoiceRecorder(); final completer = Completer(); await tester.pumpWidget( MaterialApp( home: HomeScreen( voiceRecorder: fakeRecorder, autoLoadHistory: false, onTranscribeAudio: (_) => completer.future, onAutoSendTranscript: (_) async {}, ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); expect(find.text('语音识别中...'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsOneWidget); completer.complete('识别完成'); }); testWidgets('tap send unfocuses text input after sending', ( WidgetTester tester, ) async { await tester.pumpWidget( const MaterialApp(home: HomeScreen(autoLoadHistory: false)), ); await tester.pumpAndSettle(); await tester.tap(find.byType(TextField)); await tester.pump(); await tester.enterText(find.byType(TextField), 'hello'); await tester.pump(); final editableBefore = tester.state( find.byType(EditableText), ); expect(editableBefore.widget.focusNode.hasFocus, isTrue); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); final editableAfter = tester.state( find.byType(EditableText), ); expect(editableAfter.widget.focusNode.hasFocus, isFalse); await tester.pump(const Duration(milliseconds: 300)); }); testWidgets('shows stop icon and waiting indicator while waiting agent', ( WidgetTester tester, ) async { final waitingService = _WaitingAgUiService(); final chatBloc = ChatBloc(service: waitingService); await tester.pumpWidget( MaterialApp( home: HomeScreen(autoLoadHistory: false, chatBloc: chatBloc), ), ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'hello'); await tester.pump(); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); expect(_inputActionIcon(tester), LucideIcons.square); expect(find.text('正在思考...'), findsOneWidget); waitingService.emitStepStarted('intent'); await tester.pump(); expect(find.text('意图识别中'), findsOneWidget); expect(find.text('正在思考...'), findsNothing); await tester.tap(find.byKey(const ValueKey('home_input_action_button'))); await tester.pump(); expect(find.text('已停止等待回复'), findsOneWidget); await tester.pump(const Duration(seconds: 3)); await chatBloc.close(); }); }); }