Files
BadNote/integration_test/coordinate_assertion_test.dart
Akiba So ee1b3a39f1
Some checks failed
CI / Windows build (push) Has been cancelled
feat(editor): M1 pdfrx spike + pen/touch capture
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).
2026-06-21 20:15:21 +08:00

167 lines
6.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}