From c8579d3e86ea62df64dc13c096095d270154bca0 Mon Sep 17 00:00:00 2001 From: Akiba So Date: Tue, 23 Jun 2026 03:47:19 +0800 Subject: [PATCH] test: behavioral regression guard for the absolute-snapshot pinch fix (6829076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device-independent pinch test over PenCanvas: a steady two-finger spread must grow the shared transform scale MONOTONICALLY, with no frame popping above the final scale and snapping back (the flicker invariant the absolute-from-snapshot rewrite enforces). Plus: a single stylus drag never pans/zooms (the recognizer excludes stylus) — the transform stays identity while the pen draws. Locks in 6829076 + the arbiter exclusion as regressions guards without a device. flutter analyze lib/editor clean; 235/235 tests (+2 widget). Co-Authored-By: Claude Opus 4.8 (1M context) --- test/pen_zoom_widget_test.dart | 96 ++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 test/pen_zoom_widget_test.dart diff --git a/test/pen_zoom_widget_test.dart b/test/pen_zoom_widget_test.dart new file mode 100644 index 0000000..dbf6da2 --- /dev/null +++ b/test/pen_zoom_widget_test.dart @@ -0,0 +1,96 @@ +// Behavioral test of the absolute-snapshot pinch fix (6829076) — device- +// independent (no pdfrx). Simulates a two-finger pinch over PenCanvas and +// samples the shared transform's scale each frame: a steady spread must grow +// the scale MONOTONICALLY, never spiking above the final value and snapping +// back (the reported flicker the fix removes). + +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(TransformationController controller) => MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: pageSize.width, + height: pageSize.height, + child: PenCanvas( + pageWidget: Container(color: const Color(0xFFEEEEEE)), + pageSize: pageSize, + strokes: const [], + transformationController: controller, + tool: CanvasTool.pen, + color: const Color(0xFF000000), + strokeWidth: 0.004, + onStrokeComplete: (_) {}, + onEraseStroke: (_, __) {}, + ), + ), + ), + ), + ); + + testWidgets('a steady two-finger spread grows scale monotonically (no spike)', + (tester) async { + final controller = TransformationController(); + addTearDown(controller.dispose); + await tester.pumpWidget(host(controller)); + + final center = tester.getCenter(find.byType(PenCanvas)); + final f1 = await tester.startGesture( + center + const Offset(-20, 0), kind: PointerDeviceKind.touch); + final f2 = await tester.startGesture( + center + const Offset(20, 0), kind: PointerDeviceKind.touch); + + final scales = []; + for (var i = 0; i < 8; i++) { + // Spread the fingers apart symmetrically (pure zoom-in, focal steady). + await f1.moveBy(const Offset(-8, 0)); + await f2.moveBy(const Offset(8, 0)); + await tester.pump(); + scales.add(controller.value.getMaxScaleOnAxis()); + } + await f1.up(); + await f2.up(); + await tester.pump(); + + final maxScale = scales.reduce((a, b) => a > b ? a : b); + final finalScale = scales.last; + + // Zoom actually happened. + expect(finalScale, greaterThan(1.0)); + // No frame spiked ABOVE the final scale and snapped back — the fix's + // invariant. (Allow a tiny epsilon for fp.) + expect(maxScale, lessThanOrEqualTo(finalScale + 1e-6), + reason: 'a frame popped bigger than the final scale — flicker'); + // Monotonic non-decreasing across the steady spread. + for (var i = 1; i < scales.length; i++) { + expect(scales[i], greaterThanOrEqualTo(scales[i - 1] - 1e-6), + reason: 'scale went backwards at frame $i: $scales'); + } + }); + + testWidgets('the stylus never pans/zooms (excluded from the recognizer)', + (tester) async { + final controller = TransformationController(); + addTearDown(controller.dispose); + await tester.pumpWidget(host(controller)); + + final center = tester.getCenter(find.byType(PenCanvas)); + // A single stylus drag must DRAW, not pan — the transform stays identity. + final g = await tester.startGesture(center, kind: PointerDeviceKind.stylus); + await g.moveBy(const Offset(40, 40)); + await g.up(); + await tester.pump(); + + expect(controller.value.getMaxScaleOnAxis(), closeTo(1.0, 1e-9)); + expect(controller.value.getTranslation().x, closeTo(0, 1e-6)); + expect(controller.value.getTranslation().y, closeTo(0, 1e-6)); + }); +}