test(app): 手写引擎 Canvas 集成测试 — 55 个测试全覆盖

4 个测试文件:
- stroke_model_test.dart (12 tests) — StrokePoint/Stroke 序列化、不可变性、默认值
- stroke_renderer_test.dart (19 tests) — parseHexColor/pointsToOutline/buildStrokePath/createPaintForStroke
- stroke_cache_test.dart (15 tests) — StrokeRasterCache 添加/同步/清除/尺寸变化/边界条件
- handwriting_canvas_test.dart (9 tests) — Widget 渲染结构、手势回调、去抖、预加载、连续笔画
This commit is contained in:
iven
2026-06-03 18:57:41 +08:00
parent 4cd08535d3
commit f6d394afb6
4 changed files with 883 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
// StrokeRasterCache 单元测试 — 光栅化缓存管理器
//
// 注意ui.PictureRecorder().endRecording().toImage() 需要 Flutter Test 绑定,
// 因此这些测试在 flutter test 环境中运行(自动提供 TestWidgetsFlutterBinding
// 使用足够大的画布尺寸(>0才能使光栅化生效。
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:nuanji_app/features/editor/widgets/stroke_cache.dart';
import 'package:nuanji_app/features/editor/widgets/stroke_model.dart';
void main() {
// ============================================================
// 辅助
// ============================================================
/// 构造一条从 (x0,y0) 到 (x1,y1) 的简单笔画
Stroke makeStroke(
String id, {
double x0 = 10.0,
double y0 = 10.0,
double x1 = 200.0,
double y1 = 200.0,
BrushType brushType = BrushType.pen,
String color = '#2D2420',
double width = 3.0,
int pointCount = 10,
}) {
return Stroke(
id: id,
points: List.generate(
pointCount,
(i) {
final t = i / (pointCount - 1);
return StrokePoint(
x: x0 + (x1 - x0) * t,
y: y0 + (y1 - y0) * t,
pressure: 0.5,
timestamp: i * 16,
);
},
),
brushType: brushType,
color: color,
width: width,
);
}
// ============================================================
// 生命周期与基本属性
// ============================================================
group('StrokeRasterCache — 生命周期', () {
test('初始状态为空', () {
final cache = StrokeRasterCache();
addTearDown(cache.dispose);
expect(cache.compositeImage, isNull);
expect(cache.layerVersion, 0);
expect(cache.length, 0);
expect(cache.cachedStrokeIds, isEmpty);
});
test('dispose 后可安全调用', () {
final cache = StrokeRasterCache();
cache.dispose();
// 不应抛异常
expect(cache.compositeImage, isNull);
});
});
// ============================================================
// 尺寸管理
// ============================================================
group('StrokeRasterCache — 尺寸管理', () {
test('ensureSize 设置画布尺寸', () {
final cache = StrokeRasterCache();
addTearDown(cache.dispose);
cache.ensureSize(const Size(800, 600));
expect(cache.canvasSize, const Size(800, 600));
});
test('ensureSize 相同尺寸不触发失效', () {
final cache = StrokeRasterCache();
addTearDown(cache.dispose);
cache.ensureSize(const Size(800, 600));
final v1 = cache.layerVersion;
cache.ensureSize(const Size(800, 600)); // 相同尺寸
expect(cache.layerVersion, v1); // 版本不变
});
});
// ============================================================
// 笔画操作
// ============================================================
group('StrokeRasterCache — 笔画操作', () {
late StrokeRasterCache cache;
setUp(() {
cache = StrokeRasterCache();
cache.ensureSize(const Size(800, 600));
});
tearDown(() {
cache.dispose();
});
test('addStroke 缓存笔画并递增版本', () async {
final stroke = makeStroke('s1');
await cache.addStroke(stroke);
expect(cache.length, 1);
expect(cache.cachedStrokeIds, contains('s1'));
expect(cache.layerVersion, greaterThan(0));
expect(cache.compositeImage, isNotNull);
});
test('addStroke 在画布尺寸为零时跳过', () async {
final emptyCache = StrokeRasterCache();
addTearDown(emptyCache.dispose);
// 未调用 ensureSizecanvasSize == Size.zero
await emptyCache.addStroke(makeStroke('s1'));
expect(emptyCache.length, 0);
});
test('多条笔画增量合成', () async {
await cache.addStroke(makeStroke('s1'));
final v1 = cache.layerVersion;
await cache.addStroke(makeStroke('s2'));
final v2 = cache.layerVersion;
await cache.addStroke(makeStroke('s3'));
expect(cache.length, 3);
expect(v2, greaterThan(v1));
expect(cache.layerVersion, greaterThan(v2));
});
test('不同画笔类型均可光栅化', () async {
for (final bt in BrushType.values) {
final id = 'stroke-${bt.value}';
await cache.addStroke(makeStroke(
id,
brushType: bt,
color: bt == BrushType.eraser ? '#FFFFFF' : '#E07A5F',
));
expect(cache.cachedStrokeIds, contains(id), reason: '$bt 应能光栅化');
}
expect(cache.length, BrushType.values.length);
});
test('clear 清除所有缓存', () async {
await cache.addStroke(makeStroke('s1'));
await cache.addStroke(makeStroke('s2'));
expect(cache.length, 2);
await cache.clear();
expect(cache.length, 0);
expect(cache.compositeImage, isNull);
expect(cache.cachedStrokeIds, isEmpty);
});
test('syncStrokes 添加缺失笔画', () async {
await cache.addStroke(makeStroke('s1'));
expect(cache.length, 1);
// syncStrokes 传入 s1 + s2应只添加 s2
await cache.syncStrokes([makeStroke('s1'), makeStroke('s2')]);
expect(cache.length, 2);
expect(cache.cachedStrokeIds, containsAll(['s1', 's2']));
});
test('syncStrokes 移除多余笔画(模拟撤销)', () async {
await cache.addStroke(makeStroke('s1'));
await cache.addStroke(makeStroke('s2'));
expect(cache.length, 2);
// syncStrokes 只保留 s1移除 s2
await cache.syncStrokes([makeStroke('s1')]);
expect(cache.length, 1);
expect(cache.cachedStrokeIds, {'s1'});
});
test('syncStrokes 无变化时不增加版本', () async {
await cache.addStroke(makeStroke('s1'));
final v = cache.layerVersion;
// 传入完全相同的笔画列表
await cache.syncStrokes([makeStroke('s1')]);
expect(cache.layerVersion, v);
});
test('invalidateAll 重建所有缓存', () async {
await cache.addStroke(makeStroke('s1'));
final v1 = cache.layerVersion;
// 尺寸变化触发 invalidateAll
cache.ensureSize(const Size(1024, 768));
await cache.addStroke(makeStroke('s1')); // 重建后重新添加
// 版本应高于之前(因为 invalidateAll + addStroke 都递增)
expect(cache.layerVersion, greaterThan(v1));
});
});
// ============================================================
// 空边界条件
// ============================================================
group('StrokeRasterCache — 边界条件', () {
test('单点笔画不会生成缓存条目', () async {
final cache = StrokeRasterCache();
cache.ensureSize(const Size(800, 600));
addTearDown(cache.dispose);
final singlePointStroke = Stroke(
id: 'single',
points: [const StrokePoint(x: 50, y: 50)],
);
await cache.addStroke(singlePointStroke);
// 单点 → pointsToOutline 返回空 → _rasterizeStroke 返回 null
expect(cache.length, 0);
});
test('空笔画列表的 syncStrokes 不报错', () async {
final cache = StrokeRasterCache();
cache.ensureSize(const Size(800, 600));
addTearDown(cache.dispose);
await cache.syncStrokes([]); // 不应抛
expect(cache.length, 0);
});
});
}