feat: able to add multi signatures on document view

This commit is contained in:
insleker 2025-09-03 20:55:16 +08:00
parent 0a21045761
commit fdf0d1f7a9
17 changed files with 288 additions and 215 deletions

View File

@ -20,6 +20,8 @@ flutter pub run build_runner build --delete-conflicting-outputs
flutter analyze flutter analyze
# > run unit tests and widget tests # > run unit tests and widget tests
flutter test flutter test
# > run integration tests
flutter test integration_test/ -d linux
# dart run tool/gen_view_wireframe_md.dart # dart run tool/gen_view_wireframe_md.dart
# flutter pub run dead_code_analyzer # flutter pub run dead_code_analyzer

View File

@ -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);
}

View File

@ -1,7 +1,9 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_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_service.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
@ -56,4 +58,92 @@ void main() {
// Expect success UI // Expect success UI
expect(find.textContaining('Saved:'), findsOneWidget); 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,
);
});
} }

View File

@ -173,15 +173,7 @@ final preferencesProvider =
return PreferencesNotifier(prefs); return PreferencesNotifier(prefs);
}); });
/// Safe accessor for page view mode that falls back to 'continuous' until // pageViewModeProvider removed; the app always runs in continuous mode.
/// SharedPreferences is available (useful for lightweight widget tests).
final pageViewModeProvider = Provider<String>((ref) {
final sp = ref.watch(sharedPreferencesProvider);
return sp.maybeWhen(
data: (_) => ref.watch(preferencesProvider).pageView,
orElse: () => 'continuous',
);
});
/// Derive the active ThemeMode based on preference and platform brightness /// Derive the active ThemeMode based on preference and platform brightness
final themeModeProvider = Provider<ThemeMode>((ref) { final themeModeProvider = Provider<ThemeMode>((ref) {

View File

@ -1,4 +1,5 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -10,6 +11,7 @@ class PdfController extends StateNotifier<PdfState> {
PdfController() : super(PdfState.initial()); PdfController() : super(PdfState.initial());
static const int samplePageCount = 5; static const int samplePageCount = 5;
@visibleForTesting
void openSample() { void openSample() {
state = state.copyWith( state = state.copyWith(
loaded: true, loaded: true,
@ -158,22 +160,7 @@ class PdfController extends StateNotifier<PdfState> {
removePlacement(page: state.currentPage, index: idx); removePlacement(page: state.currentPage, index: idx);
} }
// Assign a different image name to a placement on a page. // NOTE: Programmatic reassignment of images has been removed.
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<int, List<String>>.from(state.placementImageByPage);
final list = List<String>.from(imgMap[p] ?? const []);
if (index >= 0 && index < list.length) {
list[index] = image;
imgMap[p] = list;
state = state.copyWith(placementImageByPage: imgMap);
}
}
// Convenience to get image name for a placement // Convenience to get image name for a placement
String? imageOfPlacement({required int page, required int index}) { String? imageOfPlacement({required int page, required int index}) {
@ -239,8 +226,8 @@ class SignatureController extends StateNotifier<SignatureState> {
state = state.copyWith( state = state.copyWith(
rect: Rect.fromCenter( rect: Rect.fromCenter(
center: Offset( center: Offset(
(pageSize.width / 2) * Random().nextDouble() * 2 + 1, (pageSize.width / 2) * (Random().nextDouble() * 1.5 + 1),
(pageSize.height / 2) * Random().nextDouble() * 2 + 1, (pageSize.height / 2) * (Random().nextDouble() * 1.5 + 1),
), ),
width: w, width: w,
height: h, height: h,
@ -394,28 +381,26 @@ class SignatureController extends StateNotifier<SignatureState> {
(r.width / pageSize.width).clamp(0.0, 1.0), (r.width / pageSize.width).clamp(0.0, 1.0),
(r.height / pageSize.height).clamp(0.0, 1.0), (r.height / pageSize.height).clamp(0.0, 1.0),
); );
ref // Determine the image id to bind at placement time
.read(pdfProvider.notifier) String id = state.assetId ?? '';
.addPlacement(page: pdf.currentPage, rect: normalized); if (id.isEmpty) {
// 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) {
final bytes = final bytes =
ref.read(processedSignatureImageProvider) ?? state.imageBytes; ref.read(processedSignatureImageProvider) ?? state.imageBytes;
if (bytes != null) { if (bytes != null && bytes.isNotEmpty) {
id = ref id = ref
.read(signatureLibraryProvider.notifier) .read(signatureLibraryProvider.notifier)
.add(bytes, name: 'image'); .add(bytes, name: 'image');
} else {
id = 'default.png';
} }
} }
if (id != null && id.isNotEmpty && idx >= 0) { ref
ref .read(pdfProvider.notifier)
.read(pdfProvider.notifier) .addPlacement(page: pdf.currentPage, rect: normalized, image: id);
.assignImageToPlacement(page: pdf.currentPage, index: idx, 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 // Auto-select the newly placed item so the red box appears
if (idx >= 0) { if (idx >= 0) {
ref.read(pdfProvider.notifier).selectPlacement(idx); ref.read(pdfProvider.notifier).selectPlacement(idx);

View File

@ -5,7 +5,6 @@ import 'package:pdfrx/pdfrx.dart';
import '../../../../data/services/export_providers.dart'; import '../../../../data/services/export_providers.dart';
import '../view_model/view_model.dart'; import '../view_model/view_model.dart';
import '../../../../data/services/preferences_providers.dart';
import 'signature_drag_data.dart'; import 'signature_drag_data.dart';
import 'pdf_mock_continuous_list.dart'; import 'pdf_mock_continuous_list.dart';
import 'pdf_page_overlays.dart'; import 'pdf_page_overlays.dart';
@ -51,9 +50,8 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
// is instructed to align to the provider's current page once ready. // is instructed to align to the provider's current page once ready.
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final mode = ref.read(pageViewModeProvider);
final pdf = ref.read(pdfProvider); final pdf = ref.read(pdfProvider);
if (mode == 'continuous' && pdf.pickedPdfPath != null && pdf.loaded) { if (pdf.pickedPdfPath != null && pdf.loaded) {
_scrollToPage(pdf.currentPage); _scrollToPage(pdf.currentPage);
} }
}); });
@ -70,7 +68,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final pdf = ref.read(pdfProvider); final pdf = ref.read(pdfProvider);
final isContinuous = ref.read(pageViewModeProvider) == 'continuous'; const isContinuous = true;
// Real continuous: drive via PdfViewerController // Real continuous: drive via PdfViewerController
if (pdf.pickedPdfPath != null && isContinuous) { if (pdf.pickedPdfPath != null && isContinuous) {
@ -158,13 +156,12 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pdf = ref.watch(pdfProvider); final pdf = ref.watch(pdfProvider);
final pageViewMode = ref.watch(pageViewModeProvider); const pageViewMode = 'continuous';
// React to provider currentPage changes (e.g., user tapped overview) // React to provider currentPage changes (e.g., user tapped overview)
ref.listen(pdfProvider, (prev, next) { ref.listen(pdfProvider, (prev, next) {
final mode = ref.read(pageViewModeProvider);
if (_suppressProviderListen) return; if (_suppressProviderListen) return;
if (mode == 'continuous' && (prev?.currentPage != next.currentPage)) { if ((prev?.currentPage != next.currentPage)) {
final target = next.currentPage; final target = next.currentPage;
// If we're already navigating to this target, ignore; otherwise allow new target. // If we're already navigating to this target, ignore; otherwise allow new target.
if (_programmaticTargetPage != null && if (_programmaticTargetPage != null &&
@ -177,19 +174,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
} }
} }
}); });
// When switching to continuous, bring current page into view // No page view mode switching; always continuous.
ref.listen<String>(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);
}
}
});
if (!pdf.loaded) { if (!pdf.loaded) {
// In tests, AppLocalizations delegate may not be injected; fallback. // In tests, AppLocalizations delegate may not be injected; fallback.

View File

@ -33,10 +33,17 @@ class PdfPageOverlays extends ConsumerWidget {
final widgets = <Widget>[]; final widgets = <Widget>[];
for (int i = 0; i < placed.length; i++) { 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( widgets.add(
SignatureOverlay( SignatureOverlay(
pageSize: pageSize, pageSize: pageSize,
rect: placed[i], rect: uiRect,
sig: sig, sig: sig,
pageNumber: pageNumber, pageNumber: pageNumber,
interactive: false, interactive: false,

View File

@ -67,21 +67,10 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
); );
}, },
onTap: () { onTap: () {
final sel = ref.read(pdfProvider).selectedPlacementIndex; // Never reassign placed signatures via tap; only set active overlay source
final page = ref.read(pdfProvider).currentPage; ref
if (sel != null) { .read(signatureProvider.notifier)
ref .setImageFromLibrary(assetId: a.id);
.read(pdfProvider.notifier)
.assignImageToPlacement(
page: page,
index: sel,
image: a.id,
);
} else {
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(assetId: a.id);
}
}, },
), ),
), ),
@ -146,9 +135,12 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
ref.read(processedSignatureImageProvider) ?? ref.read(processedSignatureImageProvider) ??
ref.read(signatureProvider).imageBytes; ref.read(signatureProvider).imageBytes;
if (b != null) { if (b != null) {
ref final id = ref
.read(signatureLibraryProvider.notifier) .read(signatureLibraryProvider.notifier)
.add(b, name: 'image'); .add(b, name: 'image');
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(assetId: id);
} }
}, },
icon: const Icon(Icons.image_outlined), icon: const Icon(Icons.image_outlined),
@ -166,9 +158,12 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
ref.read(processedSignatureImageProvider) ?? ref.read(processedSignatureImageProvider) ??
ref.read(signatureProvider).imageBytes; ref.read(signatureProvider).imageBytes;
if (b != null) { if (b != null) {
ref final id = ref
.read(signatureLibraryProvider.notifier) .read(signatureLibraryProvider.notifier)
.add(b, name: 'drawing'); .add(b, name: 'drawing');
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(assetId: id);
} }
}, },
icon: const Icon(Icons.gesture), icon: const Icon(Icons.gesture),

View File

@ -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<void> 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);
}

View File

@ -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<void> 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);
}

View File

@ -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<void> 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);
}

