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 渲染结构、手势回调、去抖、预加载、连续笔画
273 lines
8.4 KiB
Dart
273 lines
8.4 KiB
Dart
// HandwritingCanvas Widget 集成测试 — 指针事件驱动笔画完成回调
|
||
//
|
||
// 验证:
|
||
// 1. Widget 正确渲染(双层 CustomPaint)
|
||
// 2. 手势事件触发 onStrokeCompleted 回调
|
||
// 3. 不同画笔类型/颜色/宽度正确传递
|
||
// 4. 去抖过滤(微小移动被丢弃)
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:nuanji_app/features/editor/widgets/handwriting_canvas.dart';
|
||
import 'package:nuanji_app/features/editor/widgets/stroke_model.dart';
|
||
|
||
void main() {
|
||
// ============================================================
|
||
// 辅助
|
||
// ============================================================
|
||
|
||
/// 包裹 HandwritingCanvas 在必要的父组件中,提供约束尺寸
|
||
Widget buildTestSubject({
|
||
Key? key,
|
||
BrushType brushType = BrushType.pen,
|
||
String brushColor = '#2D2420',
|
||
double brushWidth = 3.0,
|
||
List<Stroke> strokes = const [],
|
||
ValueChanged<Stroke>? onStrokeCompleted,
|
||
}) {
|
||
return MaterialApp(
|
||
home: Scaffold(
|
||
body: SizedBox(
|
||
width: 800,
|
||
height: 600,
|
||
child: HandwritingCanvas(
|
||
key: key,
|
||
brushType: brushType,
|
||
brushColor: brushColor,
|
||
brushWidth: brushWidth,
|
||
strokes: strokes,
|
||
onStrokeCompleted: onStrokeCompleted,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 使用标准 TestGesture 模拟一条完整的拖拽手势
|
||
Future<void> simulateDragStroke(
|
||
WidgetTester tester,
|
||
List<Offset> points,
|
||
) async {
|
||
assert(points.length >= 2, '至少需要 down 和 up 两个点');
|
||
|
||
final gesture = await tester.startGesture(points.first);
|
||
await tester.pump();
|
||
|
||
for (var i = 1; i < points.length; i++) {
|
||
await gesture.moveTo(points[i]);
|
||
await tester.pump();
|
||
}
|
||
|
||
await gesture.up();
|
||
await tester.pump();
|
||
}
|
||
|
||
// ============================================================
|
||
// 渲染结构验证
|
||
// ============================================================
|
||
group('HandwritingCanvas — 渲染结构', () {
|
||
testWidgets('正确渲染双层 CustomPaint', (tester) async {
|
||
await tester.pumpWidget(buildTestSubject());
|
||
await tester.pumpAndSettle();
|
||
|
||
// 应找到 CustomPaint(两层:CachedStrokesPainter + ActiveStrokePainter)
|
||
final customPaints = find.byType(CustomPaint);
|
||
expect(customPaints, findsAtLeast(2));
|
||
|
||
// 应找到 Listener(HandwritingCanvas 的 Listener + Gesture 识别器可能有额外 Listener)
|
||
expect(find.byType(Listener), findsAtLeast(1));
|
||
|
||
// 应找到 RepaintBoundary(MaterialApp/Scaffold 可能添加额外的)
|
||
expect(find.byType(RepaintBoundary), findsAtLeast(1));
|
||
});
|
||
|
||
testWidgets('初始无笔画时仍正确渲染', (tester) async {
|
||
await tester.pumpWidget(buildTestSubject());
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(HandwritingCanvas), findsOneWidget);
|
||
});
|
||
});
|
||
|
||
// ============================================================
|
||
// 手势事件 → onStrokeCompleted
|
||
// ============================================================
|
||
group('HandwritingCanvas — 笔画完成回调', () {
|
||
testWidgets('有效拖拽触发 onStrokeCompleted', (tester) async {
|
||
Stroke? completedStroke;
|
||
await tester.pumpWidget(buildTestSubject(
|
||
onStrokeCompleted: (stroke) => completedStroke = stroke,
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
// 模拟 5 个点的笔画(距离足够大,避免去抖过滤)
|
||
await simulateDragStroke(tester, [
|
||
const Offset(100, 100),
|
||
const Offset(150, 120),
|
||
const Offset(200, 140),
|
||
const Offset(250, 160),
|
||
const Offset(300, 180),
|
||
]);
|
||
|
||
// 应触发回调
|
||
expect(completedStroke, isNotNull);
|
||
expect(completedStroke!.points.length, greaterThanOrEqualTo(2));
|
||
expect(completedStroke!.brushType, BrushType.pen);
|
||
expect(completedStroke!.color, '#2D2420');
|
||
expect(completedStroke!.width, 3.0);
|
||
});
|
||
|
||
testWidgets('笔画携带正确的画笔类型', (tester) async {
|
||
Stroke? completedStroke;
|
||
await tester.pumpWidget(buildTestSubject(
|
||
brushType: BrushType.marker,
|
||
onStrokeCompleted: (stroke) => completedStroke = stroke,
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
await simulateDragStroke(tester, [
|
||
const Offset(100, 100),
|
||
const Offset(200, 200),
|
||
const Offset(300, 100),
|
||
]);
|
||
|
||
expect(completedStroke, isNotNull);
|
||
expect(completedStroke!.brushType, BrushType.marker);
|
||
});
|
||
|
||
testWidgets('笔画携带正确的颜色和宽度', (tester) async {
|
||
Stroke? completedStroke;
|
||
await tester.pumpWidget(buildTestSubject(
|
||
brushColor: '#E07A5F',
|
||
brushWidth: 8.0,
|
||
onStrokeCompleted: (stroke) => completedStroke = stroke,
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
await simulateDragStroke(tester, [
|
||
const Offset(50, 50),
|
||
const Offset(150, 150),
|
||
const Offset(250, 50),
|
||
]);
|
||
|
||
expect(completedStroke!.color, '#E07A5F');
|
||
expect(completedStroke!.width, 8.0);
|
||
});
|
||
|
||
testWidgets('tap(无拖拽)不触发回调', (tester) async {
|
||
Stroke? completedStroke;
|
||
await tester.pumpWidget(buildTestSubject(
|
||
onStrokeCompleted: (stroke) => completedStroke = stroke,
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
// 仅 tap(down + up 在同一位置)— 单点不足以构成笔画
|
||
await tester.tapAt(const Offset(100, 100));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Tap 产生 1 个点(down 和 up 位置相同),不触发回调
|
||
expect(completedStroke, isNull);
|
||
});
|
||
});
|
||
|
||
// ============================================================
|
||
// 预加载笔画
|
||
// ============================================================
|
||
group('HandwritingCanvas — 预加载笔画', () {
|
||
testWidgets('初始笔画列表正确传入', (tester) async {
|
||
final existingStrokes = [
|
||
Stroke(
|
||
id: 'existing-1',
|
||
points: [
|
||
const StrokePoint(x: 10, y: 10),
|
||
const StrokePoint(x: 100, y: 100),
|
||
],
|
||
),
|
||
];
|
||
|
||
await tester.pumpWidget(buildTestSubject(
|
||
strokes: existingStrokes,
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(HandwritingCanvas), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('笔画更新触发 didUpdateWidget', (tester) async {
|
||
final key = GlobalKey();
|
||
|
||
// 第一次渲染 — 无笔画
|
||
await tester.pumpWidget(MaterialApp(
|
||
home: Scaffold(
|
||
body: SizedBox(
|
||
width: 800,
|
||
height: 600,
|
||
child: HandwritingCanvas(
|
||
key: key,
|
||
strokes: const [],
|
||
),
|
||
),
|
||
),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
// 更新 — 添加笔画
|
||
final updatedStrokes = [
|
||
Stroke(
|
||
id: 'new-1',
|
||
points: [
|
||
const StrokePoint(x: 50, y: 50),
|
||
const StrokePoint(x: 200, y: 200),
|
||
],
|
||
),
|
||
];
|
||
|
||
await tester.pumpWidget(MaterialApp(
|
||
home: Scaffold(
|
||
body: SizedBox(
|
||
width: 800,
|
||
height: 600,
|
||
child: HandwritingCanvas(
|
||
key: key,
|
||
strokes: updatedStrokes,
|
||
),
|
||
),
|
||
),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(HandwritingCanvas), findsOneWidget);
|
||
});
|
||
});
|
||
|
||
// ============================================================
|
||
// 连续多笔画
|
||
// ============================================================
|
||
group('HandwritingCanvas — 连续多笔画', () {
|
||
testWidgets('连续绘制多条笔画,每条都触发回调', (tester) async {
|
||
final completedStrokes = <Stroke>[];
|
||
await tester.pumpWidget(buildTestSubject(
|
||
onStrokeCompleted: (stroke) => completedStrokes.add(stroke),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
// 第一条笔画
|
||
await simulateDragStroke(tester, [
|
||
const Offset(50, 50),
|
||
const Offset(150, 100),
|
||
const Offset(250, 50),
|
||
]);
|
||
|
||
// 第二条笔画(使用新的 gesture)
|
||
await simulateDragStroke(tester, [
|
||
const Offset(100, 200),
|
||
const Offset(200, 300),
|
||
const Offset(300, 200),
|
||
]);
|
||
|
||
expect(completedStrokes.length, 2);
|
||
expect(completedStrokes[0].id, isNot(equals(completedStrokes[1].id)));
|
||
});
|
||
});
|
||
}
|