Compare commits
No commits in common. "9e0ae1dcfe8cf55d9d5e0666706626cabcf0aec7" and "b2bf489af09e367ee52ca0dbc34fa87a50c4c7e9" have entirely different histories.
9e0ae1dcfe
...
b2bf489af0
File diff suppressed because it is too large
Load Diff
|
|
@ -78,23 +78,6 @@ Illustration:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Mobile PDF screen (phone)
|
|
||||||
|
|
||||||
Purpose: compact PDF viewing and signature placement on small screens.
|
|
||||||
Route: root --> opened (mobile)
|
|
||||||
|
|
||||||
Design notes:
|
|
||||||
- Top app bar with: menu (page thumbnails), title with current page and total pages (prev/next).
|
|
||||||
- Center viewport supports scroll and pinch-zoom.
|
|
||||||
- Bottom sheet button to show Signatures drawers.
|
|
||||||
- bottom drawer to add/drag a signature onto the page.
|
|
||||||
|
|
||||||
Illustration:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to view and export
|
## How to view and export
|
||||||
|
|
||||||
We keep links in this file pointing to `.excalidraw`. To preview the SVGs and generate `docs/.wireframe.md` with `.svg` links, run from repo root:
|
We keep links in this file pointing to `.excalidraw`. To preview the SVGs and generate `docs/.wireframe.md` with `.svg` links, run from repo root:
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -182,7 +182,6 @@ 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,7 +6,6 @@ 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;
|
||||||
|
|
@ -105,14 +104,15 @@ class ExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize engine (safe to call multiple times)
|
// Initialize engine (safe to call multiple times)
|
||||||
pdfrxFlutterInitialize();
|
try {
|
||||||
|
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,9 +120,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -223,7 +221,6 @@ class ExportService {
|
||||||
|
|
||||||
final bytes = await out.save();
|
final bytes = await out.save();
|
||||||
doc.dispose();
|
doc.dispose();
|
||||||
debugPrint('exportSignedPdfFromBytes succeeded');
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,7 +233,6 @@ 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,9 +325,7 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
ref
|
ref
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: pageCount, bytes: bytes);
|
.openPicked(pageCount: pageCount, bytes: bytes);
|
||||||
// Keep existing signature cards when opening a new document.
|
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
||||||
// 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,8 +221,6 @@ 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) {
|
||||||
|
|
@ -237,6 +235,7 @@ 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(
|
||||||
|
|
@ -313,17 +312,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
max: _pagesMax,
|
max: _pagesMax,
|
||||||
builder:
|
builder:
|
||||||
(context, area) => Offstage(
|
(context, area) => Offstage(
|
||||||
offstage: () {
|
offstage:
|
||||||
try {
|
!(ResponsiveBreakpoints.of(context).largerThan(MOBILE) &&
|
||||||
return !(ResponsiveBreakpoints.of(
|
_showPagesSidebar),
|
||||||
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);
|
||||||
|
|
@ -473,13 +464,6 @@ 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),
|
||||||
|
|
@ -540,28 +524,6 @@ 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,14 +68,9 @@ 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;
|
||||||
// Be defensive in tests that don't provide ResponsiveBreakpoints
|
final bool isLargerThanMobile = ResponsiveBreakpoints.of(
|
||||||
final bool isLargerThanMobile = () {
|
context,
|
||||||
try {
|
).largerThan(MOBILE);
|
||||||
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';
|
||||||
|
|
@ -147,7 +142,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isLargerThanMobile)
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
|
|
@ -181,7 +176,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
if (isLargerThanMobile) ...[
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
|
@ -217,7 +212,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (isLargerThanMobile) ...[
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
|
||||||
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,17 +1,11 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
// On modern Flutter Web (Wasm GC, e.g., Chromium), dart:html is not available.
|
import 'download_stub.dart' if (dart.library.html) 'download_web.dart' as impl;
|
||||||
// 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,9 +1,6 @@
|
||||||
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,28 +1,23 @@
|
||||||
// Implementation for Web using package:web to support Wasm GC (Chromium)
|
// ignore_for_file: deprecated_member_use
|
||||||
// without importing dart:html directly.
|
// ignore: avoid_web_libraries_in_flutter
|
||||||
import 'dart:convert';
|
import 'dart:html' as html;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'dart:typed_data';
|
||||||
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 {
|
||||||
// Use a data URL to avoid Blob/typed array interop issues under Wasm GC.
|
final blob = html.Blob([bytes], 'application/pdf');
|
||||||
final url = 'data:application/pdf;base64,${base64Encode(bytes)}';
|
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||||
|
|
||||||
// Create an anchor element and trigger a click to download
|
|
||||||
final anchor =
|
final anchor =
|
||||||
web.HTMLAnchorElement()
|
html.document.createElement('a') as html.AnchorElement
|
||||||
..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 (e, st) {
|
} catch (_) {
|
||||||
debugPrint('Error: downloadBytes failed: $e\n$st');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ 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,6 +9,13 @@ 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
|
||||||
|
|
@ -22,6 +29,15 @@ 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
|
||||||
|
|
@ -34,14 +50,3 @@ 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,9 +36,6 @@ 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 = {};
|
||||||
|
|
@ -64,8 +61,6 @@ class TestWorld {
|
||||||
nothingToSaveAttempt = false;
|
nothingToSaveAttempt = false;
|
||||||
selectedPage = null;
|
selectedPage = null;
|
||||||
pendingGoTo = null;
|
pendingGoTo = null;
|
||||||
nextDocPageCount = null;
|
|
||||||
prevPlacementsCount = null;
|
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
prefs = {};
|
prefs = {};
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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,10 +3,12 @@ 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 first page of the new document is displayed
|
/// Usage: the left pages overview highlights page {5}
|
||||||
Future<void> theFirstPageOfTheNewDocumentIsDisplayed(
|
Future<void> theLeftPagesOverviewHighlightsPage(
|
||||||
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, 1);
|
expect(c.read(pdfViewModelProvider).currentPage, n);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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