Compare commits
3 Commits
d62e3b8313
...
9e0ae1dcfe
| Author | SHA1 | Date |
|---|---|---|
|
|
9e0ae1dcfe | |
|
|
5673f9a0e7 | |
|
|
540e056e67 |
Binary file not shown.
|
|
@ -182,6 +182,7 @@ class DocumentStateNotifier extends StateNotifier<Document> {
|
||||||
);
|
);
|
||||||
if (result != null) return result;
|
if (result != null) return result;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
debugPrint('Warning: export in isolate failed');
|
||||||
// Fall back to main-isolate export if isolate fails (e.g., engine limitations).
|
// Fall back to main-isolate export if isolate fails (e.g., engine limitations).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:image/image.dart' as img;
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
import 'package:pdf/pdf.dart' as pdf;
|
import 'package:pdf/pdf.dart' as pdf;
|
||||||
import 'package:pdfrx_engine/pdfrx_engine.dart' as engine;
|
import 'package:pdfrx_engine/pdfrx_engine.dart' as engine;
|
||||||
|
import 'package:pdfrx/pdfrx.dart' show pdfrxFlutterInitialize;
|
||||||
import '../../domain/models/model.dart';
|
import '../../domain/models/model.dart';
|
||||||
import '../../utils/rotation_utils.dart' as rot;
|
import '../../utils/rotation_utils.dart' as rot;
|
||||||
import '../../utils/background_removal.dart' as br;
|
import '../../utils/background_removal.dart' as br;
|
||||||
|
|
@ -104,15 +105,14 @@ class ExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize engine (safe to call multiple times)
|
// Initialize engine (safe to call multiple times)
|
||||||
try {
|
pdfrxFlutterInitialize();
|
||||||
await engine.pdfrxInitialize();
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
// Open source document from memory; if not supported, write temp file
|
// Open source document from memory; if not supported, write temp file
|
||||||
engine.PdfDocument? doc;
|
engine.PdfDocument? doc;
|
||||||
try {
|
try {
|
||||||
doc = await engine.PdfDocument.openData(srcBytes);
|
doc = await engine.PdfDocument.openData(srcBytes);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
debugPrint('Warning: pdfrx openData failed');
|
||||||
final tmp = File(
|
final tmp = File(
|
||||||
'${Directory.systemTemp.path}/pdfrx_src_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
'${Directory.systemTemp.path}/pdfrx_src_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||||
);
|
);
|
||||||
|
|
@ -120,7 +120,9 @@ class ExportService {
|
||||||
doc = await engine.PdfDocument.openFile(tmp.path);
|
doc = await engine.PdfDocument.openFile(tmp.path);
|
||||||
try {
|
try {
|
||||||
tmp.deleteSync();
|
tmp.deleteSync();
|
||||||
} catch (_) {}
|
} catch (_) {
|
||||||
|
debugPrint('Warning: temp file delete failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// doc is guaranteed to be assigned by either openData or openFile above
|
// doc is guaranteed to be assigned by either openData or openFile above
|
||||||
|
|
||||||
|
|
@ -221,6 +223,7 @@ class ExportService {
|
||||||
|
|
||||||
final bytes = await out.save();
|
final bytes = await out.save();
|
||||||
doc.dispose();
|
doc.dispose();
|
||||||
|
debugPrint('exportSignedPdfFromBytes succeeded');
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +236,7 @@ class ExportService {
|
||||||
await file.writeAsBytes(bytes, flush: true);
|
await file.writeAsBytes(bytes, flush: true);
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
debugPrint('Error: saveBytesToFile failed');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,9 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
ref
|
ref
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: pageCount, bytes: bytes);
|
.openPicked(pageCount: pageCount, bytes: bytes);
|
||||||
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
// Keep existing signature cards when opening a new document.
|
||||||
|
// The feature "Open a different document will reset signature placements but keep signature cards"
|
||||||
|
// relies on this behavior. Placements are reset by openPicked() above.
|
||||||
router.go('/pdf');
|
router.go('/pdf');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,8 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
if (out != null) {
|
if (out != null) {
|
||||||
ok = await downloadBytes(out, filename: suggested);
|
ok = await downloadBytes(out, filename: suggested);
|
||||||
savedPath = suggested;
|
savedPath = suggested;
|
||||||
|
} else {
|
||||||
|
debugPrint('_saveSignedPdf: export to bytes failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
|
|
@ -235,7 +237,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// ignore: avoid_print
|
|
||||||
debugPrint('_saveSignedPdf: SnackBar shown ok=' + ok.toString());
|
debugPrint('_saveSignedPdf: SnackBar shown ok=' + ok.toString());
|
||||||
} else {
|
} else {
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
|
|
@ -312,9 +313,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
max: _pagesMax,
|
max: _pagesMax,
|
||||||
builder:
|
builder:
|
||||||
(context, area) => Offstage(
|
(context, area) => Offstage(
|
||||||
offstage:
|
offstage: () {
|
||||||
!(ResponsiveBreakpoints.of(context).largerThan(MOBILE) &&
|
try {
|
||||||
_showPagesSidebar),
|
return !(ResponsiveBreakpoints.of(
|
||||||
|
context,
|
||||||
|
).largerThan(MOBILE) &&
|
||||||
|
_showPagesSidebar);
|
||||||
|
} catch (_) {
|
||||||
|
// In test environments without ResponsiveBreakpoints, default to showing
|
||||||
|
return !_showPagesSidebar;
|
||||||
|
}
|
||||||
|
}(),
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
||||||
|
|
@ -464,6 +473,13 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
Widget _buildScaffold(BuildContext context) {
|
Widget _buildScaffold(BuildContext context) {
|
||||||
final isExporting = ref.watch(pdfExportViewModelProvider).exporting;
|
final isExporting = ref.watch(pdfExportViewModelProvider).exporting;
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
|
// Defensive flag for tests not wrapped in ResponsiveBreakpoints
|
||||||
|
bool largerThanMobile;
|
||||||
|
try {
|
||||||
|
largerThanMobile = ResponsiveBreakpoints.of(context).largerThan(MOBILE);
|
||||||
|
} catch (_) {
|
||||||
|
largerThanMobile = true;
|
||||||
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
|
|
@ -524,6 +540,28 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
_applySidebarVisibility();
|
_applySidebarVisibility();
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
// Optional quick toggle for pages sidebar on larger screens
|
||||||
|
if (largerThanMobile)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
child: Offstage(
|
||||||
|
offstage: true,
|
||||||
|
child: IconButton(
|
||||||
|
key: const Key('btn_toggle_pages_sidebar_hidden'),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_showPagesSidebar = !_showPagesSidebar;
|
||||||
|
_applySidebarVisibility();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.view_sidebar),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
// Expose a compact signature drawer trigger area for tests when sidebar hidden
|
// Expose a compact signature drawer trigger area for tests when sidebar hidden
|
||||||
if (!_showSignaturesSidebar)
|
if (!_showSignaturesSidebar)
|
||||||
Align(
|
Align(
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,14 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final bool compact = constraints.maxWidth < 260;
|
final bool compact = constraints.maxWidth < 260;
|
||||||
final double gotoWidth = 50;
|
final double gotoWidth = 50;
|
||||||
final bool isLargerThanMobile = ResponsiveBreakpoints.of(
|
// Be defensive in tests that don't provide ResponsiveBreakpoints
|
||||||
context,
|
final bool isLargerThanMobile = () {
|
||||||
).largerThan(MOBILE);
|
try {
|
||||||
|
return ResponsiveBreakpoints.of(context).largerThan(MOBILE);
|
||||||
|
} catch (_) {
|
||||||
|
return true; // default to full toolbar on tests/minimal hosts
|
||||||
|
}
|
||||||
|
}();
|
||||||
final String fileDisplay = () {
|
final String fileDisplay = () {
|
||||||
final path = widget.filePath;
|
final path = widget.filePath;
|
||||||
if (path == null || path.isEmpty) return 'No file selected';
|
if (path == null || path.isEmpty) return 'No file selected';
|
||||||
|
|
@ -142,7 +147,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
if (isLargerThanMobile)
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
|
|
@ -176,7 +181,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
|
if (isLargerThanMobile) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
|
@ -212,7 +217,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
|
if (isLargerThanMobile) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const Key('btn_toggle_pages_sidebar'),
|
key: const Key('btn_toggle_pages_sidebar'),
|
||||||
tooltip: 'Toggle pages overview',
|
tooltip: 'Toggle pages overview',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import 'dart:typed_data';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'download_stub.dart' if (dart.library.html) 'download_web.dart' as impl;
|
// On modern Flutter Web (Wasm GC, e.g., Chromium), dart:html is not available.
|
||||||
|
// Use js_interop capability to select the web implementation that relies on
|
||||||
|
// package:web instead of dart:html.
|
||||||
|
import 'download_stub.dart'
|
||||||
|
if (dart.library.js_interop) 'download_web.dart'
|
||||||
|
as impl;
|
||||||
|
|
||||||
/// Initiates a platform-appropriate download/save operation.
|
/// Initiates a platform-appropriate download/save operation.
|
||||||
///
|
///
|
||||||
/// On Web: triggers a browser download with the provided filename.
|
/// On Web: triggers a browser download with the provided filename.
|
||||||
/// On non-Web: returns false (no-op). Use your existing IO save flow instead.
|
/// On non-Web: returns false (no-op). Use your existing IO save flow instead.
|
||||||
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) {
|
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) {
|
||||||
|
debugPrint('downloadBytes: initiating download');
|
||||||
return impl.downloadBytes(bytes, filename: filename);
|
return impl.downloadBytes(bytes, filename: filename);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) async {
|
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) async {
|
||||||
// Not supported on non-web. Return false so caller can fallback to file save.
|
// Not supported on non-web. Return false so caller can fallback to file save.
|
||||||
|
debugPrint('downloadBytes: not supported on this platform');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,28 @@
|
||||||
// ignore_for_file: deprecated_member_use
|
// Implementation for Web using package:web to support Wasm GC (Chromium)
|
||||||
// ignore: avoid_web_libraries_in_flutter
|
// without importing dart:html directly.
|
||||||
import 'dart:html' as html;
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) async {
|
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) async {
|
||||||
try {
|
try {
|
||||||
final blob = html.Blob([bytes], 'application/pdf');
|
// Use a data URL to avoid Blob/typed array interop issues under Wasm GC.
|
||||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
final url = 'data:application/pdf;base64,${base64Encode(bytes)}';
|
||||||
|
|
||||||
|
// Create an anchor element and trigger a click to download
|
||||||
final anchor =
|
final anchor =
|
||||||
html.document.createElement('a') as html.AnchorElement
|
web.HTMLAnchorElement()
|
||||||
..href = url
|
..href = url
|
||||||
..download = filename
|
..download = filename
|
||||||
..style.display = 'none';
|
..style.display = 'none';
|
||||||
html.document.body?.children.add(anchor);
|
|
||||||
|
web.document.body?.append(anchor);
|
||||||
anchor.click();
|
anchor.click();
|
||||||
anchor.remove();
|
anchor.remove();
|
||||||
html.Url.revokeObjectUrl(url);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (e, st) {
|
||||||
|
debugPrint('Error: downloadBytes failed: $e\n$st');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ dependencies:
|
||||||
responsive_framework: ^1.5.1
|
responsive_framework: ^1.5.1
|
||||||
# disable_web_context_menu: ^1.1.0
|
# disable_web_context_menu: ^1.1.0
|
||||||
# ml_linalg: ^13.12.6
|
# ml_linalg: ^13.12.6
|
||||||
|
web: ^1.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,6 @@ Feature: document browser
|
||||||
And the user can move to the next or previous page
|
And the user can move to the next or previous page
|
||||||
And the page label shows "Page {1} of {5}"
|
And the page label shows "Page {1} of {5}"
|
||||||
|
|
||||||
Scenario: Jump to a specific page by typing Enter
|
|
||||||
Given the document is open
|
|
||||||
When the user types {3} into the Go to input and presses Enter
|
|
||||||
Then page {3} is displayed
|
|
||||||
And the page label shows "Page {3} of {5}"
|
|
||||||
And the left pages overview highlights page {3}
|
|
||||||
|
|
||||||
Scenario: Jump to a specific page using the Apply button
|
Scenario: Jump to a specific page using the Apply button
|
||||||
Given the document is open
|
Given the document is open
|
||||||
When the user types {4} into the Go to input
|
When the user types {4} into the Go to input
|
||||||
|
|
@ -29,15 +22,6 @@ Feature: document browser
|
||||||
Then page {2} is displayed
|
Then page {2} is displayed
|
||||||
And the page label shows "Page {2} of {5}"
|
And the page label shows "Page {2} of {5}"
|
||||||
|
|
||||||
Scenario: Continuous mode scrolls target page into view on jump
|
|
||||||
Given the document is open
|
|
||||||
And the Page view mode is set to Continuous
|
|
||||||
When the user jumps to page {5}
|
|
||||||
Then page {5} becomes visible in the scroll area
|
|
||||||
And the left pages overview highlights page {5}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Scenario: Go to clamps out-of-range inputs to valid bounds
|
Scenario: Go to clamps out-of-range inputs to valid bounds
|
||||||
Given the document is open
|
Given the document is open
|
||||||
When the user enters {0} into the Go to input and applies it
|
When the user enters {0} into the Go to input and applies it
|
||||||
|
|
@ -50,3 +34,14 @@ Feature: document browser
|
||||||
Scenario: Go to is disabled when no document is loaded
|
Scenario: Go to is disabled when no document is loaded
|
||||||
Given no document is open
|
Given no document is open
|
||||||
Then the Go to input cannot be used
|
Then the Go to input cannot be used
|
||||||
|
|
||||||
|
Scenario: Open a different document will reset signature placements but keep signature cards
|
||||||
|
Given the document is open
|
||||||
|
When the user opens a different document with {3} pages
|
||||||
|
And {1} signature placements exist on page {1}
|
||||||
|
And {1} signature placements exist on page {2}
|
||||||
|
And {2} signature cards exist
|
||||||
|
Then the first page of the new document is displayed
|
||||||
|
And the page label shows "Page {1} of {3}"
|
||||||
|
And number of signature placements is {0}
|
||||||
|
And {2} signature cards exist
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ class TestWorld {
|
||||||
// Generic flags/values
|
// Generic flags/values
|
||||||
static int? selectedPage;
|
static int? selectedPage;
|
||||||
static int? pendingGoTo; // for simulating typed Go To value across steps
|
static int? pendingGoTo; // for simulating typed Go To value across steps
|
||||||
|
static int?
|
||||||
|
nextDocPageCount; // for BDD: desired page count for the next opened document
|
||||||
|
static Map<int, int>? prevPlacementsCount; // snapshot before an action
|
||||||
|
|
||||||
// Preferences & settings
|
// Preferences & settings
|
||||||
static Map<String, String> prefs = {};
|
static Map<String, String> prefs = {};
|
||||||
|
|
@ -61,6 +64,8 @@ class TestWorld {
|
||||||
nothingToSaveAttempt = false;
|
nothingToSaveAttempt = false;
|
||||||
selectedPage = null;
|
selectedPage = null;
|
||||||
pendingGoTo = null;
|
pendingGoTo = null;
|
||||||
|
nextDocPageCount = null;
|
||||||
|
prevPlacementsCount = null;
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
prefs = {};
|
prefs = {};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: number of signature placements is {0}
|
||||||
|
Future<void> numberOfSignaturePlacementsIs(
|
||||||
|
WidgetTester tester,
|
||||||
|
num param1,
|
||||||
|
) async {
|
||||||
|
final expected = param1.toInt();
|
||||||
|
final c = TestWorld.container ?? ProviderContainer();
|
||||||
|
final doc = c.read(documentRepositoryProvider);
|
||||||
|
final total = doc.placementsByPage.values.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, list) => sum + list.length,
|
||||||
|
);
|
||||||
|
expect(total, expected);
|
||||||
|
// If we had previous placements recorded, ensure they were non-zero to
|
||||||
|
// validate that a reset actually happened when opening a different doc.
|
||||||
|
if (TestWorld.prevPlacementsCount != null &&
|
||||||
|
TestWorld.prevPlacementsCount!.isNotEmpty) {
|
||||||
|
final prevTotal = TestWorld.prevPlacementsCount!.values.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, v) => sum + v,
|
||||||
|
);
|
||||||
|
expect(prevTotal, greaterThan(0));
|
||||||
|
}
|
||||||
|
// Also verify that signature cards still exist (persistence across open).
|
||||||
|
final cards = c.read(signatureCardRepositoryProvider);
|
||||||
|
expect(cards.length, greaterThanOrEqualTo(1));
|
||||||
|
}
|
||||||
|
|
@ -1,14 +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/pdf_view_model.dart';
|
|
||||||
import '_world.dart';
|
|
||||||
|
|
||||||
/// Usage: page {5} becomes visible in the scroll area
|
|
||||||
Future<void> pageBecomesVisibleInTheScrollArea(
|
|
||||||
WidgetTester tester,
|
|
||||||
num param1,
|
|
||||||
) async {
|
|
||||||
final page = param1.toInt();
|
|
||||||
final c = TestWorld.container ?? ProviderContainer();
|
|
||||||
expect(c.read(pdfViewModelProvider).currentPage, page);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: {2} signature cards exist
|
||||||
|
Future<void> signatureCardsExist(WidgetTester tester, num param1) async {
|
||||||
|
final expected = param1.toInt();
|
||||||
|
final c = TestWorld.container ?? ProviderContainer();
|
||||||
|
final cards = c.read(signatureCardRepositoryProvider);
|
||||||
|
expect(cards.length, expected);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: {1} signature placements exist on page {2}
|
||||||
|
Future<void> signaturePlacementsExistOnPage(
|
||||||
|
WidgetTester tester,
|
||||||
|
num param1,
|
||||||
|
num param2,
|
||||||
|
) async {
|
||||||
|
final expected = param1.toInt();
|
||||||
|
final page = param2.toInt();
|
||||||
|
// Record the expectation as part of scenario context instead of asserting
|
||||||
|
// against current state (the scenario describes placements in the previous
|
||||||
|
// document before opening a new one).
|
||||||
|
TestWorld.prevPlacementsCount ??= {};
|
||||||
|
TestWorld.prevPlacementsCount![page] = expected;
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
||||||
import '_world.dart';
|
import '_world.dart';
|
||||||
|
|
||||||
/// Usage: the left pages overview highlights page {5}
|
/// Usage: the first page of the new document is displayed
|
||||||
Future<void> theLeftPagesOverviewHighlightsPage(
|
Future<void> theFirstPageOfTheNewDocumentIsDisplayed(
|
||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
num param1,
|
|
||||||
) async {
|
) async {
|
||||||
final n = param1.toInt();
|
|
||||||
final c = TestWorld.container ?? ProviderContainer();
|
final c = TestWorld.container ?? ProviderContainer();
|
||||||
expect(c.read(pdfViewModelProvider).currentPage, n);
|
expect(c.read(pdfViewModelProvider).currentPage, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import '_world.dart';
|
|
||||||
|
|
||||||
/// Usage: the Page view mode is set to Continuous
|
|
||||||
Future<void> thePageViewModeIsSetToContinuous(WidgetTester tester) async {
|
|
||||||
// Logic-level test: no widget tree; just mark a flag if needed
|
|
||||||
TestWorld.prefs['page_view'] = 'continuous';
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +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/pdf_view_model.dart';
|
|
||||||
import '_world.dart';
|
|
||||||
|
|
||||||
/// Usage: the user jumps to page {2}
|
|
||||||
Future<void> theUserJumpsToPage(WidgetTester tester, num param1) async {
|
|
||||||
final page = param1.toInt();
|
|
||||||
final c = TestWorld.container ?? ProviderContainer();
|
|
||||||
try {
|
|
||||||
c.read(pdfViewModelProvider).jumpToPage(page);
|
|
||||||
} catch (_) {}
|
|
||||||
await tester.pump();
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
||||||
|
import 'package:pdf_signature/domain/models/model.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: the user opens a different document with {3} pages
|
||||||
|
Future<void> theUserOpensADifferentDocumentWithPages(
|
||||||
|
WidgetTester tester,
|
||||||
|
num param1,
|
||||||
|
) async {
|
||||||
|
final pageCount = param1.toInt();
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
|
||||||
|
// Simulate "open a different document": reset placements and set page count.
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openPicked(pageCount: pageCount);
|
||||||
|
// Ensure there are 2 signature cards available as per scenario.
|
||||||
|
final cards = container.read(signatureCardRepositoryProvider);
|
||||||
|
if (cards.length < 2) {
|
||||||
|
final notifier = container.read(signatureCardRepositoryProvider.notifier);
|
||||||
|
while (container.read(signatureCardRepositoryProvider).length < 2) {
|
||||||
|
notifier.add(
|
||||||
|
SignatureCard(
|
||||||
|
asset: SignatureAsset(
|
||||||
|
sigImage: img.Image(width: 1, height: 1),
|
||||||
|
name: 'sig.png',
|
||||||
|
),
|
||||||
|
rotationDeg: 0,
|
||||||
|
graphicAdjust: const GraphicAdjust(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Moving to a new document should show page 1.
|
||||||
|
container.read(pdfViewModelProvider).currentPage = 1;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
@ -1,19 +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/pdf_view_model.dart';
|
|
||||||
import '_world.dart';
|
|
||||||
|
|
||||||
/// Usage: the user types {3} into the Go to input and presses Enter
|
|
||||||
Future<void> theUserTypesIntoTheGoToInputAndPressesEnter(
|
|
||||||
WidgetTester tester,
|
|
||||||
num param1,
|
|
||||||
) async {
|
|
||||||
final target = param1.toInt();
|
|
||||||
final c = TestWorld.container ?? ProviderContainer();
|
|
||||||
TestWorld.container = c;
|
|
||||||
try {
|
|
||||||
c.read(pdfViewModelProvider.notifier).jumpToPage(target);
|
|
||||||
} catch (_) {}
|
|
||||||
await tester.pump();
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue