Per the full-refactor plan §9 (input-independent half of P0): - engine: canonical EditorStroke (lossless InkStroke round-trip) + stroke_geometry (single getStroke outline) + revision-gated StrokeStore - render: static/live ink painters + ink_picture_cache (revision-keyed) + annotation_layer (RepaintBoundary) - persistence: DB v6 (ink, notebook_pages) + editor_repository diff-write (UPSERT changed / DELETE removed in one txn; id-set after commit) + save_scheduler - pdf_service export now FILLS the getStroke outline (R7 hairline fix) Not yet wired into the live editor (input relocation pending pen-pressure diagnostic). 28 new tests pass.
161 lines
5.6 KiB
Dart
161 lines
5.6 KiB
Dart
// test/editor_render_test.dart
|
|
//
|
|
// Unit tests for the P0 RENDER layer:
|
|
// (a) StrokeStore: add/removeById/replaceAll/clear all bump revision.
|
|
// (b) StaticInkPainter.shouldRepaint: FALSE when revision+pageSize unchanged,
|
|
// TRUE when revision changes. (The key perf invariant.)
|
|
//
|
|
// Tests are intentionally pure-logic: no widget pump, no GPU, no image
|
|
// comparisons. StaticInkPainter.shouldRepaint only reads store.revision and
|
|
// pageSize so we can exercise it without a real ui.Picture or Canvas.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:badnote/editor/engine/stroke_model.dart';
|
|
import 'package:badnote/editor/engine/stroke_store.dart';
|
|
import 'package:badnote/editor/render/ink_picture_cache.dart';
|
|
import 'package:badnote/editor/render/static_ink_painter.dart';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
EditorStroke _stroke(String id) => EditorStroke.create(
|
|
id: id,
|
|
points: const [
|
|
EditorPoint(x: 0.1, y: 0.1, pressure: 0.5),
|
|
EditorPoint(x: 0.5, y: 0.5, pressure: 0.5),
|
|
],
|
|
);
|
|
|
|
/// Builds a [StaticInkPainter] that can be interrogated via [shouldRepaint]
|
|
/// without ever calling [paint] (avoids needing a real Canvas / Picture).
|
|
StaticInkPainter _painter(StrokeStore store, Size pageSize) =>
|
|
StaticInkPainter(
|
|
hostId: 'test-host',
|
|
store: store,
|
|
pageSize: pageSize,
|
|
cache: InkPictureCache(),
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void main() {
|
|
const pageSize = Size(800.0, 600.0);
|
|
|
|
// -------------------------------------------------------------------------
|
|
group('StrokeStore revision', () {
|
|
test('starts at 0', () {
|
|
final store = StrokeStore();
|
|
expect(store.revision, 0);
|
|
});
|
|
|
|
test('add bumps revision', () {
|
|
final store = StrokeStore();
|
|
store.add(_stroke('s1'));
|
|
expect(store.revision, 1);
|
|
store.add(_stroke('s2'));
|
|
expect(store.revision, 2);
|
|
expect(store.committed.length, 2);
|
|
});
|
|
|
|
test('removeById bumps revision when stroke is found', () {
|
|
final store = StrokeStore()..add(_stroke('s1'));
|
|
final revBefore = store.revision;
|
|
store.removeById('s1');
|
|
expect(store.revision, greaterThan(revBefore));
|
|
expect(store.committed, isEmpty);
|
|
});
|
|
|
|
test('removeById does NOT bump revision when id is absent', () {
|
|
final store = StrokeStore()..add(_stroke('s1'));
|
|
final revBefore = store.revision;
|
|
store.removeById('nonexistent');
|
|
expect(store.revision, revBefore);
|
|
});
|
|
|
|
test('replaceAll bumps revision', () {
|
|
final store = StrokeStore();
|
|
store.replaceAll([_stroke('a'), _stroke('b')]);
|
|
expect(store.revision, 1);
|
|
expect(store.committed.length, 2);
|
|
|
|
store.replaceAll([_stroke('c')]);
|
|
expect(store.revision, 2);
|
|
expect(store.committed.length, 1);
|
|
});
|
|
|
|
test('clear bumps revision', () {
|
|
final store = StrokeStore()..add(_stroke('x'));
|
|
final revBefore = store.revision;
|
|
store.clear();
|
|
expect(store.revision, greaterThan(revBefore));
|
|
expect(store.committed, isEmpty);
|
|
});
|
|
|
|
test('committed returns unmodifiable list', () {
|
|
final store = StrokeStore()..add(_stroke('s'));
|
|
expect(() => store.committed.add(_stroke('bad')), throwsUnsupportedError);
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
group('StaticInkPainter.shouldRepaint', () {
|
|
test('returns false when revision and pageSize are unchanged', () {
|
|
final store = StrokeStore()..add(_stroke('s1'));
|
|
final p1 = _painter(store, pageSize);
|
|
final p2 = _painter(store, pageSize);
|
|
|
|
// Both painters wrap the same store at the same revision.
|
|
expect(p2.shouldRepaint(p1), isFalse);
|
|
});
|
|
|
|
test('returns true when revision changes', () {
|
|
final store = StrokeStore()..add(_stroke('s1'));
|
|
final p1 = _painter(store, pageSize);
|
|
|
|
// Mutate the store — revision bumps.
|
|
store.add(_stroke('s2'));
|
|
final p2 = _painter(store, pageSize);
|
|
|
|
expect(p2.shouldRepaint(p1), isTrue);
|
|
});
|
|
|
|
test('returns true when only pageSize changes', () {
|
|
final store = StrokeStore()..add(_stroke('s1'));
|
|
final p1 = _painter(store, pageSize);
|
|
final p2 = _painter(store, const Size(1024.0, 768.0));
|
|
|
|
expect(p2.shouldRepaint(p1), isTrue);
|
|
});
|
|
|
|
test('returns false after replaceAll with same content (revision differs '
|
|
'but separate store instances — uses store.revision not identity)', () {
|
|
// This test confirms shouldRepaint uses the revision INTEGER, not object
|
|
// identity, so a brand-new store at revision 0 == another at revision 0.
|
|
final storeA = StrokeStore(); // revision 0
|
|
final storeB = StrokeStore(); // revision 0
|
|
final pA = _painter(storeA, pageSize);
|
|
final pB = _painter(storeB, pageSize);
|
|
expect(pB.shouldRepaint(pA), isFalse);
|
|
});
|
|
|
|
test('returns true when old painter had higher revision than new '
|
|
'(regression: revision comparison is not directional guard)', () {
|
|
final store = StrokeStore()
|
|
..add(_stroke('a'))
|
|
..add(_stroke('b')); // revision == 2
|
|
final pOld = _painter(store, pageSize);
|
|
|
|
store.clear(); // revision == 3
|
|
final pNew = _painter(store, pageSize);
|
|
|
|
// pNew.revision (3) != pOld.revision (2) → should repaint.
|
|
expect(pNew.shouldRepaint(pOld), isTrue);
|
|
});
|
|
});
|
|
}
|