Files
nj/app/test/features/home/bloc/home_bloc_test.dart
iven 7e928ae1e1
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 修复 P2~P4 共 10 项前端问题
P2 必须修复:
- 教师布置主题 classId 从硬编码改为班级下拉选择器
- 班级日记墙使用服务端 classId 过滤替代前端过滤
- Profile 统计栏接入 JournalRepository 真实数据
- WeeklyPage 从全硬编码改为 JournalRepository 数据驱动

P3 建议改进:
- 提取 mood_utils.dart 公共函数,消除 4 处重复定义
- 贴纸库搜索框连接 StickerBloc 按名称过滤

P4 细节打磨:
- 家长页多孩子时显示 DropdownButton 选择器
- 搜索结果日记卡片点击跳转 /editor?id=
- MonthlyPage 照片数量从 JournalElement 统计
- calendar_page/mood_page/search_page 统一使用 moodToEmoji/moodToLabel
2026-06-02 20:21:51 +08:00

282 lines
8.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// HomeBloc 单元测试
//
// 覆盖:首页数据加载、今日日记检查、心情统计、连续天数计算、离线容错
// 使用 InMemoryJournalRepository 作为测试替身
import 'package:flutter_test/flutter_test.dart';
import 'package:nuanji_app/data/models/journal_element.dart';
import 'package:nuanji_app/data/models/journal_entry.dart';
import 'package:nuanji_app/data/repositories/journal_repository.dart';
import 'package:nuanji_app/features/home/bloc/home_bloc.dart';
void main() {
group('HomeBloc', () {
late HomeBloc bloc;
late InMemoryJournalRepository repo;
setUp(() {
repo = InMemoryJournalRepository();
bloc = HomeBloc(journalRepository: repo);
});
tearDown(() {
bloc.close();
});
// ===== 辅助 =====
/// 收集事件触发后的最终状态
Future<HomeState> dispatch(HomeEvent event) async {
bloc.add(event);
await Future<void>.delayed(const Duration(milliseconds: 50));
return bloc.state;
}
/// 创建测试日记条目
JournalEntry _makeEntry({
required String id,
required DateTime date,
Mood mood = Mood.happy,
}) {
return JournalEntry(
id: id,
authorId: 'user-1',
title: '测试日记',
date: date,
mood: mood,
createdAt: date,
updatedAt: date,
);
}
/// 获取当天零时的 DateTime
DateTime _today() {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day);
}
/// 往前推 n 天的日期
DateTime _daysAgo(int n) {
return _today().subtract(Duration(days: n));
}
/// 向仓库批量添加日记
Future<void> _seed(List<JournalEntry> entries) async {
for (final e in entries) {
await repo.createJournal(e);
}
}
// ===== 1. HomeLoadData 成功 → HomeLoading → HomeLoaded with journals =====
test('HomeLoadData 成功时状态经过 HomeLoading 到达 HomeLoaded', () async {
final states = <HomeState>[];
final subscription = bloc.stream.listen(states.add);
await repo.createJournal(_makeEntry(
id: 'j-1',
date: _today(),
));
bloc.add(const HomeLoadData());
await Future<void>.delayed(const Duration(milliseconds: 50));
subscription.cancel();
// 至少经历 HomeLoading → HomeLoaded
expect(states, contains(isA<HomeLoading>()));
expect(states.last, isA<HomeLoaded>());
final loaded = states.last as HomeLoaded;
expect(loaded.recentJournals.length, 1);
expect(loaded.recentJournals.first.id, 'j-1');
});
// ===== 2. HomeLoadData 有今日日记 → hasTodayEntry = true =====
test('HomeLoadData 有今日日记 → hasTodayEntry = true', () async {
await _seed([
_makeEntry(id: 'j-today', date: _today()),
]);
final state = await dispatch(const HomeLoadData());
final loaded = state as HomeLoaded;
expect(loaded.hasTodayEntry, isTrue);
});
// ===== 3. HomeLoadData 无今日日记 → hasTodayEntry = false =====
test('HomeLoadData 无今日日记 → hasTodayEntry = false', () async {
await _seed([
_makeEntry(id: 'j-yesterday', date: _daysAgo(1)),
]);
final state = await dispatch(const HomeLoadData());
final loaded = state as HomeLoaded;
expect(loaded.hasTodayEntry, isFalse);
});
// ===== 4. HomeLoadData 计算最常用心情 topMood → happy(3次) vs calm(1次) =====
test('HomeLoadData topMood 为出现次数最多的心情', () async {
await _seed([
_makeEntry(id: 'j-1', date: _daysAgo(0), mood: Mood.happy),
_makeEntry(id: 'j-2', date: _daysAgo(1), mood: Mood.happy),
_makeEntry(id: 'j-3', date: _daysAgo(2), mood: Mood.happy),
_makeEntry(id: 'j-4', date: _daysAgo(3), mood: Mood.calm),
]);
final state = await dispatch(const HomeLoadData());
final loaded = state as HomeLoaded;
expect(loaded.topMood, Mood.happy);
});
// ===== 5. HomeLoadData 计算连续天数 streakDays连续 3 天)=====
test('HomeLoadData streakDays 计算从今天开始的连续天数', () async {
// 今天、昨天、前天 — 连续 3 天
await _seed([
_makeEntry(id: 'j-today', date: _daysAgo(0)),
_makeEntry(id: 'j-yesterday', date: _daysAgo(1)),
_makeEntry(id: 'j-day-before', date: _daysAgo(2)),
// 4 天前(中断连续)
_makeEntry(id: 'j-old', date: _daysAgo(4)),
]);
final state = await dispatch(const HomeLoadData());
final loaded = state as HomeLoaded;
expect(loaded.streakDays, 3);
});
// ===== 6. HomeLoadData 失败 → HomeLoaded (空数据,离线友好) =====
test('HomeLoadData 失败时返回空 HomeLoaded离线友好', () async {
// 使用一个会在 getJournals 时抛异常的仓库
final failingRepo = _FailingJournalRepository();
final failingBloc = HomeBloc(journalRepository: failingRepo);
final state = await _dispatchOn(failingBloc, const HomeLoadData());
expect(state, isA<HomeLoaded>());
final loaded = state as HomeLoaded;
expect(loaded.recentJournals, isEmpty);
expect(loaded.hasTodayEntry, isFalse);
expect(loaded.topMood, isNull);
expect(loaded.streakDays, 0);
failingBloc.close();
});
// ===== 7. HomeRefresh → 触发 HomeLoadData =====
test('HomeRefresh 触发重新加载(经过 Loading → Loaded', () async {
final states = <HomeState>[];
final subscription = bloc.stream.listen(states.add);
await repo.createJournal(_makeEntry(
id: 'j-refresh',
date: _today(),
));
bloc.add(const HomeRefresh());
await Future<void>.delayed(const Duration(milliseconds: 50));
subscription.cancel();
// HomeRefresh 内部 add(HomeLoadData),所以应有 Loading → Loaded
expect(states, contains(isA<HomeLoading>()));
expect(states.last, isA<HomeLoaded>());
final loaded = states.last as HomeLoaded;
expect(loaded.recentJournals.length, 1);
});
// ===== 8. HomeLoadData 空数据 → HomeLoaded with defaults =====
test('HomeLoadData 空数据时返回默认 HomeLoaded', () async {
// 仓库中不添加任何日记
final state = await dispatch(const HomeLoadData());
final loaded = state as HomeLoaded;
expect(loaded.recentJournals, isEmpty);
expect(loaded.hasTodayEntry, isFalse);
expect(loaded.topMood, isNull);
expect(loaded.streakDays, 0);
});
});
}
// ===== 测试辅助:总是抛出异常的仓库 =====
/// 模拟仓库失败场景,用于测试离线容错
class _FailingJournalRepository implements JournalRepository {
@override
Future<List<JournalEntry>> getJournals({
DateTime? dateFrom,
DateTime? dateTo,
int? page,
int? pageSize,
String? mood,
String? tag,
String? classId,
}) async {
throw Exception('网络不可用');
}
@override
Future<int> getJournalCount() async {
throw Exception('网络不可用');
}
@override
Future<JournalEntry?> getJournal(String id) async {
throw UnimplementedError();
}
@override
Future<JournalEntry> createJournal(JournalEntry entry) async {
throw UnimplementedError();
}
@override
Future<JournalEntry> updateJournal(JournalEntry entry) async {
throw UnimplementedError();
}
@override
Future<void> deleteJournal(String id) async {
throw UnimplementedError();
}
@override
Future<List<JournalElement>> getElements(String journalId) async {
throw UnimplementedError();
}
@override
Future<JournalElement> addElement(JournalElement element) async {
throw UnimplementedError();
}
@override
Future<JournalElement> updateElement(JournalElement element) async {
throw UnimplementedError();
}
@override
Future<void> removeElement(String elementId) async {
throw UnimplementedError();
}
}
/// 在指定 bloc 上触发事件并等待处理完毕
Future<HomeState> _dispatchOn(HomeBloc bloc, HomeEvent event) async {
bloc.add(event);
await Future<void>.delayed(const Duration(milliseconds: 50));
return bloc.state;
}