From 458d6448d3d65556a5bae693e13e30e577e38d31 Mon Sep 17 00:00:00 2001 From: Akiba So Date: Tue, 23 Jun 2026 03:45:53 +0800 Subject: [PATCH] =?UTF-8?q?test:=20PenCanvas=20widget=20test=20=E2=80=94?= =?UTF-8?q?=20live=20drawing=20path,=20device-independent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pumps PenCanvas with a plain Container as the page widget (no pdfrx/pdfium) and simulates gestures to verify the INTEGRATED live path end-to-end: a stylus drag commits a multi-point stroke; a single finger is rejected when finger-drawing is off but draws when on; a 2nd pointer cancels an in-progress stylus stroke (pinch/palm); committed strokes render through the revision-gated render cache (eca5141) with no exception. Real behavioral evidence for the arbiter + render swap beyond static analysis — without a device. flutter analyze lib/editor clean; 233/233 tests (+5 widget). Co-Authored-By: Claude Opus 4.8 (1M context) --- test/pen_canvas_widget_test.dart | 165 +++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 test/pen_canvas_widget_test.dart diff --git a/test/pen_canvas_widget_test.dart b/test/pen_canvas_widget_test.dart new file mode 100644 index 0000000..b1908a1 --- /dev/null +++ b/test/pen_canvas_widget_test.dart @@ -0,0 +1,165 @@ +// Widget test of the INTEGRATED live drawing path (device-independent: uses a +// plain Container as the page widget, so no pdfrx/pdfium). Exercises the input +// arbiter + stroke lifecycle + the revision-gated render-cache swap (eca5141) +// end-to-end via simulated stylus / finger / pinch gestures. + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:badnote/editor/canvas/pen_canvas.dart'; +import 'package:badnote/editor/canvas/pen_stroke.dart'; + +void main() { + const pageSize = Size(400, 600); + + Widget host({ + required List strokes, + required void Function(PenStroke) onComplete, + required TransformationController controller, + bool allowFinger = false, + CanvasTool tool = CanvasTool.pen, + }) { + return MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: pageSize.width, + height: pageSize.height, + child: PenCanvas( + pageWidget: Container(color: const Color(0xFFEEEEEE)), + pageSize: pageSize, + strokes: strokes, + transformationController: controller, + tool: tool, + color: const Color(0xFF000000), + strokeWidth: 0.004, + allowFingerDrawing: allowFinger, + onStrokeComplete: onComplete, + onEraseStroke: (_, __) {}, + ), + ), + ), + ), + ); + } + + testWidgets('a stylus drag produces a committed stroke', (tester) async { + final committed = []; + final controller = TransformationController(); + addTearDown(controller.dispose); + + await tester.pumpWidget(host( + strokes: const [], + onComplete: committed.add, + controller: controller, + )); + + final center = tester.getCenter(find.byType(PenCanvas)); + final g = await tester.startGesture(center, + kind: PointerDeviceKind.stylus); + await g.moveBy(const Offset(20, 10)); + await g.moveBy(const Offset(20, 30)); + await g.moveBy(const Offset(10, 20)); + await g.up(); + await tester.pump(); + + expect(committed, hasLength(1)); + expect(committed.single.points.length, greaterThan(1), + reason: 'a multi-segment drag is not a single dot'); + }); + + testWidgets('a single finger does NOT draw when finger-drawing is off', + (tester) async { + final committed = []; + final controller = TransformationController(); + addTearDown(controller.dispose); + + await tester.pumpWidget(host( + strokes: const [], + onComplete: committed.add, + controller: controller, + allowFinger: false, + )); + + final center = tester.getCenter(find.byType(PenCanvas)); + final g = await tester.startGesture(center, kind: PointerDeviceKind.touch); + await g.moveBy(const Offset(30, 30)); + await g.up(); + await tester.pump(); + + expect(committed, isEmpty, reason: 'palm/finger rejection'); + }); + + testWidgets('a finger DOES draw when finger-drawing is on', (tester) async { + final committed = []; + final controller = TransformationController(); + addTearDown(controller.dispose); + + await tester.pumpWidget(host( + strokes: const [], + onComplete: committed.add, + controller: controller, + allowFinger: true, + )); + + final center = tester.getCenter(find.byType(PenCanvas)); + final g = await tester.startGesture(center, kind: PointerDeviceKind.touch); + await g.moveBy(const Offset(30, 30)); + await g.up(); + await tester.pump(); + + expect(committed, hasLength(1)); + }); + + testWidgets('a second pointer cancels an in-progress stylus stroke (pinch)', + (tester) async { + final committed = []; + final controller = TransformationController(); + addTearDown(controller.dispose); + + await tester.pumpWidget(host( + strokes: const [], + onComplete: committed.add, + controller: controller, + )); + + final center = tester.getCenter(find.byType(PenCanvas)); + final g1 = await tester.startGesture(center, kind: PointerDeviceKind.stylus); + await g1.moveBy(const Offset(10, 10)); + // A second finger lands → the stroke must be discarded (pinch/palm). + final g2 = await tester.startGesture( + center + const Offset(60, 0), kind: PointerDeviceKind.touch); + await g1.moveBy(const Offset(10, 10)); + await g1.up(); + await g2.up(); + await tester.pump(); + + expect(committed, isEmpty, reason: '2nd pointer cancels the stroke'); + }); + + testWidgets('renders committed strokes through the render cache without error', + (tester) async { + final controller = TransformationController(); + addTearDown(controller.dispose); + final strokes = [ + PenStroke( + points: const [PenPoint(0.1, 0.1, 0.5), PenPoint(0.4, 0.5, 0.6)], + color: 0xFF000000, + width: 0.004, + kind: PenStrokeKind.pen, + ), + ]; + + await tester.pumpWidget(host( + strokes: strokes, + onComplete: (_) {}, + controller: controller, + )); + await tester.pump(); + + // Built + painted the StaticInkPainter (render cache) with no exception. + expect(tester.takeException(), isNull); + expect(find.byType(PenCanvas), findsOneWidget); + }); +}