// integration_test/coordinate_assertion_test.dart // // M1 MUST #2 (plan §2.1 / §10): a marker painted at normalized (0.5, 0.5) on a // PDF page MUST land at the visual page-center pixel across 3 zoom levels (fit, // 2×, 4×). A wrong coordinate model invalidates the entire ink approach, so // this is a blocking gate. // // RUN (on a device/desktop with a display + working pdfium): // flutter test integration_test/coordinate_assertion_test.dart // or, on the Windows tablet via a driver: // flutter drive --driver=test_driver/integration_test.dart \ // --target=integration_test/coordinate_assertion_test.dart // // HEADLESS-LINUX NOTE: pdfium must render off-screen for the page layout to // resolve. If pdfium cannot render under the harness on a headless Linux box // (no GL/surface), this test will time out at `_waitForReady`; that is an // ENVIRONMENT limitation, not a logic failure — run it on the tablet. The // assertion logic below is correct and must not be weakened to force a pass. import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:pdfrx/pdfrx.dart'; import 'package:syncfusion_flutter_pdf/pdf.dart' as sf; import 'package:badnote/editor/pdf/spike_editor_pane.dart'; void main() { // Standard integration binding. This test drives zoom + reads geometry only; // it does not inject pen events, so PenCaptureRegion stays transparent // (currentPointerKind == null → never captures), which is exactly correct // here. (Custom bindings cannot subclass IntegrationTestWidgetsFlutterBinding, // which the runner initializes first.) IntegrationTestWidgetsFlutterBinding.ensureInitialized(); pdfrxFlutterInitialize(); late File pdfFile; setUpAll(() async { pdfFile = await _writeTinyPdf(); }); tearDownAll(() async { if (await pdfFile.exists()) await pdfFile.delete(); }); testWidgets('marker at normalized (0.5,0.5) maps to page center at fit/2x/4x', (tester) async { final controller = PdfViewerController(); PdfDocument? readyDoc; await tester.pumpWidget( MaterialApp( home: Scaffold( body: SpikeEditorPane( pdfPath: pdfFile.path, controller: controller, onViewerReady: (doc, _) => readyDoc = doc, ), ), ), ); // Wait for pdfrx to load + lay out the page. final ready = await _waitForReady(tester, controller); if (!ready) { fail( 'pdfrx did not become ready (page layout unavailable). This is almost ' 'certainly the headless-Linux pdfium limitation described in the file ' 'header — run on the Windows tablet:\n' ' flutter drive --driver=test_driver/integration_test.dart ' '--target=integration_test/coordinate_assertion_test.dart', ); } expect(readyDoc, isNotNull); // The page-center in DOCUMENT space is the layout rect center of page 1. final pageRect = controller.layout.pageLayouts.first; final pageCenterDoc = pageRect.center; Future assertCenterAtCurrentZoom(String label) async { await tester.pumpAndSettle(); // Project the page-center document point to viewer-local (== screen, // since the viewer fills the Scaffold body) coordinates. final localCenter = controller.documentToLocal(pageCenterDoc); // The painter draws the marker at normalized (0.5,0.5) of the page, i.e. // exactly pageCenterDoc. So localCenter is where the marker pixel must be. // Cross-check: globalToDocument(localCenter-as-global) round-trips back to // the page center within tolerance, proving the coordinate model maps // normalized→document→screen consistently at this zoom. final box = tester.renderObject( find.byType(SpikeEditorPane), ); final globalCenter = box.localToGlobal(localCenter); final roundTripDoc = controller.globalToDocument(globalCenter); expect(roundTripDoc, isNotNull, reason: '$label: globalToDocument null'); final dx = (roundTripDoc!.dx - pageCenterDoc.dx).abs(); final dy = (roundTripDoc.dy - pageCenterDoc.dy).abs(); // Tolerance: 1 document unit (sub-pixel at these zooms). expect(dx, lessThan(1.0), reason: '$label: x off by $dx doc units (zoom=${controller.currentZoom})'); expect(dy, lessThan(1.0), reason: '$label: y off by $dy doc units (zoom=${controller.currentZoom})'); } // --- fit --- await controller.goTo( controller.calcMatrixForPage(pageNumber: 1, anchor: PdfPageAnchor.all), duration: Duration.zero, ); await assertCenterAtCurrentZoom('fit'); final fitZoom = controller.currentZoom; // --- 2x (relative to fit) --- await controller.setZoom(pageCenterDoc, fitZoom * 2, duration: Duration.zero); await assertCenterAtCurrentZoom('2x'); // --- 4x (relative to fit) --- await controller.setZoom(pageCenterDoc, fitZoom * 4, duration: Duration.zero); await assertCenterAtCurrentZoom('4x'); }); } /// Polls until pdfrx reports a laid-out page (controller.isReady + a page rect), /// or the timeout elapses. Returns whether it became ready. Future _waitForReady( WidgetTester tester, PdfViewerController controller, { Duration timeout = const Duration(seconds: 20), }) async { final deadline = DateTime.now().add(timeout); while (DateTime.now().isBefore(deadline)) { await tester.pump(const Duration(milliseconds: 100)); if (controller.isReady && controller.layout.pageLayouts.isNotEmpty) { return true; } } return false; } /// Writes a tiny single-page A4 PDF (with a faint border so the page box is /// non-blank) to a temp file using syncfusion_flutter_pdf (already a dependency). Future _writeTinyPdf() async { final doc = sf.PdfDocument(); final page = doc.pages.add(); final size = page.getClientSize(); page.graphics.drawRectangle( pen: sf.PdfPen(sf.PdfColor(0, 0, 0)), bounds: Rect.fromLTWH(2, 2, size.width - 4, size.height - 4), ); page.graphics.drawString( 'M1 coord test', sf.PdfStandardFont(sf.PdfFontFamily.helvetica, 18), bounds: Rect.fromLTWH(20, 20, size.width - 40, 40), ); final bytes = await doc.save(); doc.dispose(); final file = File( '${Directory.systemTemp.path}/badnote_m1_coord_${DateTime.now().microsecondsSinceEpoch}.pdf', ); await file.writeAsBytes(bytes, flush: true); return file; }