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
|
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
|
||||||
|
|
|
@ -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/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,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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/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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue