// 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); }); }); }