feat: able to add multi signatures on document view
This commit is contained in:
parent
0a21045761
commit
fdf0d1f7a9
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<String>((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<ThemeMode>((ref) {
|
||||
|
|
|
@ -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<PdfState> {
|
|||
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<PdfState> {
|
|||
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<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);
|
||||
}
|
||||
}
|
||||
// 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<SignatureState> {
|
|||
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<SignatureState> {
|
|||
(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);
|
||||
|
|
|
@ -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<PdfPageArea> {
|
|||
// 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<PdfPageArea> {
|
|||
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<PdfPageArea> {
|
|||
@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<PdfPageArea> {
|
|||
}
|
||||
}
|
||||
});
|
||||
// When switching to continuous, bring current page into view
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
// No page view mode switching; always continuous.
|
||||
|
||||
if (!pdf.loaded) {
|
||||
// In tests, AppLocalizations delegate may not be injected; fallback.
|
||||
|
|
|
@ -33,10 +33,17 @@ class PdfPageOverlays extends ConsumerWidget {
|
|||
final widgets = <Widget>[];
|
||||
|
||||
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,
|
||||
|
|
|
@ -67,21 +67,10 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
|||
);
|
||||
},
|
||||
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<SignatureDrawer> {
|
|||
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<SignatureDrawer> {
|
|||
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),
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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<void> pumpWithOpenPdf(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
|
@ -18,8 +18,7 @@ Future<void> 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<void> 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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue