From cf0889d4a993df0b16354827bc689ff30e4ba5e6 Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Sat, 11 Apr 2026 16:11:06 -0400 Subject: [PATCH] feat: post-write consistency hardening (Stage 2A) Add _refreshSelection() to ProductPublishingController to preserve and refresh selectedDraft by id after all write-triggered reloads. Selection stays on the same product with latest data, or auto-selects first visible item if the original leaves the active filter. - 11 new post-write consistency tests (234 total) - dart analyze clean --- docs/development/build_execution_tracker.md | 41 ++-- docs/development/master_development_brief.md | 40 ++-- .../product_publishing_controller.dart | 7 +- .../product_publishing_controller_test.dart | 188 ++++++++++++++++++ 4 files changed, 223 insertions(+), 53 deletions(-) diff --git a/docs/development/build_execution_tracker.md b/docs/development/build_execution_tracker.md index 6d334d3..871763e 100644 --- a/docs/development/build_execution_tracker.md +++ b/docs/development/build_execution_tracker.md @@ -2,16 +2,17 @@ ## Current status -- main baseline updated through: description-only-edit -- current branch: feat/category-only-edit (implementation complete, pending merge) +- main baseline updated through: category-only-edit +- main baseline commit: `8e7e4cb` (2026-04-11) - next branch: feat/post-write-consistency -- current stage: Stage 1 — Web application completion (Stage 1B complete) +- current stage: Stage 2 — Web application operational hardening ## Slice tracker ### feat/description-only-edit - status: merged to main +- commit: `cebac4c` - inspection: complete - implementation: complete - tests: passed (212/212) @@ -20,34 +21,22 @@ ### feat/category-only-edit -- status: implementation complete, pending merge to main +- status: merged to main +- commit: `8e7e4cb` - inspection: complete - implementation: complete - tests: passed (223/223 feature_wordpress, 5/5 kell_web dashboard) - analyze: passed (dart analyze — no issues found) - brief updated: yes -- changed files: - - `lib/src/domain/product_publishing_repository.dart` — added `updateProductCategory` contract - - `lib/src/application/update_product_category.dart` — new use case (created) - - `lib/src/application/product_publishing_controller.dart` — added `CategoryActionResult`, `updateCategory`, `lastCategoryResult`, `consumeCategoryResult` - - `lib/src/data/fake_product_publishing_repository.dart` — implemented `updateProductCategory` - - `lib/src/data/wordpress_product_publishing_repository.dart` — implemented `updateProductCategory` (WP API mapping) - - `lib/src/presentation/widgets/product_preview_panel.dart` — added inline category edit UI (`onCategoryChanged`, `_buildCategoryRow`) - - `lib/src/presentation/product_publishing_page.dart` — wired `UpdateProductCategory` use case and `onCategoryChanged` callback - - `lib/src/presentation/widgets/status_action_snack_bar.dart` — added `showCategoryActionSnackBar` - - `lib/feature_wordpress.dart` — barrel export for `update_product_category.dart` - - `test/product_publishing_controller_test.dart` — added `updateCategory` test group (5 tests) - - `test/fake_product_publishing_repository_test.dart` — added `updateProductCategory` test group (6 tests) - - `test/widgets/product_publishing_page_test.dart` — updated mock to include `updateProductCategory` - - `apps/kell_web/test/dashboard/application/dashboard_controller_test.dart` — updated mock to include `updateProductCategory` - - `docs/development/master_development_brief.md` — updated baseline, validation state, next branch - - `docs/development/build_execution_tracker.md` — this file ### feat/post-write-consistency -- status: queued (Stage 2A) -- inspection: pending -- implementation: pending -- tests: pending -- analyze: pending -- brief updated: no +- status: implemented — ready for review / merge +- inspection: complete +- implementation: complete +- files changed: + - `feature_wordpress/lib/src/application/product_publishing_controller.dart` — added `_refreshSelection()` helper; wired into `load()` and all write methods to preserve/refresh `selectedDraft` by id after reload + - `feature_wordpress/test/product_publishing_controller_test.dart` — added 11 post-write consistency tests covering selection preservation, field refresh (status/price/name/description/category), lastModified, filter/search/sort persistence after writes, item repositioning under active sort, and filter-exit auto-reselection +- tests: passed (234/234 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 84b3e72..6495b9b 100644 --- a/docs/development/master_development_brief.md +++ b/docs/development/master_development_brief.md @@ -78,7 +78,7 @@ Rules: - Search/filter/sort refinement landed. - Name-only product edit landed. - ✅ Description-only product edit landed (Stage 1A complete — merged `feat/description-only-edit` → `main` at `cebac4c`, 2026-04-11). -- ✅ Category-only product edit implemented (Stage 1B — `feat/category-only-edit`, pending merge to `main`, 2026-04-11). +- ✅ Category-only product edit landed (Stage 1B complete — merged `feat/category-only-edit` → `main` at `8e7e4cb`, 2026-04-11). Stage 1 complete. ### Current narrow edit capabilities on `main` @@ -86,21 +86,28 @@ Rules: - update product price only - update product name only - update product description only -- update product category only _(on `feat/category-only-edit`, pending merge)_ +- update product category only -### Latest known validation state on `feat/category-only-edit` +### Latest known validation state on `main` - `dart analyze` clean - `feature_wordpress` tests passing - `kell_web` dashboard tests passing - latest reported count for `feature_wordpress`: `223/223 passed` - latest reported count for `kell_web` dashboard tests: `5/5 passed` -- baseline: branched from `main` at `cebac4c` (2026-04-11) +- baseline commit: `8e7e4cb` (2026-04-11) + +### Post-write consistency (Stage 2A) — on branch `feat/post-write-consistency` + +- `dart analyze` clean +- `feature_wordpress` tests: `234/234 passed` +- controller `_refreshSelection()` preserves/refreshes selection after all writes +- 11 new post-write consistency tests added ### Next recommended branch -**`feat/post-write-consistency`** — Stage 2A: Post-write consistency hardening. -Branch from `main` after merging `feat/category-only-edit`. This completes Stage 1 (controlled product editing) and moves to Stage 2 (operational hardening). +**`feat/publishing-ux-hardening`** — Stage 2B: Publishing workflow UX hardening. +Branch from `main` after merging `feat/post-write-consistency`. Stage 2A is complete on branch. --- @@ -141,25 +148,10 @@ Use one branch per edit slice: > Merged `feat/description-only-edit` → `main` at `cebac4c` (2026-04-11). > All artifacts delivered: repository contract, fake/WP repo implementations, use case, controller action/result handling, preview panel inline description edit UI, and targeted tests (212 total `feature_wordpress` tests passing). -#### Stage 1B — Category-only product edit ← **NEXT** +#### ~~Stage 1B — Category-only product edit~~ ✅ COMPLETE -##### Goal - -Allow updating product category only. - -##### Requirements - -- Use existing category representation only. -- Do not create full taxonomy management. -- Keep write mapping narrow inside WP repository. -- Use a simple constrained UI. -- Reuse the established narrow single-field edit pattern. - -##### Definition of done - -- category can be updated through a controlled workflow -- no taxonomy subsystem added -- tests/analyze clean +> Merged `feat/category-only-edit` → `main` at `8e7e4cb` (2026-04-11). +> All artifacts delivered: repository contract, fake/WP repo implementations, use case, controller action/result handling, preview panel inline category edit UI, snack bar feedback, and targeted tests (223 total `feature_wordpress` tests passing). Stage 1 complete. --- 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 261ac60..d340fe3 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 @@ -534,9 +534,10 @@ class ProductPublishingController extends ChangeNotifier { drafts = result; - // Keep selection valid; clear if the selected draft is no longer visible. - if (selectedDraft != null && !drafts.any((d) => d.id == selectedDraft!.id)) { - selectedDraft = null; + // Keep selection valid and refresh to latest values. + if (selectedDraft != null) { + final refreshed = drafts.where((d) => d.id == selectedDraft!.id).firstOrNull; + selectedDraft = refreshed; } } 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 bfc0055..0e63e23 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 @@ -829,6 +829,194 @@ void main() { }); }); + // ── Post-write consistency ─────────────────────────────────────────── + + group('post-write consistency', () { + test('selection preserved after status update', () async { + await controller.load(); + + // Select product 4 (draft). + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.id, '4'); + + // Publish it. + await controller.updateStatus('4', PublishStatus.published); + + // Selection should still point to the same product. + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + // And reflect the updated status. + expect(controller.selectedDraft!.status, PublishStatus.published); + }); + + test('selected draft reflects latest price after update', () async { + await controller.load(); + + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.price, 8.50); + + await controller.updatePrice('4', 12.00); + + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + expect(controller.selectedDraft!.price, 12.00); + }); + + test('selected draft reflects latest name after update', () async { + await controller.load(); + + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.name, 'Fabric Jar Gripper'); + + await controller.updateName('4', 'Premium Jar Gripper'); + + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + expect(controller.selectedDraft!.name, 'Premium Jar Gripper'); + }); + + test('selected draft reflects latest description after update', () async { + await controller.load(); + + controller.selectBySku('JG-BLU-004'); + final originalDesc = controller.selectedDraft!.description; + + await controller.updateDescription('4', 'Completely new description.'); + + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + expect(controller.selectedDraft!.description, 'Completely new description.'); + expect(controller.selectedDraft!.description, isNot(originalDesc)); + }); + + test('selected draft reflects latest category after update', () async { + await controller.load(); + + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.category, 'Kitchen Accessories'); + + await controller.updateCategory('4', 'Grippers'); + + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + expect(controller.selectedDraft!.category, 'Grippers'); + }); + + test('lastModified updates after write', () async { + await controller.load(); + + controller.selectBySku('JG-BLU-004'); + final originalModified = controller.selectedDraft!.lastModified; + + await controller.updatePrice('4', 15.00); + + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.lastModified.isAfter(originalModified), isTrue); + }); + + test('selection preserved with active filter after write within filter', () async { + await controller.load(); + + // Filter to drafts only. + controller.setFilter('draft'); + expect(controller.drafts.length, 2); + + // Select product 4 (a draft). + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.id, '4'); + + // Update its price — it stays a draft, so it stays in the filter. + await controller.updatePrice('4', 20.00); + + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + expect(controller.selectedDraft!.price, 20.00); + expect(controller.activeFilter, 'draft'); + }); + + test('selection moves when write causes item to leave active filter', () async { + await controller.load(); + + // Filter to drafts only. + controller.setFilter('draft'); + expect(controller.drafts.length, 2); + + // Select product 4 (a draft). + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.id, '4'); + + // Publish it — it leaves the 'draft' filter. + await controller.updateStatus('4', PublishStatus.published); + + // The original selection left the filter; load() auto-selects the + // first remaining visible draft. + expect(controller.activeFilter, 'draft'); + expect(controller.drafts.length, 1); + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, isNot('4')); + }); + + test('search persists after write', () async { + await controller.load(); + + controller.setSearchQuery('fabric'); + expect(controller.drafts.length, 1); + expect(controller.drafts.first.id, '4'); + + controller.selectDraft(controller.drafts.first); + + await controller.updatePrice('4', 25.00); + + // Search should still be active. + expect(controller.searchQuery, 'fabric'); + expect(controller.drafts.length, 1); + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.price, 25.00); + }); + + test('sort persists after write', () async { + await controller.load(); + + controller.setSort(ProductSortField.lastModified, ascending: false); + + // Select the first item (most recently modified). + controller.selectDraft(controller.drafts.first); + + // Update a different product's price — this changes its lastModified. + await controller.updatePrice('1', 99.00); + + // Sort should still be lastModified descending. + expect(controller.activeSortField, ProductSortField.lastModified); + expect(controller.sortAscending, false); + + // Product 1 should now be first (most recently modified). + expect(controller.drafts.first.id, '1'); + }); + + test('item repositions under active sort after name change', () async { + await controller.load(); + + // Default sort: name ascending. + // Order: Citrus, Fabric, Floral, Ocean, Skillet, Sublimated + expect(controller.drafts.first.name, 'Citrus Coaster Set'); + + // Select Fabric Jar Gripper (id 4, currently 2nd). + controller.selectBySku('JG-BLU-004'); + expect(controller.selectedDraft!.id, '4'); + + // Rename to 'Zzz Gripper' — should move to end of list. + await controller.updateName('4', 'Zzz Gripper'); + + // Selection should still be on the same product. + expect(controller.selectedDraft, isNotNull); + expect(controller.selectedDraft!.id, '4'); + expect(controller.selectedDraft!.name, 'Zzz Gripper'); + + // It should now be last in the sorted list. + expect(controller.drafts.last.id, '4'); + }); + }); + group('disposed guard', () { test('load does not notify after disposal', () async { // Start load, then immediately dispose.