// Regression guard for Bug 1 ("字迹写完才出现"): the PDF editor's per-page overlay // painter must read the in-progress stroke from a ValueListenable at PAINT time, // so a mid-stroke update repaints the live ink WITHOUT re-running // pageOverlaysBuilder (which only re-runs on setState). The old design captured // the live stroke as a build-time snapshot, so _bumpOverlay repainted the // painter but it still drew the stale (null) snapshot — the stroke only appeared // on pointer-up. // // These tests pump the REAL _PageOverlayPainter (via LiveStrokeOverlayHarness) // and assert (1) paint() reflects the CURRENT notifier value, and (2) updating // the notifier alone drives a CustomPaint repaint with no widget rebuild. import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:badnote/editor/canvas/pen_editor_screen.dart'; import 'package:badnote/editor/canvas/pen_stroke.dart'; import 'package:badnote/editor/engine/brush.dart'; PenStroke _stroke() => PenStroke( points: const [ PenPoint(0.2, 0.2, 0.8), PenPoint(0.5, 0.5, 0.8), PenPoint(0.8, 0.8, 0.8), ], color: 0xFF000000, width: 0.01, kind: PenStrokeKind.pen, brush: BrushKind.fountainPen, ); /// Records the number of drawPath calls a painter issues for the given size. int _drawPathCount(CustomPainter painter, Size size) { final recorder = ui.PictureRecorder(); final canvas = _CountingCanvas(Canvas(recorder)); painter.paint(canvas, size); recorder.endRecording().dispose(); return canvas.drawPathCount; } void main() { const size = Size(100, 100); test('paint() reads the CURRENT live stroke from the notifier at paint time', () { final harness = LiveStrokeOverlayHarness(pageIndex: 0); addTearDown(harness.dispose); final painter = harness.buildPainter(); // Idle: nothing to draw. expect(_drawPathCount(painter, size), 0); // Mid-stroke: pushing a live stroke into the notifier must make the SAME // painter instance draw it on the next paint (no reconstruction). harness.setLiveStroke(0, _stroke()); expect(_drawPathCount(painter, size), 1, reason: 'painter must read the live stroke at paint time, not a ' 'build-time snapshot'); // Cleared: back to nothing. harness.setLiveStroke(0, null); expect(_drawPathCount(painter, size), 0); }); test('a live stroke on a DIFFERENT page is not painted here', () { final harness = LiveStrokeOverlayHarness(pageIndex: 0); addTearDown(harness.dispose); final painter = harness.buildPainter(); harness.setLiveStroke(3, _stroke()); // belongs to page 3, not page 0 expect(_drawPathCount(painter, size), 0); }); testWidgets('updating the live-stroke notifier repaints without a rebuild', (tester) async { final harness = LiveStrokeOverlayHarness(pageIndex: 0); addTearDown(harness.dispose); var builds = 0; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Builder(builder: (context) { builds++; return CustomPaint( painter: harness.buildPainter(), size: size, ); }), ), ); expect(builds, 1); // Pushing a live stroke must trigger a repaint of the CustomPaint via its // merged repaint Listenable — WITHOUT rebuilding the widget tree (which is // exactly the path _bumpOverlay/setState would NOT cover mid-stroke). harness.setLiveStroke(0, _stroke()); await tester.pump(); expect(builds, 1, reason: 'no widget rebuild should be needed'); // And the painter now draws the live ink. expect(_drawPathCount(harness.buildPainter(), size), 1); }); } /// A [Canvas] proxy that counts drawPath calls. class _CountingCanvas implements Canvas { _CountingCanvas(this._inner); final Canvas _inner; int drawPathCount = 0; @override void drawPath(ui.Path path, ui.Paint paint) { drawPathCount++; _inner.drawPath(path, paint); } @override void noSuchMethod(Invocation invocation) => _forward(invocation); dynamic _forward(Invocation i) => null; }