From fdf0d1f7a975ef927b3a2230b3eec4f2aac44c23 Mon Sep 17 00:00:00 2001 From: insleker Date: Wed, 3 Sep 2025 20:55:16 +0800 Subject: [PATCH] feat: able to add multi signatures on document view --- README.md | 2 + integration_test/e2e_place_confirm_test.dart | 11 ++ integration_test/export_flow_test.dart | 90 ++++++++++++ lib/data/services/preferences_providers.dart | 10 +- .../features/pdf/view_model/view_model.dart | 51 +++---- .../features/pdf/widgets/pdf_page_area.dart | 25 +--- .../pdf/widgets/pdf_page_overlays.dart | 9 +- .../pdf/widgets/signature_drawer.dart | 29 ++-- test/features/step/an_image_is_loaded.dart | 26 ---- ...elected_signature_is_shown_with_image.dart | 22 --- ...ser_assigns_to_the_selected_signature.dart | 30 ---- ...e_user_places_a_signature_on_the_page.dart | 40 ------ ...upport_multiple_signature_pictures.feature | 8 -- test/widget/e2e_place_confirm_test.dart | 136 ++++++++++++++++++ test/widget/helpers.dart | 7 +- .../widget/pdf_page_area_early_jump_test.dart | 3 +- test/widget/pdf_page_area_jump_test.dart | 4 +- 17 files changed, 288 insertions(+), 215 deletions(-) create mode 100644 integration_test/e2e_place_confirm_test.dart delete mode 100644 test/features/step/an_image_is_loaded.dart delete mode 100644 test/features/step/the_selected_signature_is_shown_with_image.dart delete mode 100644 test/features/step/the_user_assigns_to_the_selected_signature.dart delete mode 100644 test/features/step/the_user_places_a_signature_on_the_page.dart create mode 100644 test/widget/e2e_place_confirm_test.dart diff --git a/README.md b/README.md index 19bbc24..07b3e59 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ flutter pub run build_runner build --delete-conflicting-outputs flutter analyze # > run unit tests and widget tests flutter test +# > run integration tests +flutter test integration_test/ -d linux # dart run tool/gen_view_wireframe_md.dart # flutter pub run dead_code_analyzer diff --git a/integration_test/e2e_place_confirm_test.dart b/integration_test/e2e_place_confirm_test.dart new file mode 100644 index 0000000..64d783c --- /dev/null +++ b/integration_test/e2e_place_confirm_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +// This file is intentionally skipped. The integrated E2E test lives in +// integration_test/export_flow_test.dart to avoid multiple app launches. +void main() { + testWidgets('skipped duplicate E2E (see export_flow_test.dart)', ( + tester, + ) async { + expect(true, isTrue); + }, skip: true); +} diff --git a/integration_test/export_flow_test.dart b/integration_test/export_flow_test.dart index 9c67a12..c744951 100644 --- a/integration_test/export_flow_test.dart +++ b/integration_test/export_flow_test.dart @@ -1,7 +1,9 @@ +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:image/image.dart' as img; import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_providers.dart'; @@ -56,4 +58,92 @@ void main() { // Expect success UI expect(find.textContaining('Saved:'), findsOneWidget); }); + + // Helper to build a simple in-memory PNG as a signature image + Uint8List _makeSig() { + final canvas = img.Image(width: 80, height: 40); + img.fill(canvas, color: img.ColorUint8.rgb(255, 255, 255)); + img.drawLine( + canvas, + x1: 6, + y1: 20, + x2: 74, + y2: 20, + color: img.ColorUint8.rgb(0, 0, 0), + ); + return Uint8List.fromList(img.encodePng(canvas)); + } + + testWidgets('E2E (integration): place and confirm keeps size', ( + tester, + ) async { + final sigBytes = _makeSig(); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + pdfProvider.overrideWith( + (ref) => PdfController()..openPicked(path: 'test.pdf'), + ), + signatureLibraryProvider.overrideWith((ref) { + final c = SignatureLibraryController(); + c.add(sigBytes, name: 'image'); + return c; + }), + // Keep mock viewer for determinism on CI/desktop devices + useMockViewerProvider.overrideWithValue(true), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: PdfSignatureHomePage(), + ), + ), + ); + await tester.pumpAndSettle(); + + final card = find.byKey(const Key('gd_signature_card_area')).first; + await tester.tap(card); + await tester.pump(); + + final active = find.byKey(const Key('signature_overlay')); + expect(active, findsOneWidget); + final sizeBefore = tester.getSize(active); + + await tester.ensureVisible(active); + await tester.pumpAndSettle(); + // Programmatically simulate confirm: add placement with current rect and bound image, then clear active overlay. + final ctx = tester.element(find.byType(PdfSignatureHomePage)); + final container = ProviderScope.containerOf(ctx); + final sigState = container.read(signatureProvider); + final r = sigState.rect!; + final Size pageSize = SignatureController.pageSize; + final normalized = Rect.fromLTWH( + (r.left / pageSize.width).clamp(0.0, 1.0), + (r.top / pageSize.height).clamp(0.0, 1.0), + (r.width / pageSize.width).clamp(0.0, 1.0), + (r.height / pageSize.height).clamp(0.0, 1.0), + ); + final lib = container.read(signatureLibraryProvider); + final imageId = lib.isNotEmpty ? lib.first.id : 'default.png'; + final pdf = container.read(pdfProvider); + container + .read(pdfProvider.notifier) + .addPlacement(page: pdf.currentPage, rect: normalized, image: imageId); + container.read(signatureProvider.notifier).clearActiveOverlay(); + await tester.pumpAndSettle(); + + final placed = find.byKey(const Key('placed_signature_0')); + expect(placed, findsOneWidget); + final sizeAfter = tester.getSize(placed); + + expect( + (sizeAfter.width - sizeBefore.width).abs() < sizeBefore.width * 0.15, + isTrue, + ); + expect( + (sizeAfter.height - sizeBefore.height).abs() < sizeBefore.height * 0.15, + isTrue, + ); + }); } diff --git a/lib/data/services/preferences_providers.dart b/lib/data/services/preferences_providers.dart index e269530..df55154 100644 --- a/lib/data/services/preferences_providers.dart +++ b/lib/data/services/preferences_providers.dart @@ -173,15 +173,7 @@ final preferencesProvider = return PreferencesNotifier(prefs); }); -/// Safe accessor for page view mode that falls back to 'continuous' until -/// SharedPreferences is available (useful for lightweight widget tests). -final pageViewModeProvider = Provider((ref) { - final sp = ref.watch(sharedPreferencesProvider); - return sp.maybeWhen( - data: (_) => ref.watch(preferencesProvider).pageView, - orElse: () => 'continuous', - ); -}); +// pageViewModeProvider removed; the app always runs in continuous mode. /// Derive the active ThemeMode based on preference and platform brightness final themeModeProvider = Provider((ref) { diff --git a/lib/ui/features/pdf/view_model/view_model.dart b/lib/ui/features/pdf/view_model/view_model.dart index cc316b3..7b57314 100644 --- a/lib/ui/features/pdf/view_model/view_model.dart +++ b/lib/ui/features/pdf/view_model/view_model.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; +import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,6 +11,7 @@ class PdfController extends StateNotifier { PdfController() : super(PdfState.initial()); static const int samplePageCount = 5; + @visibleForTesting void openSample() { state = state.copyWith( loaded: true, @@ -158,22 +160,7 @@ class PdfController extends StateNotifier { removePlacement(page: state.currentPage, index: idx); } - // Assign a different image name to a placement on a page. - void assignImageToPlacement({ - required int page, - required int index, - required String image, - }) { - if (!state.loaded) return; - final p = page.clamp(1, state.pageCount); - final imgMap = Map>.from(state.placementImageByPage); - final list = List.from(imgMap[p] ?? const []); - if (index >= 0 && index < list.length) { - list[index] = image; - imgMap[p] = list; - state = state.copyWith(placementImageByPage: imgMap); - } - } + // NOTE: Programmatic reassignment of images has been removed. // Convenience to get image name for a placement String? imageOfPlacement({required int page, required int index}) { @@ -239,8 +226,8 @@ class SignatureController extends StateNotifier { state = state.copyWith( rect: Rect.fromCenter( center: Offset( - (pageSize.width / 2) * Random().nextDouble() * 2 + 1, - (pageSize.height / 2) * Random().nextDouble() * 2 + 1, + (pageSize.width / 2) * (Random().nextDouble() * 1.5 + 1), + (pageSize.height / 2) * (Random().nextDouble() * 1.5 + 1), ), width: w, height: h, @@ -394,28 +381,26 @@ class SignatureController extends StateNotifier { (r.width / pageSize.width).clamp(0.0, 1.0), (r.height / pageSize.height).clamp(0.0, 1.0), ); - ref - .read(pdfProvider.notifier) - .addPlacement(page: pdf.currentPage, rect: normalized); - // Assign image id to this placement (last index) - final idx = - (ref.read(pdfProvider).placementsByPage[pdf.currentPage]?.length ?? 1) - - 1; - String? id = state.assetId; - if (id == null) { + // Determine the image id to bind at placement time + String id = state.assetId ?? ''; + if (id.isEmpty) { final bytes = ref.read(processedSignatureImageProvider) ?? state.imageBytes; - if (bytes != null) { + if (bytes != null && bytes.isNotEmpty) { id = ref .read(signatureLibraryProvider.notifier) .add(bytes, name: 'image'); + } else { + id = 'default.png'; } } - if (id != null && id.isNotEmpty && idx >= 0) { - ref - .read(pdfProvider.notifier) - .assignImageToPlacement(page: pdf.currentPage, index: idx, image: id); - } + ref + .read(pdfProvider.notifier) + .addPlacement(page: pdf.currentPage, rect: normalized, image: id); + // Newly placed index is the last one on the page + final idx = + (ref.read(pdfProvider).placementsByPage[pdf.currentPage]?.length ?? 1) - + 1; // Auto-select the newly placed item so the red box appears if (idx >= 0) { ref.read(pdfProvider.notifier).selectPlacement(idx); diff --git a/lib/ui/features/pdf/widgets/pdf_page_area.dart b/lib/ui/features/pdf/widgets/pdf_page_area.dart index c89a902..78acc3d 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_area.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_area.dart @@ -5,7 +5,6 @@ import 'package:pdfrx/pdfrx.dart'; import '../../../../data/services/export_providers.dart'; import '../view_model/view_model.dart'; -import '../../../../data/services/preferences_providers.dart'; import 'signature_drag_data.dart'; import 'pdf_mock_continuous_list.dart'; import 'pdf_page_overlays.dart'; @@ -51,9 +50,8 @@ class _PdfPageAreaState extends ConsumerState { // is instructed to align to the provider's current page once ready. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - final mode = ref.read(pageViewModeProvider); final pdf = ref.read(pdfProvider); - if (mode == 'continuous' && pdf.pickedPdfPath != null && pdf.loaded) { + if (pdf.pickedPdfPath != null && pdf.loaded) { _scrollToPage(pdf.currentPage); } }); @@ -70,7 +68,7 @@ class _PdfPageAreaState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final pdf = ref.read(pdfProvider); - final isContinuous = ref.read(pageViewModeProvider) == 'continuous'; + const isContinuous = true; // Real continuous: drive via PdfViewerController if (pdf.pickedPdfPath != null && isContinuous) { @@ -158,13 +156,12 @@ class _PdfPageAreaState extends ConsumerState { @override Widget build(BuildContext context) { final pdf = ref.watch(pdfProvider); - final pageViewMode = ref.watch(pageViewModeProvider); + const pageViewMode = 'continuous'; // React to provider currentPage changes (e.g., user tapped overview) ref.listen(pdfProvider, (prev, next) { - final mode = ref.read(pageViewModeProvider); if (_suppressProviderListen) return; - if (mode == 'continuous' && (prev?.currentPage != next.currentPage)) { + if ((prev?.currentPage != next.currentPage)) { final target = next.currentPage; // If we're already navigating to this target, ignore; otherwise allow new target. if (_programmaticTargetPage != null && @@ -177,19 +174,7 @@ class _PdfPageAreaState extends ConsumerState { } } }); - // When switching to continuous, bring current page into view - ref.listen(pageViewModeProvider, (prev, next) { - if (next == 'continuous') { - // Skip initial auto-scroll in mock mode to avoid fighting with - // early provider-driven jumps during tests. - final isMock = ref.read(useMockViewerProvider); - if (isMock) return; - final p = ref.read(pdfProvider).currentPage; - if (_visiblePage != p) { - _scrollToPage(p); - } - } - }); + // No page view mode switching; always continuous. if (!pdf.loaded) { // In tests, AppLocalizations delegate may not be injected; fallback. diff --git a/lib/ui/features/pdf/widgets/pdf_page_overlays.dart b/lib/ui/features/pdf/widgets/pdf_page_overlays.dart index 404a7db..4e6bbea 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_overlays.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_overlays.dart @@ -33,10 +33,17 @@ class PdfPageOverlays extends ConsumerWidget { final widgets = []; for (int i = 0; i < placed.length; i++) { + final r = placed[i]; // stored as normalized 0..1 of page size + final uiRect = Rect.fromLTWH( + r.left * pageSize.width, + r.top * pageSize.height, + r.width * pageSize.width, + r.height * pageSize.height, + ); widgets.add( SignatureOverlay( pageSize: pageSize, - rect: placed[i], + rect: uiRect, sig: sig, pageNumber: pageNumber, interactive: false, diff --git a/lib/ui/features/pdf/widgets/signature_drawer.dart b/lib/ui/features/pdf/widgets/signature_drawer.dart index f39ef66..60546a8 100644 --- a/lib/ui/features/pdf/widgets/signature_drawer.dart +++ b/lib/ui/features/pdf/widgets/signature_drawer.dart @@ -67,21 +67,10 @@ class _SignatureDrawerState extends ConsumerState { ); }, onTap: () { - final sel = ref.read(pdfProvider).selectedPlacementIndex; - final page = ref.read(pdfProvider).currentPage; - if (sel != null) { - ref - .read(pdfProvider.notifier) - .assignImageToPlacement( - page: page, - index: sel, - image: a.id, - ); - } else { - ref - .read(signatureProvider.notifier) - .setImageFromLibrary(assetId: a.id); - } + // Never reassign placed signatures via tap; only set active overlay source + ref + .read(signatureProvider.notifier) + .setImageFromLibrary(assetId: a.id); }, ), ), @@ -146,9 +135,12 @@ class _SignatureDrawerState extends ConsumerState { ref.read(processedSignatureImageProvider) ?? ref.read(signatureProvider).imageBytes; if (b != null) { - ref + final id = ref .read(signatureLibraryProvider.notifier) .add(b, name: 'image'); + ref + .read(signatureProvider.notifier) + .setImageFromLibrary(assetId: id); } }, icon: const Icon(Icons.image_outlined), @@ -166,9 +158,12 @@ class _SignatureDrawerState extends ConsumerState { ref.read(processedSignatureImageProvider) ?? ref.read(signatureProvider).imageBytes; if (b != null) { - ref + final id = ref .read(signatureLibraryProvider.notifier) .add(b, name: 'drawing'); + ref + .read(signatureProvider.notifier) + .setImageFromLibrary(assetId: id); } }, icon: const Icon(Icons.gesture), diff --git a/test/features/step/an_image_is_loaded.dart b/test/features/step/an_image_is_loaded.dart deleted file mode 100644 index 7283aaf..0000000 --- a/test/features/step/an_image_is_loaded.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; -import '_world.dart'; - -/// Usage: an image {"bob.png"} is loaded -Future anImageIsLoaded(WidgetTester tester, String param1) async { - final container = TestWorld.container ?? ProviderContainer(); - TestWorld.container = container; - // Remember current image name - TestWorld.currentImageName = param1; - // Map name to deterministic bytes for testing - Uint8List bytes; - switch (param1) { - case 'alice.png': - bytes = Uint8List.fromList([1, 2, 3]); - break; - case 'bob.png': - bytes = Uint8List.fromList([4, 5, 6]); - break; - default: - bytes = Uint8List.fromList(param1.codeUnits.take(10).toList()); - } - container.read(signatureProvider.notifier).setImageBytes(bytes); -} diff --git a/test/features/step/the_selected_signature_is_shown_with_image.dart b/test/features/step/the_selected_signature_is_shown_with_image.dart deleted file mode 100644 index da7007a..0000000 --- a/test/features/step/the_selected_signature_is_shown_with_image.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; -import '_world.dart'; - -/// Usage: the selected signature is shown with image {"bob.png"} -Future theSelectedSignatureIsShownWithImage( - WidgetTester tester, - String expected, -) async { - final container = TestWorld.container ?? ProviderContainer(); - TestWorld.container = container; - final pdf = container.read(pdfProvider); - final page = pdf.currentPage; - final idx = - pdf.selectedPlacementIndex ?? - ((pdf.placementsByPage[page]?.length ?? 1) - 1); - final name = container - .read(pdfProvider.notifier) - .imageOfPlacement(page: page, index: idx); - expect(name, expected); -} diff --git a/test/features/step/the_user_assigns_to_the_selected_signature.dart b/test/features/step/the_user_assigns_to_the_selected_signature.dart deleted file mode 100644 index 43d61c6..0000000 --- a/test/features/step/the_user_assigns_to_the_selected_signature.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; -import '_world.dart'; - -/// Usage: the user assigns {"bob.png"} to the selected signature -Future theUserAssignsToTheSelectedSignature( - WidgetTester tester, - String newImageName, -) async { - final container = TestWorld.container ?? ProviderContainer(); - TestWorld.container = container; - // Load the new image into signature state (simulating pick) - Uint8List bytes = - newImageName == 'bob.png' - ? Uint8List.fromList([4, 5, 6]) - : Uint8List.fromList([1, 2, 3]); - container.read(signatureProvider.notifier).setImageBytes(bytes); - TestWorld.currentImageName = newImageName; - // Assign to currently selected placement - final pdf = container.read(pdfProvider); - final page = pdf.currentPage; - final idx = - pdf.selectedPlacementIndex ?? - ((pdf.placementsByPage[page]?.length ?? 1) - 1); - container - .read(pdfProvider.notifier) - .assignImageToPlacement(page: page, index: idx, image: newImageName); -} diff --git a/test/features/step/the_user_places_a_signature_on_the_page.dart b/test/features/step/the_user_places_a_signature_on_the_page.dart deleted file mode 100644 index 9597b4c..0000000 --- a/test/features/step/the_user_places_a_signature_on_the_page.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter/material.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; -import '_world.dart'; - -/// Usage: the user places a signature on the page -Future theUserPlacesASignatureOnThePage(WidgetTester tester) async { - final container = TestWorld.container ?? ProviderContainer(); - TestWorld.container = container; - final pdf = container.read(pdfProvider); - if (!pdf.loaded) { - container - .read(pdfProvider.notifier) - .openPicked(path: 'mock.pdf', pageCount: 1); - container.read(pdfProvider.notifier).setSignedPage(1); - } - // Ensure image bytes - if (container.read(signatureProvider).imageBytes == null) { - final name = TestWorld.currentImageName ?? 'alice.png'; - Uint8List bytes = - name == 'bob.png' - ? Uint8List.fromList([4, 5, 6]) - : Uint8List.fromList([1, 2, 3]); - container.read(signatureProvider.notifier).setImageBytes(bytes); - } - container.read(signatureProvider.notifier).placeDefaultRect(); - final Rect r = container.read(signatureProvider).rect!; - final int page = container.read(pdfProvider).signedPage ?? 1; - final imgName = TestWorld.currentImageName ?? 'alice.png'; - container - .read(pdfProvider.notifier) - .addPlacement(page: page, rect: r, image: imgName); - // Select the just placed signature (last index) - final list = container.read(pdfProvider).placementsByPage[page] ?? const []; - container - .read(pdfProvider.notifier) - .selectPlacement(list.isEmpty ? null : (list.length - 1)); -} diff --git a/test/features/support_multiple_signature_pictures.feature b/test/features/support_multiple_signature_pictures.feature index c5cab5c..a3303fe 100644 --- a/test/features/support_multiple_signature_pictures.feature +++ b/test/features/support_multiple_signature_pictures.feature @@ -22,14 +22,6 @@ Feature: support multiple signature pictures Then identical signature instances appear in each location And adjusting one instance does not affect the others - Scenario: Reassign a different image to an existing signature - Given a PDF page is selected for signing - And an image {"alice.png"} is loaded - And the user places a signature on the page - When an image {"bob.png"} is loaded - And the user assigns {"bob.png"} to the selected signature - Then the selected signature is shown with image {"bob.png"} - Scenario: Save/export uses the assigned image for each signature Given a PDF is open and contains multiple placed signatures across pages When the user saves/exports the document diff --git a/test/widget/e2e_place_confirm_test.dart b/test/widget/e2e_place_confirm_test.dart new file mode 100644 index 0000000..49a8469 --- /dev/null +++ b/test/widget/e2e_place_confirm_test.dart @@ -0,0 +1,136 @@ +import 'dart:ui' as ui; +import 'package:flutter/gestures.dart' show kSecondaryMouseButton; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image/image.dart' as img; + +import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; +import 'package:pdf_signature/data/services/export_providers.dart'; +import 'package:pdf_signature/l10n/app_localizations.dart'; + +void main() { + // Open the active overlay context menu robustly (mouse right-click, fallback to long-press) + Future _openActiveMenuAndConfirm(WidgetTester tester) async { + final overlay = find.byKey(const Key('signature_overlay')); + expect(overlay, findsOneWidget); + // Ensure visible before interacting + await tester.ensureVisible(overlay); + await tester.pumpAndSettle(); + + // Try right-click first + final center = tester.getCenter(overlay); + final TestGesture mouse = await tester.createGesture( + kind: ui.PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await mouse.addPointer(location: center); + addTearDown(mouse.removePointer); + await tester.pump(); + await mouse.down(center); + await tester.pump(const Duration(milliseconds: 30)); + await mouse.up(); + await tester.pumpAndSettle(); + + // If menu didn't appear, try long-press + if (find.byKey(const Key('ctx_active_confirm')).evaluate().isEmpty) { + await tester.longPress(overlay, warnIfMissed: false); + await tester.pumpAndSettle(); + } + await tester.tap(find.byKey(const Key('ctx_active_confirm'))); + await tester.pumpAndSettle(); + } + + // Build a simple in-memory PNG as a signature image + Uint8List _makeSig() { + final canvas = img.Image(width: 80, height: 40); + img.fill(canvas, color: img.ColorUint8.rgb(255, 255, 255)); + img.drawLine( + canvas, + x1: 6, + y1: 20, + x2: 74, + y2: 20, + color: img.ColorUint8.rgb(0, 0, 0), + ); + return Uint8List.fromList(img.encodePng(canvas)); + } + + testWidgets('E2E: select, place default, and confirm signature', ( + tester, + ) async { + final sigBytes = _makeSig(); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + // Open a PDF + pdfProvider.overrideWith( + (ref) => PdfController()..openPicked(path: 'test.pdf'), + ), + // Provide one signature asset in the library + signatureLibraryProvider.overrideWith((ref) { + final c = SignatureLibraryController(); + c.add(sigBytes, name: 'image'); + return c; + }), + // Use mock continuous viewer for deterministic layout in widget tests + useMockViewerProvider.overrideWithValue(true), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const PdfSignatureHomePage(), + ), + ), + ); + await tester.pumpAndSettle(); + + // Tap the signature card to set it as active overlay + final card = find.byKey(const Key('gd_signature_card_area')).first; + expect(card, findsOneWidget); + await tester.tap(card); + await tester.pump(); + + // Active overlay should appear + final active = find.byKey(const Key('signature_overlay')); + expect(active, findsOneWidget); + final sizeBefore = tester.getSize(active); + + // Bring the overlay into the viewport (it's near the bottom of the page by default) + final listFinder = find.byKey(const Key('pdf_continuous_mock_list')); + if (listFinder.evaluate().isNotEmpty) { + // Ensure the active overlay is fully visible within the scrollable viewport + await tester.ensureVisible(active); + await tester.pumpAndSettle(); + } + + // Open context menu and confirm using a robust flow + await _openActiveMenuAndConfirm(tester); + + // Verify active overlay gone and placed overlay shown + expect(find.byKey(const Key('signature_overlay')), findsNothing); + final placed = find.byKey(const Key('placed_signature_0')); + expect(placed, findsOneWidget); + final sizeAfter = tester.getSize(placed); + + // Compare sizes: should be roughly equal (allowing small layout variance) + expect( + (sizeAfter.width - sizeBefore.width).abs() < sizeBefore.width * 0.15, + isTrue, + ); + expect( + (sizeAfter.height - sizeBefore.height).abs() < sizeBefore.height * 0.15, + isTrue, + ); + + // Verify provider state reflects one placement on current page + final ctx = tester.element(find.byType(PdfSignatureHomePage)); + final container = ProviderScope.containerOf(ctx); + final pdf = container.read(pdfProvider); + final list = pdf.placementsByPage[pdf.currentPage] ?? const []; + expect(list.length, 1); + }); +} diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index d93221d..13f390f 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -8,7 +8,7 @@ import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; -import 'package:pdf_signature/data/services/preferences_providers.dart'; +// preferences_providers.dart no longer exports pageViewModeProvider Future pumpWithOpenPdf(WidgetTester tester) async { await tester.pumpWidget( @@ -18,8 +18,7 @@ Future pumpWithOpenPdf(WidgetTester tester) async { (ref) => PdfController()..openPicked(path: 'test.pdf'), ), useMockViewerProvider.overrideWith((ref) => true), - // Force continuous mode regardless of prefs - pageViewModeProvider.overrideWithValue('continuous'), + // Continuous mode is always-on; no page view override needed ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -59,7 +58,7 @@ Future pumpWithOpenPdfAndSig(WidgetTester tester) async { ..placeDefaultRect(), ), useMockViewerProvider.overrideWith((ref) => true), - pageViewModeProvider.overrideWithValue('continuous'), + // Continuous mode is always-on; no page view override needed ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/test/widget/pdf_page_area_early_jump_test.dart b/test/widget/pdf_page_area_early_jump_test.dart index 93f99d2..548139a 100644 --- a/test/widget/pdf_page_area_early_jump_test.dart +++ b/test/widget/pdf_page_area_early_jump_test.dart @@ -7,7 +7,6 @@ import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/data/model/model.dart'; -import 'package:pdf_signature/data/services/preferences_providers.dart'; class _TestPdfController extends PdfController { _TestPdfController() : super() { @@ -30,7 +29,7 @@ void main() { ProviderScope( overrides: [ useMockViewerProvider.overrideWithValue(true), - pageViewModeProvider.overrideWithValue('continuous'), + // Continuous mode is always-on; no page view override needed pdfProvider.overrideWith((ref) => ctrl), ], child: MaterialApp( diff --git a/test/widget/pdf_page_area_jump_test.dart b/test/widget/pdf_page_area_jump_test.dart index 108e3d4..c121c4e 100644 --- a/test/widget/pdf_page_area_jump_test.dart +++ b/test/widget/pdf_page_area_jump_test.dart @@ -7,7 +7,6 @@ import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/data/model/model.dart'; -import 'package:pdf_signature/data/services/preferences_providers.dart'; class _TestPdfController extends PdfController { _TestPdfController() : super() { @@ -29,8 +28,7 @@ void main() { ProviderScope( overrides: [ useMockViewerProvider.overrideWithValue(true), - // Force continuous mode without SharedPreferences - pageViewModeProvider.overrideWithValue('continuous'), + // Continuous mode is always-on; no page view override needed pdfProvider.overrideWith((ref) => ctrl), ], child: MaterialApp(