Files
social-app/apps/test/features/home/ui/screens/home_screen_test.dart
T
zl-q 7b8865e256 feat: 添加 Agent 步骤事件与图片附件功能
- 新增 stepStarted/stepFinished 事件类型支持
- 前端实现图片附件上传和预览功能
- 后端增强工具结果存储和事件处理
- 完善相关单元测试和集成测试
2026-03-12 09:29:57 +08:00

324 lines
10 KiB
Dart

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<void> start() async {
started = true;
}
@override
Future<String?> stop() async {
started = false;
stoppedPath ??=
'${Directory.systemTemp.path}/test-audio-${DateTime.now().microsecondsSinceEpoch}.wav';
return stoppedPath;
}
@override
Future<void> dispose() async {}
}
class _WaitingAgUiService extends AgUiService {
_WaitingAgUiService() : super(onEvent: (_) {});
final Completer<void> _pending = Completer<void>();
@override
Future<void> sendMessage(String content, {List<XFile>? 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<InboxApi>()) {
sl.registerSingleton<InboxApi>(InboxApi(MockApiClient()));
}
});
IconData _inputActionIcon(WidgetTester tester) {
final icon = tester.widget<Icon>(
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<String>();
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<EditableTextState>(
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<EditableTextState>(
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();
});
});
}