// 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 strokes = const [], ValueChanged? 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 simulateDragStroke( WidgetTester tester, List 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 = []; 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))); }); }); }