diff --git a/frontend/src/components/items/GroceryListItem.jsx b/frontend/src/components/items/GroceryListItem.jsx index 1d85504..abdc152 100644 --- a/frontend/src/components/items/GroceryListItem.jsx +++ b/frontend/src/components/items/GroceryListItem.jsx @@ -1,11 +1,15 @@ import { memo, useRef, useState } from "react"; import AddImageModal from "../modals/AddImageModal"; -import ConfirmBuyModal from "../modals/ConfirmBuyModal"; -function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [], compact = false }) { +function GroceryListItem({ + item, + onClick, + onOpenBuyModal, + onImageAdded, + onLongPress, + compact = false +}) { const [showAddImageModal, setShowAddImageModal] = useState(false); - const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false); - const [currentItem, setCurrentItem] = useState(item); const longPressTimer = useRef(null); const pressStartPos = useRef({ x: 0, y: 0 }); @@ -56,32 +60,14 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = const handleItemClick = () => { if (onClick) { - setCurrentItem(item); - setShowConfirmBuyModal(true); + onClick(item); } }; - const handleConfirmBuy = (quantity) => { - if (onClick) { - onClick(currentItem.id, quantity); - } - setShowConfirmBuyModal(false); - }; - - const handleCancelBuy = () => { - setShowConfirmBuyModal(false); - }; - - const handleNavigate = (newItem) => { - setCurrentItem(newItem); - }; - const handleImageClick = (e) => { e.stopPropagation(); // Prevent triggering the bought action - if (item.item_image) { - // Open buy modal which now shows the image - setCurrentItem(item); - setShowConfirmBuyModal(true); + if (item.item_image && onOpenBuyModal) { + onOpenBuyModal(item); } else { setShowAddImageModal(true); } @@ -168,16 +154,6 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = onAddImage={handleAddImage} /> )} - - {showConfirmBuyModal && ( - - )} ); } @@ -195,8 +171,9 @@ export default memo(GroceryListItem, (prevProps, nextProps) => { prevProps.item.zone === nextProps.item.zone && prevProps.item.added_by_users?.join(',') === nextProps.item.added_by_users?.join(',') && prevProps.onClick === nextProps.onClick && + prevProps.onOpenBuyModal === nextProps.onOpenBuyModal && prevProps.onImageAdded === nextProps.onImageAdded && prevProps.onLongPress === nextProps.onLongPress && - prevProps.allItems?.length === nextProps.allItems?.length + prevProps.compact === nextProps.compact ); }); diff --git a/frontend/src/components/modals/ConfirmBuyModal.jsx b/frontend/src/components/modals/ConfirmBuyModal.jsx index 2029766..57d093a 100644 --- a/frontend/src/components/modals/ConfirmBuyModal.jsx +++ b/frontend/src/components/modals/ConfirmBuyModal.jsx @@ -9,45 +9,54 @@ export default function ConfirmBuyModal({ onNavigate }) { const [quantity, setQuantity] = useState(item.quantity); + const [isSubmitting, setIsSubmitting] = useState(false); const maxQuantity = item.quantity; - // Update quantity when item changes (navigation) useEffect(() => { setQuantity(item.quantity); + setIsSubmitting(false); }, [item.id, item.quantity]); - // Find current index and check for prev/next - const currentIndex = allItems.findIndex(i => i.id === item.id); + const currentIndex = allItems.findIndex((listItem) => listItem.id === item.id); const hasPrev = currentIndex > 0; const hasNext = currentIndex < allItems.length - 1; const handleIncrement = () => { - if (quantity < maxQuantity) { - setQuantity(prev => prev + 1); + if (!isSubmitting && quantity < maxQuantity) { + setQuantity((prev) => prev + 1); } }; const handleDecrement = () => { - if (quantity > 1) { - setQuantity(prev => prev - 1); + if (!isSubmitting && quantity > 1) { + setQuantity((prev) => prev - 1); } }; - const handleConfirm = () => { - onConfirm(quantity); + const handleConfirm = async () => { + if (isSubmitting) return; + + setIsSubmitting(true); + try { + await onConfirm(quantity); + } finally { + setIsSubmitting(false); + } }; const handlePrev = () => { + if (isSubmitting) return; + if (hasPrev && onNavigate) { - const prevItem = allItems[currentIndex - 1]; - onNavigate(prevItem); + onNavigate(allItems[currentIndex - 1]); } }; const handleNext = () => { + if (isSubmitting) return; + if (hasNext && onNavigate) { - const nextItem = allItems[currentIndex + 1]; - onNavigate(nextItem); + onNavigate(allItems[currentIndex + 1]); } }; @@ -56,8 +65,15 @@ export default function ConfirmBuyModal({ : null; return ( -
-
e.stopPropagation()}> +
{ + if (!isSubmitting) { + onCancel(); + } + }} + > +
event.stopPropagation()}>
{item.zone &&
{item.zone}
}

