From 36277a9e67bcdb40af8dcf4663ad6a4834478614 Mon Sep 17 00:00:00 2001 From: Nico Date: Sat, 28 Mar 2026 23:41:03 -0700 Subject: [PATCH] fix(ui): manage household store items in store settings --- .../controllers/available-items.controller.js | 12 +- backend/models/available-item.model.js | 174 +++++++++++++----- backend/models/list.model.v2.js | 3 +- backend/tests/available-item.model.test.js | 43 ++++- .../tests/available-items.controller.test.js | 18 +- .../src/components/manage/ManageStores.jsx | 2 +- .../manage/StoreAvailableItemsManager.jsx | 102 ++-------- .../modals/AvailableItemEditorModal.jsx | 3 +- .../tests/available-items-catalog.spec.ts | 119 ++++++------ 9 files changed, 281 insertions(+), 195 deletions(-) diff --git a/backend/controllers/available-items.controller.js b/backend/controllers/available-items.controller.js index 8242519..f06767c 100644 --- a/backend/controllers/available-items.controller.js +++ b/backend/controllers/available-items.controller.js @@ -276,12 +276,16 @@ exports.deleteAvailableItem = async (req, res) => { return sendError(res, 400, "Item ID must be a positive integer"); } - const deleted = await AvailableItems.deleteAvailableItem(householdId, storeId, itemId); - if (!deleted) { - return sendError(res, 404, "Available item not found"); + const [deletedCatalogEntry, deletedClassification] = await Promise.all([ + AvailableItems.deleteAvailableItem(householdId, storeId, itemId), + List.deleteClassification(householdId, storeId, itemId), + ]); + + if (!deletedCatalogEntry && !deletedClassification) { + return sendError(res, 404, "Managed item settings not found"); } - res.json({ message: "Available item removed" }); + res.json({ message: "Store item settings cleared" }); } catch (error) { if (isCatalogTableMissing(error)) { return sendError( diff --git a/backend/models/available-item.model.js b/backend/models/available-item.model.js index 764475f..f9cf29a 100644 --- a/backend/models/available-item.model.js +++ b/backend/models/available-item.model.js @@ -31,25 +31,52 @@ async function findOrCreateItem(itemName) { async function getAvailableItemRecord(householdId, storeId, itemId) { const result = await pool.query( - `SELECT - hsai.item_id, - i.name AS item_name, - ENCODE(hsai.custom_image, 'base64') AS item_image, - hsai.custom_image_mime_type AS image_mime_type, - hic.item_type, - hic.item_group, - hic.zone, - hsai.created_at, - hsai.updated_at - FROM household_store_available_items hsai - JOIN items i ON i.id = hsai.item_id - LEFT JOIN household_item_classifications hic - ON hic.household_id = hsai.household_id - AND hic.store_id = hsai.store_id - AND hic.item_id = hsai.item_id - WHERE hsai.household_id = $1 - AND hsai.store_id = $2 - AND hsai.item_id = $3`, + `WITH manageable_items AS ( + SELECT DISTINCT hl.item_id + FROM household_lists hl + WHERE hl.household_id = $1 + AND hl.store_id = $2 + UNION + SELECT hsai.item_id + FROM household_store_available_items hsai + WHERE hsai.household_id = $1 + AND hsai.store_id = $2 + ), + latest_list_items AS ( + SELECT DISTINCT ON (hl.item_id) + hl.item_id, + hl.custom_image, + hl.custom_image_mime_type, + hl.modified_on, + hl.id + FROM household_lists hl + WHERE hl.household_id = $1 + AND hl.store_id = $2 + ORDER BY hl.item_id, hl.modified_on DESC NULLS LAST, hl.id DESC + ) + SELECT + mi.item_id, + i.name AS item_name, + ENCODE(COALESCE(hsai.custom_image, lli.custom_image), 'base64') AS item_image, + COALESCE(hsai.custom_image_mime_type, lli.custom_image_mime_type) AS image_mime_type, + hic.item_type, + hic.item_group, + hic.zone, + hsai.created_at, + hsai.updated_at, + (hsai.item_id IS NOT NULL OR hic.item_id IS NOT NULL) AS has_managed_settings + FROM manageable_items mi + JOIN items i ON i.id = mi.item_id + LEFT JOIN latest_list_items lli ON lli.item_id = mi.item_id + LEFT JOIN household_store_available_items hsai + ON hsai.household_id = $1 + AND hsai.store_id = $2 + AND hsai.item_id = mi.item_id + LEFT JOIN household_item_classifications hic + ON hic.household_id = $1 + AND hic.store_id = $2 + AND hic.item_id = mi.item_id + WHERE mi.item_id = $3`, [householdId, storeId, itemId] ); @@ -63,31 +90,58 @@ exports.listAvailableItems = async (householdId, storeId, query = "") => { if (trimmedQuery) { values.push(`%${trimmedQuery}%`); - filterClause = "AND i.name ILIKE $3"; + filterClause = "WHERE i.name ILIKE $3"; } const result = await pool.query( - `SELECT - hsai.item_id, - i.name AS item_name, - ENCODE(hsai.custom_image, 'base64') AS item_image, - hsai.custom_image_mime_type AS image_mime_type, - hic.item_type, - hic.item_group, - hic.zone, - hsai.created_at, - hsai.updated_at - FROM household_store_available_items hsai - JOIN items i ON i.id = hsai.item_id - LEFT JOIN household_item_classifications hic - ON hic.household_id = hsai.household_id - AND hic.store_id = hsai.store_id - AND hic.item_id = hsai.item_id - WHERE hsai.household_id = $1 - AND hsai.store_id = $2 - ${filterClause} - ORDER BY i.name ASC - LIMIT 100`, + `WITH manageable_items AS ( + SELECT DISTINCT hl.item_id + FROM household_lists hl + WHERE hl.household_id = $1 + AND hl.store_id = $2 + UNION + SELECT hsai.item_id + FROM household_store_available_items hsai + WHERE hsai.household_id = $1 + AND hsai.store_id = $2 + ), + latest_list_items AS ( + SELECT DISTINCT ON (hl.item_id) + hl.item_id, + hl.custom_image, + hl.custom_image_mime_type, + hl.modified_on, + hl.id + FROM household_lists hl + WHERE hl.household_id = $1 + AND hl.store_id = $2 + ORDER BY hl.item_id, hl.modified_on DESC NULLS LAST, hl.id DESC + ) + SELECT + mi.item_id, + i.name AS item_name, + ENCODE(COALESCE(hsai.custom_image, lli.custom_image), 'base64') AS item_image, + COALESCE(hsai.custom_image_mime_type, lli.custom_image_mime_type) AS image_mime_type, + hic.item_type, + hic.item_group, + hic.zone, + hsai.created_at, + hsai.updated_at, + (hsai.item_id IS NOT NULL OR hic.item_id IS NOT NULL) AS has_managed_settings + FROM manageable_items mi + JOIN items i ON i.id = mi.item_id + LEFT JOIN latest_list_items lli ON lli.item_id = mi.item_id + LEFT JOIN household_store_available_items hsai + ON hsai.household_id = $1 + AND hsai.store_id = $2 + AND hsai.item_id = mi.item_id + LEFT JOIN household_item_classifications hic + ON hic.household_id = $1 + AND hic.store_id = $2 + AND hic.item_id = mi.item_id + ${filterClause} + ORDER BY i.name ASC + LIMIT 100`, values ); @@ -143,17 +197,55 @@ exports.updateAvailableItem = async (householdId, storeId, itemId, updates = {}) removeImage = false, } = updates; + const existing = await pool.query( + `SELECT item_id + FROM household_store_available_items + WHERE household_id = $1 + AND store_id = $2 + AND item_id = $3`, + [householdId, storeId, itemId] + ); + const assignments = ["updated_at = NOW()"]; const values = [householdId, storeId, itemId]; let parameterIndex = values.length; + let targetItemId = itemId; if (itemName !== undefined && String(itemName).trim() !== "") { const { itemId: nextItemId } = await findOrCreateItem(itemName); + targetItemId = nextItemId; parameterIndex += 1; assignments.push(`item_id = $${parameterIndex}`); values.push(nextItemId); } + if (existing.rowCount === 0) { + if (imageBuffer && mimeType) { + await pool.query( + `INSERT INTO household_store_available_items + (household_id, store_id, item_id, custom_image, custom_image_mime_type, updated_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (household_id, store_id, item_id) + DO UPDATE SET + custom_image = EXCLUDED.custom_image, + custom_image_mime_type = EXCLUDED.custom_image_mime_type, + updated_at = NOW()`, + [householdId, storeId, targetItemId, imageBuffer, mimeType] + ); + } else if (targetItemId !== itemId) { + await pool.query( + `INSERT INTO household_store_available_items + (household_id, store_id, item_id, updated_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (household_id, store_id, item_id) + DO UPDATE SET updated_at = NOW()`, + [householdId, storeId, targetItemId] + ); + } + + return getAvailableItemRecord(householdId, storeId, targetItemId); + } + if (removeImage) { assignments.push("custom_image = NULL", "custom_image_mime_type = NULL"); } else if (imageBuffer && mimeType) { diff --git a/backend/models/list.model.v2.js b/backend/models/list.model.v2.js index ed33d32..03a2cd1 100644 --- a/backend/models/list.model.v2.js +++ b/backend/models/list.model.v2.js @@ -377,13 +377,14 @@ exports.upsertClassification = async (householdId, storeId, itemId, classificati * @param {number} itemId - Item ID */ exports.deleteClassification = async (householdId, storeId, itemId) => { - await pool.query( + const result = await pool.query( `DELETE FROM household_item_classifications WHERE household_id = $1 AND store_id = $2 AND item_id = $3`, [householdId, storeId, itemId] ); + return result.rowCount > 0; }; /** diff --git a/backend/tests/available-item.model.test.js b/backend/tests/available-item.model.test.js index ae5533c..9279ad5 100644 --- a/backend/tests/available-item.model.test.js +++ b/backend/tests/available-item.model.test.js @@ -10,6 +10,38 @@ describe("available-item.model", () => { pool.query.mockReset(); }); + test("lists manageable items from household/store history even without stored overrides", async () => { + pool.query.mockResolvedValueOnce({ + rowCount: 1, + rows: [ + { + item_id: 55, + item_name: "milk", + item_image: null, + image_mime_type: null, + item_type: null, + item_group: null, + zone: null, + has_managed_settings: false, + }, + ], + }); + + const result = await AvailableItems.listAvailableItems(1, 2); + + expect(result).toEqual([ + expect.objectContaining({ + item_id: 55, + item_name: "milk", + has_managed_settings: false, + }), + ]); + expect(pool.query).toHaveBeenCalledWith( + expect.stringContaining("WITH manageable_items AS"), + [1, 2] + ); + }); + test("creates an available item using an existing catalog item", async () => { pool.query .mockResolvedValueOnce({ rowCount: 1, rows: [{ id: 55, name: "milk" }] }) @@ -72,10 +104,17 @@ describe("available-item.model", () => { test("updates available item images and returns refreshed data", async () => { const imageBuffer = Buffer.from("abc"); pool.query + .mockResolvedValueOnce({ rowCount: 1, rows: [{ item_id: 55 }] }) .mockResolvedValueOnce({ rowCount: 1, rows: [{ item_id: 55 }] }) .mockResolvedValueOnce({ rowCount: 1, - rows: [{ item_id: 55, item_name: "milk", item_image: "YWJj", image_mime_type: "image/jpeg" }], + rows: [{ + item_id: 55, + item_name: "milk", + item_image: "YWJj", + image_mime_type: "image/jpeg", + has_managed_settings: true, + }], }); const result = await AvailableItems.updateAvailableItem(1, 2, 55, { @@ -85,7 +124,7 @@ describe("available-item.model", () => { expect(result).toEqual(expect.objectContaining({ item_id: 55, image_mime_type: "image/jpeg" })); expect(pool.query).toHaveBeenNthCalledWith( - 1, + 2, expect.stringContaining("UPDATE household_store_available_items"), [1, 2, 55, imageBuffer, "image/jpeg"] ); diff --git a/backend/tests/available-items.controller.test.js b/backend/tests/available-items.controller.test.js index 16828ce..a93a587 100644 --- a/backend/tests/available-items.controller.test.js +++ b/backend/tests/available-items.controller.test.js @@ -43,7 +43,7 @@ describe("available-items.controller", () => { AvailableItems.importCurrentListItems.mockResolvedValue(2); AvailableItems.listAvailableItems.mockResolvedValue([]); List.upsertClassification.mockResolvedValue(undefined); - List.deleteClassification.mockResolvedValue(undefined); + List.deleteClassification.mockResolvedValue(false); }); test("creates an available item and persists classification metadata", async () => { @@ -135,6 +135,22 @@ describe("available-items.controller", () => { ); }); + test("clears managed settings without removing the underlying item", async () => { + const req = { + params: { householdId: "1", storeId: "2", itemId: "99" }, + }; + const res = createResponse(); + + AvailableItems.deleteAvailableItem.mockResolvedValueOnce(false); + List.deleteClassification.mockResolvedValueOnce(true); + + await controller.deleteAvailableItem(req, res); + + expect(AvailableItems.deleteAvailableItem).toHaveBeenCalledWith("1", "2", 99); + expect(List.deleteClassification).toHaveBeenCalledWith("1", "2", 99); + expect(res.json).toHaveBeenCalledWith({ message: "Store item settings cleared" }); + }); + test("returns an empty catalog payload when the available items table is missing", async () => { const req = { params: { householdId: "1", storeId: "2" }, diff --git a/frontend/src/components/manage/ManageStores.jsx b/frontend/src/components/manage/ManageStores.jsx index 1edec68..67ea8f0 100644 --- a/frontend/src/components/manage/ManageStores.jsx +++ b/frontend/src/components/manage/ManageStores.jsx @@ -92,7 +92,7 @@ export default function ManageStores() {

Your Stores ({householdStores.length})

- Available item management lives inside each store card below. + Item management lives inside each store card below for items already used in that household/store.

{!isAdmin && (

diff --git a/frontend/src/components/manage/StoreAvailableItemsManager.jsx b/frontend/src/components/manage/StoreAvailableItemsManager.jsx index aece269..d903eff 100644 --- a/frontend/src/components/manage/StoreAvailableItemsManager.jsx +++ b/frontend/src/components/manage/StoreAvailableItemsManager.jsx @@ -1,9 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { - createAvailableItem, deleteAvailableItem, getAvailableItems, - importCurrentAvailableItems, updateAvailableItem, } from "../../api/availableItems"; import useActionToast from "../../hooks/useActionToast"; @@ -61,28 +59,6 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin loadItems(query); }, [expanded, query, loadItems]); - const handleCreate = async (payload) => { - if (!catalogReady) { - toast.info( - "Store item catalog unavailable", - catalogMessage || "Store item catalog is unavailable until the latest database migration is applied." - ); - return; - } - - try { - await createAvailableItem(householdId, store.id, payload); - toast.success("Added store item", `Added ${payload.itemName} to ${store.name}`); - setShowEditor(false); - setEditorItem(null); - await loadItems(query); - } catch (error) { - const message = getApiErrorMessage(error, "Failed to add available item"); - toast.error("Add store item failed", `Add store item failed: ${message}`); - throw error; - } - }; - const handleUpdate = async (payload) => { if (!catalogReady) { toast.info( @@ -94,7 +70,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin try { await updateAvailableItem(householdId, store.id, editorItem.item_id, payload); - toast.success("Updated store item", `Updated ${payload.itemName} for ${store.name}`); + toast.success("Updated store item", `Updated ${editorItem.item_name} for ${store.name}`); setShowEditor(false); setEditorItem(null); await loadItems(query); @@ -120,36 +96,11 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin try { await deleteAvailableItem(householdId, store.id, item.item_id); - toast.success("Removed store item", `Removed ${item.item_name} from ${store.name}`); + toast.success("Cleared store item settings", `Cleared settings for ${item.item_name} in ${store.name}`); await loadItems(query); } catch (error) { - const message = getApiErrorMessage(error, "Failed to remove available item"); - toast.error("Remove store item failed", `Remove store item failed: ${message}`); - } - }; - - const handleImport = async () => { - if (!catalogReady) { - toast.info( - "Store item catalog unavailable", - catalogMessage || "Store item catalog is unavailable until the latest database migration is applied." - ); - return; - } - - try { - const response = await importCurrentAvailableItems(householdId, store.id); - const importedCount = response.data.imported_count || 0; - toast.success( - "Imported current list items", - importedCount > 0 - ? `Imported ${importedCount} current list items into ${store.name}` - : `No current list items to import for ${store.name}` - ); - await loadItems(query); - } catch (error) { - const message = getApiErrorMessage(error, "Failed to import current list items"); - toast.error("Import store items failed", `Import store items failed: ${message}`); + const message = getApiErrorMessage(error, "Failed to clear store item settings"); + toast.error("Clear store item settings failed", `Clear store item settings failed: ${message}`); } }; @@ -162,7 +113,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin

Store Item Catalog

-

Manage the available item list for {store.name}.

+

Manage settings for items already used in {store.name} for this household.

- -
{!catalogReady ? ( @@ -216,7 +146,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin ) : loading ? (

Loading store items...

) : items.length === 0 ? ( -

No available items saved for this store yet.

+

No household items found for this store yet.

) : (
{items.map((item) => { @@ -248,13 +178,15 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin > Edit - + {item.has_managed_settings ? ( + + ) : null}
); @@ -271,7 +203,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin setShowEditor(false); setEditorItem(null); }} - onSave={editorItem ? handleUpdate : handleCreate} + onSave={handleUpdate} /> ); diff --git a/frontend/src/components/modals/AvailableItemEditorModal.jsx b/frontend/src/components/modals/AvailableItemEditorModal.jsx index 1465857..9e54191 100644 --- a/frontend/src/components/modals/AvailableItemEditorModal.jsx +++ b/frontend/src/components/modals/AvailableItemEditorModal.jsx @@ -115,6 +115,7 @@ export default function AvailableItemEditorModal({ isOpen, item = null, onCancel value={itemName} onChange={(event) => setItemName(event.target.value)} placeholder="Enter item name" + disabled={Boolean(item)} /> @@ -156,7 +157,7 @@ export default function AvailableItemEditorModal({ isOpen, item = null, onCancel onClick={handleSubmit} disabled={saving} > - {saving ? "Saving..." : item ? "Save Changes" : "Add Item"} + {saving ? "Saving..." : "Save Changes"} diff --git a/frontend/tests/available-items-catalog.spec.ts b/frontend/tests/available-items-catalog.spec.ts index 7acad8c..97a4ccc 100644 --- a/frontend/tests/available-items-catalog.spec.ts +++ b/frontend/tests/available-items-catalog.spec.ts @@ -45,7 +45,7 @@ async function mockHouseholdAndStoreShell(page: import("@playwright/test").Page) }); } -test("manage stores lets admins import and curate available items", async ({ page }) => { +test("manage stores lets admins edit settings for existing household/store items", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); await mockHouseholdAndStoreShell(page); @@ -59,6 +59,17 @@ test("manage stores lets admins import and curate available items", async ({ pag item_type: "dairy", item_group: "Milk", zone: "Dairy & Refrigerated", + has_managed_settings: true, + }, + { + item_id: 777, + item_name: "apples", + item_image: null, + image_mime_type: null, + item_type: null, + item_group: null, + zone: null, + has_managed_settings: false, }, ]; @@ -70,30 +81,6 @@ test("manage stores lets admins import and curate available items", async ({ pag }); }); - await page.route("**/households/1/stores/10/available-items/import-current", async (route) => { - availableItems = [ - ...availableItems, - { - item_id: 777, - item_name: "granola", - item_image: null, - image_mime_type: null, - item_type: null, - item_group: null, - zone: null, - }, - ]; - - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - message: "Imported current list items", - imported_count: 1, - }), - }); - }); - await page.route("**/households/1/stores/10/available-items*", async (route) => { const request = route.request(); const url = new URL(request.url()); @@ -104,30 +91,33 @@ test("manage stores lets admins import and curate available items", async ({ pag await route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ items: filteredItems }), + body: JSON.stringify({ items: filteredItems, catalog_ready: true }), }); return; } - if (request.method() === "POST") { - availableItems = [ - ...availableItems, - { - item_id: 888, - item_name: "trail mix", - item_image: null, - image_mime_type: null, - item_type: "snack", - item_group: "Trail Mix", - zone: "Snacks & Candy", - }, - ]; + await route.fulfill({ status: 500 }); + }); + + await page.route("**/households/1/stores/10/available-items/777", async (route) => { + if (route.request().method() === "PATCH") { + availableItems = availableItems.map((item) => + item.item_id === 777 + ? { + ...item, + item_type: "produce", + item_group: "Fruits", + zone: "Produce & Fresh Vegetables", + has_managed_settings: true, + } + : item + ); await route.fulfill({ - status: 201, + status: 200, contentType: "application/json", body: JSON.stringify({ - message: "Available item added", - item: availableItems[availableItems.length - 1], + message: "Available item updated", + item: availableItems.find((item) => item.item_id === 777), }), }); return; @@ -136,13 +126,23 @@ test("manage stores lets admins import and curate available items", async ({ pag await route.fulfill({ status: 500 }); }); - await page.route("**/households/1/stores/10/available-items/888", async (route) => { + await page.route("**/households/1/stores/10/available-items/501", async (route) => { if (route.request().method() === "DELETE") { - availableItems = availableItems.filter((item) => item.item_id !== 888); + availableItems = availableItems.map((item) => + item.item_id === 501 + ? { + ...item, + item_type: null, + item_group: null, + zone: null, + has_managed_settings: false, + } + : item + ); await route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ message: "Available item removed" }), + body: JSON.stringify({ message: "Store item settings cleared" }), }); return; } @@ -157,26 +157,27 @@ test("manage stores lets admins import and curate available items", async ({ pag await expect(storeCard.getByText("Store Item Catalog")).toBeVisible(); await expect(storeCard.getByText("milk")).toBeVisible(); + await expect(storeCard.getByText("apples")).toBeVisible(); + await expect(storeCard.getByRole("button", { name: "Add Item" })).toHaveCount(0); + await expect(storeCard.getByRole("button", { name: "Import Current List" })).toHaveCount(0); - await storeCard.getByRole("button", { name: "Import Current List" }).click(); - await expect(page.locator(".action-toast.action-toast-success")).toContainText("Imported current list items"); - await expect(storeCard.getByText("granola")).toBeVisible(); - - await storeCard.getByRole("button", { name: "Add Item" }).click(); + await storeCard.locator(".store-available-items-card").filter({ hasText: "apples" }).getByRole("button", { name: "Edit" }).click(); const editorModal = page.locator(".available-item-editor-modal"); await expect(editorModal).toBeVisible(); - await editorModal.getByLabel("Item Name").fill("trail mix"); - await editorModal.locator(".available-item-editor-select").nth(0).selectOption("snack"); - await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Trail Mix"); - await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Snacks & Candy"); - await editorModal.getByRole("button", { name: "Add Item" }).click(); + await expect(editorModal.getByLabel("Item Name")).toBeDisabled(); + await editorModal.locator(".available-item-editor-select").nth(0).selectOption("produce"); + await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Fruits"); + await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Produce & Fresh Vegetables"); + await editorModal.getByRole("button", { name: "Save Changes" }).click(); - await expect(page.locator(".action-toast.action-toast-success")).toContainText("Added store item"); - await expect(storeCard.getByText("trail mix")).toBeVisible(); + await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated store item"); + await expect(storeCard.getByText("produce | Fruits | Produce & Fresh Vegetables")).toBeVisible(); page.once("dialog", (dialog) => dialog.accept()); - await storeCard.locator(".store-available-items-card").filter({ hasText: "trail mix" }).getByRole("button", { name: "Remove" }).click(); - await expect(page.locator(".action-toast.action-toast-success")).toContainText("Removed store item"); + await storeCard.locator(".store-available-items-card").filter({ hasText: "milk" }).getByRole("button", { name: "Clear Settings" }).click(); + await expect(page.locator(".action-toast.action-toast-success")).toContainText("Cleared store item settings"); + await expect(storeCard.locator(".store-available-items-card").filter({ hasText: "milk" }).getByText("No store defaults set")).toBeVisible(); + await expect(storeCard.locator(".store-available-items-card").filter({ hasText: "milk" }).getByRole("button", { name: "Clear Settings" })).toHaveCount(0); }); test("grocery page remains unchanged and does not show a store items picker", async ({ page }) => {