From 5a2848ebcfdbbe1dc054b26ec4802f7e4a75ae7a Mon Sep 17 00:00:00 2001 From: Nico Date: Tue, 31 Mar 2026 01:24:22 -0700 Subject: [PATCH] refactor: use slide confirmation for role changes --- .../src/components/manage/ManageHousehold.jsx | 40 +++++++++++++++---- frontend/tests/invite-link-management.spec.ts | 25 ++++++++++-- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/manage/ManageHousehold.jsx b/frontend/src/components/manage/ManageHousehold.jsx index 488cf71..8971d3a 100644 --- a/frontend/src/components/manage/ManageHousehold.jsx +++ b/frontend/src/components/manage/ManageHousehold.jsx @@ -69,6 +69,7 @@ export default function ManageHousehold() { const [singleUseMode, setSingleUseMode] = useState("UNLIMITED"); const [pendingDecisionId, setPendingDecisionId] = useState(null); const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false); + const [pendingRoleChange, setPendingRoleChange] = useState(null); const isManager = ["owner", "admin"].includes(activeHousehold?.role); const isOwner = activeHousehold?.role === "owner"; @@ -277,15 +278,10 @@ export default function ManageHousehold() { } }; - const handleUpdateRole = async (memberId, nextRole, memberName) => { - if (!nextRole) return; + const handleConfirmRoleChange = async () => { + if (!pendingRoleChange) return; - if ( - nextRole === "owner" && - !window.confirm(`Make ${memberName} the household owner? You will become an admin.`) - ) { - return; - } + const { memberId, nextRole, memberName } = pendingRoleChange; try { await updateMemberRole(activeHousehold.id, memberId, nextRole); @@ -298,12 +294,19 @@ export default function ManageHousehold() { } else { toast.success("Updated member role", `Updated role for ${memberName} to ${nextRole}`); } + setPendingRoleChange(null); } catch (error) { const message = getApiErrorMessage(error, "Failed to update member role"); toast.error("Update member role failed", `Update member role failed: ${message}`); } }; + const handleUpdateRole = (memberId, nextRole, memberName) => { + if (!nextRole) return; + + setPendingRoleChange({ memberId, nextRole, memberName }); + }; + const handleRemoveMember = async (memberId, username) => { if (!confirm(`Remove ${username} from this household?`)) return; @@ -640,6 +643,27 @@ export default function ManageHousehold() { onClose={() => setIsLeaveModalOpen(false)} onConfirm={handleLeaveHousehold} /> + + setPendingRoleChange(null)} + onConfirm={handleConfirmRoleChange} + /> ); } diff --git a/frontend/tests/invite-link-management.spec.ts b/frontend/tests/invite-link-management.spec.ts index 5033850..aea47e8 100644 --- a/frontend/tests/invite-link-management.spec.ts +++ b/frontend/tests/invite-link-management.spec.ts @@ -23,6 +23,25 @@ async function mockConfig(page: import("@playwright/test").Page) { }); } +async function confirmSlide(page: import("@playwright/test").Page) { + const confirmModal = page.locator(".confirm-slide-modal"); + await expect(confirmModal).toBeVisible(); + + const slider = confirmModal.locator(".confirm-slide-handle"); + const track = confirmModal.locator(".confirm-slide-track"); + const sliderBox = await slider.boundingBox(); + const trackBox = await track.boundingBox(); + + if (!sliderBox || !trackBox) { + throw new Error("Confirm slide control was not measurable"); + } + + await page.mouse.move(sliderBox.x + sliderBox.width / 2, sliderBox.y + sliderBox.height / 2); + await page.mouse.down(); + await page.mouse.move(trackBox.x + trackBox.width - 4, sliderBox.y + sliderBox.height / 2, { steps: 8 }); + await page.mouse.up(); +} + test("join household modal accepts invite links but rejects legacy invite codes", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); @@ -285,15 +304,13 @@ test("household owner can transfer ownership from household settings", async ({ }); }); - page.on("dialog", async (dialog) => { - await dialog.accept(); - }); - await page.goto("/manage?tab=household"); await expect(page.getByRole("button", { name: "Make Owner" })).toBeVisible(); await page.getByRole("button", { name: "Make Owner" }).click(); + await expect(page.getByText("Transfer ownership to nico-admin?")).toBeVisible(); + await confirmSlide(page); await expect(page.locator(".action-toast.action-toast-success")).toContainText("Transferred household ownership"); await expect(page.locator(".action-toast.action-toast-success")).toContainText("nico-admin");