167 lines
6.4 KiB
Dart
167 lines
6.4 KiB
Dart
|
|
// 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<void> 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<RenderBox>(
|
|||
|
|
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<bool> _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<File> _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;
|
|||
|
|
}
|