Some checks failed
CI / Windows build (push) Has been cancelled
Add pdfrx 2.4.4. PenCaptureRegion routes stylus to ink (arena-bypass via PenCaptureBinding) while touch falls through to pdfrx scroll/zoom. Spike pane hosts PdfViewer with page-overlay ink at normalized coords + frame-time HUD; reachable from home screen for on-device testing. Bench/coordinate harness under integration_test. Generated assets are gitignored (regenerate via tool/gen_*.dart).
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;
|
||
}
|