// Unit tests for continuous-single windowing math (P0.5 step 10, automatable // slice). Pure geometry — no widgets, no pdfrx. import 'package:flutter_test/flutter_test.dart'; import 'package:badnote/editor/layout/page_viewport.dart'; void main() { group('PageStackMetrics geometry', () { test('cumulative tops and total extent (uniform pages + gap)', () { final m = PageStackMetrics(pageHeights: [100, 100, 100], gap: 10); expect(m.pageCount, 3); expect(m.offsetOf(0), 0); expect(m.offsetOf(1), 110); // 100 + 10 gap expect(m.offsetOf(2), 220); expect(m.totalExtent, 320); // 220 + last height 100, no trailing gap }); test('variable page heights', () { final m = PageStackMetrics(pageHeights: [50, 200, 75]); expect(m.offsetOf(0), 0); expect(m.offsetOf(1), 50); expect(m.offsetOf(2), 250); expect(m.totalExtent, 325); }); test('empty document', () { final m = PageStackMetrics(pageHeights: []); expect(m.pageCount, 0); expect(m.totalExtent, 0); expect(m.visibleRange(0, 800), PageWindow.empty); }); }); group('PageStackMetrics.visibleRange', () { final m = PageStackMetrics(pageHeights: List.filled(10, 100)); // 0..1000 test('top of document shows the first pages', () { // viewport [0,250) → pages 0,1,2 (page 2 is [200,300), overlaps top 250) expect(m.visibleRange(0, 250), const PageWindow(0, 2)); }); test('mid scroll shows the intersecting band', () { // viewport [420,720) → pages 4 [400,500) .. 7 [700,800) expect(m.visibleRange(420, 300), const PageWindow(4, 7)); }); test('cacheExtent grows the window on both sides', () { // base [420,720) → 4..7; +150 cache → [270,870) → pages 2..8 expect( m.visibleRange(420, 300, cacheExtent: 150), const PageWindow(2, 8), ); }); test('exact page boundary is half-open (no spurious extra page)', () { // viewport [0,200): page 0 [0,100), page 1 [100,200). Page 2 starts at // 200 which is the exclusive bottom → NOT included. expect(m.visibleRange(0, 200), const PageWindow(0, 1)); }); test('last page at the bottom', () { expect(m.visibleRange(950, 50), const PageWindow(9, 9)); }); test('clamps to valid indices at the ends', () { // Overscroll below 0 (cache reaches negative) still starts at page 0. expect(m.visibleRange(0, 100, cacheExtent: 500).first, 0); // Overscroll past the end still ends at the last page. expect(m.visibleRange(900, 200, cacheExtent: 500).last, 9); }); test('window entirely above content is empty', () { // Scrolled far past the end with no cache reaching back. expect(m.visibleRange(5000, 300), PageWindow.empty); }); test('window entirely below content (negative, no overlap) is empty', () { // viewport sitting above page 0 with bottom <= 0. expect(m.visibleRange(-1000, 500), PageWindow.empty); }); test('PageWindow helpers', () { const w = PageWindow(2, 5); expect(w.count, 4); expect(w.contains(2), isTrue); expect(w.contains(5), isTrue); expect(w.contains(6), isFalse); expect(PageWindow.empty.isEmpty, isTrue); expect(PageWindow.empty.count, 0); }); }); group('visibleRange with gaps', () { test('gap bands are not page bands (a scroll inside a gap shows neighbors)', () { final m = PageStackMetrics(pageHeights: [100, 100, 100], gap: 20); // page0 [0,100), gap [100,120), page1 [120,220), gap [220,240), page2 [240,340) // viewport [105,115) sits inside the first gap → nearest pages 0 and 1 // both within a tiny window? window [105,115): page0 bottom 100 < 105 so // page0 excluded; page1 top 120 > 115 so excluded → empty band in gap. expect(m.visibleRange(105, 10), PageWindow.empty); // A slightly taller viewport spanning the gap catches both. expect(m.visibleRange(90, 40), const PageWindow(0, 1)); }); }); }