diff --git a/.scaffold/go.sum b/.scaffold/go.sum index 40d03dd1..36f3018c 100644 --- a/.scaffold/go.sum +++ b/.scaffold/go.sum @@ -8,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sysadminsmedia/homebox/backend v0.0.0-20251228163253-2bd6ff580a7f h1:+5m3FRu/Ja3kH2XgFn0GYCWOKIe4O+7PbLawLXvb4gA= -github.com/sysadminsmedia/homebox/backend v0.0.0-20251228163253-2bd6ff580a7f/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c= +github.com/sysadminsmedia/homebox/backend v0.0.0-20251228172914-2a6773d1d610 h1:kNLtnxaPaOryBUZ7RgUHPQVWxIExXYR/q9pYCbum5Vk= +github.com/sysadminsmedia/homebox/backend v0.0.0-20251228172914-2a6773d1d610/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go index 9766ed96..9f3ccd3b 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_actions.go +++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go @@ -10,6 +10,7 @@ import ( "github.com/hay-kot/httpkit/server" "github.com/rs/zerolog/log" "github.com/sysadminsmedia/homebox/backend/internal/core/services" + "github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus" "github.com/sysadminsmedia/homebox/backend/internal/sys/validate" ) @@ -143,6 +144,16 @@ func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc { return validate.NewRequestError(err, http.StatusInternalServerError) } + // Publish mutation events for wiped resources + if ctrl.bus != nil { + if options.WipeLabels { + ctrl.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: ctx.GID}) + } + if options.WipeLocations { + ctrl.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: ctx.GID}) + } + } + return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted}) } } diff --git a/frontend/components/WipeInventoryDialog.vue b/frontend/components/WipeInventoryDialog.vue index 64bf33c2..29eca418 100644 --- a/frontend/components/WipeInventoryDialog.vue +++ b/frontend/components/WipeInventoryDialog.vue @@ -54,9 +54,9 @@ {{ $t("global.cancel") }} - + @@ -67,7 +67,6 @@ import { useDialog } from "~/components/ui/dialog-provider"; import { AlertDialog, - AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, @@ -75,6 +74,7 @@ AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; + import { Button } from "@/components/ui/button"; const { registerOpenDialogCallback, closeDialog, addAlert, removeAlert } = useDialog(); @@ -82,12 +82,14 @@ const wipeLabels = ref(false); const wipeLocations = ref(false); const wipeMaintenance = ref(false); + const isConfirming = ref(false); registerOpenDialogCallback(DialogID.WipeInventory, () => { dialog.value = true; wipeLabels.value = false; wipeLocations.value = false; wipeMaintenance.value = false; + isConfirming.value = false; }); watch( @@ -103,7 +105,7 @@ ); function handleOpenChange(open: boolean) { - if (!open) { + if (!open && !isConfirming.value) { close(); } } @@ -114,12 +116,14 @@ } function confirm() { - dialog.value = false; + isConfirming.value = true; const result = { wipeLabels: wipeLabels.value, wipeLocations: wipeLocations.value, wipeMaintenance: wipeMaintenance.value, }; closeDialog(DialogID.WipeInventory, result); + dialog.value = false; + isConfirming.value = false; } diff --git a/frontend/test/e2e/wipe-inventory.browser.spec.ts b/frontend/test/e2e/wipe-inventory.browser.spec.ts new file mode 100644 index 00000000..d7952466 --- /dev/null +++ b/frontend/test/e2e/wipe-inventory.browser.spec.ts @@ -0,0 +1,129 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Wipe Inventory E2E Test", () => { + test.beforeEach(async ({ page }) => { + // Login as demo user (owner with permissions) + await page.goto("/"); + await page.fill("input[type='text']", "demo@example.com"); + await page.fill("input[type='password']", "demo"); + await page.click("button[type='submit']"); + await expect(page).toHaveURL("/home"); + }); + + test("should open wipe inventory dialog with all options", async ({ page }) => { + // Navigate to Tools page + await page.goto("/tools"); + await page.waitForLoadState("networkidle"); + + // Scroll to the bottom where wipe inventory is located + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(500); + + // Find and click the Wipe Inventory button + const wipeButton = page.locator("button", { hasText: "Wipe Inventory" }).last(); + await expect(wipeButton).toBeVisible(); + await wipeButton.click(); + + // Wait for dialog to appear + await page.waitForTimeout(1000); + + // Verify dialog title is visible + await expect(page.locator("text=Wipe Inventory").first()).toBeVisible(); + + // Verify all checkboxes are present + await expect(page.locator("input#wipe-labels-checkbox")).toBeVisible(); + await expect(page.locator("input#wipe-locations-checkbox")).toBeVisible(); + await expect(page.locator("input#wipe-maintenance-checkbox")).toBeVisible(); + + // Verify labels for checkboxes + await expect(page.locator("label[for='wipe-labels-checkbox']")).toBeVisible(); + await expect(page.locator("label[for='wipe-locations-checkbox']")).toBeVisible(); + await expect(page.locator("label[for='wipe-maintenance-checkbox']")).toBeVisible(); + + // Verify both Cancel and Confirm buttons are present + await expect(page.locator("button", { hasText: "Cancel" })).toBeVisible(); + const confirmButton = page.locator("button", { hasText: "Confirm" }); + await expect(confirmButton).toBeVisible(); + + // Take screenshot of the modal + await page.screenshot({ + path: "/tmp/playwright-logs/wipe-inventory-modal-initial.png", + }); + console.log("✅ Screenshot saved: wipe-inventory-modal-initial.png"); + + // Check all three options + await page.check("input#wipe-labels-checkbox"); + await page.check("input#wipe-locations-checkbox"); + await page.check("input#wipe-maintenance-checkbox"); + await page.waitForTimeout(500); + + // Verify checkboxes are checked + await expect(page.locator("input#wipe-labels-checkbox")).toBeChecked(); + await expect(page.locator("input#wipe-locations-checkbox")).toBeChecked(); + await expect(page.locator("input#wipe-maintenance-checkbox")).toBeChecked(); + + // Take screenshot with all options checked + await page.screenshot({ + path: "/tmp/playwright-logs/wipe-inventory-modal-options-checked.png", + }); + console.log("✅ Screenshot saved: wipe-inventory-modal-options-checked.png"); + + // Click Confirm button + await confirmButton.click(); + await page.waitForTimeout(2000); + + // Wait for the dialog to close (verify button is no longer visible) + await expect(confirmButton).not.toBeVisible({ timeout: 5000 }); + + // Check for success toast notification + // The toast should contain text about items being deleted + const toastLocator = page.locator("[role='status'], [class*='toast'], [class*='sonner']"); + await expect(toastLocator.first()).toBeVisible({ timeout: 10000 }); + + // Take screenshot of the page after confirmation + await page.screenshot({ + path: "/tmp/playwright-logs/after-wipe-confirmation.png", + fullPage: true, + }); + console.log("✅ Screenshot saved: after-wipe-confirmation.png"); + + console.log("✅ Test completed successfully!"); + console.log("✅ Wipe Inventory dialog opened correctly"); + console.log("✅ All three options (labels, locations, maintenance) are available"); + console.log("✅ Confirm button triggers the action"); + console.log("✅ Dialog closes after confirmation"); + }); + + test("should cancel wipe inventory operation", async ({ page }) => { + // Navigate to Tools page + await page.goto("/tools"); + await page.waitForLoadState("networkidle"); + + // Scroll to wipe inventory section + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(500); + + // Click Wipe Inventory button + const wipeButton = page.locator("button", { hasText: "Wipe Inventory" }).last(); + await wipeButton.click(); + await page.waitForTimeout(1000); + + // Verify dialog is open + await expect(page.locator("text=Wipe Inventory").first()).toBeVisible(); + + // Click Cancel button + const cancelButton = page.locator("button", { hasText: "Cancel" }); + await cancelButton.click(); + await page.waitForTimeout(1000); + + // Verify dialog is closed + await expect(page.locator("text=Wipe Inventory").first()).not.toBeVisible({ timeout: 5000 }); + + // Take screenshot after cancel + await page.screenshot({ + path: "/tmp/playwright-logs/after-cancel.png", + }); + console.log("✅ Screenshot saved: after-cancel.png"); + console.log("✅ Cancel button works correctly"); + }); +});