diff --git a/integration_test/export_flow_test.dart b/integration_test/export_flow_test.dart index f2222c8..5ebb5a0 100644 --- a/integration_test/export_flow_test.dart +++ b/integration_test/export_flow_test.dart @@ -30,6 +30,30 @@ class RecordingExporter extends ExportService { } } +// Lightweight fake exporter to avoid invoking heavy rasterization during tests +class LightweightExporter extends ExportService { + @override + Future exportSignedPdfFromBytes({ + required Uint8List srcBytes, + required Size uiPageSize, + required Uint8List? signatureImageBytes, + Map>? placementsByPage, + Map? libraryBytes, + double targetDpi = 144.0, + }) async { + // Return minimal non-empty bytes; content isn't used further in tests + return Uint8List.fromList([1, 2, 3]); + } + + @override + Future saveBytesToFile({ + required Uint8List bytes, + required String outputPath, + }) async { + return true; + } +} + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -225,13 +249,13 @@ void main() { await tester.pumpAndSettle(); final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); - expect(container.read(pdfViewModelProvider), 1); + expect(container.read(pdfViewModelProvider).currentPage, 1); container.read(pdfViewModelProvider.notifier).jumpToPage(2); await tester.pumpAndSettle(); - expect(container.read(pdfViewModelProvider), 2); + expect(container.read(pdfViewModelProvider).currentPage, 2); container.read(pdfViewModelProvider.notifier).jumpToPage(3); await tester.pumpAndSettle(); - expect(container.read(pdfViewModelProvider), 3); + expect(container.read(pdfViewModelProvider).currentPage, 3); }); testWidgets('PDF View: zoom in/out', (tester) async { @@ -319,7 +343,7 @@ void main() { await tester.pumpAndSettle(); final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); - expect(container.read(pdfViewModelProvider), 1); + expect(container.read(pdfViewModelProvider).currentPage, 1); final pagesSidebar = find.byType(PagesSidebar); expect(pagesSidebar, findsOneWidget); @@ -332,7 +356,7 @@ void main() { expect(page3Thumb, findsOneWidget); await tester.tap(page3Thumb); await tester.pumpAndSettle(); - expect(container.read(pdfViewModelProvider), 3); + expect(container.read(pdfViewModelProvider).currentPage, 3); }); testWidgets('PDF View: thumbnails scroll and select', (tester) async { @@ -371,15 +395,70 @@ void main() { await tester.pumpAndSettle(); final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); - expect(container.read(pdfViewModelProvider), 1); + expect(container.read(pdfViewModelProvider).currentPage, 1); final sidebar = find.byType(PagesSidebar); expect(sidebar, findsOneWidget); await tester.drag(sidebar, const Offset(0, -200)); await tester.pumpAndSettle(); expect(find.text('1'), findsOneWidget); - expect(container.read(pdfViewModelProvider), 1); + expect(container.read(pdfViewModelProvider).currentPage, 1); await tester.tap(find.text('2')); await tester.pumpAndSettle(); - expect(container.read(pdfViewModelProvider), 2); + expect(container.read(pdfViewModelProvider).currentPage, 2); + }); + + testWidgets('PDF View: tap viewer after export does not crash', ( + tester, + ) async { + final pdfBytes = + await File('integration_test/data/sample-local-pdf.pdf').readAsBytes(); + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + preferencesRepositoryProvider.overrideWith( + (ref) => PreferencesStateNotifier(prefs), + ), + documentRepositoryProvider.overrideWith( + (ref) => + DocumentStateNotifier() + ..openPicked(pageCount: 3, bytes: pdfBytes), + ), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), + exportServiceProvider.overrideWith((ref) => LightweightExporter()), + savePathPickerProvider.overrideWith( + (_) => () async => 'C:/tmp/output-after-export.pdf', + ), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('en'), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Trigger export + await tester.tap(find.byKey(const Key('btn_save_pdf'))); + await tester.pumpAndSettle(); + + // Tap on the page area; should not crash + final pageArea = find.byKey(const ValueKey('pdf_page_area')); + expect(pageArea, findsOneWidget); + await tester.tap(pageArea); + await tester.pumpAndSettle(); + + // Still present and responsive + expect(pageArea, findsOneWidget); }); } diff --git a/lib/ui/features/pdf/view_model/pdf_view_model.dart b/lib/ui/features/pdf/view_model/pdf_view_model.dart index fa8574c..10b9713 100644 --- a/lib/ui/features/pdf/view_model/pdf_view_model.dart +++ b/lib/ui/features/pdf/view_model/pdf_view_model.dart @@ -34,7 +34,7 @@ class PdfViewModel extends ChangeNotifier { PdfViewModel(this.ref, {bool? useMockViewer}) : _useMockViewer = useMockViewer ?? - bool.fromEnvironment('FLUTTER_TEST', defaultValue: false); + const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false); bool get useMockViewer => _useMockViewer; diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index 72b287e..cc79108 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -13,6 +13,7 @@ import 'pdf_page_area.dart'; import 'pages_sidebar.dart'; import 'signatures_sidebar.dart'; import 'ui_services.dart'; +import 'package:pdf_signature/utils/download.dart'; import '../view_model/pdf_view_model.dart'; class PdfSignatureHomePage extends ConsumerStatefulWidget { @@ -168,6 +169,21 @@ class _PdfSignatureHomePageState extends ConsumerState { if (out != null) { ok = await exporter.saveBytesToFile(bytes: out, outputPath: fullPath); } + } else { + // Web: export and trigger browser download + final src = pdf.pickedPdfBytes ?? Uint8List(0); + final out = await exporter.exportSignedPdfFromBytes( + srcBytes: src, + uiPageSize: _pageSize, + signatureImageBytes: null, + placementsByPage: pdf.placementsByPage, + targetDpi: targetDpi, + ); + if (out != null) { + // Use a sensible default filename (cannot prompt path on web) + ok = await downloadBytes(out, filename: 'signed.pdf'); + savedPath = 'signed.pdf'; + } } if (!kIsWeb) { if (ok) { @@ -185,6 +201,19 @@ class _PdfSignatureHomePageState extends ConsumerState { ), ); } + } else { + // Web: show a toast-like confirmation + messenger.showSnackBar( + SnackBar( + content: Text( + ok + ? AppLocalizations.of( + context, + ).savedWithPath(savedPath ?? 'signed.pdf') + : AppLocalizations.of(context).failedToSavePdf, + ), + ), + ); } } finally { ref.read(exportingProvider.notifier).state = false; diff --git a/lib/utils/download.dart b/lib/utils/download.dart new file mode 100644 index 0000000..90d88e7 --- /dev/null +++ b/lib/utils/download.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +import 'download_stub.dart' if (dart.library.html) 'download_web.dart' as impl; + +/// Initiates a platform-appropriate download/save operation. +/// +/// On Web: triggers a browser download with the provided filename. +/// On non-Web: returns false (no-op). Use your existing IO save flow instead. +Future downloadBytes(Uint8List bytes, {required String filename}) { + return impl.downloadBytes(bytes, filename: filename); +} diff --git a/lib/utils/download_stub.dart b/lib/utils/download_stub.dart new file mode 100644 index 0000000..654d280 --- /dev/null +++ b/lib/utils/download_stub.dart @@ -0,0 +1,6 @@ +import 'dart:typed_data'; + +Future downloadBytes(Uint8List bytes, {required String filename}) async { + // Not supported on non-web. Return false so caller can fallback to file save. + return false; +} diff --git a/lib/utils/download_web.dart b/lib/utils/download_web.dart new file mode 100644 index 0000000..b9f6ac8 --- /dev/null +++ b/lib/utils/download_web.dart @@ -0,0 +1,22 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; +import 'dart:typed_data'; + +Future downloadBytes(Uint8List bytes, {required String filename}) async { + try { + final blob = html.Blob([bytes], 'application/pdf'); + final url = html.Url.createObjectUrlFromBlob(blob); + final anchor = + html.document.createElement('a') as html.AnchorElement + ..href = url + ..download = filename + ..style.display = 'none'; + html.document.body?.children.add(anchor); + anchor.click(); + anchor.remove(); + html.Url.revokeObjectUrl(url); + return true; + } catch (_) { + return false; + } +}