View File

@ -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<void> 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));
}

View File

@ -22,14 +22,6 @@ Feature: support multiple signature pictures
Then identical signature instances appear in each location Then identical signature instances appear in each location
And adjusting one instance does not affect the others 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 Scenario: Save/export uses the assigned image for each signature
Given a PDF is open and contains multiple placed signatures across pages Given a PDF is open and contains multiple placed signatures across pages
When the user saves/exports the document When the user saves/exports the document

View File

@ -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<void> _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);
});
}

View File

@ -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/ui/features/pdf/view_model/view_model.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.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<void> pumpWithOpenPdf(WidgetTester tester) async { Future<void> pumpWithOpenPdf(WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
@ -18,8 +18,7 @@ Future<void> pumpWithOpenPdf(WidgetTester tester) async {
(ref) => PdfController()..openPicked(path: 'test.pdf'), (ref) => PdfController()..openPicked(path: 'test.pdf'),
), ),
useMockViewerProvider.overrideWith((ref) => true), useMockViewerProvider.overrideWith((ref) => true),
// Force continuous mode regardless of prefs // Continuous mode is always-on; no page view override needed
pageViewModeProvider.overrideWithValue('continuous'),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
@ -59,7 +58,7 @@ Future<void> pumpWithOpenPdfAndSig(WidgetTester tester) async {
..placeDefaultRect(), ..placeDefaultRect(),
), ),
useMockViewerProvider.overrideWith((ref) => true), useMockViewerProvider.overrideWith((ref) => true),
pageViewModeProvider.overrideWithValue('continuous'), // Continuous mode is always-on; no page view override needed
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,

