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