{item.item_name}

@@ -67,27 +83,27 @@ export default function ConfirmBuyModal({
{imageUrl ? ( {item.item_name} ) : ( -
📦
+
[ ]
)}
@@ -96,9 +112,9 @@ export default function ConfirmBuyModal({ = maxQuantity} + disabled={quantity >= maxQuantity || isSubmitting} > + @@ -117,11 +133,11 @@ export default function ConfirmBuyModal({
- -
diff --git a/frontend/src/pages/GroceryList.jsx b/frontend/src/pages/GroceryList.jsx index 15f22d2..f1ad642 100644 --- a/frontend/src/pages/GroceryList.jsx +++ b/frontend/src/pages/GroceryList.jsx @@ -15,13 +15,13 @@ import SortDropdown from "../components/common/SortDropdown"; import AddItemForm from "../components/forms/AddItemForm"; import GroceryListItem from "../components/items/GroceryListItem"; import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal"; +import ConfirmBuyModal from "../components/modals/ConfirmBuyModal"; import ConfirmAddExistingModal from "../components/modals/ConfirmAddExistingModal"; -import EditItemModal from "../components/modals/EditItemModal"; -import SimilarItemModal from "../components/modals/SimilarItemModal"; -import StoreTabs from "../components/store/StoreTabs"; -import { ZONE_FLOW } from "../constants/classifications"; -import { ROLES } from "../constants/roles"; -import { AuthContext } from "../context/AuthContext"; +import EditItemModal from "../components/modals/EditItemModal"; +import SimilarItemModal from "../components/modals/SimilarItemModal"; +import StoreTabs from "../components/store/StoreTabs"; +import { ZONE_FLOW } from "../constants/classifications"; +import { AuthContext } from "../context/AuthContext"; import { HouseholdContext } from "../context/HouseholdContext"; import { IMAGE_UPLOAD_SUCCESS_EVENT } from "../context/UploadQueueContext"; import { SettingsContext } from "../context/SettingsContext"; @@ -31,8 +31,52 @@ import useUploadQueue from "../hooks/useUploadQueue"; import getApiErrorMessage from "../lib/getApiErrorMessage"; import "../styles/pages/GroceryList.css"; import { findSimilarItems } from "../utils/stringSimilarity"; - - + +function sortItemsForMode(items, sortMode) { + const sorted = [...items]; + + if (sortMode === "az") sorted.sort((a, b) => a.item_name.localeCompare(b.item_name)); + if (sortMode === "za") sorted.sort((a, b) => b.item_name.localeCompare(a.item_name)); + if (sortMode === "qty-high") sorted.sort((a, b) => b.quantity - a.quantity); + if (sortMode === "qty-low") sorted.sort((a, b) => a.quantity - b.quantity); + if (sortMode === "zone") { + sorted.sort((a, b) => { + if (!a.zone && b.zone) return 1; + if (a.zone && !b.zone) return -1; + if (!a.zone && !b.zone) return a.item_name.localeCompare(b.item_name); + + const aZoneIndex = ZONE_FLOW.indexOf(a.zone); + const bZoneIndex = ZONE_FLOW.indexOf(b.zone); + const aIndex = aZoneIndex === -1 ? ZONE_FLOW.length : aZoneIndex; + const bIndex = bZoneIndex === -1 ? ZONE_FLOW.length : bZoneIndex; + + const zoneCompare = aIndex - bIndex; + if (zoneCompare !== 0) return zoneCompare; + + const typeCompare = (a.item_type || "").localeCompare(b.item_type || ""); + if (typeCompare !== 0) return typeCompare; + + const groupCompare = (a.item_group || "").localeCompare(b.item_group || ""); + if (groupCompare !== 0) return groupCompare; + + return a.item_name.localeCompare(b.item_name); + }); + } + + return sorted; +} + +function getNextModalItem(sortedItems, currentIndex, excludedItemId) { + const remainingItems = sortedItems.filter((item) => item.id !== excludedItemId); + + if (remainingItems.length === 0) { + return null; + } + + return remainingItems[currentIndex] || remainingItems[0]; +} + + export default function GroceryList() { const pageTitle = "Grocery List"; const { userId } = useContext(AuthContext); @@ -43,9 +87,10 @@ export default function GroceryList() { const { enqueueImageUpload } = useUploadQueue(); const navigate = useNavigate(); - // Get household role for permissions - const householdRole = activeHousehold?.role; + // Get household role for permissions + const householdRole = activeHousehold?.role; const isHouseholdAdmin = ["owner", "admin"].includes(householdRole); + const canEditList = Boolean(householdRole && householdRole !== "viewer"); // === State === // const [items, setItems] = useState([]); @@ -63,9 +108,10 @@ export default function GroceryList() { const [showEditModal, setShowEditModal] = useState(false); const [editingItem, setEditingItem] = useState(null); const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed); - const [collapsedZones, setCollapsedZones] = useState({}); - const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false); - const [confirmAddExistingData, setConfirmAddExistingData] = useState(null); + const [collapsedZones, setCollapsedZones] = useState({}); + const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false); + const [confirmAddExistingData, setConfirmAddExistingData] = useState(null); + const [buyModalState, setBuyModalState] = useState(null); // === Data Loading === @@ -106,6 +152,10 @@ export default function GroceryList() { loadRecentlyBought(); }, [activeHousehold?.id, activeStore?.id]); + useEffect(() => { + setBuyModalState(null); + }, [activeHousehold?.id, activeStore?.id]); + useEffect(() => { const loadHouseholdMembers = async () => { if (!activeHousehold?.id) { @@ -183,46 +233,37 @@ export default function GroceryList() { }; // === Sorted Items Computation === - const sortedItems = useMemo(() => { - const sorted = [...items]; - - if (sortMode === "az") sorted.sort((a, b) => a.item_name.localeCompare(b.item_name)); - if (sortMode === "za") sorted.sort((a, b) => b.item_name.localeCompare(a.item_name)); - if (sortMode === "qty-high") sorted.sort((a, b) => b.quantity - a.quantity); - if (sortMode === "qty-low") sorted.sort((a, b) => a.quantity - b.quantity); - if (sortMode === "zone") { - sorted.sort((a, b) => { - // Items without classification go to the end - if (!a.zone && b.zone) return 1; - if (a.zone && !b.zone) return -1; - if (!a.zone && !b.zone) return a.item_name.localeCompare(b.item_name); - - // Sort by ZONE_FLOW order - const aZoneIndex = ZONE_FLOW.indexOf(a.zone); - const bZoneIndex = ZONE_FLOW.indexOf(b.zone); - - // If zone not in ZONE_FLOW, put at end - const aIndex = aZoneIndex === -1 ? ZONE_FLOW.length : aZoneIndex; - const bIndex = bZoneIndex === -1 ? ZONE_FLOW.length : bZoneIndex; - - const zoneCompare = aIndex - bIndex; - if (zoneCompare !== 0) return zoneCompare; - - // Then by item_type - const typeCompare = (a.item_type || "").localeCompare(b.item_type || ""); - if (typeCompare !== 0) return typeCompare; - - // Then by item_group - const groupCompare = (a.item_group || "").localeCompare(b.item_group || ""); - if (groupCompare !== 0) return groupCompare; - - // Finally by name - return a.item_name.localeCompare(b.item_name); - }); - } - - return sorted; - }, [items, sortMode]); + const sortedItems = useMemo(() => { + return sortItemsForMode(items, sortMode); + }, [items, sortMode]); + + const visibleRecentlyBoughtItems = useMemo( + () => recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount), + [recentlyBoughtItems, recentlyBoughtDisplayCount] + ); + + const buyModalItems = useMemo(() => { + if (!buyModalState) return []; + + return buyModalState.source === "active" + ? sortedItems + : visibleRecentlyBoughtItems; + }, [buyModalState, sortedItems, visibleRecentlyBoughtItems]); + + useEffect(() => { + if (!buyModalState) return; + + const refreshedItem = buyModalItems.find((item) => item.id === buyModalState.item.id); + if (!refreshedItem || refreshedItem === buyModalState.item) return; + + setBuyModalState((prev) => { + if (!prev || prev.item.id !== refreshedItem.id || prev.source !== buyModalState.source) { + return prev; + } + + return { ...prev, item: refreshedItem }; + }); + }, [buyModalItems, buyModalState]); // === Suggestion Handler === @@ -536,35 +577,90 @@ export default function GroceryList() { // === Item Action Handlers === - const handleBought = useCallback(async (id, quantity) => { + const handleBought = useCallback(async (quantity) => { if (!activeHousehold?.id || !activeStore?.id) return; + if (!buyModalState || buyModalState.source !== "active") { + setBuyModalState(null); + return; + } - const item = items.find(i => i.id === id); + const item = items.find((listItem) => listItem.id === buyModalState.item.id) || buyModalState.item; if (!item) return; try { + const currentIndex = sortedItems.findIndex((listItem) => listItem.id === item.id); + const resolvedIndex = currentIndex >= 0 ? currentIndex : 0; + await markBought(activeHousehold.id, activeStore.id, item.item_name, quantity, true); - // If buying full quantity, remove from list + let nextItems = items; + if (quantity >= item.quantity) { - setItems(prevItems => prevItems.filter((existingItem) => existingItem.id !== id)); + nextItems = items.filter((existingItem) => existingItem.id !== item.id); } else { - // If partial, fetch updated item const response = await getItemByName(activeHousehold.id, activeStore.id, item.item_name); const updatedItem = response.data; - setItems((prevItems) => - prevItems.map((existingItem) => (existingItem.id === id ? updatedItem : existingItem)) + nextItems = items.map((existingItem) => + existingItem.id === item.id ? updatedItem : existingItem ); } + setItems(nextItems); + + const nextSortedItems = sortItemsForMode(nextItems, sortMode); + const nextModalItem = getNextModalItem(nextSortedItems, resolvedIndex, item.id); + + setBuyModalState( + nextModalItem + ? { + item: nextModalItem, + source: "active", + canConfirm: true, + } + : null + ); + toast.success("Marked item bought", `Marked item ${item.item_name} as bought`); loadRecentlyBought(); } catch (error) { const message = getApiErrorMessage(error, "Failed to mark item as bought"); toast.error("Mark item bought failed", `Mark item bought failed: ${message}`); } - }, [activeHousehold?.id, activeStore?.id, items, toast]); + }, [activeHousehold?.id, activeStore?.id, buyModalState, items, sortMode, sortedItems, toast]); + + const openActiveBuyModal = useCallback((item) => { + setBuyModalState({ + item, + source: "active", + canConfirm: canEditList, + }); + }, [canEditList]); + + const openRecentBuyModal = useCallback((item) => { + setBuyModalState({ + item, + source: "recent", + canConfirm: false, + }); + }, []); + + const handleBuyModalCancel = useCallback(() => { + setBuyModalState(null); + }, []); + + const handleBuyModalNavigate = useCallback((item) => { + setBuyModalState((prev) => (prev ? { ...prev, item } : prev)); + }, []); + + const handleBuyModalConfirm = useCallback(async (quantity) => { + if (!buyModalState?.canConfirm) { + setBuyModalState(null); + return; + } + + await handleBought(quantity); + }, [buyModalState?.canConfirm, handleBought]); const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile, source = "add_image_modal") => { if (!activeHousehold?.id || !activeStore?.id) return; @@ -754,7 +850,7 @@ export default function GroceryList() { - {householdRole && householdRole !== 'viewer' && ( + {canEditList && ( {grouped[zone].map((item) => ( - - householdRole && householdRole !== 'viewer' && handleBought(id, quantity) - } - onImageAdded={ - householdRole && householdRole !== 'viewer' ? handleImageAdded : null - } - onLongPress={ - householdRole && householdRole !== 'viewer' ? handleLongPress : null - } - /> + ))} )} @@ -815,21 +909,19 @@ export default function GroceryList() { ) : (
    {sortedItems.map((item) => ( - - [ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity) - } - onImageAdded={ - [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null - } - onLongPress={ - [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleLongPress : null - } - /> + ))}
)} @@ -849,21 +941,21 @@ export default function GroceryList() { {!recentlyBoughtCollapsed && ( <>
    - {recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => ( - - ))} + {visibleRecentlyBoughtItems.map((item) => ( + + ))}
{recentlyBoughtDisplayCount < recentlyBoughtItems.length && (
@@ -900,15 +992,25 @@ export default function GroceryList() { /> )} - {showEditModal && editingItem && ( - - )} - + /> + )} + + {buyModalState && ( + + )} + {showConfirmAddExisting && confirmAddExistingData && ( { + localStorage.setItem("token", "test-token"); + localStorage.setItem("userId", "1"); + localStorage.setItem("role", "admin"); + localStorage.setItem("username", "buy-modal-user"); + }); +} + +async function mockConfig(page: import("@playwright/test").Page) { + await page.route("**/config", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + maxFileSizeMB: 20, + maxImageDimension: 800, + imageQuality: 85, + }), + }); + }); +} + +function makeItem( + id: number, + itemName: string, + quantity: number, + overrides: Partial = {} +): MockItem { + return { + id, + item_id: id + 500, + item_name: itemName, + quantity, + bought: false, + item_image: null, + image_mime_type: null, + added_by_users: ["Owner User"], + last_added_on: "2026-03-28T12:00:00.000Z", + item_type: null, + item_group: null, + zone: "Produce", + ...overrides, + }; +} + +async function setupBuyModalRoutes( + page: import("@playwright/test").Page, + initialItems: MockItem[] +) { + let activeItems = initialItems.map((item) => ({ ...item })); + let recentItems: MockItem[] = []; + + await page.route("**/households", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([ + { id: 1, name: "Auto Advance House", role: "admin", invite_code: "ABCD1234" }, + ]), + }); + }); + + await page.route("**/stores/household/1", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([ + { id: 10, name: "Costco", location: "Warehouse", is_default: true }, + ]), + }); + }); + + await page.route("**/households/1/members", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([ + { id: 1, username: "owner", name: "Owner User", display_name: "Owner User", role: "owner" }, + ]), + }); + }); + + await page.route("**/households/1/stores/10/list/recent", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(recentItems), + }); + }); + + await page.route("**/households/1/stores/10/list/suggestions**", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([]), + }); + }); + + await page.route("**/households/1/stores/10/list/classification**", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ classification: null }), + }); + }); + + await page.route("**/households/1/stores/10/list/item", async (route) => { + const request = route.request(); + + if (request.method() === "PATCH") { + const body = request.postDataJSON() as { + item_name?: string; + quantity_bought?: number | null; + }; + const itemName = String(body.item_name || "").toLowerCase(); + const quantityBought = Number(body.quantity_bought ?? 0); + const currentItem = activeItems.find((item) => item.item_name === itemName); + + if (!currentItem) { + await route.fulfill({ + status: 404, + contentType: "application/json", + body: JSON.stringify({ error: { message: "Item not found" } }), + }); + return; + } + + const remainingQuantity = currentItem.quantity - quantityBought; + recentItems = [ + { + ...currentItem, + quantity: quantityBought, + bought: true, + }, + ...recentItems, + ]; + + if (remainingQuantity <= 0) { + activeItems = activeItems.filter((item) => item.id !== currentItem.id); + } else { + activeItems = activeItems.map((item) => + item.id === currentItem.id + ? { ...item, quantity: remainingQuantity } + : item + ); + } + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + message: "Item updated", + item: { + id: currentItem.id, + item_name: currentItem.item_name, + quantity: Math.max(remainingQuantity, 0), + bought: remainingQuantity <= 0, + }, + }), + }); + return; + } + + const url = new URL(request.url()); + const itemName = (url.searchParams.get("item_name") || "").toLowerCase(); + const item = activeItems.find((entry) => entry.item_name === itemName); + + await route.fulfill({ + status: item ? 200 : 404, + contentType: "application/json", + body: JSON.stringify(item || { message: "Item not found" }), + }); + }); + + await page.route("**/households/1/stores/10/list", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + items: activeItems, + }), + }); + }); +} + +async function openBuyModal(page: import("@playwright/test").Page, itemName: string) { + const row = page.locator(".glist-li").filter({ hasText: itemName }); + await row.click(); + await expect(page.locator(".confirm-buy-modal")).toBeVisible(); +} + +test("buying an item advances to the next one in the current sort order", async ({ page }) => { + await seedAuthStorage(page); + await mockConfig(page); + await setupBuyModalRoutes(page, [ + makeItem(1, "milk", 2), + makeItem(2, "bread", 5), + makeItem(3, "apples", 3), + ]); + + await page.goto("/"); + await page.locator(".glist-sort").selectOption("qty-high"); + + await openBuyModal(page, "bread"); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect(page.locator(".confirm-buy-item-name")).toHaveText("apples"); +}); + +test("buying the last item in the current order wraps to the first remaining item", async ({ page }) => { + await seedAuthStorage(page); + await mockConfig(page); + await setupBuyModalRoutes(page, [ + makeItem(1, "apples", 3), + makeItem(2, "bread", 5), + makeItem(3, "milk", 2), + ]); + + await page.goto("/"); + await page.locator(".glist-sort").selectOption("az"); + + await openBuyModal(page, "milk"); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect(page.locator(".confirm-buy-item-name")).toHaveText("apples"); +}); + +test("partial buy keeps the item on the list and advances past it", async ({ page }) => { + await seedAuthStorage(page); + await mockConfig(page); + await setupBuyModalRoutes(page, [ + makeItem(1, "alpha", 1), + makeItem(2, "bravo", 3), + makeItem(3, "charlie", 5), + ]); + + await page.goto("/"); + await page.locator(".glist-sort").selectOption("qty-low"); + + await openBuyModal(page, "bravo"); + await page.locator(".confirm-buy-counter-btn").nth(0).click(); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect(page.locator(".confirm-buy-item-name")).toHaveText("charlie"); + await expect(page.locator(".glist-li").filter({ hasText: "bravo" })).toContainText("x2"); +}); + +test("buying the only remaining item closes the modal", async ({ page }) => { + await seedAuthStorage(page); + await mockConfig(page); + await setupBuyModalRoutes(page, [ + makeItem(1, "solo", 1), + ]); + + await page.goto("/"); + + await openBuyModal(page, "solo"); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect(page.locator(".confirm-buy-modal")).toBeHidden(); +});