Compare commits

..

No commits in common. "c1b7824cbd13509bac89fa09e531f391e8f60ee4" and "fba880e1be64ca205371bd2cc388b17b4a606cc9" have entirely different histories.

158 changed files with 1001 additions and 1546 deletions

2
.gitignore vendored
View File

@ -135,5 +135,3 @@ AppDir/bundle/
appimage-build/
/*.AppImage
.vscode/settings.json
*.patch

View File

@ -6,7 +6,7 @@ Additionally read relevant files depends on task.
* If want to modify use cases (files at `test/features/*.feature`)
* read [`FRs.md`](docs/FRs.md)
* If want to modify code (implement or test) of `ViewModel`, `View` of MVVM (UI widget) (files at `lib/ui/features/*/widgets/*`)
* If want to modify code (implement or test) of `View` of MVVM (UI widget) (files at `lib/ui/features/*/widgets/*`)
* read [`wireframe.md`](docs/wireframe.md), [`NFRs.md`](docs/NFRs.md), `test/features/*.feature`
* If want to modify code (implement or test) of non-View e.g. `Model`, services...
* If want to modify code (implement or test) of non-View e.g. `Model`, `View Model`, services...
* read `test/features/*.feature`, [`NFRs.md`](docs/NFRs.md)

View File

@ -2,27 +2,25 @@
## user stories
The following user stories may not use formal terminology as [meta-arch.md](./meta-arch.md) and use cases(`test/*.feature`), but use oral descriptions.
* name: [PDF browser](../test/features/pdf_browser.feature)
* role: user
* functionality: view and navigate PDF documents
* benefit: select page to add signature
* name: [load signature](../test/features/load_signature.feature)
* name: [load signature picture](../test/features/load_signature_picture.feature)
* role: user
* functionality: load a signature asset file and create a signature card
* functionality: load a signature picture file
* benefit: easily add signature to PDF
* name: [geometrically adjust signature picture](../test/features/geometrically_adjust_signature_picture.feature)
* role: user
* functionality: adjust the scale, rotation and position of the signature placement on the PDF page
* functionality: adjust the size and position of the signature picture
* benefit: ensure the signature fits well on the PDF page
* name: [graphically adjust signature picture](../test/features/graphically_adjust_signature_picture.feature)
* role: user
* functionality: background removal, contrast adjustment... to enhance the appearance of the signature asset within the signature card
* functionality: background removal, contrast adjustment...
* benefit: easily improve the appearance of the signature on the PDF without additional software.
* name: [draw signature](../test/features/draw_signature.feature)
* role: user
* functionality: draw a signature asset using mouse or touch input
* functionality: draw a signature using mouse or touch input
* benefit: create a custom signature directly on the PDF if no pre-made signature is available.
* name: [save signed PDF](../test/features/save_signed_pdf.feature)
* role: user
@ -30,7 +28,7 @@ The following user stories may not use formal terminology as [meta-arch.md](./me
* benefit: easily keep a copy of the signed document for records.
* name: [preferences for app](../test/features/app_preferences.feature)
* role: user
* functionality: configure app preferences such as `language`, `theme`, `theme-color`.
* functionality: configure app preferences such as `theme`, `language`.
* benefit: customize the app experience to better fit user needs
* name: [remember preferences](../test/features/remember_preferences.feature)
* role: user

View File

@ -1,9 +1,6 @@
# meta archietecture
* [MVVM](https://docs.flutter.dev/app-architecture/guide)
* [Data layer](https://docs.flutter.dev/app-architecture/case-study/data-layer)
* View ⇆ ViewModel ⇆ Repository ⇆ Service
* Model is used across.
## Package structure
@ -14,24 +11,6 @@ The repo structure follows official [Package structure](https://docs.flutter.dev
* `test/widget/` contains UI widget(component) tests which focus on `View` from MVVM of each component.
* `integration_test/` for integration tests. They should be volatile to follow UI layout changes.
Some rule of thumb:
* `<object>Provider` only placed at `/lib/data/repositories/` or `/lib/data/services/` to provide data source.
## Abstraction
### terminology
* signature asset
* image file of a signature, stored in the device or cloud storage
* can drawing from canvas
* signature card
* template of signature placement
* It will include modifications such as brightness, contrast, background removal, rotation of the signature asset.
* signature placement
* placed modified signature asset from signature card on a specific position on a specific page of a specific PDF document
* document
* PDF document to be signed
## key dependencies
* [pdfrx](https://pub.dev/packages/pdfrx)

View File

@ -7,9 +7,9 @@ import 'package:image/image.dart' as img;
import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_library.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
@ -122,11 +122,11 @@ void main() {
final sigState = container.read(signatureProvider);
final r = sigState.rect!;
final lib = container.read(signatureLibraryProvider);
final asset = lib.isNotEmpty ? lib.first : null;
final imageId = lib.isNotEmpty ? lib.first.id : 'default.png';
final pdf = container.read(pdfProvider);
container
.read(pdfProvider.notifier)
.addPlacement(page: pdf.currentPage, rect: r, asset: asset);
.addPlacement(page: pdf.currentPage, rect: r, imageId: imageId);
container.read(signatureProvider.notifier).clearActiveOverlay();
await tester.pumpAndSettle();

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
import 'data/services/preferences_providers.dart';
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';

View File

@ -1,91 +1,32 @@
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
/// A simple library of signature images available to the user in the sidebar.
class SignatureAsset {
final String id; // unique id
final Uint8List bytes;
// List<List<Offset>>? strokes;
final String? name; // optional display name (e.g., filename)
const SignatureAsset({required this.id, required this.bytes, this.name});
}
class GraphicAdjust {
final double contrast;
final double brightness;
final bool bgRemoval;
const GraphicAdjust({
this.contrast = 1.0,
this.brightness = 0.0,
this.bgRemoval = false,
});
GraphicAdjust copyWith({
double? contrast,
double? brightness,
bool? bgRemoval,
}) => GraphicAdjust(
contrast: contrast ?? this.contrast,
brightness: brightness ?? this.brightness,
bgRemoval: bgRemoval ?? this.bgRemoval,
);
}
/**
* signature card is template of signature placement
*/
class SignatureCard {
final double rotationDeg;
final SignatureAsset asset;
final GraphicAdjust graphicAdjust;
const SignatureCard({
required this.rotationDeg,
required this.asset,
this.graphicAdjust = const GraphicAdjust(),
});
SignatureCard copyWith({
double? rotationDeg,
SignatureAsset? asset,
GraphicAdjust? graphicAdjust,
}) => SignatureCard(
rotationDeg: rotationDeg ?? this.rotationDeg,
asset: asset ?? this.asset,
graphicAdjust: graphicAdjust ?? this.graphicAdjust,
);
}
/// Represents a single signature placement on a page combining both the
/// geometric rectangle (UI coordinate space) and the signature asset
/// assigned to that placement.
/// geometric rectangle (UI coordinate space) and the identifier of the
/// image/signature asset assigned to that placement.
class SignaturePlacement {
// The bounding box of this placement in UI coordinate space, implies scaling and position.
final Rect rect;
/// Rotation in degrees to apply when rendering/exporting this placement.
final double rotationDeg;
final GraphicAdjust graphicAdjust;
final SignatureAsset asset;
/// Identifier of the image (e.g., filename / asset id) assigned to this placement.
/// Nullable to allow a placement reserved before an image is chosen.
final String? imageId;
const SignaturePlacement({
required this.rect,
required this.asset,
this.imageId,
this.rotationDeg = 0.0,
this.graphicAdjust = const GraphicAdjust(),
});
SignaturePlacement copyWith({
Rect? rect,
SignatureAsset? asset,
String? imageId,
double? rotationDeg,
GraphicAdjust? graphicAdjust,
}) => SignaturePlacement(
rect: rect ?? this.rect,
asset: asset ?? this.asset,
imageId: imageId ?? this.imageId,
rotationDeg: rotationDeg ?? this.rotationDeg,
graphicAdjust: graphicAdjust ?? this.graphicAdjust,
);
}
@ -96,7 +37,7 @@ class PdfState {
final String? pickedPdfPath;
final Uint8List? pickedPdfBytes;
final int? signedPage;
// Multiple signature placements per page, each combines geometry and asset.
// Multiple signature placements per page, each combines geometry and optional image id.
final Map<int, List<SignaturePlacement>> placementsByPage;
// UI state: selected placement index on the current page (if any)
final int? selectedPlacementIndex;
@ -151,8 +92,8 @@ class SignatureState {
final double rotation;
final List<List<Offset>> strokes;
final Uint8List? imageBytes;
// The signature asset the current overlay is based on (from library)
final SignatureAsset? asset;
// The ID of the signature asset the current overlay is based on (from library)
final String? assetId;
// When true, the active signature overlay is movable/resizable and should not be exported.
// When false, the overlay is confirmed (unmovable) and eligible for export.
final bool editingEnabled;
@ -165,7 +106,7 @@ class SignatureState {
this.rotation = 0.0,
required this.strokes,
this.imageBytes,
this.asset,
this.assetId,
this.editingEnabled = false,
});
factory SignatureState.initial() => const SignatureState(
@ -177,7 +118,7 @@ class SignatureState {
rotation: 0.0,
strokes: [],
imageBytes: null,
asset: null,
assetId: null,
editingEnabled: false,
);
SignatureState copyWith({
@ -189,7 +130,7 @@ class SignatureState {
double? rotation,
List<List<Offset>>? strokes,
Uint8List? imageBytes,
SignatureAsset? asset,
String? assetId,
bool? editingEnabled,
}) => SignatureState(
rect: rect ?? this.rect,
@ -200,7 +141,7 @@ class SignatureState {
rotation: rotation ?? this.rotation,
strokes: strokes ?? this.strokes,
imageBytes: imageBytes ?? this.imageBytes,
asset: asset ?? this.asset,
assetId: assetId ?? this.assetId,
editingEnabled: editingEnabled ?? this.editingEnabled,
);
}

View File

@ -148,8 +148,8 @@ class ExportService {
final w = r.width / uiPageSize.width * widthPts;
final h = r.height / uiPageSize.height * heightPts;
Uint8List? bytes;
final id = placement.asset.id;
if (id.isNotEmpty) {
final id = placement.imageId;
if (id != null) {
bytes = libraryBytes?[id];
}
bytes ??= signatureImageBytes; // fallback
@ -275,8 +275,8 @@ class ExportService {
final w = r.width / uiPageSize.width * widthPts;
final h = r.height / uiPageSize.height * heightPts;
Uint8List? bytes;
final id = placement.asset.id;
if (id.isNotEmpty) {
final id = placement.imageId;
if (id != null) {
bytes = libraryBytes?[id];
}
bytes ??= signatureImageBytes; // fallback

View File

@ -65,7 +65,7 @@ class PdfController extends StateNotifier<PdfState> {
void addPlacement({
required int page,
required Rect rect,
SignatureAsset? asset,
String? imageId = 'default.png',
double rotationDeg = 0.0,
}) {
if (!state.loaded) return;
@ -75,7 +75,7 @@ class PdfController extends StateNotifier<PdfState> {
list.add(
SignaturePlacement(
rect: rect,
asset: asset ?? SignatureAsset(id: '', bytes: Uint8List(0)),
imageId: imageId,
rotationDeg: rotationDeg,
),
);
@ -165,11 +165,11 @@ class PdfController extends StateNotifier<PdfState> {
// NOTE: Programmatic reassignment of images has been removed.
// Convenience to get asset for a placement
SignatureAsset? assetOfPlacement({required int page, required int index}) {
// Convenience to get image name for a placement
String? imageOfPlacement({required int page, required int index}) {
final list = state.placementsByPage[page] ?? const [];
if (index < 0 || index >= list.length) return null;
return list[index].asset;
return list[index].imageId;
}
}

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import '../../signature/view_model/signature_controller.dart';
class AdjustmentsPanel extends ConsumerWidget {
const AdjustmentsPanel({super.key, required this.sig});

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import 'adjustments_panel.dart';
import '../../signature/widgets/rotated_signature_image.dart';

View File

@ -4,8 +4,8 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdfrx/pdfrx.dart';
import '../../../../data/services/export_providers.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import '../../signature/widgets/signature_drag_data.dart';
import 'pdf_mock_continuous_list.dart';
import 'pdf_page_overlays.dart';
@ -340,11 +340,11 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
final cx = (local.dx / size.width) * widget.pageSize.width;
final cy = (local.dy / size.height) * widget.pageSize.height;
final data = details.data;
if (data is SignatureDragData && data.asset != null) {
if (data is SignatureDragData && data.assetId != null) {
// Set current overlay to use this asset
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(asset: data.asset!);
.setImageFromLibrary(assetId: data.assetId!);
}
ref.read(signatureProvider.notifier).placeAtCenter(Offset(cx, cy));
ref

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import '../../../../data/model/model.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '../view_model/pdf_controller.dart';
import 'signature_overlay.dart';
/// Builds all overlays for a given page: placed signatures and the active one.

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
import '../../../../data/services/export_providers.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '../view_model/pdf_controller.dart';
class PdfPagesOverview extends ConsumerWidget {
const PdfPagesOverview({super.key});

View File

@ -10,9 +10,9 @@ import 'package:multi_split_view/multi_split_view.dart';
import '../../../../data/services/export_providers.dart';
import 'package:image/image.dart' as img;
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import '../../signature/view_model/signature_library.dart';
import 'draw_canvas.dart';
import 'pdf_toolbar.dart';
import 'pdf_page_area.dart';

View File

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '../view_model/pdf_controller.dart';
class PdfToolbar extends ConsumerStatefulWidget {
const PdfToolbar({

View File

@ -2,11 +2,10 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/model/model.dart' as model;
import '../../../../data/services/export_providers.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import '../../signature/view_model/signature_library.dart';
import 'image_editor_dialog.dart';
import '../../signature/widgets/signature_card.dart';
@ -53,14 +52,14 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
child: SignatureCard(
key: ValueKey('sig_card_${a.id}'),
asset:
(sig.asset?.id == a.id)
? model.SignatureAsset(
(sig.assetId == a.id)
? SignatureAsset(
id: a.id,
bytes: (processed ?? a.bytes),
name: a.name,
)
: a,
rotationDeg: (sig.asset?.id == a.id) ? sig.rotation : 0.0,
rotationDeg: (sig.assetId == a.id) ? sig.rotation : 0.0,
disabled: disabled,
onDelete:
() => ref
@ -69,7 +68,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
onAdjust: () async {
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(asset: a);
.setImageFromLibrary(assetId: a.id);
if (!mounted) return;
await showDialog(
context: context,
@ -80,7 +79,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
// Never reassign placed signatures via tap; only set active overlay source
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(asset: a);
.setImageFromLibrary(assetId: a.id);
},
),
),
@ -97,11 +96,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
bytes == null
? Text(l.noSignatureLoaded)
: SignatureCard(
asset: model.SignatureAsset(
id: '',
bytes: bytes,
name: '',
),
asset: SignatureAsset(id: '', bytes: bytes, name: ''),
rotationDeg: sig.rotation,
disabled: disabled,
useCurrentBytesForDrag: true,
@ -153,14 +148,9 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
final id = ref
.read(signatureLibraryProvider.notifier)
.add(b, name: 'image');
final asset = ref
.read(signatureLibraryProvider.notifier)
.byId(id);
if (asset != null) {
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(asset: asset);
}
.setImageFromLibrary(assetId: id);
}
},
icon: const Icon(Icons.image_outlined),
@ -181,14 +171,9 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
final id = ref
.read(signatureLibraryProvider.notifier)
.add(b, name: 'drawing');
final asset = ref
.read(signatureLibraryProvider.notifier)
.byId(id);
if (asset != null) {
ref
.read(signatureProvider.notifier)
.setImageFromLibrary(asset: asset);
}
.setImageFromLibrary(assetId: id);
}
},
icon: const Icon(Icons.gesture),

View File

@ -5,9 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import '../../signature/view_model/signature_library.dart';
import 'image_editor_dialog.dart';
import '../../signature/widgets/rotated_signature_image.dart';
@ -245,8 +245,8 @@ class _SignatureImage extends ConsumerWidget {
(placementList != null && placedIndex! < placementList.length)
? placementList[placedIndex!]
: null;
final imgId = (placement?.asset)?.id;
if (imgId != null && imgId.isNotEmpty) {
final imgId = placement?.imageId;
if (imgId != null) {
final lib = ref.watch(signatureLibraryProvider);
for (final a in lib) {
if (a.id == imgId) {

View File

@ -7,7 +7,8 @@ import 'package:image/image.dart' as img;
import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart';
import 'pdf_repository.dart';
import '../../pdf/view_model/pdf_controller.dart';
import 'signature_library.dart';
class SignatureController extends StateNotifier<SignatureState> {
SignatureController() : super(SignatureState.initial());
@ -138,7 +139,7 @@ class SignatureController extends StateNotifier<SignatureState> {
}
void setImageBytes(Uint8List bytes) {
state = state.copyWith(imageBytes: bytes, asset: null);
state = state.copyWith(imageBytes: bytes, assetId: null);
if (state.rect == null) {
placeDefaultRect();
}
@ -147,8 +148,8 @@ class SignatureController extends StateNotifier<SignatureState> {
}
// Select image from the shared signature library
void setImageFromLibrary({required SignatureAsset asset}) {
state = state.copyWith(asset: asset);
void setImageFromLibrary({required String assetId}) {
state = state.copyWith(assetId: assetId);
if (state.rect == null) {
placeDefaultRect();
}
@ -176,17 +177,18 @@ class SignatureController extends StateNotifier<SignatureState> {
if (!pdf.loaded) return null;
// Bind the processed image at placement time (so placed preview matches adjustments).
// If processed bytes exist, always create a new asset for this placement.
// Prefer reusing an existing library asset when the active overlay is
// Prefer reusing an existing library asset id when the active overlay is
// based on a library item. If there is no library asset, do NOT create
// a new library card here keep the placement's asset empty so the
// a new library card here keep the placement's image id empty so the
// UI and exporter will fall back to using the processed/current bytes.
String id = state.assetId ?? '';
// Store as UI-space rect (consistent with export and rendering paths)
ref
.read(pdfProvider.notifier)
.addPlacement(
page: pdf.currentPage,
rect: r,
asset: state.asset,
imageId: id,
rotationDeg: state.rotation,
);
// Newly placed index is the last one on the page
@ -210,14 +212,15 @@ class SignatureController extends StateNotifier<SignatureState> {
if (r == null) return null;
final pdf = container.read(pdfProvider);
if (!pdf.loaded) return null;
// Reuse existing library asset if present; otherwise leave empty so the
// Reuse existing library id if present; otherwise leave empty so the
// placement will reference the current bytes via fallback paths.
String id = state.assetId ?? '';
container
.read(pdfProvider.notifier)
.addPlacement(
page: pdf.currentPage,
rect: r,
asset: state.asset,
imageId: id,
rotationDeg: state.rotation,
);
final idx =
@ -227,11 +230,9 @@ class SignatureController extends StateNotifier<SignatureState> {
?.length ??
1) -
1;
// Auto-select the newly placed item so the red box appears
if (idx >= 0) {
container.read(pdfProvider.notifier).selectPlacement(idx);
}
// Freeze editing: keep rect for preview but disable interaction
state = state.copyWith(editingEnabled: false);
return r;
}
@ -252,9 +253,7 @@ final signatureProvider =
/// Returns null if no image is loaded. The output is a PNG to preserve alpha.
final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
// Watch only the fields that affect pixel processing to avoid recompute on rotation.
final SignatureAsset? asset = ref.watch(
signatureProvider.select((s) => s.asset),
);
final String? assetId = ref.watch(signatureProvider.select((s) => s.assetId));
final Uint8List? directBytes = ref.watch(
signatureProvider.select((s) => s.imageBytes),
);
@ -270,8 +269,14 @@ final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
// If active overlay is based on a library asset, pull its bytes
Uint8List? bytes;
if (asset != null) {
bytes = asset.bytes;
if (assetId != null) {
final lib = ref.watch(signatureLibraryProvider);
for (final a in lib) {
if (a.id == assetId) {
bytes = a.bytes;
break;
}
}
} else {
bytes = directBytes;
}

View File

@ -1,6 +1,13 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/model/model.dart';
/// A simple library of signature images available to the user in the sidebar.
class SignatureAsset {
final String id; // unique id
final Uint8List bytes;
final String? name; // optional display name (e.g., filename)
const SignatureAsset({required this.id, required this.bytes, this.name});
}
class SignatureLibraryController extends StateNotifier<List<SignatureAsset>> {
SignatureLibraryController() : super(const []);

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:pdf_signature/data/model/model.dart';
import '../view_model/signature_library.dart';
import 'signature_drag_data.dart';
import 'rotated_signature_image.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
@ -142,7 +142,7 @@ class SignatureCard extends StatelessWidget {
data:
useCurrentBytesForDrag
? const SignatureDragData()
: SignatureDragData(asset: asset),
: SignatureDragData(assetId: asset.id),
feedback: Opacity(
opacity: 0.9,
child: ConstrainedBox(

View File

@ -1,6 +1,4 @@
import 'package:pdf_signature/data/model/model.dart';
class SignatureDragData {
final SignatureAsset? asset; // null means use current processed signature
const SignatureDragData({this.asset});
final String? assetId; // null means use current processed signature
const SignatureDragData({this.assetId});
}

View File

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '../../signature/view_model/signature_controller.dart';
import '../../pdf/view_model/pdf_controller.dart';
// Settings dialog is provided via global AppBar in MyApp
// Abstraction to make drop handling testable without constructing

View File

@ -1,10 +1,10 @@
Feature: draw signature asset
Feature: draw signature
Scenario: Draw with mouse or touch and place on page
Given an empty signature canvas
When the user draws strokes and confirms
Then a signature asset is created
And signature placement occurs on the selected page
Then a signature image is created
And it is placed on the selected page
Scenario: Clear and redraw
Given a drawn signature exists in the canvas

View File

@ -1,13 +1,12 @@
Feature: geometrically adjust signature asset
Feature: geometrically adjust signature picture
Scenario: Resize and move the signature within page bounds
Given a signature asset is placed on the page
Given a signature image is placed on the page
When the user drags handles to resize and drags to reposition
Then the size and position update in real time
And the signature placement remains within the page area
And the signature remains within the page area
Scenario: Rotate the signature
Given a signature asset is placed on the page
When the user uses rotate controls
Then the signature placement rotates around its center in real time
And resize to fit within bounding box
Scenario: Lock aspect ratio while resizing
Given a signature image is selected
When the user enables aspect ratio lock and resizes
Then the image scales proportionally

View File

@ -1,13 +1,13 @@
Feature: graphically adjust signature asset
Feature: graphically adjust signature picture
Scenario: Remove background
Given a signature asset is selected
Given a signature image is selected
When the user enables background removal
Then near-white background becomes transparent in the preview
And the user can apply the change
Scenario: Adjust contrast and brightness
Given a signature asset is selected
Given a signature image is selected
When the user changes contrast and brightness controls
Then the preview updates immediately
And the user can apply or reset adjustments

View File

@ -1,27 +0,0 @@
Feature: load signature asset
Scenario Outline: Handle invalid or unsupported files
Given the user selects "<file>"
When the app attempts to load the asset
Then the user is notified of the issue
And the asset is not added to the document
Examples:
| file |
| 'corrupted.png' |
| 'signature.bmp' |
| 'empty.jpg' |
Scenario: Import a signature asset
When the user chooses a image file as a signature asset
Then the asset is loaded and shown as a signature asset
Scenario: Import a signature card
When the user chooses a signature asset to created a signature card
Then the asset is loaded and shown as a signature card
Scenario: Import a signature placement
Given a created signature card
When the user drags this signature card on the page of the document to place a signature placement
Then a signature placement appears on the page based on the signature card

View File

@ -0,0 +1,18 @@
Feature: load signature picture
Scenario: Import a signature image
Given a PDF page is selected for signing
When the user chooses a signature image file
Then the image is loaded and shown as a signature asset
Scenario Outline: Handle invalid or unsupported files
Given the user selects "<file>"
When the app attempts to load the image
Then the user is notified of the issue
And the image is not added to the document
Examples:
| file |
| 'corrupted.png' |
| 'signature.bmp' |
| 'empty.jpg' |

View File

@ -1,9 +1,9 @@
Feature: document browser
Feature: PDF browser
Background:
Given a sample multi-page document (5 pages) is available
Given a sample multi-page PDF (5 pages) is available
Scenario: Open a document and navigate pages
Scenario: Open a PDF and navigate pages
When the user opens the document
Then the first page is displayed
And the user can move to the next or previous page
@ -47,6 +47,6 @@ Feature: document browser
Then the last page is displayed (page {5})
And the page label shows "Page {5} of {5}"
Scenario: Go to is disabled when no document is loaded
Scenario: Go to is disabled when no PDF is loaded
Given no document is open
Then the Go to input cannot be used

View File

@ -1,26 +1,26 @@
Feature: save signed document
Feature: save signed PDF
Scenario: Export the signed document to a new file
Given a document is open and contains at least one signature placement
Given a PDF is open and contains at least one placed signature
When the user saves/exports the document
Then a new document file is saved at specified full path, location and file name
And the signature placements appear on the corresponding page in the output
Then a new PDF file is saved at specified full path, location and file name
And the signatures appear on the corresponding page in the output
And keep other unchanged content(pages) intact in the output
Scenario: Vector-accurate stamping into PDF page coordinates
Given a signature placement is placed with a position and size relative to the page
Given a signature is placed with a position and size relative to the page
When the user saves/exports the document
Then the signature placement is stamped at the exact PDF page coordinates and size
Then the signature is stamped at the exact PDF page coordinates and size
And the stamp remains crisp at any zoom level (not rasterized by the screen)
And other page content remains vector and unaltered
Scenario: Prevent saving when nothing is placed
Given a document is open with no signature placements placed
Given a PDF is open with no signatures placed
When the user attempts to save
Then the user is notified there is nothing to save
Scenario: Loading sign when exporting/saving files
Given a signature placement is placed with a position and size relative to the page
Given a signature is placed with a position and size relative to the page
When the user starts exporting the document
And the export process is not yet finished
Then the user is notified that the export is still in progress

View File

@ -1,19 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a created signature card
Future<void> aCreatedSignatureCard(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Create a dummy signature asset
final asset = SignatureAsset(
id: 'test_card',
bytes: Uint8List(100),
name: 'Test Card',
);
container.read(signatureLibraryProvider.notifier).state = [asset];
}

View File

@ -1,25 +0,0 @@
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:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a document is open and contains at least one signature placement
Future<void> aDocumentIsOpenAndContainsAtLeastOneSignaturePlacement(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'test.pdf', pageCount: 5);
container
.read(pdfProvider.notifier)
.addPlacement(
page: 1,
rect: Rect.fromLTWH(10, 10, 100, 50),
asset: SignatureAsset(id: 'sig.png', bytes: Uint8List(0)),
);
}

View File

@ -1,40 +0,0 @@
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:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a document is open and contains multiple placed signature placements across pages
Future<void>
aDocumentIsOpenAndContainsMultiplePlacedSignaturePlacementsAcrossPages(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'multi.pdf', pageCount: 5);
container
.read(pdfProvider.notifier)
.addPlacement(
page: 1,
rect: Rect.fromLTWH(10, 10, 100, 50),
asset: SignatureAsset(id: 'sig1.png', bytes: Uint8List(0)),
);
container
.read(pdfProvider.notifier)
.addPlacement(
page: 2,
rect: Rect.fromLTWH(20, 20, 100, 50),
asset: SignatureAsset(id: 'sig2.png', bytes: Uint8List(0)),
);
container
.read(pdfProvider.notifier)
.addPlacement(
page: 3,
rect: Rect.fromLTWH(30, 30, 100, 50),
asset: SignatureAsset(id: 'sig3.png', bytes: Uint8List(0)),
);
}

View File

@ -1,16 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: a document is open with no signature placements placed
Future<void> aDocumentIsOpenWithNoSignaturePlacementsPlaced(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'empty.pdf', pageCount: 5);
// No placements added
}

View File

@ -1,12 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: a document page is selected for signing
Future<void> aDocumentPageIsSelectedForSigning(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container.read(pdfProvider.notifier).setSignedPage(1);
container.read(pdfProvider.notifier).jumpTo(1);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: a drawn signature exists in the canvas

View File

@ -1,19 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a multi-page document is open
Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container.read(signatureLibraryProvider.notifier).state = [];
container.read(pdfProvider.notifier).state = PdfState.initial();
container.read(signatureProvider.notifier).state = SignatureState.initial();
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
}

View File

@ -1,15 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a sample multi-page document (5 pages) is available
Future<void> aSampleMultipageDocument5PagesIsAvailable(
WidgetTester tester,
) async {
/// Usage: a multi-page PDF is open
Future<void> aMultipagePdfIsOpen(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'sample.pdf', pageCount: 5);
.openPicked(path: 'sample.pdf', pageCount: 10);
}

View File

@ -1,24 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: a new document file is saved at specified full path, location and file name
Future<void> aNewDocumentFileIsSavedAtSpecifiedFullPathLocationAndFileName(
WidgetTester tester,
) async {
// Verify that export bytes were generated
expect(
TestWorld.lastExportBytes,
isNotNull,
reason: 'Export bytes should be generated after save',
);
// Simulate a saved path (in a real implementation this would come from file picker)
TestWorld.lastSavedPath =
TestWorld.lastSavedPath ?? '/tmp/signed_document.pdf';
expect(
TestWorld.lastSavedPath,
isNotNull,
reason: 'A save path should be specified',
);
}

View File

@ -0,0 +1,15 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: a new PDF file is saved at specified full path, location and file name
Future<void> aNewPdfFileIsSavedAtSpecifiedFullPathLocationAndFileName(
WidgetTester tester,
) async {
if (TestWorld.lastSavedPath != null) {
expect(File(TestWorld.lastSavedPath!).existsSync(), isTrue);
} else {
expect(TestWorld.lastExportBytes, isNotNull);
expect(TestWorld.lastExportBytes!.isNotEmpty, isTrue);
}
}

View File

@ -0,0 +1,26 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a PDF is open and contains at least one placed signature
Future<void> aPdfIsOpenAndContainsAtLeastOnePlacedSignature(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(
path: 'mock.pdf',
pageCount: 2,
bytes: Uint8List.fromList([1, 2, 3]),
);
container.read(pdfProvider.notifier).setSignedPage(1);
container.read(signatureProvider.notifier).placeDefaultRect();
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}

View File

@ -0,0 +1,32 @@
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/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a PDF is open and contains multiple placed signatures across pages
Future<void> aPdfIsOpenAndContainsMultiplePlacedSignaturesAcrossPages(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 6);
// Ensure signature image exists
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
// Place on two pages
container
.read(pdfProvider.notifier)
.addPlacement(page: 1, rect: const Rect.fromLTWH(10, 10, 80, 40));
container
.read(pdfProvider.notifier)
.addPlacement(page: 4, rect: const Rect.fromLTWH(120, 200, 100, 50));
// Keep backward compatibility with existing export step expectations
container.read(pdfProvider.notifier).setSignedPage(1);
container.read(signatureProvider.notifier).placeDefaultRect();
}

View File

@ -0,0 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a PDF is open with no signatures placed
Future<void> aPdfIsOpenWithNoSignaturesPlaced(WidgetTester tester) async {
// Fresh world for this scenario to avoid leftover rect/image from previous tests
TestWorld.reset();
final container = ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 1);
container.read(signatureProvider.notifier).resetForNewPage();
}

View File

@ -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_controller.dart';
import '_world.dart';
/// Usage: a PDF page is selected for signing
Future<void> aPdfPageIsSelectedForSigning(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 1);
container.read(pdfProvider.notifier).setSignedPage(1);
}

View File

@ -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_controller.dart';
import '_world.dart';
/// Usage: a sample multi-page PDF (5 pages) is available
Future<void> aSampleMultipagePdf5PagesIsAvailable(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Open a mock document with 5 pages
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
}

View File

@ -1,39 +0,0 @@
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:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature asset is created
Future<void> aSignatureAssetIsCreated(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Ensure PDF is open
if (!container.read(pdfProvider).loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
}
// Create a dummy signature asset
final asset = SignatureAsset(
id: 'test_asset',
bytes: Uint8List(100),
name: 'Test Asset',
);
container.read(signatureLibraryProvider.notifier).state = [asset];
// Place it on the current page
final pdf = container.read(pdfProvider);
container
.read(pdfProvider.notifier)
.addPlacement(
page: pdf.currentPage,
rect: Rect.fromLTWH(50, 50, 100, 50),
asset: asset,
);
}

View File

@ -1,21 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature asset is loaded or drawn
Future<void> aSignatureAssetIsLoadedOrDrawn(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container.read(signatureLibraryProvider.notifier).state = [];
container.read(pdfProvider.notifier).state = PdfState.initial();
container.read(signatureProvider.notifier).state = SignatureState.initial();
final bytes = Uint8List.fromList([1, 2, 3, 4, 5]);
container
.read(signatureLibraryProvider.notifier)
.add(bytes, name: 'test.png');
}

View File

@ -1,46 +0,0 @@
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:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature asset is placed on the page
Future<void> aSignatureAssetIsPlacedOnThePage(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Ensure PDF is open
if (!container.read(pdfProvider).loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
}
// Get or create an asset
var library = container.read(signatureLibraryProvider);
SignatureAsset asset;
if (library.isNotEmpty) {
asset = library.first;
} else {
final bytes = Uint8List.fromList([1, 2, 3, 4, 5]);
final id = container
.read(signatureLibraryProvider.notifier)
.add(bytes, name: 'test.png');
asset = container
.read(signatureLibraryProvider)
.firstWhere((a) => a.id == id);
}
// Place it on the current page
final pdf = container.read(pdfProvider);
container
.read(pdfProvider.notifier)
.addPlacement(
page: pdf.currentPage,
rect: Rect.fromLTWH(50, 50, 100, 50),
asset: asset,
);
}

View File

@ -1,32 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature asset is selected
Future<void> aSignatureAssetIsSelected(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
var library = container.read(signatureLibraryProvider);
// If library is empty, add a dummy asset
if (library.isEmpty) {
final asset = SignatureAsset(
id: 'selected_asset',
bytes: Uint8List(100),
name: 'Selected Asset',
);
container.read(signatureLibraryProvider.notifier).state = [asset];
// Re-read the library
library = container.read(signatureLibraryProvider);
}
expect(
library.isNotEmpty,
true,
reason: 'Library should have at least one asset',
);
// For test purposes, we consider the first asset as selected
}

View File

@ -1,23 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature asset loaded or drawn is wrapped in a signature card
Future<void> aSignatureAssetLoadedOrDrawnIsWrappedInASignatureCard(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container.read(signatureLibraryProvider.notifier).state = [];
container.read(pdfProvider.notifier).state = PdfState.initial();
container.read(signatureProvider.notifier).state = SignatureState.initial();
final bytes = Uint8List.fromList([1, 2, 3, 4, 5]);
container
.read(signatureLibraryProvider.notifier)
.add(bytes, name: 'test.png');
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: a signature image is created
Future<void> aSignatureImageIsCreated(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(signatureProvider).imageBytes, isNotNull);
}

View File

@ -0,0 +1,14 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: a signature image is loaded or drawn
Future<void> aSignatureImageIsLoadedOrDrawn(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}

View File

@ -0,0 +1,20 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a signature image is placed on the page
Future<void> aSignatureImageIsPlacedOnThePage(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
container.read(pdfProvider.notifier).setSignedPage(1);
// Set an image to ensure rect exists
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}

View File

@ -0,0 +1,28 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a signature image is selected
Future<void> aSignatureImageIsSelected(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 2);
container.read(pdfProvider.notifier).setSignedPage(1);
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
// Allow provider scheduler to process queued updates fully
await tester.pumpAndSettle();
// Extra pump with a non-zero duration to flush zero-delay timers
await tester.pump(const Duration(milliseconds: 1));
// Teardown to avoid pending timers from Riverpod's scheduler
addTearDown(() {
TestWorld.container?.dispose();
TestWorld.container = null;
});
}

View File

@ -0,0 +1,25 @@
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/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a signature is placed on page {2}
Future<void> aSignatureIsPlacedOnPage(WidgetTester tester, num page) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 6);
// Ensure image and rect
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
container.read(signatureProvider.notifier).placeDefaultRect();
final Rect r = container.read(signatureProvider).rect!;
container
.read(pdfProvider.notifier)
.addPlacement(page: page.toInt(), rect: r, imageId: 'default.png');
}

View File

@ -0,0 +1,37 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: a signature is placed with a position and size relative to the page
Future<void> aSignatureIsPlacedWithAPositionAndSizeRelativeToThePage(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(
path: 'mock.pdf',
pageCount: 2,
bytes: Uint8List.fromList([1, 2, 3]),
);
container.read(pdfProvider.notifier).setSignedPage(1);
final r = Rect.fromLTWH(50, 100, 120, 60);
final sigN = container.read(signatureProvider.notifier);
sigN.placeDefaultRect();
// overwrite to desired rect
final sig = container.read(signatureProvider);
sigN
..toggleAspect(true)
..resize(Offset(r.width - sig.rect!.width, r.height - sig.rect!.height));
// move to target top-left
final movedDelta = Offset(r.left - sig.rect!.left, r.top - sig.rect!.top);
sigN.drag(movedDelta);
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([4, 5, 6]));
}

View File

@ -1,17 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: a signature placement appears on the page based on the signature card
Future<void> aSignaturePlacementAppearsOnThePageBasedOnTheSignatureCard(
WidgetTester tester,
) async {
final container = TestWorld.container!;
final pdf = container.read(pdfProvider);
final placements = pdf.placementsByPage[pdf.currentPage] ?? [];
expect(
placements.isNotEmpty,
true,
reason: 'A signature placement should appear on the page',
);
}

View File

@ -1,24 +0,0 @@
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:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature placement is placed on page {2}
Future<void> aSignaturePlacementIsPlacedOnPage(
WidgetTester tester,
num param1,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final page = param1.toInt();
container
.read(pdfProvider.notifier)
.addPlacement(
page: page,
rect: Rect.fromLTWH(20, 20, 100, 50),
asset: SignatureAsset(id: 'test.png', bytes: Uint8List(0)),
);
}

View File

@ -1,23 +0,0 @@
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:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import '_world.dart';
/// Usage: a signature placement is placed with a position and size relative to the page
Future<void> aSignaturePlacementIsPlacedWithAPositionAndSizeRelativeToThePage(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final pdf = container.read(pdfProvider);
container
.read(pdfProvider.notifier)
.addPlacement(
page: pdf.currentPage,
rect: Rect.fromLTWH(50, 50, 200, 100),
asset: SignatureAsset(id: 'test.png', bytes: Uint8List(0)),
);
}

View File

@ -1,7 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: adjusting one instance does not affect the others
@ -15,7 +14,7 @@ Future<void> adjustingOneInstanceDoesNotAffectTheOthers(
container.read(pdfProvider.notifier).removePlacement(page: 2, index: 0);
container
.read(pdfProvider.notifier)
.addPlacement(page: 2, rect: modified, asset: before[0].asset);
.addPlacement(page: 2, rect: modified, imageId: before[0].imageId);
final after = container.read(pdfProvider.notifier).placementsOn(2);
expect(after.any((p) => p.rect == before[1].rect), isTrue);
}

View File

@ -1,22 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: adjusting one of the signature placements does not affect the others
Future<void> adjustingOneOfTheSignaturePlacementsDoesNotAffectTheOthers(
WidgetTester tester,
) async {
final container = TestWorld.container!;
final pdf = container.read(pdfProvider);
final placements =
pdf.placementsByPage.values.expand((list) => list).toList();
// All placements should have the same asset ID (reusing the same asset)
final assetIds = placements.map((p) => p.asset.id).toSet();
expect(assetIds.length, 1);
// All should have default rotation (0.0) since none were adjusted
final rotations = placements.map((p) => p.rotationDeg).toSet();
expect(rotations.length, 1);
expect(rotations.first, 0.0);
}

View File

@ -1,18 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: all placed signature placements appear on their corresponding pages in the output
Future<void>
allPlacedSignaturePlacementsAppearOnTheirCorrespondingPagesInTheOutput(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
final totalPlacements = pdf.placementsByPage.values.fold(
0,
(sum, list) => sum + list.length,
);
expect(totalPlacements, greaterThan(1));
}

View File

@ -0,0 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: all placed signatures appear on their corresponding pages in the output
Future<void> allPlacedSignaturesAppearOnTheirCorrespondingPagesInTheOutput(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(pdfProvider.notifier).placementsOn(1), isNotEmpty);
// One of 4 or 5 depending on scenario
final p4 = container.read(pdfProvider.notifier).placementsOn(4);
final p5 = container.read(pdfProvider.notifier).placementsOn(5);
expect(p4.isNotEmpty || p5.isNotEmpty, isTrue);
expect(TestWorld.lastExportBytes, isNotNull);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: an empty signature canvas

View File

@ -1,14 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: both signature placements are shown on their respective pages
Future<void> bothSignaturePlacementsAreShownOnTheirRespectivePages(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
expect(pdf.placementsByPage[1], isNotEmpty);
expect(pdf.placementsByPage[3], isNotEmpty);
}

View File

@ -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_controller.dart';
import '_world.dart';
/// Usage: both signatures are shown on their respective pages
Future<void> bothSignaturesAreShownOnTheirRespectivePages(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final p1 = container.read(pdfProvider.notifier).placementsOn(1);
final p3 = container.read(pdfProvider.notifier).placementsOn(3);
expect(p1, isNotEmpty);
expect(p3, isNotEmpty);
}

View File

@ -1,8 +1,7 @@
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/data/model/model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: dragging or resizing one does not change the other
@ -21,7 +20,7 @@ Future<void> draggingOrResizingOneDoesNotChangeTheOther(
.addPlacement(
page: 1,
rect: changed,
asset: list[1].asset,
imageId: list[1].imageId,
rotationDeg: list[1].rotationDeg,
);
final after = container.read(pdfProvider.notifier).placementsOn(1);

View File

@ -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_controller.dart';
import '_world.dart';
/// Usage: each signature can be dragged and resized independently
Future<void> eachSignatureCanBeDraggedAndResizedIndependently(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final list = container.read(pdfProvider.notifier).placementsOn(1);
expect(list.length, greaterThanOrEqualTo(2));
// Independence is modeled by distinct rects; ensure not equal and both within page
expect(list[0].rect, isNot(equals(list[1].rect)));
for (final p in list.take(2)) {
expect(p.rect.left, greaterThanOrEqualTo(0));
expect(p.rect.top, greaterThanOrEqualTo(0));
}
}

View File

@ -1,14 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: each signature placement can be dragged and resized independently
Future<void> eachSignaturePlacementCanBeDraggedAndResizedIndependently(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
final placements = pdf.placementsByPage[pdf.currentPage] ?? [];
expect(placements.length, greaterThan(1));
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: identical signature instances appear in each location

View File

@ -1,16 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: identical signature placements appear in each location
Future<void> identicalSignaturePlacementsAppearInEachLocation(
WidgetTester tester,
) async {
final container = TestWorld.container!;
final pdf = container.read(pdfProvider);
final allPlacements =
pdf.placementsByPage.values.expand((list) => list).toList();
final assetIds = allPlacements.map((p) => p.asset.id).toSet();
expect(assetIds.length, 1); // All the same
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: it is placed on the selected page
Future<void> itIsPlacedOnTheSelectedPage(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(signatureProvider).imageBytes, isNotNull);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: multiple strokes were drawn

View File

@ -2,7 +2,7 @@ import 'dart:typed_data';
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/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: near-white background becomes transparent in the preview

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: only the selected signature is removed
Future<void> onlyTheSelectedSignatureIsRemoved(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final list = container.read(pdfProvider.notifier).placementsOn(1);
expect(list.length, 2);
}

View File

@ -1,14 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: only the selected signature placement is removed
Future<void> onlyTheSelectedSignaturePlacementIsRemoved(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
final placements = pdf.placementsByPage[pdf.currentPage] ?? [];
expect(placements.length, 2); // Started with 3, removed 1, should have 2
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: page {5} becomes visible in the scroll area

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: page {1} is displayed

View File

@ -1,25 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: resize to fit within bounding box
Future<void> resizeToFitWithinBoundingBox(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
if (pdf.selectedPlacementIndex != null) {
final placements = pdf.placementsByPage[pdf.currentPage] ?? [];
if (pdf.selectedPlacementIndex! < placements.length) {
final placement = placements[pdf.selectedPlacementIndex!];
// Assume page size is 800x600 for testing
const pageWidth = 800.0;
const pageHeight = 600.0;
expect(placement.rect.left, greaterThanOrEqualTo(0));
expect(placement.rect.top, greaterThanOrEqualTo(0));
expect(placement.rect.right, lessThanOrEqualTo(pageWidth));
expect(placement.rect.bottom, lessThanOrEqualTo(pageHeight));
}
}
}

View File

@ -1,17 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: signature placement occurs on the selected page
Future<void> signaturePlacementOccursOnTheSelectedPage(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final pdf = container.read(pdfProvider);
// Check that there's at least one placement on the current page
final placements = pdf.placementsByPage[pdf.currentPage] ?? [];
expect(placements.isNotEmpty, true);
}

View File

@ -1,13 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '_world.dart';
/// Usage: the app attempts to load the asset
Future<void> theAppAttemptsToLoadTheAsset(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Simulate attempting to load an asset - for now just ensure library is accessible
final library = container.read(signatureLibraryProvider);
expect(library, isNotNull);
}

View File

@ -0,0 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
/// Usage: the app attempts to load the image
Future<void> theAppAttemptsToLoadTheImage(WidgetTester tester) async {
// No-op for logic-level test; selection step already applied state.
}

View File

@ -1,16 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '_world.dart';
/// Usage: the asset is loaded and shown as a signature asset
Future<void> theAssetIsLoadedAndShownAsASignatureAsset(
WidgetTester tester,
) async {
final container = TestWorld.container!;
final library = container.read(signatureLibraryProvider);
expect(
library.isNotEmpty,
true,
reason: 'Asset should be loaded and shown in library',
);
}

View File

@ -1,16 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '_world.dart';
/// Usage: the asset is loaded and shown as a signature card
Future<void> theAssetIsLoadedAndShownAsASignatureCard(
WidgetTester tester,
) async {
final container = TestWorld.container!;
final library = container.read(signatureLibraryProvider);
expect(
library.isNotEmpty,
true,
reason: 'Asset should be loaded and shown as a card',
);
}

View File

@ -1,14 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/data/repositories/signature_library_repository.dart';
import '_world.dart';
/// Usage: the asset is not added to the document
Future<void> theAssetIsNotAddedToTheDocument(WidgetTester tester) async {
final container = TestWorld.container!;
final library = container.read(signatureLibraryProvider);
expect(
library.isEmpty,
true,
reason: 'Invalid asset should not be added to library',
);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the canvas becomes blank

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the document is open

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the first page is displayed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the Go to input cannot be used

View File

@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the image is loaded and shown as a signature asset
Future<void> theImageIsLoadedAndShownAsASignatureAsset(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.imageBytes, isNotNull);
expect(sig.rect, isNotNull);
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the image is not added to the document
Future<void> theImageIsNotAddedToTheDocument(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.rect, isNull);
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the image scales proportionally
Future<void> theImageScalesProportionally(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
final aspect = sig.rect!.width / sig.rect!.height;
expect((aspect - (TestWorld.prevAspect ?? aspect)).abs() < 0.05, isTrue);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the last page is displayed (page {5})

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the last stroke is removed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the left pages overview highlights page {5}

View File

@ -1,13 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import '_world.dart';
/// Usage: the other signature placements remain unchanged
Future<void> theOtherSignaturePlacementsRemainUnchanged(
WidgetTester tester,
) async {
final container = TestWorld.container!;
final pdf = container.read(pdfProvider);
final placements = pdf.placementsByPage[pdf.currentPage] ?? [];
expect(placements.length, 2); // Should have 2 remaining after deleting 1
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the other signatures remain unchanged
Future<void> theOtherSignaturesRemainUnchanged(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final list = container.read(pdfProvider.notifier).placementsOn(1);
// After deleting index 1, two should remain
expect(list.length, 2);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/pdf_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart';
/// Usage: the page label shows "Page {5} of {5}"

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/signature_repository.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the preview updates immediately

View File

@ -0,0 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/signature/view_model/signature_controller.dart';
import '_world.dart';
/// Usage: the signature is stamped at the exact PDF page coordinates and size
Future<void> theSignatureIsStampedAtTheExactPdfPageCoordinatesAndSize(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.rect, isNotNull);
expect(sig.rect!.width, greaterThan(0));
expect(sig.rect!.height, greaterThan(0));
}

Some files were not shown because too many files have changed in this diff Show More