Files
nj/app/test/features/editor/widgets/stroke_cache_test.dart
iven f6d394afb6 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 渲染结构、手势回调、去抖、预加载、连续笔画
2026-06-03 18:57:41 +08:00

239 lines
7.3 KiB
Dart
Raw Permalink 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.
// 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);
});
});
}