Add store item bulk delete mode #9
@ -24,6 +24,7 @@ This directory contains practical project documentation. Root-level rules still
|
|||||||
## Guides
|
## Guides
|
||||||
- `guides/api-documentation.md`: REST API reference. Verify against current code before changing APIs.
|
- `guides/api-documentation.md`: REST API reference. Verify against current code before changing APIs.
|
||||||
- `guides/frontend-readme.md`: frontend development notes.
|
- `guides/frontend-readme.md`: frontend development notes.
|
||||||
|
- `guides/management-modal-patterns.md`: reusable modal patterns for managing scoped item/list records.
|
||||||
- `guides/MOBILE_RESPONSIVE_AUDIT.md`: mobile design and audit checklist.
|
- `guides/MOBILE_RESPONSIVE_AUDIT.md`: mobile design and audit checklist.
|
||||||
- `guides/setup-checklist.md`: older setup checklist; prefer `DEVELOPMENT.md` for current commands.
|
- `guides/setup-checklist.md`: older setup checklist; prefer `DEVELOPMENT.md` for current commands.
|
||||||
|
|
||||||
|
|||||||
42
docs/guides/management-modal-patterns.md
Normal file
42
docs/guides/management-modal-patterns.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Management Modal Patterns
|
||||||
|
|
||||||
|
Use this guide for modals that manage scoped lists of app-owned records, such as store items now and store zones or locations later.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
- Management modals should keep users in the current workflow while they inspect, edit, add, or remove records for a scoped parent.
|
||||||
|
- The parent scope must be obvious in the title, for example `Costco Items`.
|
||||||
|
- Modals should avoid repeating table labels inside every row. Use row layout, grouping, and the edit surface for detail.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
- Header: title, one short description when the scope is not obvious, and a close button.
|
||||||
|
- Primary toolbar: search input plus the primary create action inline.
|
||||||
|
- Bulk toolbar: destructive or multi-select actions above the list, separate from search/create controls.
|
||||||
|
- List: compact rows with the record's primary identity and any essential visual affordance.
|
||||||
|
- Editor: clicking or tapping a row opens the edit/settings modal for that record.
|
||||||
|
- Confirmation: destructive actions must use `ConfirmSlideModal`, not browser dialogs.
|
||||||
|
|
||||||
|
## Row Behavior
|
||||||
|
- Normal mode: the entire row opens settings for that record.
|
||||||
|
- Delete mode: the row toggles selected/unselected state and does not open settings.
|
||||||
|
- Selection state must be visible on the row and must not rely only on color.
|
||||||
|
- Avoid per-row action buttons when the same action applies to every row.
|
||||||
|
|
||||||
|
## Bulk Delete Pattern
|
||||||
|
- Show a `Delete Items` button above the list for users with delete permission.
|
||||||
|
- Clicking `Delete Items` enters delete mode, clears any previous selection, and changes the button to `Confirm Delete (# selected)`.
|
||||||
|
- Show a `Cancel` button while delete mode is active.
|
||||||
|
- Disable confirm while zero items are selected.
|
||||||
|
- Clicking confirm opens `ConfirmSlideModal`; only the slide confirmation performs the mutation.
|
||||||
|
- On success, exit delete mode, clear selection, refresh the list, and show a toast.
|
||||||
|
- On failure, keep the modal open and show a toast with the API error summary.
|
||||||
|
|
||||||
|
## Permission Rules
|
||||||
|
- Keep authorization server-side. Client visibility only improves UX.
|
||||||
|
- Members can open item settings when the API allows them to manage item details.
|
||||||
|
- Delete controls should be shown only to owners/admins when deletion is admin-scoped.
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
- Modal containers should use dialog semantics when practical.
|
||||||
|
- Rows that perform actions should be keyboard reachable.
|
||||||
|
- Delete-mode rows should expose selected state with `aria-pressed` or an equivalent state.
|
||||||
|
- Buttons must have stable labels that describe the action in the current mode.
|
||||||
@ -31,7 +31,12 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editorItem, setEditorItem] = useState(null);
|
const [editorItem, setEditorItem] = useState(null);
|
||||||
const [showEditor, setShowEditor] = useState(false);
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
const [pendingDeleteItem, setPendingDeleteItem] = useState(null);
|
const [deleteMode, setDeleteMode] = useState(false);
|
||||||
|
const [selectedDeleteIds, setSelectedDeleteIds] = useState(() => new Set());
|
||||||
|
const [pendingDeleteItems, setPendingDeleteItems] = useState([]);
|
||||||
|
|
||||||
|
const selectedDeleteItems = items.filter((item) => selectedDeleteIds.has(item.item_id));
|
||||||
|
const selectedDeleteCount = selectedDeleteItems.length;
|
||||||
|
|
||||||
const loadItems = useCallback(async (search = query) => {
|
const loadItems = useCallback(async (search = query) => {
|
||||||
if (!householdId || !store?.id) {
|
if (!householdId || !store?.id) {
|
||||||
@ -82,7 +87,9 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
|
|
||||||
const closeManager = () => {
|
const closeManager = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setPendingDeleteItem(null);
|
setDeleteMode(false);
|
||||||
|
setSelectedDeleteIds(new Set());
|
||||||
|
setPendingDeleteItems([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (payload) => {
|
const handleUpdate = async (payload) => {
|
||||||
@ -115,19 +122,64 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditor = (item) => {
|
||||||
|
setEditorItem(item);
|
||||||
|
setShowEditor(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDeleteSelection = (itemId) => {
|
||||||
|
setSelectedDeleteIds((currentIds) => {
|
||||||
|
const nextIds = new Set(currentIds);
|
||||||
|
|
||||||
|
if (nextIds.has(itemId)) {
|
||||||
|
nextIds.delete(itemId);
|
||||||
|
} else {
|
||||||
|
nextIds.add(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextIds;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDeleteMode = () => {
|
||||||
|
setDeleteMode(true);
|
||||||
|
setSelectedDeleteIds(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDeleteMode = () => {
|
||||||
|
setDeleteMode(false);
|
||||||
|
setSelectedDeleteIds(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmSelectedDelete = () => {
|
||||||
|
if (selectedDeleteCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPendingDeleteItems(selectedDeleteItems);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!pendingDeleteItem) {
|
if (pendingDeleteItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteAvailableItem(householdId, store.id, pendingDeleteItem.item_id);
|
await Promise.all(
|
||||||
toast.success("Deleted store item", `Deleted ${pendingDeleteItem.item_name} from ${store.display_name || store.name}`);
|
pendingDeleteItems.map((item) => deleteAvailableItem(householdId, store.id, item.item_id))
|
||||||
setPendingDeleteItem(null);
|
);
|
||||||
|
const count = pendingDeleteItems.length;
|
||||||
|
toast.success(
|
||||||
|
count === 1 ? "Deleted store item" : "Deleted store items",
|
||||||
|
`Deleted ${count} ${count === 1 ? "item" : "items"} from ${store.display_name || store.name}`
|
||||||
|
);
|
||||||
|
setPendingDeleteItems([]);
|
||||||
|
setDeleteMode(false);
|
||||||
|
setSelectedDeleteIds(new Set());
|
||||||
await loadItems(query);
|
await loadItems(query);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getApiErrorMessage(error, "Failed to delete store item");
|
const message = getApiErrorMessage(error, "Failed to delete store item");
|
||||||
toast.error("Delete store item failed", `Delete store item failed: ${message}`);
|
toast.error("Delete store items failed", `Delete store items failed: ${message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -186,6 +238,28 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isAdmin && catalogReady && items.length > 0 ? (
|
||||||
|
<div className="store-items-bulk-toolbar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-danger btn-small store-items-delete-toggle"
|
||||||
|
disabled={deleteMode ? selectedDeleteCount === 0 : loading}
|
||||||
|
onClick={deleteMode ? confirmSelectedDelete : startDeleteMode}
|
||||||
|
>
|
||||||
|
{deleteMode ? `Confirm Delete (${selectedDeleteCount} selected)` : "Delete Items"}
|
||||||
|
</button>
|
||||||
|
{deleteMode ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-secondary btn-small store-items-delete-cancel"
|
||||||
|
onClick={cancelDeleteMode}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="store-items-modal-body">
|
<div className="store-items-modal-body">
|
||||||
{!catalogReady ? (
|
{!catalogReady ? (
|
||||||
<p className="empty-message">Run the latest database migrations to enable store item management.</p>
|
<p className="empty-message">Run the latest database migrations to enable store item management.</p>
|
||||||
@ -198,9 +272,27 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
<div className="store-items-table-body">
|
<div className="store-items-table-body">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const imageSrc = itemImageSource(item);
|
const imageSrc = itemImageSource(item);
|
||||||
|
const isSelectedForDelete = selectedDeleteIds.has(item.item_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.item_id} className="store-items-table-row">
|
<button
|
||||||
|
key={item.item_id}
|
||||||
|
type="button"
|
||||||
|
className={`store-items-table-row store-items-table-row-button ${deleteMode ? "is-delete-selectable" : ""} ${isSelectedForDelete ? "is-selected" : ""}`}
|
||||||
|
aria-label={
|
||||||
|
deleteMode
|
||||||
|
? `${isSelectedForDelete ? "Deselect" : "Select"} ${item.item_name} for deletion`
|
||||||
|
: `Edit settings for ${item.item_name}`
|
||||||
|
}
|
||||||
|
aria-pressed={deleteMode ? isSelectedForDelete : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (deleteMode) {
|
||||||
|
toggleDeleteSelection(item.item_id);
|
||||||
|
} else {
|
||||||
|
openEditor(item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="store-items-table-cell store-items-table-item">
|
<div className="store-items-table-cell store-items-table-item">
|
||||||
<div className="store-available-items-summary">
|
<div className="store-available-items-summary">
|
||||||
{imageSrc ? (
|
{imageSrc ? (
|
||||||
@ -219,30 +311,12 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="store-items-table-cell store-items-table-actions">
|
{deleteMode ? (
|
||||||
<div className="store-available-items-actions">
|
<span className="store-items-delete-indicator" aria-hidden="true">
|
||||||
<button
|
{isSelectedForDelete ? "✓" : ""}
|
||||||
type="button"
|
</span>
|
||||||
className="btn-secondary btn-small store-available-items-action"
|
) : null}
|
||||||
onClick={() => {
|
</button>
|
||||||
setEditorItem(item);
|
|
||||||
setShowEditor(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit Settings
|
|
||||||
</button>
|
|
||||||
{isAdmin ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-danger btn-small store-available-items-action"
|
|
||||||
onClick={() => setPendingDeleteItem(item)}
|
|
||||||
>
|
|
||||||
Delete Item
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -265,15 +339,19 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmSlideModal
|
<ConfirmSlideModal
|
||||||
isOpen={Boolean(pendingDeleteItem)}
|
isOpen={pendingDeleteItems.length > 0}
|
||||||
title={pendingDeleteItem ? `Delete ${pendingDeleteItem.item_name}?` : "Delete item?"}
|
title={
|
||||||
|
pendingDeleteItems.length === 1
|
||||||
|
? `Delete ${pendingDeleteItems[0].item_name}?`
|
||||||
|
: `Delete ${pendingDeleteItems.length} items?`
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
pendingDeleteItem
|
pendingDeleteItems.length > 0
|
||||||
? `Slide to confirm. This permanently deletes ${pendingDeleteItem.item_name} from ${store.display_name || store.name} for this household, including current list entries and history.`
|
? `Slide to confirm. This permanently deletes ${pendingDeleteItems.length === 1 ? pendingDeleteItems[0].item_name : `${pendingDeleteItems.length} items`} from ${store.display_name || store.name} for this household, including current list entries and history.`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
confirmLabel="Delete Item"
|
confirmLabel={pendingDeleteItems.length === 1 ? "Delete Item" : "Delete Items"}
|
||||||
onClose={() => setPendingDeleteItem(null)}
|
onClose={() => setPendingDeleteItems([])}
|
||||||
onConfirm={handleDeleteConfirm}
|
onConfirm={handleDeleteConfirm}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -72,6 +72,19 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-items-bulk-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-items-delete-toggle,
|
||||||
|
.store-items-delete-cancel {
|
||||||
|
min-height: 38px;
|
||||||
|
min-width: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
.store-available-items-search {
|
.store-available-items-search {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -110,6 +123,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-items-table-row-button {
|
||||||
|
width: 100%;
|
||||||
|
appearance: none;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.store-items-table-body {
|
.store-items-table-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -123,6 +145,22 @@
|
|||||||
background: var(--color-bg-surface);
|
background: var(--color-bg-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-items-table-row-button:hover,
|
||||||
|
.store-items-table-row-button:focus-visible {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-items-table-row-button.is-delete-selectable {
|
||||||
|
border-color: color-mix(in srgb, var(--color-danger) 35%, var(--color-border-light));
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-items-table-row-button.is-selected {
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
background: var(--color-danger-light);
|
||||||
|
}
|
||||||
|
|
||||||
.store-items-table-cell {
|
.store-items-table-cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@ -176,18 +214,23 @@
|
|||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-available-items-actions {
|
.store-items-delete-indicator {
|
||||||
display: flex;
|
width: 28px;
|
||||||
gap: var(--spacing-xs);
|
height: 28px;
|
||||||
flex-wrap: wrap;
|
display: inline-flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: var(--border-width-thin) solid var(--color-border-light);
|
||||||
|
border-radius: var(--border-radius-full);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-available-items-action {
|
.store-items-table-row-button.is-selected .store-items-delete-indicator {
|
||||||
min-width: 112px;
|
border-color: var(--color-danger);
|
||||||
min-height: 36px;
|
background: var(--color-danger);
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
padding-inline: var(--spacing-md);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
@ -201,16 +244,16 @@
|
|||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-items-table-actions {
|
.store-items-table-row-button.is-delete-selectable {
|
||||||
justify-self: stretch;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-available-items-actions {
|
.store-items-bulk-toolbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-available-items-actions button {
|
.store-items-bulk-toolbar button {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,6 +118,9 @@ test("manage stores opens a modal to edit and delete household store items", asy
|
|||||||
await expect(managerModal.locator(".store-available-items-thumb-placeholder").first()).toHaveText("\uD83D\uDCE6");
|
await expect(managerModal.locator(".store-available-items-thumb-placeholder").first()).toHaveText("\uD83D\uDCE6");
|
||||||
await expect(managerModal.getByText("Store Defaults")).toHaveCount(0);
|
await expect(managerModal.getByText("Store Defaults")).toHaveCount(0);
|
||||||
await expect(managerModal.getByText("No store defaults set")).toHaveCount(0);
|
await expect(managerModal.getByText("No store defaults set")).toHaveCount(0);
|
||||||
|
await expect(managerModal.getByText("Edit Settings", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(managerModal.getByText("Delete Item", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(managerModal.locator(".store-available-items-action")).toHaveCount(0);
|
||||||
|
|
||||||
const searchBox = await managerModal.getByPlaceholder("Search household/store items").boundingBox();
|
const searchBox = await managerModal.getByPlaceholder("Search household/store items").boundingBox();
|
||||||
const addButtonBox = await managerModal.getByRole("button", { name: "Add Item" }).boundingBox();
|
const addButtonBox = await managerModal.getByRole("button", { name: "Add Item" }).boundingBox();
|
||||||
@ -130,13 +133,10 @@ test("manage stores opens a modal to edit and delete household store items", asy
|
|||||||
)
|
)
|
||||||
).toBeLessThan(2);
|
).toBeLessThan(2);
|
||||||
|
|
||||||
const appleRow = managerModal.locator(".store-items-table-row").filter({ hasText: "apples" });
|
const appleRow = managerModal.getByRole("button", { name: "Edit settings for apples" });
|
||||||
const appleEditButton = appleRow.getByRole("button", { name: "Edit Settings" });
|
const milkRow = managerModal.locator(".store-items-table-row").filter({ hasText: "milk" });
|
||||||
const milkDeleteButton = managerModal.locator(".store-items-table-row").filter({ hasText: "milk" }).getByRole("button", { name: "Delete Item" });
|
|
||||||
await expect(appleEditButton).toHaveClass(/store-available-items-action/);
|
|
||||||
await expect(milkDeleteButton).toHaveClass(/store-available-items-action/);
|
|
||||||
|
|
||||||
await appleEditButton.click();
|
await appleRow.click();
|
||||||
const editorModal = page.locator(".available-item-editor-modal");
|
const editorModal = page.locator(".available-item-editor-modal");
|
||||||
await expect(editorModal).toBeVisible();
|
await expect(editorModal).toBeVisible();
|
||||||
await expect(editorModal.getByLabel("Item Name")).toBeDisabled();
|
await expect(editorModal.getByLabel("Item Name")).toBeDisabled();
|
||||||
@ -148,7 +148,20 @@ test("manage stores opens a modal to edit and delete household store items", asy
|
|||||||
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated store item");
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated store item");
|
||||||
await expect(managerModal.getByText("produce | Fruits | Produce & Fresh Vegetables")).toHaveCount(0);
|
await expect(managerModal.getByText("produce | Fruits | Produce & Fresh Vegetables")).toHaveCount(0);
|
||||||
|
|
||||||
await milkDeleteButton.click();
|
await managerModal.getByRole("button", { name: "Delete Items" }).click();
|
||||||
|
await expect(managerModal.getByRole("button", { name: "Confirm Delete (0 selected)" })).toBeDisabled();
|
||||||
|
await expect(managerModal.getByRole("button", { name: "Cancel" })).toBeVisible();
|
||||||
|
|
||||||
|
await milkRow.click();
|
||||||
|
await expect(managerModal.getByRole("button", { name: "Confirm Delete (1 selected)" })).toBeEnabled();
|
||||||
|
await expect(milkRow).toHaveClass(/is-selected/);
|
||||||
|
|
||||||
|
await milkRow.click();
|
||||||
|
await expect(managerModal.getByRole("button", { name: "Confirm Delete (0 selected)" })).toBeDisabled();
|
||||||
|
await expect(milkRow).not.toHaveClass(/is-selected/);
|
||||||
|
|
||||||
|
await milkRow.click();
|
||||||
|
await managerModal.getByRole("button", { name: "Confirm Delete (1 selected)" }).click();
|
||||||
const confirmModal = page.locator(".confirm-slide-modal");
|
const confirmModal = page.locator(".confirm-slide-modal");
|
||||||
await expect(confirmModal).toBeVisible();
|
await expect(confirmModal).toBeVisible();
|
||||||
await expect(confirmModal.getByText("Delete milk?")).toBeVisible();
|
await expect(confirmModal.getByText("Delete milk?")).toBeVisible();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user