ClassBloc (13 tests): - 班级列表加载(成功/失败/loading 状态) - 选中班级详情 + 子事件触发 - 成员/日记墙/主题/评语加载 - 创建班级 + 错误处理 - 加入班级 + 列表刷新 - 布置主题 - sharedToClass 过滤逻辑 SearchBloc (20 tests): - 关键词搜索(标题/摘要/标签匹配、大小写不敏感) - 心情筛选(null/有值/失败) - 标签搜索 - 搜索历史(记录/去重) - 清除搜索 + Tab 切换 - hasActiveFilter 属性
294 lines
9.6 KiB
Dart
294 lines
9.6 KiB
Dart
// SearchBloc 单元测试
|
||
//
|
||
// 覆盖:关键词搜索、标签搜索、心情筛选、搜索历史、清除搜索、tab 切换
|
||
// 使用 mocktail mock JournalRepository
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:mocktail/mocktail.dart';
|
||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||
import 'package:nuanji_app/features/search/bloc/search_bloc.dart';
|
||
|
||
// ===== Mock =====
|
||
|
||
class MockJournalRepository extends Mock implements JournalRepository {}
|
||
|
||
// ===== 测试数据工厂 =====
|
||
|
||
JournalEntry _makeJournal({
|
||
String id = 'j-1',
|
||
String title = '今天的心情日记',
|
||
Mood mood = Mood.happy,
|
||
String? contentExcerpt,
|
||
List<String> tags = const [],
|
||
}) {
|
||
return JournalEntry(
|
||
id: id,
|
||
authorId: 'user-1',
|
||
title: title,
|
||
date: DateTime(2026, 6, 1),
|
||
mood: mood,
|
||
contentExcerpt: contentExcerpt,
|
||
tags: tags,
|
||
createdAt: DateTime(2026, 6, 1),
|
||
updatedAt: DateTime(2026, 6, 1),
|
||
);
|
||
}
|
||
|
||
void main() {
|
||
late MockJournalRepository mockJournalRepo;
|
||
late SearchBloc bloc;
|
||
|
||
setUp(() {
|
||
mockJournalRepo = MockJournalRepository();
|
||
bloc = SearchBloc(journalRepository: mockJournalRepo);
|
||
});
|
||
|
||
tearDown(() {
|
||
bloc.close();
|
||
});
|
||
|
||
/// 辅助:dispatch 并等待处理完成
|
||
Future<SearchState> dispatch(SearchEvent event) async {
|
||
bloc.add(event);
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
return bloc.state;
|
||
}
|
||
|
||
// ===== 关键词搜索 =====
|
||
|
||
group('SearchByKeyword', () {
|
||
test('空关键词不触发搜索,返回空结果', () async {
|
||
final state = await dispatch(const SearchByKeyword(''));
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).results, isEmpty);
|
||
});
|
||
|
||
test('纯空格关键词视为空', () async {
|
||
final state = await dispatch(const SearchByKeyword(' '));
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).results, isEmpty);
|
||
});
|
||
|
||
test('匹配标题中的关键词', () async {
|
||
final journals = [
|
||
_makeJournal(id: 'j1', title: '今天的心情日记'),
|
||
_makeJournal(id: 'j2', title: '周末旅行记'),
|
||
_makeJournal(id: 'j3', title: '读后感'),
|
||
];
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => journals);
|
||
|
||
final state = await dispatch(const SearchByKeyword('心情'));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
final loaded = state as SearchLoaded;
|
||
expect(loaded.results.length, 1);
|
||
expect(loaded.results.first.id, 'j1');
|
||
expect(loaded.activeKeyword, '心情');
|
||
});
|
||
|
||
test('匹配内容摘要中的关键词', () async {
|
||
final journals = [
|
||
_makeJournal(id: 'j1', title: '日记', contentExcerpt: '今天心情很好,阳光明媚'),
|
||
_makeJournal(id: 'j2', title: '随笔', contentExcerpt: '天气阴沉'),
|
||
];
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => journals);
|
||
|
||
final state = await dispatch(const SearchByKeyword('阳光'));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).results.length, 1);
|
||
expect((state as SearchLoaded).results.first.id, 'j1');
|
||
});
|
||
|
||
test('匹配标签中的关键词', () async {
|
||
final journals = [
|
||
_makeJournal(id: 'j1', tags: ['旅行', '周末']),
|
||
_makeJournal(id: 'j2', tags: ['学习']),
|
||
];
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => journals);
|
||
|
||
final state = await dispatch(const SearchByKeyword('旅行'));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).results.length, 1);
|
||
});
|
||
|
||
test('大小写不敏感搜索', () async {
|
||
final journals = [
|
||
_makeJournal(id: 'j1', title: 'Happy Day'),
|
||
_makeJournal(id: 'j2', title: 'happy mood'),
|
||
];
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => journals);
|
||
|
||
final state = await dispatch(const SearchByKeyword('HAPPY'));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).results.length, 2);
|
||
});
|
||
|
||
test('搜索失败返回 SearchError', () async {
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenThrow(Exception('网络错误'));
|
||
|
||
final state = await dispatch(const SearchByKeyword('测试'));
|
||
|
||
expect(state, isA<SearchError>());
|
||
});
|
||
|
||
test('搜索历史记录', () async {
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => []);
|
||
|
||
await dispatch(const SearchByKeyword('关键词A'));
|
||
await dispatch(const SearchByKeyword('关键词B'));
|
||
|
||
final state = await dispatch(const SearchByKeyword(''));
|
||
final loaded = state as SearchLoaded;
|
||
expect(loaded.searchHistory, ['关键词B', '关键词A']);
|
||
});
|
||
|
||
test('搜索历史去重', () async {
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => []);
|
||
|
||
await dispatch(const SearchByKeyword('重复'));
|
||
await dispatch(const SearchByKeyword('其他'));
|
||
await dispatch(const SearchByKeyword('重复'));
|
||
|
||
final state = await dispatch(const SearchByKeyword(''));
|
||
final loaded = state as SearchLoaded;
|
||
expect(loaded.searchHistory, ['重复', '其他']);
|
||
});
|
||
});
|
||
|
||
// ===== 心情筛选 =====
|
||
|
||
group('SearchByMood', () {
|
||
test('null mood 返回空结果', () async {
|
||
final state = await dispatch(const SearchByMood(null));
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).results, isEmpty);
|
||
expect((state as SearchLoaded).activeMood, isNull);
|
||
});
|
||
|
||
test('按心情筛选日记', () async {
|
||
final journals = [
|
||
_makeJournal(id: 'j1', mood: Mood.happy),
|
||
_makeJournal(id: 'j2', mood: Mood.happy),
|
||
_makeJournal(id: 'j3', mood: Mood.sad),
|
||
];
|
||
when(() => mockJournalRepo.getJournals(mood: 'happy', page: 1, pageSize: 50))
|
||
.thenAnswer((_) async => journals.where((j) => j.mood == Mood.happy).toList());
|
||
|
||
final state = await dispatch(const SearchByMood(Mood.happy));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
final loaded = state as SearchLoaded;
|
||
expect(loaded.results.length, 2);
|
||
expect(loaded.activeMood, 'happy');
|
||
});
|
||
|
||
test('心情筛选失败返回 SearchError', () async {
|
||
when(() => mockJournalRepo.getJournals(mood: 'happy', page: 1, pageSize: 50))
|
||
.thenThrow(Exception('网络错误'));
|
||
|
||
final state = await dispatch(const SearchByMood(Mood.happy));
|
||
|
||
expect(state, isA<SearchError>());
|
||
});
|
||
});
|
||
|
||
// ===== 标签搜索 =====
|
||
|
||
group('SearchByTag', () {
|
||
test('按标签筛选日记', () async {
|
||
final journals = [
|
||
_makeJournal(id: 'j1', tags: ['旅行', '周末']),
|
||
];
|
||
when(() => mockJournalRepo.getJournals(tag: '旅行', page: 1, pageSize: 50))
|
||
.thenAnswer((_) async => journals);
|
||
|
||
final state = await dispatch(const SearchByTag('旅行'));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
final loaded = state as SearchLoaded;
|
||
expect(loaded.results.length, 1);
|
||
expect(loaded.activeTag, '旅行');
|
||
expect(loaded.searchHistory, contains('旅行'));
|
||
});
|
||
|
||
test('标签搜索失败返回 SearchError', () async {
|
||
when(() => mockJournalRepo.getJournals(tag: '不存在', page: 1, pageSize: 50))
|
||
.thenThrow(Exception('网络错误'));
|
||
|
||
final state = await dispatch(const SearchByTag('不存在'));
|
||
|
||
expect(state, isA<SearchError>());
|
||
});
|
||
});
|
||
|
||
// ===== 清除搜索 =====
|
||
|
||
group('SearchClear', () {
|
||
test('清除搜索返回空结果', () async {
|
||
// 先执行一次搜索
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => [_makeJournal()]);
|
||
await dispatch(const SearchByKeyword('测试'));
|
||
|
||
// 清除
|
||
final state = await dispatch(const SearchClear());
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
final loaded = state as SearchLoaded;
|
||
expect(loaded.results, isEmpty);
|
||
expect(loaded.activeKeyword, isNull);
|
||
expect(loaded.activeMood, isNull);
|
||
expect(loaded.activeTag, isNull);
|
||
});
|
||
});
|
||
|
||
// ===== Tab 切换 =====
|
||
|
||
group('SearchTabChanged', () {
|
||
test('切换 tab 更新 activeTab', () async {
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => []);
|
||
await dispatch(const SearchByKeyword('测试'));
|
||
|
||
final state = await dispatch(const SearchTabChanged(SearchResultTab.journal));
|
||
|
||
expect(state, isA<SearchLoaded>());
|
||
expect((state as SearchLoaded).activeTab, SearchResultTab.journal);
|
||
});
|
||
|
||
test('非 SearchLoaded 状态下切换 tab 无效', () async {
|
||
// 初始状态 SearchInitial,不响应 tab 切换
|
||
final state = await dispatch(const SearchTabChanged(SearchResultTab.tag));
|
||
expect(state, isA<SearchInitial>());
|
||
});
|
||
});
|
||
|
||
// ===== hasActiveFilter =====
|
||
|
||
group('SearchLoaded.hasActiveFilter', () {
|
||
test('无筛选条件时 hasActiveFilter 为 false', () async {
|
||
final state = await dispatch(const SearchClear());
|
||
expect((state as SearchLoaded).hasActiveFilter, isFalse);
|
||
});
|
||
|
||
test('有关键词时 hasActiveFilter 为 true', () async {
|
||
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
|
||
.thenAnswer((_) async => []);
|
||
final state = await dispatch(const SearchByKeyword('测试'));
|
||
expect((state as SearchLoaded).hasActiveFilter, isTrue);
|
||
});
|
||
});
|
||
}
|