chore: harden reliability checks #2
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import "../../styles/components/AssignItemForModal.css";
|
import "../../styles/components/AssignItemForModal.css";
|
||||||
|
|
||||||
function getMemberLabel(member) {
|
function getMemberLabel(member) {
|
||||||
@ -19,7 +20,9 @@ export default function AssignItemForModal({
|
|||||||
}) {
|
}) {
|
||||||
const [selectedUserId, setSelectedUserId] = useState("");
|
const [selectedUserId, setSelectedUserId] = useState("");
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const dropdownRef = useRef(null);
|
const [dropdownStyle, setDropdownStyle] = useState(null);
|
||||||
|
const triggerRef = useRef(null);
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
const hasMembers = members.length > 0;
|
const hasMembers = members.length > 0;
|
||||||
const selectedMember = useMemo(
|
const selectedMember = useMemo(
|
||||||
@ -27,10 +30,39 @@ export default function AssignItemForModal({
|
|||||||
[members, selectedUserId]
|
[members, selectedUserId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateDropdownPosition = useCallback(() => {
|
||||||
|
if (!triggerRef.current) return;
|
||||||
|
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
const viewportPadding = 16;
|
||||||
|
const menuGap = 6;
|
||||||
|
const width = Math.min(rect.width, window.innerWidth - (2 * viewportPadding));
|
||||||
|
const left = Math.min(
|
||||||
|
Math.max(viewportPadding, rect.left),
|
||||||
|
window.innerWidth - width - viewportPadding
|
||||||
|
);
|
||||||
|
const availableBelow = window.innerHeight - rect.bottom - menuGap - viewportPadding;
|
||||||
|
const availableAbove = rect.top - menuGap - viewportPadding;
|
||||||
|
const shouldOpenAbove = availableBelow < 140 && availableAbove > availableBelow;
|
||||||
|
const maxHeight = Math.max(
|
||||||
|
120,
|
||||||
|
Math.min(240, Math.floor(shouldOpenAbove ? availableAbove : availableBelow))
|
||||||
|
);
|
||||||
|
|
||||||
|
setDropdownStyle({
|
||||||
|
left: `${Math.round(left)}px`,
|
||||||
|
width: `${Math.round(width)}px`,
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
top: shouldOpenAbove ? "auto" : `${Math.round(rect.bottom + menuGap)}px`,
|
||||||
|
bottom: shouldOpenAbove ? `${Math.round(window.innerHeight - rect.top + menuGap)}px` : "auto",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setSelectedUserId(members[0] ? String(members[0].id) : "");
|
setSelectedUserId(members[0] ? String(members[0].id) : "");
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
|
setDropdownStyle(null);
|
||||||
}, [isOpen, members]);
|
}, [isOpen, members]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -54,8 +86,10 @@ export default function AssignItemForModal({
|
|||||||
if (!isOpen || !isDropdownOpen) return undefined;
|
if (!isOpen || !isDropdownOpen) return undefined;
|
||||||
|
|
||||||
const handlePointerDown = (event) => {
|
const handlePointerDown = (event) => {
|
||||||
if (!dropdownRef.current) return;
|
const clickedTrigger = triggerRef.current?.contains(event.target);
|
||||||
if (!dropdownRef.current.contains(event.target)) {
|
const clickedMenu = menuRef.current?.contains(event.target);
|
||||||
|
|
||||||
|
if (!clickedTrigger && !clickedMenu) {
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -64,6 +98,24 @@ export default function AssignItemForModal({
|
|||||||
return () => window.removeEventListener("pointerdown", handlePointerDown);
|
return () => window.removeEventListener("pointerdown", handlePointerDown);
|
||||||
}, [isDropdownOpen, isOpen]);
|
}, [isDropdownOpen, isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !isDropdownOpen) return undefined;
|
||||||
|
|
||||||
|
updateDropdownPosition();
|
||||||
|
|
||||||
|
const handleViewportChange = () => {
|
||||||
|
updateDropdownPosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleViewportChange);
|
||||||
|
window.addEventListener("scroll", handleViewportChange, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleViewportChange);
|
||||||
|
window.removeEventListener("scroll", handleViewportChange, true);
|
||||||
|
};
|
||||||
|
}, [isDropdownOpen, isOpen, updateDropdownPosition]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
@ -71,6 +123,52 @@ export default function AssignItemForModal({
|
|||||||
onConfirm(selectedMember.id);
|
onConfirm(selectedMember.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleDropdown = () => {
|
||||||
|
if (isDropdownOpen) {
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDropdownPosition();
|
||||||
|
setIsDropdownOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownMenu = isDropdownOpen && dropdownStyle
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="assign-item-for-dropdown-menu"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="Household member"
|
||||||
|
style={dropdownStyle}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
{members.map((member) => {
|
||||||
|
const memberId = String(member.id);
|
||||||
|
const isSelected = memberId === String(selectedUserId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={member.id}
|
||||||
|
type="button"
|
||||||
|
className={`assign-item-for-dropdown-option ${isSelected ? "is-selected" : ""}`}
|
||||||
|
role="option"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedUserId(memberId);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
title={getMemberLabel(member)}
|
||||||
|
>
|
||||||
|
{getMemberOptionLabel(member)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay" onClick={onCancel}>
|
<div className="modal-overlay" onClick={onCancel}>
|
||||||
<div className="modal assign-item-for-modal" onClick={(event) => event.stopPropagation()}>
|
<div className="modal assign-item-for-modal" onClick={(event) => event.stopPropagation()}>
|
||||||
@ -81,13 +179,14 @@ export default function AssignItemForModal({
|
|||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
Household member
|
Household member
|
||||||
</label>
|
</label>
|
||||||
<div className="assign-item-for-dropdown" ref={dropdownRef}>
|
<div className="assign-item-for-dropdown">
|
||||||
<button
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
className={`assign-item-for-dropdown-trigger ${isDropdownOpen ? "is-open" : ""}`}
|
className={`assign-item-for-dropdown-trigger ${isDropdownOpen ? "is-open" : ""}`}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={isDropdownOpen}
|
aria-expanded={isDropdownOpen}
|
||||||
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
onClick={handleToggleDropdown}
|
||||||
>
|
>
|
||||||
<span className="assign-item-for-dropdown-label">
|
<span className="assign-item-for-dropdown-label">
|
||||||
{selectedMember ? getMemberOptionLabel(selectedMember) : "Select member"}
|
{selectedMember ? getMemberOptionLabel(selectedMember) : "Select member"}
|
||||||
@ -96,32 +195,6 @@ export default function AssignItemForModal({
|
|||||||
{isDropdownOpen ? "▲" : "▼"}
|
{isDropdownOpen ? "▲" : "▼"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isDropdownOpen ? (
|
|
||||||
<div className="assign-item-for-dropdown-menu" role="listbox" aria-label="Household member">
|
|
||||||
{members.map((member) => {
|
|
||||||
const memberId = String(member.id);
|
|
||||||
const isSelected = memberId === String(selectedUserId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={member.id}
|
|
||||||
type="button"
|
|
||||||
className={`assign-item-for-dropdown-option ${isSelected ? "is-selected" : ""}`}
|
|
||||||
role="option"
|
|
||||||
aria-selected={isSelected}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedUserId(memberId);
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
title={getMemberLabel(member)}
|
|
||||||
>
|
|
||||||
{getMemberOptionLabel(member)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -144,6 +217,7 @@ export default function AssignItemForModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{dropdownMenu}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
width: min(420px, calc(100vw - (2 * var(--spacing-md))));
|
width: min(420px, calc(100vw - (2 * var(--spacing-md))));
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assign-item-for-modal-field {
|
.assign-item-for-modal-field {
|
||||||
@ -54,13 +55,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assign-item-for-dropdown-menu {
|
.assign-item-for-dropdown-menu {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: calc(100% + 6px);
|
z-index: var(--z-tooltip);
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 3;
|
|
||||||
max-height: 180px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
background: var(--color-bg-surface);
|
background: var(--color-bg-surface);
|
||||||
border: var(--border-width-thin) solid var(--input-border-color);
|
border: var(--border-width-thin) solid var(--input-border-color);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
|
|||||||
233
frontend/tests/grocery-list-assignment.spec.ts
Normal file
233
frontend/tests/grocery-list-assignment.spec.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
function seedAuthStorage(page: import("@playwright/test").Page) {
|
||||||
|
return page.addInitScript(() => {
|
||||||
|
localStorage.setItem("token", "test-token");
|
||||||
|
localStorage.setItem("userId", "1");
|
||||||
|
localStorage.setItem("role", "admin");
|
||||||
|
localStorage.setItem("username", "assignment-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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("assigned items render selected users and keep the picker menu outside the modal", async ({ page }) => {
|
||||||
|
await seedAuthStorage(page);
|
||||||
|
await mockConfig(page);
|
||||||
|
|
||||||
|
const members = [
|
||||||
|
{ id: 1, username: "owner", name: "Owner User", display_name: "Owner User", role: "owner" },
|
||||||
|
{ id: 2, username: "casey", name: "Casey Client", display_name: "Casey Client", role: "member" },
|
||||||
|
{ id: 3, username: "jordan", name: "Jordan Client", display_name: "Jordan Client", role: "member" },
|
||||||
|
{ id: 4, username: "alex", name: "Alex Member", display_name: "Alex Member", role: "member" },
|
||||||
|
{ id: 5, username: "morgan", name: "Morgan Member", display_name: "Morgan Member", role: "member" },
|
||||||
|
{ id: 6, username: "sam", name: "Sam Member", display_name: "Sam Member", role: "member" },
|
||||||
|
{ id: 7, username: "jamie", name: "Jamie Member", display_name: "Jamie Member", role: "member" },
|
||||||
|
{ id: 8, username: "pat", name: "Pat Member", display_name: "Pat Member", role: "member" },
|
||||||
|
{ id: 9, username: "drew", name: "Drew Member", display_name: "Drew Member", role: "member" },
|
||||||
|
{ id: 10, username: "kai", name: "Kai Member", display_name: "Kai Member", role: "member" },
|
||||||
|
{ id: 11, username: "blair", name: "Blair Member", display_name: "Blair Member", role: "member" },
|
||||||
|
{ id: 12, username: "quinn", name: "Quinn Member", display_name: "Quinn Member", role: "member" },
|
||||||
|
{ id: 13, username: "rowan", name: "Rowan Member", display_name: "Rowan Member", role: "member" },
|
||||||
|
{ id: 14, username: "sage", name: "Sage Member", display_name: "Sage Member", role: "member" },
|
||||||
|
{ id: 15, username: "taylor", name: "Taylor Member", display_name: "Taylor Member", role: "member" },
|
||||||
|
{ id: 16, username: "river", name: "River Member", display_name: "River Member", role: "member" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let listItems: Array<{
|
||||||
|
id: number;
|
||||||
|
item_id: number;
|
||||||
|
item_name: string;
|
||||||
|
quantity: number;
|
||||||
|
bought: boolean;
|
||||||
|
item_image: string | null;
|
||||||
|
image_mime_type: string | null;
|
||||||
|
added_by_users: string[];
|
||||||
|
last_added_on: string;
|
||||||
|
item_type: string | null;
|
||||||
|
item_group: string | null;
|
||||||
|
zone: string | null;
|
||||||
|
}> = [];
|
||||||
|
let addCallCount = 0;
|
||||||
|
|
||||||
|
await page.route("**/households", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify([
|
||||||
|
{ id: 1, name: "Assignment 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(members),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/households/1/stores/10/list/recent", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify([]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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/item**", async (route) => {
|
||||||
|
const url = new URL(route.request().url());
|
||||||
|
const itemName = (url.searchParams.get("item_name") || "").toLowerCase();
|
||||||
|
const item = listItems.find((candidate) => candidate.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/add", async (route) => {
|
||||||
|
addCallCount += 1;
|
||||||
|
|
||||||
|
if (addCallCount === 1) {
|
||||||
|
listItems = [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
item_id: 501,
|
||||||
|
item_name: "bananas",
|
||||||
|
quantity: 1,
|
||||||
|
bought: false,
|
||||||
|
item_image: null,
|
||||||
|
image_mime_type: null,
|
||||||
|
added_by_users: ["Casey Client"],
|
||||||
|
last_added_on: "2026-03-28T12:00:00.000Z",
|
||||||
|
item_type: null,
|
||||||
|
item_group: null,
|
||||||
|
zone: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
listItems = [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
item_id: 501,
|
||||||
|
item_name: "bananas",
|
||||||
|
quantity: 2,
|
||||||
|
bought: false,
|
||||||
|
item_image: null,
|
||||||
|
image_mime_type: null,
|
||||||
|
added_by_users: ["Casey Client", "Jordan Client"],
|
||||||
|
last_added_on: "2026-03-28T12:05:00.000Z",
|
||||||
|
item_type: null,
|
||||||
|
item_group: null,
|
||||||
|
zone: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: addCallCount === 1 ? "Item added" : "Item updated",
|
||||||
|
item: {
|
||||||
|
id: 201,
|
||||||
|
item_name: "bananas",
|
||||||
|
quantity: addCallCount === 1 ? 1 : 2,
|
||||||
|
bought: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/households/1/stores/10/list", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ items: listItems }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "Grocery List" })).toBeVisible();
|
||||||
|
await page.getByPlaceholder("Enter item name").fill("bananas");
|
||||||
|
await page.getByRole("button", { name: "Others" }).click();
|
||||||
|
|
||||||
|
const assignModal = page.locator(".assign-item-for-modal");
|
||||||
|
await expect(assignModal).toBeVisible();
|
||||||
|
|
||||||
|
await assignModal.getByRole("button", { name: "Select member" }).click();
|
||||||
|
|
||||||
|
const portalMenu = page.locator("body > .assign-item-for-dropdown-menu");
|
||||||
|
await expect(portalMenu).toBeVisible();
|
||||||
|
await expect(page.locator(".assign-item-for-modal .assign-item-for-dropdown-menu")).toHaveCount(0);
|
||||||
|
|
||||||
|
const dropdownMetrics = await portalMenu.evaluate((element) => {
|
||||||
|
const menu = element as HTMLDivElement;
|
||||||
|
return {
|
||||||
|
position: window.getComputedStyle(menu).position,
|
||||||
|
scrollable: menu.scrollHeight > menu.clientHeight,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dropdownMetrics.position).toBe("fixed");
|
||||||
|
expect(dropdownMetrics.scrollable).toBe(true);
|
||||||
|
|
||||||
|
await portalMenu.getByRole("option", { name: "Casey Client" }).click();
|
||||||
|
await assignModal.getByRole("button", { name: "Confirm" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("Adding for: Casey Client")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Create + Add" }).click();
|
||||||
|
await page.getByRole("button", { name: "Skip All" }).click();
|
||||||
|
|
||||||
|
const bananasRow = page.locator(".glist-li").filter({ hasText: "bananas" });
|
||||||
|
await expect(bananasRow).toContainText("Casey Client");
|
||||||
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Added item");
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Enter item name").fill("bananas");
|
||||||
|
await page.getByRole("button", { name: "Others" }).click();
|
||||||
|
await assignModal.getByRole("button", { name: "Select member" }).click();
|
||||||
|
await portalMenu.getByRole("option", { name: "Jordan Client" }).click();
|
||||||
|
await assignModal.getByRole("button", { name: "Confirm" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("Adding for: Jordan Client")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Create + Add" }).click();
|
||||||
|
await page.getByRole("button", { name: "Update Quantity" }).click();
|
||||||
|
|
||||||
|
await expect(bananasRow).toContainText("Casey Client");
|
||||||
|
await expect(bananasRow).toContainText("Jordan Client");
|
||||||
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated item quantity");
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user