Files
BadNote/test/pen_editor_live_stroke_test.dart

127 lines
4.1 KiB
Dart
Raw Normal View History

// 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;
}