diff --git a/kell_creations_apps/apps/kell_web/test/dashboard/application/dashboard_controller_test.dart b/kell_creations_apps/apps/kell_web/test/dashboard/application/dashboard_controller_test.dart index 6211e5b..95c0007 100644 --- a/kell_creations_apps/apps/kell_web/test/dashboard/application/dashboard_controller_test.dart +++ b/kell_creations_apps/apps/kell_web/test/dashboard/application/dashboard_controller_test.dart @@ -35,6 +35,10 @@ class _StubProductPublishingRepository implements ProductPublishingRepository { @override Future updateProductName(String id, String name) => throw UnimplementedError(); + + @override + Future updateProductDescription(String id, String description) => + throw UnimplementedError(); } class _StubOrdersRepository implements OrdersRepository { diff --git a/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart b/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart index c27e8dc..3d157f9 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart @@ -13,6 +13,7 @@ export 'src/domain/publish_status.dart'; // Application export 'src/application/product_publishing_controller.dart' show ProductSortField; +export 'src/application/update_product_description.dart'; export 'src/application/update_product_name.dart'; export 'src/application/update_product_price.dart'; export 'src/application/update_product_status.dart'; diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart index d35e2ab..b9198b9 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart @@ -4,6 +4,7 @@ import '../domain/product_draft.dart'; import '../domain/publish_status.dart'; import 'get_product_drafts.dart'; import 'publish_product.dart'; +import 'update_product_description.dart'; import 'update_product_name.dart'; import 'update_product_price.dart'; import 'update_product_status.dart'; @@ -71,6 +72,21 @@ class NameActionResult { }); } +/// The outcome of a description-update action. +/// +/// Consumed once by the UI to show a SnackBar, then cleared. +class DescriptionActionResult { + final bool success; + final String productName; + final String? errorMessage; + + const DescriptionActionResult({ + required this.success, + required this.productName, + this.errorMessage, + }); +} + /// Controller that manages the product publishing workspace state, including /// filtering by publish status, free-text search, and draft selection. class ProductPublishingController extends ChangeNotifier { @@ -79,6 +95,7 @@ class ProductPublishingController extends ChangeNotifier { final UpdateProductStatus _updateProductStatus; final UpdateProductPrice _updateProductPrice; final UpdateProductName _updateProductName; + final UpdateProductDescription _updateProductDescription; ProductPublishingController( this._getProductDrafts, @@ -86,6 +103,7 @@ class ProductPublishingController extends ChangeNotifier { this._updateProductStatus, this._updateProductPrice, this._updateProductName, + this._updateProductDescription, ); bool _disposed = false; @@ -149,6 +167,13 @@ class ProductPublishingController extends ChangeNotifier { /// to clear it. NameActionResult? lastNameResult; + /// The result of the last description-update action. + /// + /// Set after [updateDescription] completes. The UI should read this once to + /// show feedback (e.g. a SnackBar) and then call [consumeDescriptionResult] + /// to clear it. + DescriptionActionResult? lastDescriptionResult; + /// Clears [lastActionResult] so the same result is not shown twice. void consumeActionResult() { lastActionResult = null; @@ -164,6 +189,11 @@ class ProductPublishingController extends ChangeNotifier { lastNameResult = null; } + /// Clears [lastDescriptionResult] so the same result is not shown twice. + void consumeDescriptionResult() { + lastDescriptionResult = null; + } + /// Loads all product drafts and applies any current filter / search. Future load() async { isLoading = true; @@ -356,6 +386,35 @@ class ProductPublishingController extends ChangeNotifier { } } + /// Updates only the description of the product with [id]. + /// + /// Follows the same per-row updating pattern as [updateStatus]. + Future updateDescription(String id, String description) async { + if (updatingIds.contains(id)) return; + + final productName = _productNameById(id); + + updatingIds.add(id); + _safeNotify(); + + try { + await _updateProductDescription(id, description); + if (_disposed) return; + updatingIds.remove(id); + lastDescriptionResult = DescriptionActionResult(success: true, productName: productName); + await load(); + } catch (e) { + if (_disposed) return; + updatingIds.remove(id); + lastDescriptionResult = DescriptionActionResult( + success: false, + productName: productName, + errorMessage: e.toString(), + ); + _safeNotify(); + } + } + // ── Lifecycle ─────────────────────────────────────────────────────────── @override diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/application/update_product_description.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/application/update_product_description.dart new file mode 100644 index 0000000..a04820c --- /dev/null +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/application/update_product_description.dart @@ -0,0 +1,14 @@ +import '../domain/product_draft.dart'; +import '../domain/product_publishing_repository.dart'; + +/// Use case: update only the description of a single product by its [id]. +/// +/// This is a narrow description mutation — not a generic product edit. +class UpdateProductDescription { + final ProductPublishingRepository repository; + + UpdateProductDescription(this.repository); + + Future call(String id, String description) => + repository.updateProductDescription(id, description); +} diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/data/fake_product_publishing_repository.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/data/fake_product_publishing_repository.dart index f665da0..fc64f64 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/data/fake_product_publishing_repository.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/data/fake_product_publishing_repository.dart @@ -155,4 +155,19 @@ class FakeProductPublishingRepository implements ProductPublishingRepository { _drafts[index] = updated; return updated; } + + @override + Future updateProductDescription(String id, String description) async { + await Future.delayed(const Duration(milliseconds: 400)); + + final index = _drafts.indexWhere((d) => d.id == id); + if (index == -1) { + throw StateError('Draft with id $id not found'); + } + + final original = _drafts[index]; + final updated = original.copyWith(description: description, lastModified: DateTime.now()); + _drafts[index] = updated; + return updated; + } } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/data/wordpress_product_publishing_repository.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/data/wordpress_product_publishing_repository.dart index 10d527b..cd42f39 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/data/wordpress_product_publishing_repository.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/data/wordpress_product_publishing_repository.dart @@ -48,4 +48,10 @@ class WordPressProductPublishingRepository implements ProductPublishingRepositor final json = await _apiClient.updateProduct(id, {'name': name}); return _mapper.fromJson(json); } + + @override + Future updateProductDescription(String id, String description) async { + final json = await _apiClient.updateProduct(id, {'description': description}); + return _mapper.fromJson(json); + } } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/domain/product_publishing_repository.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/domain/product_publishing_repository.dart index 4cb596c..faef746 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/domain/product_publishing_repository.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/domain/product_publishing_repository.dart @@ -27,4 +27,9 @@ abstract class ProductPublishingRepository { /// /// Returns the updated [ProductDraft] reflecting the new name. Future updateProductName(String id, String name); + + /// Updates only the description of the product identified by [id]. + /// + /// Returns the updated [ProductDraft] reflecting the new description. + Future updateProductDescription(String id, String description); } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart index 8c2252e..0da7eef 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import '../application/get_product_drafts.dart'; import '../application/product_publishing_controller.dart'; import '../application/publish_product.dart'; +import '../application/update_product_description.dart'; import '../application/update_product_name.dart'; import '../application/update_product_price.dart'; import '../application/update_product_status.dart'; @@ -59,6 +60,7 @@ class _ProductPublishingPageState extends State { UpdateProductStatus(repo), UpdateProductPrice(repo), UpdateProductName(repo), + UpdateProductDescription(repo), ); controller.addListener(_onControllerChanged); @@ -104,6 +106,12 @@ class _ProductPublishingPageState extends State { controller.consumeNameResult(); showNameActionSnackBar(context, nameResult); } + + final descriptionResult = controller.lastDescriptionResult; + if (descriptionResult != null) { + controller.consumeDescriptionResult(); + showDescriptionActionSnackBar(context, descriptionResult); + } } @override @@ -240,6 +248,7 @@ class _ProductPublishingPageState extends State { onMoveToDraft: () => controller.updateStatus(selected.id, PublishStatus.draft), onPriceChanged: (price) => controller.updatePrice(selected.id, price), onNameChanged: (name) => controller.updateName(selected.id, name), + onDescriptionChanged: (desc) => controller.updateDescription(selected.id, desc), onViewPolicy: widget.onViewPolicy, ); } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart index 6078bbc..3e0582b 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart @@ -26,6 +26,9 @@ class ProductPreviewPanel extends StatefulWidget { /// Callback to update the product name. Receives the new name value. final ValueChanged? onNameChanged; + /// Callback to update the product description. Receives the new description value. + final ValueChanged? onDescriptionChanged; + /// Whether this product currently has an in-flight status update. /// When true, the action button is disabled and a progress indicator is shown. final bool isUpdating; @@ -40,6 +43,7 @@ class ProductPreviewPanel extends StatefulWidget { this.onMoveToDraft, this.onPriceChanged, this.onNameChanged, + this.onDescriptionChanged, this.isUpdating = false, this.onViewPolicy, }); @@ -55,11 +59,15 @@ class _ProductPreviewPanelState extends State { bool _editingName = false; late TextEditingController _nameController; + bool _editingDescription = false; + late TextEditingController _descriptionController; + @override void initState() { super.initState(); _priceController = TextEditingController(text: widget.draft.price.toStringAsFixed(2)); _nameController = TextEditingController(text: widget.draft.name); + _descriptionController = TextEditingController(text: widget.draft.description); } @override @@ -73,12 +81,18 @@ class _ProductPreviewPanelState extends State { _editingName = false; _nameController.text = widget.draft.name; } + if (oldWidget.draft.id != widget.draft.id || + oldWidget.draft.description != widget.draft.description) { + _editingDescription = false; + _descriptionController.text = widget.draft.description; + } } @override void dispose() { _priceController.dispose(); _nameController.dispose(); + _descriptionController.dispose(); super.dispose(); } @@ -108,6 +122,19 @@ class _ProductPreviewPanelState extends State { setState(() => _editingName = false); } + void _submitDescription() { + final trimmed = _descriptionController.text.trim(); + if (trimmed.isNotEmpty) { + widget.onDescriptionChanged?.call(trimmed); + setState(() => _editingDescription = false); + } + } + + void _cancelDescriptionEdit() { + _descriptionController.text = widget.draft.description; + setState(() => _editingDescription = false); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -139,9 +166,7 @@ class _ProductPreviewPanelState extends State { const SizedBox(height: KcSpacing.md), // ── Description ──────────────────────────────────────────── - Text('Description', style: theme.textTheme.titleLarge), - const SizedBox(height: KcSpacing.sm), - Text(draft.description, style: theme.textTheme.bodyLarge), + _buildDescriptionSection(context), const SizedBox(height: KcSpacing.xl), // ── Policy link ──────────────────────────────────────────── @@ -335,6 +360,77 @@ class _ProductPreviewPanelState extends State { ), ); } + + /// Builds the Description section — either a static display with an edit + /// icon, or a multi-line text field with save/cancel actions. + Widget _buildDescriptionSection(BuildContext context) { + final theme = Theme.of(context); + + if (_editingDescription) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Description', style: theme.textTheme.titleLarge), + const Spacer(), + IconButton( + icon: const Icon(Icons.check, size: 20), + onPressed: _submitDescription, + tooltip: 'Save description', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: _cancelDescriptionEdit, + tooltip: 'Cancel description edit', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: KcSpacing.sm), + TextField( + controller: _descriptionController, + maxLines: 5, + minLines: 3, + style: theme.textTheme.bodyLarge, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(8), + border: OutlineInputBorder(), + ), + autofocus: true, + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Description', style: theme.textTheme.titleLarge), + if (widget.onDescriptionChanged != null && !widget.isUpdating) ...[ + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.edit, size: 18), + onPressed: () => setState(() => _editingDescription = true), + tooltip: 'Edit description', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ], + ), + const SizedBox(height: KcSpacing.sm), + Text(widget.draft.description, style: theme.textTheme.bodyLarge), + ], + ); + } } class _MetadataRow extends StatelessWidget { diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart index e9d7cd9..d55bf5e 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart @@ -45,6 +45,42 @@ void showStatusActionSnackBar(BuildContext context, StatusActionResult result) { ); } +/// Shows a [SnackBar] for the given [DescriptionActionResult]. +void showDescriptionActionSnackBar(BuildContext context, DescriptionActionResult result) { + final messenger = ScaffoldMessenger.maybeOf(context); + if (messenger == null) return; + + final String message; + final Color backgroundColor; + final IconData icon; + + if (result.success) { + message = '${result.productName} description updated.'; + backgroundColor = KcColors.success; + icon = Icons.check_circle_outline; + } else { + message = 'Failed to update description for ${result.productName}.'; + backgroundColor = KcColors.danger; + icon = Icons.error_outline; + } + + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(icon, color: Colors.white, size: 20), + const SizedBox(width: 8), + Expanded(child: Text(message)), + ], + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + duration: result.success ? const Duration(seconds: 3) : const Duration(seconds: 5), + ), + ); +} + /// Past-tense verb for success messages (e.g. "X published successfully"). String _pastVerbForStatus(PublishStatus status) { switch (status) { diff --git a/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart b/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart index 5e52062..b1d7592 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart @@ -243,5 +243,61 @@ void main() { ); }); }); + + group('updateProductDescription', () { + test('updates description of the target product', () async { + final updated = await repository.updateProductDescription('4', 'A brand new description.'); + + expect(updated.id, '4'); + expect(updated.description, 'A brand new description.'); + }); + + test('persists the description change in the list', () async { + await repository.updateProductDescription('4', 'A brand new description.'); + + final drafts = await repository.getProductDrafts(); + final product4 = drafts.firstWhere((d) => d.id == '4'); + expect(product4.description, 'A brand new description.'); + }); + + test('preserves all fields except description and lastModified', () async { + final draftsBefore = await repository.getProductDrafts(); + final before = draftsBefore.firstWhere((d) => d.id == '4'); + + final updated = await repository.updateProductDescription('4', 'A brand new description.'); + + expect(updated.name, before.name); + expect(updated.price, before.price); + expect(updated.sku, before.sku); + expect(updated.category, before.category); + expect(updated.imageUrl, before.imageUrl); + expect(updated.status, before.status); + expect(updated.description, 'A brand new description.'); + expect(updated.lastModified.isAfter(before.lastModified), isTrue); + }); + + test('preserves other products unchanged', () async { + final draftsBefore = await repository.getProductDrafts(); + + await repository.updateProductDescription('4', 'A brand new description.'); + + final draftsAfter = await repository.getProductDrafts(); + for (final before in draftsBefore) { + if (before.id == '4') continue; + final after = draftsAfter.firstWhere((d) => d.id == before.id); + expect(after.description, before.description); + expect(after.name, before.name); + expect(after.price, before.price); + expect(after.status, before.status); + } + }); + + test('throws StateError for unknown id', () async { + expect( + () => repository.updateProductDescription('unknown', 'New desc'), + throwsA(isA()), + ); + }); + }); }); } diff --git a/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart b/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart index 9efea93..b88d320 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart @@ -17,6 +17,7 @@ void main() { UpdateProductStatus(repository), UpdateProductPrice(repository), UpdateProductName(repository), + UpdateProductDescription(repository), ); }); @@ -428,6 +429,69 @@ void main() { }); }); + group('updateDescription', () { + test('updates description and reloads', () async { + await controller.load(); + + // Product 4 starts with a known description. + await controller.updateDescription('4', 'A brand new description.'); + + final updated = controller.drafts.firstWhere((d) => d.id == '4'); + expect(updated.description, 'A brand new description.'); + expect(controller.error, isNull); + expect(controller.updatingIds, isEmpty); + }); + + test('sets lastDescriptionResult on success', () async { + await controller.load(); + + await controller.updateDescription('4', 'Updated desc.'); + + expect(controller.lastDescriptionResult, isNotNull); + expect(controller.lastDescriptionResult!.success, isTrue); + expect(controller.lastDescriptionResult!.productName, 'Fabric Jar Gripper'); + expect(controller.lastDescriptionResult!.errorMessage, isNull); + }); + + test('sets lastDescriptionResult on failure', () async { + await controller.load(); + + await controller.updateDescription('unknown', 'New desc.'); + + expect(controller.lastDescriptionResult, isNotNull); + expect(controller.lastDescriptionResult!.success, isFalse); + expect(controller.lastDescriptionResult!.errorMessage, isNotNull); + expect(controller.updatingIds, isEmpty); + expect(controller.error, isNull); + }); + + test('consumeDescriptionResult clears the result', () async { + await controller.load(); + + await controller.updateDescription('4', 'Updated desc.'); + expect(controller.lastDescriptionResult, isNotNull); + + controller.consumeDescriptionResult(); + expect(controller.lastDescriptionResult, isNull); + }); + + test('prevents duplicate calls while row is already updating', () async { + await controller.load(); + + final first = controller.updateDescription('4', 'First description'); + expect(controller.isUpdating('4'), isTrue); + + final second = controller.updateDescription('4', 'Second description'); + await first; + await second; + + // Only the first description should have been applied. + final updated = controller.drafts.firstWhere((d) => d.id == '4'); + expect(updated.description, 'First description'); + expect(controller.updatingIds, isEmpty); + }); + }); + // ── Search refinements ────────────────────────────────────────────── group('search: category matching', () { diff --git a/kell_creations_apps/packages/feature_wordpress/test/widgets/product_preview_panel_test.dart b/kell_creations_apps/packages/feature_wordpress/test/widgets/product_preview_panel_test.dart index 4f2e470..8f12685 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/widgets/product_preview_panel_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/widgets/product_preview_panel_test.dart @@ -72,6 +72,7 @@ void main() { VoidCallback? onMoveToDraft, ValueChanged? onPriceChanged, ValueChanged? onNameChanged, + ValueChanged? onDescriptionChanged, bool isUpdating = false, }) { return MaterialApp( @@ -84,6 +85,7 @@ void main() { onMoveToDraft: onMoveToDraft, onPriceChanged: onPriceChanged, onNameChanged: onNameChanged, + onDescriptionChanged: onDescriptionChanged, isUpdating: isUpdating, ), ), @@ -461,4 +463,101 @@ void main() { expect(find.byType(TextField), findsOneWidget); }); }); + + group('description editing', () { + testWidgets('shows edit description icon when onDescriptionChanged is provided', ( + tester, + ) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {})); + expect(find.byTooltip('Edit description'), findsOneWidget); + }); + + testWidgets('hides edit description icon when onDescriptionChanged is null', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft)); + expect(find.byTooltip('Edit description'), findsNothing); + }); + + testWidgets('hides edit description icon while updating', (tester) async { + await tester.pumpWidget( + buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}, isUpdating: true), + ); + expect(find.byTooltip('Edit description'), findsNothing); + }); + + testWidgets('tapping edit description icon shows text field with current description', ( + tester, + ) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + // Should show a multi-line TextField. + final textFields = find.byType(TextField); + expect(textFields, findsOneWidget); + final textField = tester.widget(textFields); + expect(textField.controller!.text, 'A beautifully crafted test product.'); + }); + + testWidgets('tapping check icon submits the new description', (tester) async { + String? receivedDescription; + await tester.pumpWidget( + buildTestWidget(sampleDraft, onDescriptionChanged: (d) => receivedDescription = d), + ); + + // Enter edit mode. + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + // Clear and type a new description. + await tester.enterText(find.byType(TextField), 'Updated description text.'); + await tester.tap(find.byTooltip('Save description')); + await tester.pump(); + + expect(receivedDescription, 'Updated description text.'); + // Should exit edit mode — no more text field. + expect(find.byType(TextField), findsNothing); + }); + + testWidgets('tapping close icon cancels description editing', (tester) async { + String? receivedDescription; + await tester.pumpWidget( + buildTestWidget(sampleDraft, onDescriptionChanged: (d) => receivedDescription = d), + ); + + // Enter edit mode. + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + // Type a new description but cancel. + await tester.enterText(find.byType(TextField), 'Cancelled description'); + await tester.tap(find.byTooltip('Cancel description edit')); + await tester.pump(); + + expect(receivedDescription, isNull); + // Should exit edit mode and show original description. + expect(find.byType(TextField), findsNothing); + expect(find.text('A beautifully crafted test product.'), findsOneWidget); + }); + + testWidgets('does not submit empty description', (tester) async { + String? receivedDescription; + await tester.pumpWidget( + buildTestWidget(sampleDraft, onDescriptionChanged: (d) => receivedDescription = d), + ); + + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + // Clear the field and try to submit. + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save description')); + await tester.pump(); + + // Should not have called the callback. + expect(receivedDescription, isNull); + // Should still be in edit mode. + expect(find.byType(TextField), findsOneWidget); + }); + }); } diff --git a/kell_creations_apps/packages/feature_wordpress/test/widgets/product_publishing_page_test.dart b/kell_creations_apps/packages/feature_wordpress/test/widgets/product_publishing_page_test.dart index 2f41e54..8ae3c63 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/widgets/product_publishing_page_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/widgets/product_publishing_page_test.dart @@ -29,6 +29,10 @@ class _FailingRepository implements ProductPublishingRepository { @override Future updateProductName(String id, String name) async => throw UnimplementedError(); + + @override + Future updateProductDescription(String id, String description) async => + throw UnimplementedError(); } void main() {