Files
nj/app/test/features/editor/widgets/stroke_renderer_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

215 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.
// StrokeRenderer 单元测试 — 纯函数验证
//
// 覆盖pointsToOutline、buildStrokePath、parseHexColor、createPaintForStroke
// 不依赖 Flutter 绑定dart:ui 的 Canvas/Image仅测试纯逻辑。
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:nuanji_app/features/editor/widgets/stroke_model.dart';
import 'package:nuanji_app/features/editor/widgets/stroke_renderer.dart';
void main() {
// ============================================================
// parseHexColor
// ============================================================
group('parseHexColor', () {
test('解析标准 #RRGGBB 格式', () {
final color = parseHexColor('#E07A5F');
expect(color.value, const Color(0xFFE07A5F).value);
});
test('解析不带 # 的 6 位十六进制', () {
// parseHexColor 会先 replaceFirst('#', ''),所以直接传 6 位也应该工作
final color = parseHexColor('#2D2420');
expect(color, const Color(0xFF2D2420));
});
test('全黑 #000000', () {
expect(parseHexColor('#000000').value, const Color(0xFF000000).value);
});
test('全白 #FFFFFF', () {
expect(parseHexColor('#FFFFFF').value, const Color(0xFFFFFFFF).value);
});
test('无效长度回退到默认色', () {
const fallback = Color(0xFF2D2420);
expect(parseHexColor('#FFF').value, fallback.value);
expect(parseHexColor('#12345').value, fallback.value);
expect(parseHexColor('').value, fallback.value);
});
test('无效字符回退到默认色', () {
expect(parseHexColor('#GGGGGG').value, const Color(0xFF2D2420).value);
});
});
// ============================================================
// pointsToOutline
// ============================================================
group('pointsToOutline', () {
/// 构造 N 个均匀分布的点
List<StrokePoint> makeLinearPoints(int count) => List.generate(
count,
(i) => StrokePoint(
x: i * 10.0,
y: i * 10.0,
pressure: 0.5,
timestamp: i * 16,
),
);
test('少于 2 个点返回空列表', () {
final empty = pointsToOutline([], BrushType.pen, 3.0);
expect(empty, isEmpty);
final onePoint = pointsToOutline(
[const StrokePoint(x: 0, y: 0)],
BrushType.pen,
3.0,
);
expect(onePoint, isEmpty);
});
test('2 个点生成非空轮廓', () {
final points = makeLinearPoints(2);
final outline = pointsToOutline(points, BrushType.pen, 3.0);
expect(outline, isNotEmpty);
// perfect_freehand 生成的是封闭轮廓,点数远多于输入点
expect(outline.length, greaterThan(points.length));
});
test('4 种画笔类型均能生成轮廓', () {
final points = makeLinearPoints(10);
for (final bt in BrushType.values) {
final outline = pointsToOutline(points, bt, 3.0);
expect(outline, isNotEmpty, reason: '$bt 应生成非空轮廓');
}
});
test('宽度影响轮廓大小 — 更大的 width 产生更大的轮廓', () {
final points = makeLinearPoints(10);
final outlineThin = pointsToOutline(points, BrushType.pen, 1.0);
final outlineThick = pointsToOutline(points, BrushType.pen, 8.0);
// 计算轮廓的包围盒面积作为粗略大小指标
double bboxArea(List<Offset> pts) {
if (pts.isEmpty) return 0;
double minX = double.infinity, maxX = double.negativeInfinity;
double minY = double.infinity, maxY = double.negativeInfinity;
for (final p in pts) {
if (p.dx < minX) minX = p.dx;
if (p.dx > maxX) maxX = p.dx;
if (p.dy < minY) minY = p.dy;
if (p.dy > maxY) maxY = p.dy;
}
return (maxX - minX) * (maxY - minY);
}
expect(bboxArea(outlineThick), greaterThan(bboxArea(outlineThin)));
});
test('isComplete 参数影响输出(端点处理)', () {
final points = makeLinearPoints(5);
final complete = pointsToOutline(points, BrushType.pen, 3.0, isComplete: true);
final active = pointsToOutline(points, BrushType.pen, 3.0, isComplete: false);
// 两者都应该生成轮廓,但端点处理不同
expect(complete, isNotEmpty);
expect(active, isNotEmpty);
});
});
// ============================================================
// buildStrokePath
// ============================================================
group('buildStrokePath', () {
test('空列表返回空 Path', () {
final path = buildStrokePath([]);
// 空 path 的 bounds 是零矩形
expect(path.getBounds().isEmpty, isTrue);
});
test('非空点列表返回有效 Path', () {
final points = [
const Offset(0, 0),
const Offset(10, 10),
const Offset(20, 5),
const Offset(30, 15),
];
final path = buildStrokePath(points);
expect(path.getBounds().isEmpty, isFalse);
expect(path.getBounds().width, greaterThan(0));
expect(path.getBounds().height, greaterThan(0));
});
test('路径包围盒包含所有输入点', () {
final points = [
const Offset(5, 5),
const Offset(100, 50),
const Offset(200, 100),
];
final path = buildStrokePath(points);
final bounds = path.getBounds();
for (final p in points) {
expect(bounds.left, lessThanOrEqualTo(p.dx));
expect(bounds.top, lessThanOrEqualTo(p.dy));
expect(bounds.right, greaterThanOrEqualTo(p.dx));
expect(bounds.bottom, greaterThanOrEqualTo(p.dy));
}
});
});
// ============================================================
// createPaintForStroke
// ============================================================
group('createPaintForStroke', () {
Stroke makeStroke(BrushType type, {String color = '#2D2420', double width = 3.0}) {
return Stroke(
id: 'test',
points: [
const StrokePoint(x: 0, y: 0),
const StrokePoint(x: 100, y: 100),
],
brushType: type,
color: color,
width: width,
);
}
test('钢笔 — 不透明实心填充', () {
final paint = createPaintForStroke(makeStroke(BrushType.pen));
expect(paint.color.value, parseHexColor('#2D2420').value);
expect(paint.style, PaintingStyle.fill);
expect(paint.isAntiAlias, isTrue);
});
test('铅笔 — 不透明实心填充', () {
final paint = createPaintForStroke(makeStroke(BrushType.pencil));
expect(paint.style, PaintingStyle.fill);
expect(paint.isAntiAlias, isTrue);
});
test('马克笔 — 半透明', () {
final paint = createPaintForStroke(makeStroke(BrushType.marker, color: '#E07A5F'));
// alpha = 0.4
expect(paint.color.alpha, closeTo(102, 1)); // 0.4 * 255 ≈ 102
expect(paint.style, PaintingStyle.fill);
});
test('橡皮擦 — dstOut 混合模式', () {
final paint = createPaintForStroke(makeStroke(BrushType.eraser));
expect(paint.blendMode, BlendMode.dstOut);
expect(paint.style, PaintingStyle.fill);
expect(paint.isAntiAlias, isTrue);
});
test('颜色正确传递', () {
final paint = createPaintForStroke(makeStroke(BrushType.pen, color: '#81B29A'));
expect(paint.color.value, parseHexColor('#81B29A').value);
});
});
}