chore: adopt bdd_widget_test for BDD

This commit is contained in:
insleker 2025-08-28 23:51:36 +08:00
parent 3551cdf274
commit 60630c6ddd
105 changed files with 1724 additions and 122 deletions

View File

@ -10,6 +10,7 @@ checkout [`docs/FRs.md`](docs/FRs.md)
```bash
flutter pub get
# flutter run build_runner build --delete-conflicting-outputs
# run the app
flutter run

View File

@ -0,0 +1,4 @@
# meta archietecture
* [MVVM](https://docs.flutter.dev/app-architecture/guide)

View File

@ -2,126 +2,5 @@
Use cases are derived from `FRs.md` (user stories) and `meta-arch.md`. Each Feature name matches the corresponding user story; scenarios focus on observable behavior without restating story details.
```gherkin
Feature: PDF browser
Scenario: Open a PDF and navigate pages
Given a PDF document is available
When the user opens the document
Then the first page is displayed
And the user can move to the next or previous page
Scenario: Jump to a specific page
Given a multi-page PDF is open
When the user selects a specific page number
Then that page is displayed
Scenario: Select a page for signing
Given a PDF is open
When the user marks the current page for signing
Then the page is set as the signature target
```
```gherkin
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 |
```
```gherkin
Feature: geometrically adjust signature picture
Scenario: Resize and move the signature within page bounds
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 remains within the page area
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
```
```gherkin
Feature: graphically adjust signature picture
Scenario: Remove background
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 image is selected
When the user changes contrast and brightness controls
Then the preview updates immediately
And the user can apply or reset adjustments
```
```gherkin
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 image is created
And it is placed on the selected page
Scenario: Clear and redraw
Given a drawn signature exists in the canvas
When the user clears the canvas
Then the canvas becomes blank
Scenario: Undo the last stroke
Given multiple strokes were drawn
When the user chooses undo
Then the last stroke is removed
```
```gherkin
Feature: save signed PDF
Scenario: Export the signed document to a new file
Given a PDF is open and contains at least one placed signature
When the user saves/exports the document
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 is placed with a position and size relative to the page
When the user saves/exports the document
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 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 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
And the user cannot edit the document
```
The Gherkin scenarios live in runnable BDD feature files under `test/features/`.

View File

@ -49,6 +49,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.12
bdd_widget_test: ^2.0.1
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is

View File

@ -0,0 +1,17 @@
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 image is created
And it is placed on the selected page
Scenario: Clear and redraw
Given a drawn signature exists in the canvas
When the user clears the canvas
Then the canvas becomes blank
Scenario: Undo the last stroke
Given multiple strokes were drawn
When the user chooses undo
Then the last stroke is removed

View File

@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/an_empty_signature_canvas.dart';
import './step/the_user_draws_strokes_and_confirms.dart';
import './step/a_signature_image_is_created.dart';
import './step/it_is_placed_on_the_selected_page.dart';
import './step/a_drawn_signature_exists_in_the_canvas.dart';
import './step/the_user_clears_the_canvas.dart';
import './step/the_canvas_becomes_blank.dart';
import './step/multiple_strokes_were_drawn.dart';
import './step/the_user_chooses_undo.dart';
import './step/the_last_stroke_is_removed.dart';
void main() {
group('''draw signature''', () {
testWidgets('''Draw with mouse or touch and place on page''',
(tester) async {
await anEmptySignatureCanvas(tester);
await theUserDrawsStrokesAndConfirms(tester);
await aSignatureImageIsCreated(tester);
await itIsPlacedOnTheSelectedPage(tester);
});
testWidgets('''Clear and redraw''', (tester) async {
await aDrawnSignatureExistsInTheCanvas(tester);
await theUserClearsTheCanvas(tester);
await theCanvasBecomesBlank(tester);
});
testWidgets('''Undo the last stroke''', (tester) async {
await multipleStrokesWereDrawn(tester);
await theUserChoosesUndo(tester);
await theLastStrokeIsRemoved(tester);
});
});
}

View File

@ -0,0 +1,12 @@
Feature: geometrically adjust signature picture
Scenario: Resize and move the signature within page bounds
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 remains within the page area
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

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_signature_image_is_placed_on_the_page.dart';
import './step/the_user_drags_handles_to_resize_and_drags_to_reposition.dart';
import './step/the_size_and_position_update_in_real_time.dart';
import './step/the_signature_remains_within_the_page_area.dart';
import './step/a_signature_image_is_selected.dart';
import './step/the_user_enables_aspect_ratio_lock_and_resizes.dart';
import './step/the_image_scales_proportionally.dart';
void main() {
group('''geometrically adjust signature picture''', () {
testWidgets('''Resize and move the signature within page bounds''',
(tester) async {
await aSignatureImageIsPlacedOnThePage(tester);
await theUserDragsHandlesToResizeAndDragsToReposition(tester);
await theSizeAndPositionUpdateInRealTime(tester);
await theSignatureRemainsWithinThePageArea(tester);
});
testWidgets('''Lock aspect ratio while resizing''', (tester) async {
await aSignatureImageIsSelected(tester);
await theUserEnablesAspectRatioLockAndResizes(tester);
await theImageScalesProportionally(tester);
});
});
}

View File

@ -0,0 +1,13 @@
Feature: graphically adjust signature picture
Scenario: Remove background
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 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

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_signature_image_is_selected.dart';
import './step/the_user_enables_background_removal.dart';
import './step/nearwhite_background_becomes_transparent_in_the_preview.dart';
import './step/the_user_can_apply_the_change.dart';
import './step/the_user_changes_contrast_and_brightness_controls.dart';
import './step/the_preview_updates_immediately.dart';
import './step/the_user_can_apply_or_reset_adjustments.dart';
void main() {
group('''graphically adjust signature picture''', () {
testWidgets('''Remove background''', (tester) async {
await aSignatureImageIsSelected(tester);
await theUserEnablesBackgroundRemoval(tester);
await nearwhiteBackgroundBecomesTransparentInThePreview(tester);
await theUserCanApplyTheChange(tester);
});
testWidgets('''Adjust contrast and brightness''', (tester) async {
await aSignatureImageIsSelected(tester);
await theUserChangesContrastAndBrightnessControls(tester);
await thePreviewUpdatesImmediately(tester);
await theUserCanApplyOrResetAdjustments(tester);
});
});
}

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

@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_pdf_page_is_selected_for_signing.dart';
import './step/the_user_chooses_a_signature_image_file.dart';
import './step/the_image_is_loaded_and_shown_as_a_signature_asset.dart';
import './step/the_user_selects.dart';
import './step/the_app_attempts_to_load_the_image.dart';
import './step/the_user_is_notified_of_the_issue.dart';
import './step/the_image_is_not_added_to_the_document.dart';
import './step/_tokens.dart';
void main() {
group('''load signature picture''', () {
testWidgets('''Import a signature image''', (tester) async {
await aPdfPageIsSelectedForSigning(tester);
await theUserChoosesASignatureImageFile(tester);
await theImageIsLoadedAndShownAsASignatureAsset(tester);
});
testWidgets(
'''Outline: Handle invalid or unsupported files (corrupted.png)''',
(tester) async {
await theUserSelects(tester, corrupted.png);
await theAppAttemptsToLoadTheImage(tester);
await theUserIsNotifiedOfTheIssue(tester);
await theImageIsNotAddedToTheDocument(tester);
},
);
testWidgets(
'''Outline: Handle invalid or unsupported files (signature.bmp)''',
(tester) async {
await theUserSelects(tester, signature.bmp);
await theAppAttemptsToLoadTheImage(tester);
await theUserIsNotifiedOfTheIssue(tester);
await theImageIsNotAddedToTheDocument(tester);
},
);
testWidgets(
'''Outline: Handle invalid or unsupported files (empty.jpg)''',
(tester) async {
await theUserSelects(tester, empty.jpg);
await theAppAttemptsToLoadTheImage(tester);
await theUserIsNotifiedOfTheIssue(tester);
await theImageIsNotAddedToTheDocument(tester);
},
);
});
}

View File

@ -0,0 +1,12 @@
Feature: PDF browser
Scenario: Open a PDF and navigate pages
Given a PDF document is available
When the user opens the document
Then the first page is displayed
And the user can move to the next or previous page
Scenario: Jump to a specific page
Given a multi-page PDF is open
When the user selects a specific page number
Then that page is displayed

View File

@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_pdf_document_is_available.dart';
import './step/the_user_opens_the_document.dart';
import './step/the_first_page_is_displayed.dart';
import './step/the_user_can_move_to_the_next_or_previous_page.dart';
import './step/a_multipage_pdf_is_open.dart';
import './step/the_user_selects_a_specific_page_number.dart';
import './step/that_page_is_displayed.dart';
void main() {
group('''PDF browser''', () {
testWidgets('''Open a PDF and navigate pages''', (tester) async {
await aPdfDocumentIsAvailable(tester);
await theUserOpensTheDocument(tester);
await theFirstPageIsDisplayed(tester);
await theUserCanMoveToTheNextOrPreviousPage(tester);
});
testWidgets('''Jump to a specific page''', (tester) async {
await aMultipagePdfIsOpen(tester);
await theUserSelectsASpecificPageNumber(tester);
await thatPageIsDisplayed(tester);
});
});
}

View File

@ -0,0 +1,29 @@
Feature: PDF state logic
Scenario: openPicked loads document and initializes state
Given a new provider container
When I openPicked with path {'test.pdf'} and pageCount {7}
Then pdf state is loaded {true}
And pdf picked path is {'test.pdf'}
And pdf page count is {7}
And pdf current page is {1}
And pdf marked for signing is {false}
Scenario: jumpTo clamps within page boundaries
Given a new provider container
And a pdf is open with path {'test.pdf'} and pageCount {5}
When I jumpTo {10}
Then pdf current page is {5}
When I jumpTo {0}
Then pdf current page is {1}
When I jumpTo {3}
Then pdf current page is {3}
Scenario: setPageCount updates count without toggling other flags
Given a new provider container
And a pdf is open with path {'test.pdf'} and pageCount {2}
When I toggle mark
And I set page count {9}
Then pdf page count is {9}
And pdf state is loaded {true}
And pdf marked for signing is {true}

View File

@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_new_provider_container.dart';
import './step/i_openpicked_with_path_and_pagecount.dart';
import './step/pdf_state_is_loaded.dart';
import './step/pdf_picked_path_is.dart';
import './step/pdf_page_count_is.dart';
import './step/pdf_current_page_is.dart';
import './step/pdf_marked_for_signing_is.dart';
import './step/a_pdf_is_open_with_path_and_pagecount.dart';
import './step/i_jumpto.dart';
import './step/i_toggle_mark.dart';
import './step/i_set_page_count.dart';
void main() {
group('''PDF state logic''', () {
testWidgets('''openPicked loads document and initializes state''',
(tester) async {
await aNewProviderContainer(tester);
await iOpenpickedWithPathAndPagecount(tester, 'test.pdf', 7);
await pdfStateIsLoaded(tester, true);
await pdfPickedPathIs(tester, 'test.pdf');
await pdfPageCountIs(tester, 7);
await pdfCurrentPageIs(tester, 1);
await pdfMarkedForSigningIs(tester, false);
});
testWidgets('''jumpTo clamps within page boundaries''', (tester) async {
await aNewProviderContainer(tester);
await aPdfIsOpenWithPathAndPagecount(tester, 'test.pdf', 5);
await iJumpto(tester, 10);
await pdfCurrentPageIs(tester, 5);
await iJumpto(tester, 0);
await pdfCurrentPageIs(tester, 1);
await iJumpto(tester, 3);
await pdfCurrentPageIs(tester, 3);
});
testWidgets('''setPageCount updates count without toggling other flags''',
(tester) async {
await aNewProviderContainer(tester);
await aPdfIsOpenWithPathAndPagecount(tester, 'test.pdf', 2);
await iToggleMark(tester);
await iSetPageCount(tester, 9);
await pdfPageCountIs(tester, 9);
await pdfStateIsLoaded(tester, true);
await pdfMarkedForSigningIs(tester, true);
});
});
}

View File

@ -0,0 +1,27 @@
Feature: save signed PDF
Scenario: Export the signed document to a new file
Given a PDF is open and contains at least one placed signature
When the user saves/exports the document
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 is placed with a position and size relative to the page
When the user saves/exports the document
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 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 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
And the user cannot edit the document

View File

@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_pdf_is_open_and_contains_at_least_one_placed_signature.dart';
import './step/the_user_savesexports_the_document.dart';
import './step/a_new_pdf_file_is_saved_at_specified_full_path_location_and_file_name.dart';
import './step/the_signatures_appear_on_the_corresponding_page_in_the_output.dart';
import './step/keep_other_unchanged_contentpages_intact_in_the_output.dart';
import './step/a_signature_is_placed_with_a_position_and_size_relative_to_the_page.dart';
import './step/the_signature_is_stamped_at_the_exact_pdf_page_coordinates_and_size.dart';
import './step/the_stamp_remains_crisp_at_any_zoom_level_not_rasterized_by_the_screen.dart';
import './step/other_page_content_remains_vector_and_unaltered.dart';
import './step/a_pdf_is_open_with_no_signatures_placed.dart';
import './step/the_user_attempts_to_save.dart';
import './step/the_user_is_notified_there_is_nothing_to_save.dart';
import './step/the_user_starts_exporting_the_document.dart';
import './step/the_export_process_is_not_yet_finished.dart';
import './step/the_user_is_notified_that_the_export_is_still_in_progress.dart';
import './step/the_user_cannot_edit_the_document.dart';
void main() {
group('''save signed PDF''', () {
testWidgets(
'''Export the signed document to a new file''',
(tester) async {
await aPdfIsOpenAndContainsAtLeastOnePlacedSignature(tester);
await theUserSavesexportsTheDocument(tester);
await aNewPdfFileIsSavedAtSpecifiedFullPathLocationAndFileName(tester);
await theSignaturesAppearOnTheCorrespondingPageInTheOutput(tester);
await keepOtherUnchangedContentpagesIntactInTheOutput(tester);
},
timeout: const Timeout(Duration(seconds: 30)),
);
testWidgets(
'''Vector-accurate stamping into PDF page coordinates''',
(tester) async {
await aSignatureIsPlacedWithAPositionAndSizeRelativeToThePage(tester);
await theUserSavesexportsTheDocument(tester);
await theSignatureIsStampedAtTheExactPdfPageCoordinatesAndSize(tester);
await theStampRemainsCrispAtAnyZoomLevelNotRasterizedByTheScreen(
tester,
);
await otherPageContentRemainsVectorAndUnaltered(tester);
},
timeout: const Timeout(Duration(seconds: 30)),
);
testWidgets(
'''Prevent saving when nothing is placed''',
(tester) async {
await aPdfIsOpenWithNoSignaturesPlaced(tester);
await theUserAttemptsToSave(tester);
await theUserIsNotifiedThereIsNothingToSave(tester);
},
timeout: const Timeout(Duration(seconds: 30)),
);
testWidgets(
'''Loading sign when exporting/saving files''',
(tester) async {
await aSignatureIsPlacedWithAPositionAndSizeRelativeToThePage(tester);
await theUserStartsExportingTheDocument(tester);
await theExportProcessIsNotYetFinished(tester);
await theUserIsNotifiedThatTheExportIsStillInProgress(tester);
await theUserCannotEditTheDocument(tester);
},
timeout: const Timeout(Duration(seconds: 30)),
);
});
}

View File

@ -0,0 +1,40 @@
Feature: Signature state logic
Scenario: placeDefaultRect centers a reasonable default rect
Given a new provider container
Then signature rect is null
When I place default signature rect
Then signature rect left >= {0}
And signature rect top >= {0}
And signature rect right <= {400}
And signature rect bottom <= {560}
And signature rect width > {50}
And signature rect height > {20}
Scenario: drag clamps to canvas bounds
Given a new provider container
And a default signature rect is placed
When I drag signature by {Offset(10000, -10000)}
Then signature rect left >= {0}
And signature rect top >= {0}
And signature rect right <= {400}
And signature rect bottom <= {560}
And signature rect moved from center
Scenario: resize respects aspect lock and clamps
Given a new provider container
And a default signature rect is placed
And aspect lock is {true}
When I resize signature by {Offset(1000, 1000)}
Then signature aspect ratio is preserved within {0.05}
And signature rect left >= {0}
And signature rect top >= {0}
And signature rect right <= {400}
And signature rect bottom <= {560}
Scenario: setImageBytes ensures a rect exists for display
Given a new provider container
Then signature rect is null
When I set tiny signature image bytes
Then signature image bytes is not null
And signature rect is not null

View File

@ -0,0 +1,70 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import './step/a_new_provider_container.dart';
import './step/signature_rect_is_null.dart';
import './step/i_place_default_signature_rect.dart';
import './step/signature_rect_left.dart';
import './step/signature_rect_top.dart';
import './step/signature_rect_right.dart';
import './step/signature_rect_bottom.dart';
import './step/signature_rect_width.dart';
import './step/signature_rect_height.dart';
import './step/a_default_signature_rect_is_placed.dart';
import './step/i_drag_signature_by.dart';
import './step/signature_rect_moved_from_center.dart';
import './step/aspect_lock_is.dart';
import './step/i_resize_signature_by.dart';
import './step/signature_aspect_ratio_is_preserved_within.dart';
import './step/i_set_tiny_signature_image_bytes.dart';
import './step/signature_image_bytes_is_not_null.dart';
import './step/signature_rect_is_not_null.dart';
void main() {
group('''Signature state logic''', () {
testWidgets('''placeDefaultRect centers a reasonable default rect''',
(tester) async {
await aNewProviderContainer(tester);
await signatureRectIsNull(tester);
await iPlaceDefaultSignatureRect(tester);
await signatureRectLeft(tester, 0);
await signatureRectTop(tester, 0);
await signatureRectRight(tester, 400);
await signatureRectBottom(tester, 560);
await signatureRectWidth(tester, 50);
await signatureRectHeight(tester, 20);
});
testWidgets('''drag clamps to canvas bounds''', (tester) async {
await aNewProviderContainer(tester);
await aDefaultSignatureRectIsPlaced(tester);
await iDragSignatureBy(tester, Offset(10000, -10000));
await signatureRectLeft(tester, 0);
await signatureRectTop(tester, 0);
await signatureRectRight(tester, 400);
await signatureRectBottom(tester, 560);
await signatureRectMovedFromCenter(tester);
});
testWidgets('''resize respects aspect lock and clamps''', (tester) async {
await aNewProviderContainer(tester);
await aDefaultSignatureRectIsPlaced(tester);
await aspectLockIs(tester, true);
await iResizeSignatureBy(tester, Offset(1000, 1000));
await signatureAspectRatioIsPreservedWithin(tester, 0.05);
await signatureRectLeft(tester, 0);
await signatureRectTop(tester, 0);
await signatureRectRight(tester, 400);
await signatureRectBottom(tester, 560);
});
testWidgets('''setImageBytes ensures a rect exists for display''',
(tester) async {
await aNewProviderContainer(tester);
await signatureRectIsNull(tester);
await iSetTinySignatureImageBytes(tester);
await signatureImageBytesIsNotNull(tester);
await signatureRectIsNotNull(tester);
});
});
}

View File

@ -0,0 +1,68 @@
import 'dart:typed_data';
import 'dart:ui' show Rect, Size;
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
// A lightweight fake exporter to avoid platform rasterization in tests.
class FakeExportService {
Future<bool> exportSignedPdfFromFile({
required String inputPath,
required String outputPath,
required int? signedPage,
required Rect? signatureRectUi,
required Size uiPageSize,
required Uint8List? signatureImageBytes,
double targetDpi = 144.0,
}) async {
final bytes = await exportSignedPdfFromBytes(
srcBytes: Uint8List.fromList([0x25, 0x50, 0x44, 0x46]),
signedPage: signedPage,
signatureRectUi: signatureRectUi,
uiPageSize: uiPageSize,
signatureImageBytes: signatureImageBytes,
targetDpi: targetDpi,
);
if (bytes == null) return false;
try {
final file = File(outputPath);
await file.writeAsBytes(bytes, flush: true);
return true;
} catch (_) {
return false;
}
}
Future<Uint8List?> exportSignedPdfFromBytes({
required Uint8List srcBytes,
required int? signedPage,
required Rect? signatureRectUi,
required Size uiPageSize,
required Uint8List? signatureImageBytes,
double targetDpi = 144.0,
}) async {
// Return a deterministic tiny PDF-like byte array
final header = <int>[0x25, 0x50, 0x44, 0x46, 0x2D]; // %PDF-
final payload = <int>[...srcBytes.take(4)];
final sigFlag =
(signatureRectUi != null &&
signatureImageBytes != null &&
signatureImageBytes.isNotEmpty)
? 1
: 0;
final meta = <int>[
sigFlag,
uiPageSize.width.toInt() & 0xFF,
uiPageSize.height.toInt() & 0xFF,
];
return Uint8List.fromList([...header, ...payload, ...meta]);
}
}
ProviderContainer getOrCreateContainer() {
if (TestWorld.container != null) return TestWorld.container!;
final container = ProviderContainer();
TestWorld.container = container;
return container;
}

View File

@ -0,0 +1,16 @@
class _Token {
final String base;
const _Token(this.base);
String get png => '$base.png';
String get jpg => '$base.jpg';
String get jpeg => '$base.jpeg';
String get webp => '$base.webp';
String get bmp => '$base.bmp';
@override
String toString() => base;
}
// Tokens used by generated Scenario Outline substitutions
const corrupted = _Token('corrupted');
const signature = _Token('signature');
const empty = _Token('empty');

View File

@ -0,0 +1,36 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// A tiny shared world for BDD steps to share state within a scenario.
class TestWorld {
static ProviderContainer? container;
// Signature helpers
static Offset? prevCenter;
static double? prevAspect;
static double? prevContrast;
static double? prevBrightness;
// Export/save helpers
static Uint8List? lastExportBytes;
static String? lastSavedPath;
static bool exportInProgress = false;
static bool nothingToSaveAttempt = false;
// Generic flags/values
static int? selectedPage;
static void reset() {
prevCenter = null;
prevAspect = null;
prevContrast = null;
prevBrightness = null;
lastExportBytes = null;
lastSavedPath = null;
exportInProgress = false;
nothingToSaveAttempt = false;
selectedPage = null;
}
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: a default signature rect is placed
Future<void> aDefaultSignatureRectIsPlaced(WidgetTester tester) async {
final c = TestWorld.container!;
c.read(signatureProvider.notifier).placeDefaultRect();
// remember center for movement checks
TestWorld.prevCenter = c.read(signatureProvider).rect!.center;
}

View File

@ -0,0 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: a drawn signature exists in the canvas
Future<void> aDrawnSignatureExistsInTheCanvas(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sigN = container.read(signatureProvider.notifier);
sigN.setStrokes([
[const Offset(0, 0), const Offset(1, 1)],
]);
}

View File

@ -0,0 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// 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: 10);
}

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,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '_world.dart';
/// Usage: a new provider container
Future<void> aNewProviderContainer(WidgetTester tester) async {
// Ensure a fresh world per scenario
TestWorld.container?.dispose();
TestWorld.reset();
TestWorld.container = ProviderContainer();
addTearDown(() {
TestWorld.container?.dispose();
TestWorld.container = null;
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: a PDF document is available
Future<void> aPdfDocumentIsAvailable(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container.read(pdfProvider.notifier).openSample();
}

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:pdf_signature/features/pdf/viewer.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).toggleMark();
container.read(signatureProvider.notifier).placeDefaultRect();
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: a pdf is open with path {'test.pdf'} and pageCount {5}
Future<void> aPdfIsOpenWithPathAndPagecount(
WidgetTester tester,
String path,
int pageCount,
) async {
final c = TestWorld.container!;
c.read(pdfProvider.notifier).openPicked(path: path, pageCount: pageCount);
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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).toggleMark();
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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,19 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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).toggleMark();
// Set an image to ensure rect exists
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}

View File

@ -0,0 +1,18 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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).toggleMark();
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}

View File

@ -0,0 +1,36 @@
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/features/pdf/viewer.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).toggleMark();
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

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: an empty signature canvas
Future<void> anEmptySignatureCanvas(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container.read(signatureProvider.notifier).setStrokes([]);
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: aspect lock is {true}
Future<void> aspectLockIs(WidgetTester tester, bool value) async {
final c = TestWorld.container!;
// snapshot current aspect for later validation
final r = c.read(signatureProvider).rect;
if (r != null) {
TestWorld.prevAspect = r.width / r.height;
}
c.read(signatureProvider.notifier).toggleAspect(value);
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I drag signature by {Offset(10000, -10000)}
Future<void> iDragSignatureBy(WidgetTester tester, Offset delta) async {
final c = TestWorld.container!;
c.read(signatureProvider.notifier).drag(delta);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I jumpTo {10}
Future<void> iJumpto(WidgetTester tester, int page) async {
final c = TestWorld.container!;
c.read(pdfProvider.notifier).jumpTo(page);
}

View File

@ -0,0 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I openPicked with path {'test.pdf'} and pageCount {7}
Future<void> iOpenpickedWithPathAndPagecount(
WidgetTester tester,
String path,
int pageCount,
) async {
final c = TestWorld.container!;
c.read(pdfProvider.notifier).openPicked(path: path, pageCount: pageCount);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I place default signature rect
Future<void> iPlaceDefaultSignatureRect(WidgetTester tester) async {
final c = TestWorld.container!;
c.read(signatureProvider.notifier).placeDefaultRect();
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I resize signature by {Offset(1000, 1000)}
Future<void> iResizeSignatureBy(WidgetTester tester, Offset delta) async {
final c = TestWorld.container!;
c.read(signatureProvider.notifier).resize(delta);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I set page count {9}
Future<void> iSetPageCount(WidgetTester tester, int count) async {
final c = TestWorld.container!;
c.read(pdfProvider.notifier).setPageCount(count);
}

View File

@ -0,0 +1,11 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I set signature image bytes {Uint8List.fromList([0, 1, 2])}
Future<void> iSetSignatureImageBytes(WidgetTester tester, dynamic value) async {
final c = TestWorld.container!;
final bytes = value as Uint8List;
c.read(signatureProvider.notifier).setImageBytes(bytes);
}

View File

@ -0,0 +1,11 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I set tiny signature image bytes
Future<void> iSetTinySignatureImageBytes(WidgetTester tester) async {
final c = TestWorld.container!;
final bytes = Uint8List.fromList([0, 1, 2, 3]);
c.read(signatureProvider.notifier).setImageBytes(bytes);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: I toggle mark
Future<void> iToggleMark(WidgetTester tester) async {
final c = TestWorld.container!;
c.read(pdfProvider.notifier).toggleMark();
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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

@ -0,0 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
/// Usage: keep other unchanged content(pages) intact in the output
Future<void> keepOtherUnchangedContentpagesIntactInTheOutput(
WidgetTester tester,
) async {
// Logic-only: no additional checks here.
}

View File

@ -0,0 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: multiple strokes were drawn
Future<void> multipleStrokesWereDrawn(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
container.read(signatureProvider.notifier).setStrokes([
[const Offset(0, 0), const Offset(1, 1)],
[const Offset(2, 2), const Offset(3, 3)],
]);
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: near-white background becomes transparent in the preview
Future<void> nearwhiteBackgroundBecomesTransparentInThePreview(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(signatureProvider).bgRemoval, isTrue);
}

View File

@ -0,0 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
/// Usage: other page content remains vector and unaltered
Future<void> otherPageContentRemainsVectorAndUnaltered(
WidgetTester tester,
) async {}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: pdf current page is {1}
Future<void> pdfCurrentPageIs(WidgetTester tester, int expected) async {
final c = TestWorld.container!;
expect(c.read(pdfProvider).currentPage, expected);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: pdf marked for signing is {false}
Future<void> pdfMarkedForSigningIs(WidgetTester tester, bool expected) async {
final c = TestWorld.container!;
expect(c.read(pdfProvider).markedForSigning, expected);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: pdf page count is {7}
Future<void> pdfPageCountIs(WidgetTester tester, int expected) async {
final c = TestWorld.container!;
expect(c.read(pdfProvider).pageCount, expected);
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: pdf picked path is {'test.pdf'}
Future<void> pdfPickedPathIs(WidgetTester tester, String expected) async {
final c = TestWorld.container!;
final s = c.read(pdfProvider);
expect(s.pickedPdfPath, expected);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: pdf state is loaded {true}
Future<void> pdfStateIsLoaded(WidgetTester tester, bool expected) async {
final c = TestWorld.container!;
expect(c.read(pdfProvider).loaded, expected);
}

View File

@ -0,0 +1,20 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature aspect ratio is preserved within {0.05}
Future<void> signatureAspectRatioIsPreservedWithin(
WidgetTester tester,
num tolerance,
) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
final before = TestWorld.prevAspect;
if (before == null) {
// save and pass
TestWorld.prevAspect = r.width / r.height;
return;
}
final after = r.width / r.height;
expect((after - before).abs(), lessThanOrEqualTo(tolerance.toDouble()));
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature image bytes is not null
Future<void> signatureImageBytesIsNotNull(WidgetTester tester) async {
final c = TestWorld.container!;
expect(c.read(signatureProvider).imageBytes, isNotNull);
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect bottom <= {560}
Future<void> signatureRectBottom(WidgetTester tester, num maxBottom) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
expect(r.bottom, lessThanOrEqualTo(maxBottom.toDouble()));
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect height > {20}
Future<void> signatureRectHeight(WidgetTester tester, num minHeight) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
expect(r.height, greaterThan(minHeight.toDouble()));
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect is not null
Future<void> signatureRectIsNotNull(WidgetTester tester) async {
final c = TestWorld.container!;
expect(c.read(signatureProvider).rect, isNotNull);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect is null
Future<void> signatureRectIsNull(WidgetTester tester) async {
final c = TestWorld.container!;
expect(c.read(signatureProvider).rect, isNull);
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect left >= {0}
Future<void> signatureRectLeft(WidgetTester tester, num minLeft) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
expect(r.left, greaterThanOrEqualTo(minLeft.toDouble()));
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect moved from center
Future<void> signatureRectMovedFromCenter(WidgetTester tester) async {
final c = TestWorld.container!;
final prev = TestWorld.prevCenter;
final now = c.read(signatureProvider).rect!.center;
expect(prev, isNotNull);
expect(now, isNot(equals(prev)));
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect right <= {400}
Future<void> signatureRectRight(WidgetTester tester, num maxRight) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
expect(r.right, lessThanOrEqualTo(maxRight.toDouble()));
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect top >= {0}
Future<void> signatureRectTop(WidgetTester tester, num minTop) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
expect(r.top, greaterThanOrEqualTo(minTop.toDouble()));
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: signature rect width > {50}
Future<void> signatureRectWidth(WidgetTester tester, num minWidth) async {
final c = TestWorld.container!;
final r = c.read(signatureProvider).rect!;
expect(r.width, greaterThan(minWidth.toDouble()));
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: that page is displayed
Future<void> thatPageIsDisplayed(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(pdfProvider).currentPage, 3);
}

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

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the canvas becomes blank
Future<void> theCanvasBecomesBlank(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(signatureProvider).strokes, isEmpty);
}

View File

@ -0,0 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the export process is not yet finished
Future<void> theExportProcessIsNotYetFinished(WidgetTester tester) async {
expect(TestWorld.exportInProgress, isTrue);
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the first page is displayed
Future<void> theFirstPageIsDisplayed(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
expect(pdf.currentPage, 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/features/pdf/viewer.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/features/pdf/viewer.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/features/pdf/viewer.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

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the last stroke is removed
Future<void> theLastStrokeIsRemoved(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.strokes.length, 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/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the preview updates immediately
Future<void> thePreviewUpdatesImmediately(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.contrast, closeTo(1.3, 1e-6));
expect(sig.brightness, closeTo(0.2, 1e-6));
}

View File

@ -0,0 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.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));
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the signature remains within the page area
Future<void> theSignatureRemainsWithinThePageArea(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
final r = sig.rect!;
expect(r.left >= 0 && r.top >= 0, isTrue);
expect(r.right <= SignatureController.pageSize.width, isTrue);
expect(r.bottom <= SignatureController.pageSize.height, isTrue);
}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the signatures appear on the corresponding page in the output
Future<void> theSignaturesAppearOnTheCorrespondingPageInTheOutput(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(pdfProvider);
final sig = container.read(signatureProvider);
expect(pdf.signedPage, isNotNull);
expect(sig.rect, isNotNull);
expect(sig.imageBytes, isNotNull);
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the size and position update in real time
Future<void> theSizeAndPositionUpdateInRealTime(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.rect, isNotNull);
expect(sig.rect!.center, isNot(TestWorld.prevCenter));
}

View File

@ -0,0 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
/// Usage: the stamp remains crisp at any zoom level (not rasterized by the screen)
Future<void> theStampRemainsCrispAtAnyZoomLevelNotRasterizedByTheScreen(
WidgetTester tester,
) async {}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user attempts to save
Future<void> theUserAttemptsToSave(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final pdf = container.read(pdfProvider);
final sig = container.read(signatureProvider);
// Simulate save attempt: since rect is null, mark flag
if (!pdf.loaded || sig.rect == null) {
TestWorld.nothingToSaveAttempt = true;
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user can apply or reset adjustments
Future<void> theUserCanApplyOrResetAdjustments(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
expect(sig.contrast, isNotNull);
expect(sig.brightness, isNotNull);
}

View File

@ -0,0 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
/// Usage: the user can apply the change
Future<void> theUserCanApplyTheChange(WidgetTester tester) async {
// No-op in logic tests.
}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user can move to the next or previous page
Future<void> theUserCanMoveToTheNextOrPreviousPage(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final pdfN = container.read(pdfProvider.notifier);
final pdf = container.read(pdfProvider);
expect(pdf.currentPage, 1);
pdfN.jumpTo(2);
expect(container.read(pdfProvider).currentPage, 2);
pdfN.jumpTo(1);
expect(container.read(pdfProvider).currentPage, 1);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user cannot edit the document
Future<void> theUserCannotEditTheDocument(WidgetTester tester) async {
expect(TestWorld.exportInProgress, isTrue);
// Reset flag to simulate export completion
TestWorld.exportInProgress = false;
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user changes contrast and brightness controls
Future<void> theUserChangesContrastAndBrightnessControls(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
container.read(signatureProvider.notifier)
..setContrast(1.3)
..setBrightness(0.2);
}

View File

@ -0,0 +1,82 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user chooses a signature image file
Future<void> theUserChoosesASignatureImageFile(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Simulate loading a tiny valid PNG/JPEG bytes; using 1x1 transparent PNG
final bytes = Uint8List.fromList([
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
]);
container.read(signatureProvider.notifier).setImageBytes(bytes);
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user chooses undo
Future<void> theUserChoosesUndo(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
if (sig.strokes.isNotEmpty) {
final newStrokes = List<List<Offset>>.from(sig.strokes)..removeLast();
container.read(signatureProvider.notifier).setStrokes(newStrokes);
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user clears the canvas
Future<void> theUserClearsTheCanvas(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
container.read(signatureProvider.notifier).setStrokes([]);
}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user drags handles to resize and drags to reposition
Future<void> theUserDragsHandlesToResizeAndDragsToReposition(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final sigN = container.read(signatureProvider.notifier);
final sig = container.read(signatureProvider);
TestWorld.prevCenter = sig.rect?.center;
sigN.resize(const Offset(50, 30));
sigN.drag(const Offset(20, -10));
}

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/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user draws strokes and confirms
Future<void> theUserDrawsStrokesAndConfirms(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Simulate drawn signature bytes
final bytes = Uint8List.fromList([1, 2, 3]);
container.read(signatureProvider.notifier).setImageBytes(bytes);
}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user enables aspect ratio lock and resizes
Future<void> theUserEnablesAspectRatioLockAndResizes(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
final sigN = container.read(signatureProvider.notifier);
final sig = container.read(signatureProvider);
TestWorld.prevAspect = sig.rect!.width / sig.rect!.height;
sigN.toggleAspect(true);
sigN.resize(const Offset(100, 50));
}

View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user enables background removal
Future<void> theUserEnablesBackgroundRemoval(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
container.read(signatureProvider.notifier).setBgRemoval(true);
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/features/pdf/viewer.dart';
import '_world.dart';
/// Usage: the user is notified of the issue
Future<void> theUserIsNotifiedOfTheIssue(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
final sig = container.read(signatureProvider);
// For our logic simulation: invalid selections result in no usable bytes
expect(sig.imageBytes == null || sig.imageBytes!.isEmpty, isTrue);
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user is notified that the export is still in progress
Future<void> theUserIsNotifiedThatTheExportIsStillInProgress(
WidgetTester tester,
) async {
expect(TestWorld.exportInProgress, isTrue);
}

View File

@ -0,0 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user is notified there is nothing to save
Future<void> theUserIsNotifiedThereIsNothingToSave(WidgetTester tester) async {
expect(TestWorld.nothingToSaveAttempt, isTrue);
}

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