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 渲染结构、手势回调、去抖、预加载、连续笔画
239 lines
7.3 KiB
Dart
239 lines
7.3 KiB
Dart
// 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);
|
||
|
||
// 未调用 ensureSize,canvasSize == 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);
|
||
});
|
||
});
|
||
}
|