test(app): 前端第一批测试 — EditorBloc 19用例 + JournalElement 11用例 + InMemoryRepo 12用例
添加 mocktail 测试依赖 + 字体文件占位 总计 42 测试通过,覆盖工具/笔画/元素/撤销/重做/序列化/乐观锁/CRUD
This commit is contained in:
266
app/test/features/editor/bloc/editor_bloc_test.dart
Normal file
266
app/test/features/editor/bloc/editor_bloc_test.dart
Normal file
@@ -0,0 +1,266 @@
|
||||
// EditorBloc 单元测试
|
||||
//
|
||||
// 覆盖:工具切换、笔画管理(添加/撤销/重做/清空)、元素管理(添加/删除/移动/选中)、加载已有数据
|
||||
// 不使用 bloc_test,手动收集状态变化
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nuanji_app/data/models/journal_element.dart';
|
||||
import 'package:nuanji_app/features/editor/bloc/editor_bloc.dart';
|
||||
import 'package:nuanji_app/features/editor/widgets/stroke_model.dart';
|
||||
|
||||
void main() {
|
||||
group('EditorBloc', () {
|
||||
late EditorBloc bloc;
|
||||
|
||||
setUp(() {
|
||||
bloc = EditorBloc();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
bloc.close();
|
||||
});
|
||||
|
||||
// ===== 辅助 =====
|
||||
|
||||
/// 收集事件触发后的最终状态
|
||||
Future<EditorState> dispatch(EditorEvent event) async {
|
||||
bloc.add(event);
|
||||
// 等待 BLoC 处理完毕
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
return bloc.state;
|
||||
}
|
||||
|
||||
Stroke _testStroke({String id = 's1'}) {
|
||||
return Stroke(
|
||||
id: id,
|
||||
points: [StrokePoint(x: 10, y: 20)],
|
||||
brushType: BrushType.pen,
|
||||
color: '#000000',
|
||||
width: 3.0,
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 初始状态 =====
|
||||
|
||||
test('初始状态:pen 工具、空笔画、空元素、非 dirty', () {
|
||||
expect(bloc.state.activeTool, EditorTool.pen);
|
||||
expect(bloc.state.strokes, isEmpty);
|
||||
expect(bloc.state.elements, isEmpty);
|
||||
expect(bloc.state.isDirty, isFalse);
|
||||
expect(bloc.state.selectedElementId, isNull);
|
||||
});
|
||||
|
||||
// ===== 工具切换 =====
|
||||
|
||||
test('切换到文字工具', () async {
|
||||
final state = await dispatch(ToolChanged(EditorTool.text));
|
||||
expect(state.activeTool, EditorTool.text);
|
||||
});
|
||||
|
||||
test('切换到贴纸工具', () async {
|
||||
final state = await dispatch(ToolChanged(EditorTool.sticker));
|
||||
expect(state.activeTool, EditorTool.sticker);
|
||||
});
|
||||
|
||||
test('切换工具时清除元素选中', () async {
|
||||
// 先添加元素并选中
|
||||
bloc.add(ElementAdded(JournalElement.createSticker(
|
||||
journalId: 'j-1', emoji: '🐱', position: Offset.zero,
|
||||
)));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
bloc.add(ElementSelected(bloc.state.elements.first.id));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(bloc.state.selectedElementId, isNotNull);
|
||||
|
||||
// 切换工具 → 清除选中
|
||||
final state = await dispatch(ToolChanged(EditorTool.text));
|
||||
expect(state.selectedElementId, isNull);
|
||||
});
|
||||
|
||||
// ===== 画笔设置 =====
|
||||
|
||||
test('切换画笔类型和颜色', () async {
|
||||
final state = await dispatch(
|
||||
BrushChanged(type: BrushType.marker, color: '#FF0000', width: 8.0),
|
||||
);
|
||||
expect(state.brushType, BrushType.marker);
|
||||
expect(state.brushColor, '#FF0000');
|
||||
expect(state.brushWidth, 8.0);
|
||||
});
|
||||
|
||||
// ===== 笔画管理 =====
|
||||
|
||||
test('添加笔画标记 dirty', () async {
|
||||
final state = await dispatch(StrokeCompleted(_testStroke()));
|
||||
expect(state.strokes.length, 1);
|
||||
expect(state.strokes.first.id, 's1');
|
||||
expect(state.isDirty, isTrue);
|
||||
});
|
||||
|
||||
test('撤销笔画移到 redoStack', () async {
|
||||
bloc.add(StrokeCompleted(_testStroke(id: 's1')));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(Undo());
|
||||
expect(state.strokes, isEmpty);
|
||||
expect(state.redoStack.length, 1);
|
||||
expect(state.redoStack.first.id, 's1');
|
||||
});
|
||||
|
||||
test('重做笔画从 redoStack 恢复', () async {
|
||||
bloc.add(StrokeCompleted(_testStroke(id: 's1')));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
bloc.add(Undo());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(Redo());
|
||||
expect(state.strokes.length, 1);
|
||||
expect(state.strokes.first.id, 's1');
|
||||
expect(state.redoStack, isEmpty);
|
||||
});
|
||||
|
||||
test('清空画布清除所有笔画和重做栈', () async {
|
||||
bloc.add(StrokeCompleted(_testStroke(id: 's1')));
|
||||
bloc.add(StrokeCompleted(_testStroke(id: 's2')));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(ClearCanvas());
|
||||
expect(state.strokes, isEmpty);
|
||||
expect(state.redoStack, isEmpty);
|
||||
});
|
||||
|
||||
test('添加新笔画时清除重做栈', () async {
|
||||
bloc.add(StrokeCompleted(_testStroke(id: 's1')));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
bloc.add(Undo());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(bloc.state.redoStack.length, 1);
|
||||
|
||||
// 添加新笔画 → 重做栈应清空
|
||||
final state = await dispatch(StrokeCompleted(_testStroke(id: 's2')));
|
||||
expect(state.strokes.length, 1);
|
||||
expect(state.strokes.first.id, 's2');
|
||||
expect(state.redoStack, isEmpty);
|
||||
});
|
||||
|
||||
// ===== 元素管理 =====
|
||||
|
||||
test('添加文字元素', () async {
|
||||
final state = await dispatch(ElementAdded(
|
||||
JournalElement.createText(
|
||||
journalId: 'j-1',
|
||||
text: '你好',
|
||||
position: Offset.zero,
|
||||
),
|
||||
));
|
||||
expect(state.elements.length, 1);
|
||||
expect(state.elements.first.elementType, ElementType.text);
|
||||
expect(state.elements.first.content['text'], '你好');
|
||||
expect(state.isDirty, isTrue);
|
||||
});
|
||||
|
||||
test('添加贴纸元素', () async {
|
||||
final state = await dispatch(ElementAdded(
|
||||
JournalElement.createSticker(
|
||||
journalId: 'j-1',
|
||||
emoji: '🌸',
|
||||
position: const Offset(50, 50),
|
||||
),
|
||||
));
|
||||
expect(state.elements.length, 1);
|
||||
expect(state.elements.first.content['emoji'], '🌸');
|
||||
});
|
||||
|
||||
test('删除元素', () async {
|
||||
final elem = JournalElement.createText(
|
||||
journalId: 'j-1', text: '删除测试', position: Offset.zero,
|
||||
);
|
||||
bloc.add(ElementAdded(elem));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(ElementRemoved(elem.id));
|
||||
expect(state.elements, isEmpty);
|
||||
});
|
||||
|
||||
test('移动元素更新位置', () async {
|
||||
final elem = JournalElement.createText(
|
||||
journalId: 'j-1', text: '移动测试', position: Offset.zero,
|
||||
);
|
||||
bloc.add(ElementAdded(elem));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(ElementMoved(
|
||||
elementId: elem.id,
|
||||
positionX: 100.0,
|
||||
positionY: 200.0,
|
||||
));
|
||||
final moved = state.elements.first;
|
||||
expect(moved.positionX, 100.0);
|
||||
expect(moved.positionY, 200.0);
|
||||
});
|
||||
|
||||
test('选中元素', () async {
|
||||
final elem = JournalElement.createSticker(
|
||||
journalId: 'j-1', emoji: '🐱', position: Offset.zero,
|
||||
);
|
||||
bloc.add(ElementAdded(elem));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(ElementSelected(elem.id));
|
||||
expect(state.selectedElementId, elem.id);
|
||||
});
|
||||
|
||||
test('取消选中元素', () async {
|
||||
final elem = JournalElement.createSticker(
|
||||
journalId: 'j-1', emoji: '🐱', position: Offset.zero,
|
||||
);
|
||||
bloc.add(ElementAdded(elem));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
bloc.add(ElementSelected(elem.id));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final state = await dispatch(ElementSelected(null));
|
||||
expect(state.selectedElementId, isNull);
|
||||
});
|
||||
|
||||
// ===== 加载已有数据 =====
|
||||
|
||||
test('加载已有笔画', () async {
|
||||
final state = await dispatch(StrokesLoaded([
|
||||
_testStroke(id: 's1'),
|
||||
_testStroke(id: 's2'),
|
||||
]));
|
||||
expect(state.strokes.length, 2);
|
||||
});
|
||||
|
||||
test('加载已有元素', () async {
|
||||
final state = await dispatch(ElementsLoaded([
|
||||
JournalElement.createText(journalId: 'j-1', text: '已有文字', position: Offset.zero),
|
||||
JournalElement.createSticker(journalId: 'j-1', emoji: '🌸', position: const Offset(50, 50)),
|
||||
]));
|
||||
expect(state.elements.length, 2);
|
||||
});
|
||||
|
||||
// ===== 自动保存 =====
|
||||
|
||||
test('onSave 回调在状态变更后记录', () async {
|
||||
final savedStates = <EditorState>[];
|
||||
final bloc = EditorBloc(
|
||||
onSave: (state) => savedStates.add(state),
|
||||
);
|
||||
|
||||
bloc.add(StrokeCompleted(_testStroke()));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
// onSave 通过 debounce 触发(2秒),短时间等待可能未触发
|
||||
// 但我们验证 bloc 的 isDirty 已被标记
|
||||
expect(bloc.state.isDirty, isTrue);
|
||||
|
||||
bloc.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user