View File

@ -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/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/model/model.dart'; import 'package:pdf_signature/data/model/model.dart';
import 'package:pdf_signature/data/services/preferences_providers.dart';
class _TestPdfController extends PdfController { class _TestPdfController extends PdfController {
_TestPdfController() : super() { _TestPdfController() : super() {
@ -30,7 +29,7 @@ void main() {
ProviderScope( ProviderScope(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), useMockViewerProvider.overrideWithValue(true),
pageViewModeProvider.overrideWithValue('continuous'), // Continuous mode is always-on; no page view override needed
pdfProvider.overrideWith((ref) => ctrl), pdfProvider.overrideWith((ref) => ctrl),
], ],
child: MaterialApp( child: MaterialApp(

View File

@ -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/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/model/model.dart'; import 'package:pdf_signature/data/model/model.dart';
import 'package:pdf_signature/data/services/preferences_providers.dart';
class _TestPdfController extends PdfController { class _TestPdfController extends PdfController {
_TestPdfController() : super() { _TestPdfController() : super() {
@ -29,8 +28,7 @@ void main() {
ProviderScope( ProviderScope(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), useMockViewerProvider.overrideWithValue(true),
// Force continuous mode without SharedPreferences // Continuous mode is always-on; no page view override needed
pageViewModeProvider.overrideWithValue('continuous'),
pdfProvider.overrideWith((ref) => ctrl), pdfProvider.overrideWith((ref) => ctrl),
], ],
child: MaterialApp( child: MaterialApp(