Files
nj/app/test/features/calendar/bloc/calendar_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

237 lines
8.2 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.
// CalendarBloc 单元测试
//
// 覆盖:月份切换加载、日期选择、视图模式切换、状态保持
// 使用 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/calendar/bloc/calendar_bloc.dart';
void main() {
group('CalendarBloc', () {
late CalendarBloc bloc;
late InMemoryJournalRepository repo;
setUp(() {
repo = InMemoryJournalRepository();
bloc = CalendarBloc(journalRepository: repo);
});
tearDown(() {
bloc.close();
});
// ===== 辅助 =====
/// 收集事件触发后的最终状态
Future<CalendarState> dispatch(CalendarEvent event) async {
bloc.add(event);
await Future<void>.delayed(const Duration(milliseconds: 50));
return bloc.state;
}
/// 创建测试日记条目
///
/// 日期放在月份中间15 号),避免 InMemoryJournalRepository
/// 使用 isAfter/isBefore严格大于/小于)时的边界问题。
JournalEntry _makeEntry({required String id, required DateTime date}) {
return JournalEntry(
id: id,
authorId: 'user-1',
title: '日记',
date: date,
createdAt: date,
updatedAt: date,
);
}
// ===== 初始状态 =====
test('初始状态为 CalendarInitial', () {
expect(bloc.state, isA<CalendarInitial>());
});
// ===== 月份切换 =====
test('CalendarMonthChanged 成功加载日记 → journalsByDate 包含数据', () async {
// 在 6 月 15 日创建日记
final june15 = DateTime(2026, 6, 15);
await repo.createJournal(_makeEntry(id: 'j-1', date: june15));
final state = await dispatch(CalendarMonthChanged(DateTime(2026, 6, 1)));
expect(state, isA<CalendarLoaded>());
final loaded = state as CalendarLoaded;
expect(loaded.focusedMonth, DateTime(2026, 6, 1));
expect(loaded.isLoading, isFalse);
expect(loaded.journalsByDate, isNotEmpty);
final key = DateTime(2026, 6, 15);
expect(loaded.journalsByDate[key], isNotNull);
expect(loaded.journalsByDate[key]!.length, 1);
expect(loaded.journalsByDate[key]!.first.id, 'j-1');
});
test('CalendarMonthChanged 失败 → isLoading: false保留旧数据', () async {
// 使用一个会抛异常的仓库替身
final failingRepo = _FailingJournalRepository();
final failingBloc = CalendarBloc(journalRepository: failingRepo);
// 先加载一次成功数据(这里直接 emit 一个 loaded 状态)
failingBloc.add(CalendarMonthChanged(DateTime(2026, 6, 1)));
await Future<void>.delayed(const Duration(milliseconds: 50));
// 此时状态应为 CalendarLoadedisLoading: false因为加载失败
expect(failingBloc.state, isA<CalendarLoaded>());
final loaded = failingBloc.state as CalendarLoaded;
expect(loaded.isLoading, isFalse);
expect(loaded.journalsByDate, isEmpty);
await failingBloc.close();
});
test('CalendarMonthChanged 保留上一个 viewMode', () async {
// 先加载月份,进入 CalendarLoaded 状态
await dispatch(CalendarMonthChanged(DateTime(2026, 6, 1)));
expect(bloc.state, isA<CalendarLoaded>());
// 切换视图模式为 week
await dispatch(CalendarViewModeChanged(CalendarViewMode.week));
expect((bloc.state as CalendarLoaded).viewMode, CalendarViewMode.week);
// 再切换月份viewMode 应该保留为 week
final state = await dispatch(CalendarMonthChanged(DateTime(2026, 7, 1)));
final loaded = state as CalendarLoaded;
expect(loaded.viewMode, CalendarViewMode.week);
expect(loaded.focusedMonth, DateTime(2026, 7, 1));
});
// ===== 日期选择 =====
test('CalendarDaySelected 有日记 → selectedDayJournals 不为空', () async {
final june15 = DateTime(2026, 6, 15);
await repo.createJournal(_makeEntry(id: 'j-1', date: june15));
// 先加载月份
await dispatch(CalendarMonthChanged(DateTime(2026, 6, 1)));
// 选择 6 月 15 日
final state = await dispatch(CalendarDaySelected(june15));
final loaded = state as CalendarLoaded;
expect(loaded.selectedDay, june15);
expect(loaded.selectedDayJournals, isNotEmpty);
expect(loaded.selectedDayJournals.length, 1);
expect(loaded.selectedDayJournals.first.id, 'j-1');
});
test('CalendarDaySelected 无日记 → selectedDayJournals 为空', () async {
// 先加载月份(空数据)
await dispatch(CalendarMonthChanged(DateTime(2026, 6, 1)));
// 选择 6 月 10 日(无日记)
final june10 = DateTime(2026, 6, 10);
final state = await dispatch(CalendarDaySelected(june10));
final loaded = state as CalendarLoaded;
expect(loaded.selectedDay, june10);
expect(loaded.selectedDayJournals, isEmpty);
});
test('CalendarDaySelected 在 CalendarInitial 状态不改变状态', () async {
// 不先加载月份,初始状态为 CalendarInitial
expect(bloc.state, isA<CalendarInitial>());
final state = await dispatch(CalendarDaySelected(DateTime(2026, 6, 15)));
// 状态应保持为 CalendarInitial
expect(state, isA<CalendarInitial>());
});
// ===== 视图模式 =====
test('CalendarViewModeChanged 更新 viewMode', () async {
// 先进入 CalendarLoaded 状态
await dispatch(CalendarMonthChanged(DateTime(2026, 6, 1)));
final state = await dispatch(CalendarViewModeChanged(CalendarViewMode.timeline));
final loaded = state as CalendarLoaded;
expect(loaded.viewMode, CalendarViewMode.timeline);
});
test('CalendarViewModeChanged 在 CalendarInitial 状态不改变状态', () async {
expect(bloc.state, isA<CalendarInitial>());
final state = await dispatch(CalendarViewModeChanged(CalendarViewMode.week));
// 状态应保持为 CalendarInitial
expect(state, isA<CalendarInitial>());
});
// ===== 日期键归一化 =====
test('CalendarMonthChanged 日期键归一化 — 同一天不同时间映射到同一个 key', () async {
// 创建两篇日记,日期都是 6 月 15 日但时间不同
final june15Morning = DateTime(2026, 6, 15, 8, 30);
final june15Afternoon = DateTime(2026, 6, 15, 14, 45);
await repo.createJournal(_makeEntry(id: 'j-1', date: june15Morning));
await repo.createJournal(_makeEntry(id: 'j-2', date: june15Afternoon));
final state = await dispatch(CalendarMonthChanged(DateTime(2026, 6, 1)));
final loaded = state as CalendarLoaded;
// 归一化后的 key 应为 DateTime(2026, 6, 15)
final normalizedKey = DateTime(2026, 6, 15);
expect(loaded.journalsByDate.containsKey(normalizedKey), isTrue);
// 两篇日记都映射到同一个 key
expect(loaded.journalsByDate[normalizedKey]!.length, 2);
expect(
loaded.journalsByDate[normalizedKey]!.map((j) => j.id).toList()
..sort(),
['j-1', 'j-2'],
);
});
});
}
/// 加载日记时总是抛异常的仓库 — 用于测试错误路径
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 => 0;
@override
Future<JournalEntry?> getJournal(String id) async => null;
@override
Future<JournalEntry> createJournal(JournalEntry entry) async => entry;
@override
Future<JournalEntry> updateJournal(JournalEntry entry) async => entry;
@override
Future<void> deleteJournal(String id) async {}
@override
Future<List<JournalElement>> getElements(String journalId) async => [];
@override
Future<JournalElement> addElement(JournalElement element) async => element;
@override
Future<JournalElement> updateElement(JournalElement element) async => element;
@override
Future<void> removeElement(String elementId) async {}
}