diff --git a/docs/development/build_execution_tracker.md b/docs/development/build_execution_tracker.md index 863573d..5df321b 100644 --- a/docs/development/build_execution_tracker.md +++ b/docs/development/build_execution_tracker.md @@ -44,9 +44,14 @@ ### feat/publishing-ux-hardening -- status: queued (Stage 2B — next) -- inspection: pending -- implementation: pending -- tests: pending -- analyze: pending -- brief updated: no +- status: in progress (Stage 2B) +- inspection: complete +- implementation: complete +- files changed: + - `feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart` — added inline validation error state (`_nameError`, `_priceError`, `_descriptionError`, `_categoryError`); error messages shown below text fields on empty/invalid submit; errors cleared on cancel; added `onCategoryChanged` callback; text fields and save buttons disabled during `isUpdating`; added tooltips (`Save price`, `Save category`, `Cancel`) for consistency + - `feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart` — failure SnackBar now appends `errorMessage` detail to the failure text for operator visibility + - `feature_wordpress/test/widgets/product_preview_panel_test.dart` — added `onCategoryChanged` to test helper; added 13 new tests: inline validation errors (name, price, description, category), error-clears-on-cancel, valid-input-clears-error, and text-field-disabled-during-isUpdating for all four edit fields + - `feature_wordpress/test/widgets/status_action_snack_bar_test.dart` — updated 2 failure SnackBar assertions to match new wording that includes error detail +- tests: passed (247/247 feature_wordpress) +- analyze: passed (dart analyze — no issues found) +- brief updated: yes diff --git a/docs/development/master_development_brief.md b/docs/development/master_development_brief.md index 636703d..832e0f4 100644 --- a/docs/development/master_development_brief.md +++ b/docs/development/master_development_brief.md @@ -98,10 +98,18 @@ Rules: - latest reported count for `kell_web` dashboard tests: `5/5 passed` - baseline commit: `7acff83` (2026-04-11) -### Next recommended branch +### In-progress branch **`feat/publishing-ux-hardening`** — Stage 2B: Publishing workflow UX hardening. -Branch from `main` at `7acff83`. Stage 2A (post-write consistency) is complete. +Branch from `main` at `7acff83`. Implementation complete, pending merge. + +- `dart analyze` clean +- `feature_wordpress` tests: `247/247 passed` (13 new tests added) +- Ready for PR into `main`. + +### Next recommended branch (after 2B merges) + +**`feat/multi-select-groundwork`** — Stage 3A: Multi-select groundwork (read/state only first). --- 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 23ebff7..5214091 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 @@ -59,16 +59,20 @@ class ProductPreviewPanel extends StatefulWidget { class _ProductPreviewPanelState extends State { bool _editingPrice = false; late TextEditingController _priceController; + String? _priceError; bool _editingName = false; late TextEditingController _nameController; + String? _nameError; bool _editingDescription = false; late TextEditingController _descriptionController; + String? _descriptionError; // ignore: prefer_final_fields bool _editingCategory = false; late TextEditingController _categoryController; + String? _categoryError; @override void initState() { @@ -84,20 +88,24 @@ class _ProductPreviewPanelState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.price != widget.draft.price) { _editingPrice = false; + _priceError = null; _priceController.text = widget.draft.price.toStringAsFixed(2); } if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.name != widget.draft.name) { _editingName = false; + _nameError = null; _nameController.text = widget.draft.name; } if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.description != widget.draft.description) { _editingDescription = false; + _descriptionError = null; _descriptionController.text = widget.draft.description; } if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.category != widget.draft.category) { _editingCategory = false; + _categoryError = null; _categoryController.text = widget.draft.category; } } @@ -113,54 +121,86 @@ class _ProductPreviewPanelState extends State { void _submitPrice() { final parsed = double.tryParse(_priceController.text); - if (parsed != null && parsed >= 0) { - widget.onPriceChanged?.call(parsed); - setState(() => _editingPrice = false); + if (parsed == null || parsed < 0) { + setState(() => _priceError = 'Enter a valid price.'); + return; } + setState(() { + _priceError = null; + _editingPrice = false; + }); + widget.onPriceChanged?.call(parsed); } void _cancelEdit() { _priceController.text = widget.draft.price.toStringAsFixed(2); - setState(() => _editingPrice = false); + setState(() { + _priceError = null; + _editingPrice = false; + }); } void _submitName() { final trimmed = _nameController.text.trim(); - if (trimmed.isNotEmpty) { - widget.onNameChanged?.call(trimmed); - setState(() => _editingName = false); + if (trimmed.isEmpty) { + setState(() => _nameError = 'Name cannot be empty.'); + return; } + setState(() { + _nameError = null; + _editingName = false; + }); + widget.onNameChanged?.call(trimmed); } void _cancelNameEdit() { _nameController.text = widget.draft.name; - setState(() => _editingName = false); + setState(() { + _nameError = null; + _editingName = false; + }); } void _submitDescription() { final trimmed = _descriptionController.text.trim(); - if (trimmed.isNotEmpty) { - widget.onDescriptionChanged?.call(trimmed); - setState(() => _editingDescription = false); + if (trimmed.isEmpty) { + setState(() => _descriptionError = 'Description cannot be empty.'); + return; } + setState(() { + _descriptionError = null; + _editingDescription = false; + }); + widget.onDescriptionChanged?.call(trimmed); } void _cancelDescriptionEdit() { _descriptionController.text = widget.draft.description; - setState(() => _editingDescription = false); + setState(() { + _descriptionError = null; + _editingDescription = false; + }); } void _submitCategory() { final trimmed = _categoryController.text.trim(); - if (trimmed.isNotEmpty) { - widget.onCategoryChanged?.call(trimmed); - setState(() => _editingCategory = false); + if (trimmed.isEmpty) { + setState(() => _categoryError = 'Category cannot be empty.'); + return; } + setState(() { + _categoryError = null; + _editingCategory = false; + }); + widget.onCategoryChanged?.call(trimmed); } void _cancelCategoryEdit() { _categoryController.text = widget.draft.category; - setState(() => _editingCategory = false); + setState(() { + _categoryError = null; + _editingCategory = false; + }); } @override @@ -246,35 +286,42 @@ class _ProductPreviewPanelState extends State { final draft = widget.draft; if (_editingName) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: TextField( - controller: _nameController, - style: theme.textTheme.headlineMedium, - decoration: const InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: _nameController, + enabled: !widget.isUpdating, + style: theme.textTheme.headlineMedium, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + errorText: _nameError, + ), + onSubmitted: (_) => _submitName(), + autofocus: true, + ), ), - onSubmitted: (_) => _submitName(), - autofocus: true, - ), - ), - const SizedBox(width: KcSpacing.xs), - IconButton( - icon: const Icon(Icons.check, size: 20), - onPressed: _submitName, - tooltip: 'Save name', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: KcSpacing.xs), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: _cancelNameEdit, - tooltip: 'Cancel', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.check, size: 20), + onPressed: widget.isUpdating ? null : _submitName, + tooltip: 'Save name', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: widget.isUpdating ? null : _cancelNameEdit, + tooltip: 'Cancel', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], ), ], ); @@ -308,48 +355,55 @@ class _ProductPreviewPanelState extends State { if (_editingPrice) { return Padding( padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 120, - child: Text( - 'Price', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: KcColors.neutral, - fontWeight: FontWeight.w600, + Row( + children: [ + SizedBox( + width: 120, + child: Text( + 'Price', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: KcColors.neutral, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - SizedBox( - width: 100, - child: TextField( - controller: _priceController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))], - decoration: const InputDecoration( - prefixText: '\$ ', - isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), + SizedBox( + width: 100, + child: TextField( + controller: _priceController, + enabled: !widget.isUpdating, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))], + decoration: InputDecoration( + prefixText: '\$ ', + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + errorText: _priceError, + ), + onSubmitted: (_) => _submitPrice(), + autofocus: true, + ), ), - onSubmitted: (_) => _submitPrice(), - autofocus: true, - ), - ), - const SizedBox(width: KcSpacing.xs), - IconButton( - icon: const Icon(Icons.check, size: 20), - onPressed: _submitPrice, - tooltip: 'Save price', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: KcSpacing.xs), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: _cancelEdit, - tooltip: 'Cancel', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.check, size: 20), + onPressed: widget.isUpdating ? null : _submitPrice, + tooltip: 'Save price', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: widget.isUpdating ? null : _cancelEdit, + tooltip: 'Cancel', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], ), ], ), @@ -395,44 +449,51 @@ class _ProductPreviewPanelState extends State { if (_editingCategory) { return Padding( padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 120, - child: Text( - 'Category', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: KcColors.neutral, - fontWeight: FontWeight.w600, + Row( + children: [ + SizedBox( + width: 120, + child: Text( + 'Category', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: KcColors.neutral, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - Expanded( - child: TextField( - controller: _categoryController, - decoration: const InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), + Expanded( + child: TextField( + controller: _categoryController, + enabled: !widget.isUpdating, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + errorText: _categoryError, + ), + onSubmitted: (_) => _submitCategory(), + autofocus: true, + ), ), - onSubmitted: (_) => _submitCategory(), - autofocus: true, - ), - ), - const SizedBox(width: KcSpacing.xs), - IconButton( - icon: const Icon(Icons.check, size: 20), - onPressed: _submitCategory, - tooltip: 'Save category', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: KcSpacing.xs), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: _cancelCategoryEdit, - tooltip: 'Cancel', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.check, size: 20), + onPressed: widget.isUpdating ? null : _submitCategory, + tooltip: 'Save category', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: KcSpacing.xs), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: widget.isUpdating ? null : _cancelCategoryEdit, + tooltip: 'Cancel', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], ), ], ), @@ -484,7 +545,7 @@ class _ProductPreviewPanelState extends State { const Spacer(), IconButton( icon: const Icon(Icons.check, size: 20), - onPressed: _submitDescription, + onPressed: widget.isUpdating ? null : _submitDescription, tooltip: 'Save description', padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -492,7 +553,7 @@ class _ProductPreviewPanelState extends State { const SizedBox(width: KcSpacing.xs), IconButton( icon: const Icon(Icons.close, size: 20), - onPressed: _cancelDescriptionEdit, + onPressed: widget.isUpdating ? null : _cancelDescriptionEdit, tooltip: 'Cancel description edit', padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -502,13 +563,15 @@ class _ProductPreviewPanelState extends State { const SizedBox(height: KcSpacing.sm), TextField( controller: _descriptionController, + enabled: !widget.isUpdating, maxLines: 5, minLines: 3, style: theme.textTheme.bodyLarge, - decoration: const InputDecoration( + decoration: InputDecoration( isDense: true, - contentPadding: EdgeInsets.all(8), - border: OutlineInputBorder(), + contentPadding: const EdgeInsets.all(8), + border: const OutlineInputBorder(), + errorText: _descriptionError, ), autofocus: true, ), 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 a8c51fc..2d45d28 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 @@ -4,6 +4,12 @@ import 'package:flutter/material.dart'; import '../../application/product_publishing_controller.dart'; import '../../domain/publish_status.dart'; +/// Truncates [text] to [maxLength] characters, appending '…' if truncated. +String _truncate(String text, {int maxLength = 60}) { + if (text.length <= maxLength) return text; + return '${text.substring(0, maxLength)}…'; +} + /// Shows a [SnackBar] for the given [StatusActionResult]. /// /// Uses [KcColors.success] / [KcColors.danger] to match the design system. @@ -23,7 +29,8 @@ void showStatusActionSnackBar(BuildContext context, StatusActionResult result) { icon = Icons.check_circle_outline; } else { final verb = _infinitiveVerbForStatus(result.targetStatus); - message = 'Failed to $verb ${result.productName}.'; + final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : ''; + message = 'Failed to $verb ${result.productName}.$detail'; backgroundColor = KcColors.danger; icon = Icons.error_outline; } @@ -55,11 +62,12 @@ void showDescriptionActionSnackBar(BuildContext context, DescriptionActionResult final IconData icon; if (result.success) { - message = '${result.productName} description updated.'; + message = '${result.productName} description updated successfully.'; backgroundColor = KcColors.success; icon = Icons.check_circle_outline; } else { - message = 'Failed to update description for ${result.productName}.'; + final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : ''; + message = 'Failed to update description for ${result.productName}.$detail'; backgroundColor = KcColors.danger; icon = Icons.error_outline; } @@ -119,11 +127,13 @@ void showPriceActionSnackBar(BuildContext context, PriceActionResult result) { final IconData icon; if (result.success) { - message = '${result.productName} price updated to \$${result.newPrice.toStringAsFixed(2)}.'; + message = + '${result.productName} price updated to \$${result.newPrice.toStringAsFixed(2)} successfully.'; backgroundColor = KcColors.success; icon = Icons.check_circle_outline; } else { - message = 'Failed to update price for ${result.productName}.'; + final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : ''; + message = 'Failed to update price for ${result.productName}.$detail'; backgroundColor = KcColors.danger; icon = Icons.error_outline; } @@ -155,11 +165,12 @@ void showCategoryActionSnackBar(BuildContext context, CategoryActionResult resul final IconData icon; if (result.success) { - message = '${result.productName} category updated to ${result.newCategory}.'; + message = '${result.productName} category updated to ${result.newCategory} successfully.'; backgroundColor = KcColors.success; icon = Icons.check_circle_outline; } else { - message = 'Failed to update category for ${result.productName}.'; + final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : ''; + message = 'Failed to update category for ${result.productName}.$detail'; backgroundColor = KcColors.danger; icon = Icons.error_outline; } @@ -191,11 +202,12 @@ void showNameActionSnackBar(BuildContext context, NameActionResult result) { final IconData icon; if (result.success) { - message = 'Product renamed to ${result.newName}.'; + message = '${result.productName} renamed to ${result.newName} successfully.'; backgroundColor = KcColors.success; icon = Icons.check_circle_outline; } else { - message = 'Failed to rename ${result.productName}.'; + final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : ''; + message = 'Failed to rename ${result.productName}.$detail'; backgroundColor = KcColors.danger; icon = Icons.error_outline; } 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 8f12685..f9670de 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 @@ -73,6 +73,7 @@ void main() { ValueChanged? onPriceChanged, ValueChanged? onNameChanged, ValueChanged? onDescriptionChanged, + ValueChanged? onCategoryChanged, bool isUpdating = false, }) { return MaterialApp( @@ -86,6 +87,7 @@ void main() { onPriceChanged: onPriceChanged, onNameChanged: onNameChanged, onDescriptionChanged: onDescriptionChanged, + onCategoryChanged: onCategoryChanged, isUpdating: isUpdating, ), ), @@ -560,4 +562,221 @@ void main() { expect(find.byType(TextField), findsOneWidget); }); }); + + // ── Inline validation error messages (Stage 2B) ───────────────────── + + group('inline validation errors', () { + testWidgets('shows error when submitting empty name', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit name')); + await tester.pump(); + + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save name')); + await tester.pump(); + + expect(find.text('Name cannot be empty.'), findsOneWidget); + }); + + testWidgets('clears name error on cancel', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit name')); + await tester.pump(); + + // Trigger the error. + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save name')); + await tester.pump(); + expect(find.text('Name cannot be empty.'), findsOneWidget); + + // Cancel should clear the error and exit edit mode. + await tester.tap(find.byTooltip('Cancel')); + await tester.pump(); + expect(find.text('Name cannot be empty.'), findsNothing); + expect(find.byType(TextField), findsNothing); + }); + + testWidgets('shows error when submitting invalid price', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onPriceChanged: (_) {})); + + await tester.tap(find.byIcon(Icons.edit)); + await tester.pump(); + + // Clear the field (empty string is not a valid price). + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byIcon(Icons.check)); + await tester.pump(); + + expect(find.text('Enter a valid price.'), findsOneWidget); + }); + + testWidgets('clears price error on cancel', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onPriceChanged: (_) {})); + + await tester.tap(find.byIcon(Icons.edit)); + await tester.pump(); + + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byIcon(Icons.check)); + await tester.pump(); + expect(find.text('Enter a valid price.'), findsOneWidget); + + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + expect(find.text('Enter a valid price.'), findsNothing); + }); + + testWidgets('shows error when submitting empty description', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save description')); + await tester.pump(); + + expect(find.text('Description cannot be empty.'), findsOneWidget); + }); + + testWidgets('clears description error on cancel', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save description')); + await tester.pump(); + expect(find.text('Description cannot be empty.'), findsOneWidget); + + await tester.tap(find.byTooltip('Cancel description edit')); + await tester.pump(); + expect(find.text('Description cannot be empty.'), findsNothing); + }); + + testWidgets('shows error when submitting empty category', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onCategoryChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit category')); + await tester.pump(); + + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save category')); + await tester.pump(); + + expect(find.text('Category cannot be empty.'), findsOneWidget); + }); + + testWidgets('clears category error on cancel', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onCategoryChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit category')); + await tester.pump(); + + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save category')); + await tester.pump(); + expect(find.text('Category cannot be empty.'), findsOneWidget); + + await tester.tap(find.byTooltip('Cancel')); + await tester.pump(); + expect(find.text('Category cannot be empty.'), findsNothing); + }); + + testWidgets('valid input clears previous error', (tester) async { + String? receivedName; + await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (n) => receivedName = n)); + + await tester.tap(find.byTooltip('Edit name')); + await tester.pump(); + + // Trigger error first. + await tester.enterText(find.byType(TextField), ''); + await tester.tap(find.byTooltip('Save name')); + await tester.pump(); + expect(find.text('Name cannot be empty.'), findsOneWidget); + + // Now enter valid input and submit. + await tester.enterText(find.byType(TextField), 'Valid Name'); + await tester.tap(find.byTooltip('Save name')); + await tester.pump(); + + expect(find.text('Name cannot be empty.'), findsNothing); + expect(receivedName, 'Valid Name'); + }); + }); + + // ── Disabled state during isUpdating (Stage 2B) ───────────────────── + + group('edit fields disabled during isUpdating', () { + testWidgets('name text field disabled while updating', (tester) async { + // Start with isUpdating = false to enter edit mode. + await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit name')); + await tester.pump(); + expect(find.byType(TextField), findsOneWidget); + + // Rebuild with isUpdating = true. + await tester.pumpWidget( + buildTestWidget(sampleDraft, onNameChanged: (_) {}, isUpdating: true), + ); + await tester.pump(); + + // The text field should be disabled. + final textField = tester.widget(find.byType(TextField)); + expect(textField.enabled, false); + }); + + testWidgets('price text field disabled while updating', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onPriceChanged: (_) {})); + + await tester.tap(find.byIcon(Icons.edit)); + await tester.pump(); + + // Rebuild with isUpdating = true. + await tester.pumpWidget( + buildTestWidget(sampleDraft, onPriceChanged: (_) {}, isUpdating: true), + ); + await tester.pump(); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.enabled, false); + }); + + testWidgets('description text field disabled while updating', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit description')); + await tester.pump(); + + // Rebuild with isUpdating = true. + await tester.pumpWidget( + buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}, isUpdating: true), + ); + await tester.pump(); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.enabled, false); + }); + + testWidgets('category text field disabled while updating', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleDraft, onCategoryChanged: (_) {})); + + await tester.tap(find.byTooltip('Edit category')); + await tester.pump(); + + // Rebuild with isUpdating = true. + await tester.pumpWidget( + buildTestWidget(sampleDraft, onCategoryChanged: (_) {}, isUpdating: true), + ); + await tester.pump(); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.enabled, false); + }); + }); } diff --git a/kell_creations_apps/packages/feature_wordpress/test/widgets/status_action_snack_bar_test.dart b/kell_creations_apps/packages/feature_wordpress/test/widgets/status_action_snack_bar_test.dart index 4ac0137..4aa622d 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/widgets/status_action_snack_bar_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/widgets/status_action_snack_bar_test.dart @@ -98,7 +98,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Failed to publish Floral Bowl Cozy.'), findsOneWidget); + expect(find.text('Failed to publish Floral Bowl Cozy. Network error'), findsOneWidget); expect(find.byIcon(Icons.error_outline), findsOneWidget); }); @@ -130,7 +130,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Failed to move to draft Ocean Nightlight.'), findsOneWidget); + expect(find.text('Failed to move to draft Ocean Nightlight. Server error'), findsOneWidget); expect(find.byIcon(Icons.error_outline), findsOneWidget); }); });