Compare commits

...

5 Commits

Author SHA1 Message Date
dependabot[bot]
10ce256b6a Bump the npm_and_yarn group across 2 directories with 1 update (#1203)
Bumps the npm_and_yarn group with 1 update in the / directory: [qs](https://github.com/ljharb/qs).
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [qs](https://github.com/ljharb/qs).


Updates `qs` from 6.14.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

Updates `qs` from 6.14.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 21:11:11 -05:00
Matthew Kilgore
cb2c6ef8f2 Stop publishing sha releases with tags 2026-01-01 21:10:12 -05:00
Matt
abad8ad1ee Add pikapods hosting 2025-12-31 08:50:50 -05:00
Phil
b02b39c1b3 Refactor wipe inventory E2E playwright tests 2025-12-29 16:28:41 +00:00
Phil
b290175bb0 Refactor Wipe Inventory E2E tests
Initial commit
2025-12-29 16:16:20 +00:00
7 changed files with 195 additions and 113 deletions

View File

@@ -78,6 +78,15 @@ jobs:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=schedule,pattern=nightly
flavor: |
suffix=-hardened,onlatest=true
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1

View File

@@ -81,6 +81,15 @@ jobs:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=schedule,pattern=nightly
flavor: |
suffix=-rootless,onlatest=true
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1

View File

@@ -76,6 +76,13 @@ jobs:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=schedule,pattern=nightly
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1

View File

@@ -21,6 +21,9 @@
<img src="https://img.shields.io/mastodon/follow/110749314839831923?domain=infosec.exchange"/>
<img src="https://img.shields.io/lemmy/homebox%40lemmy.world?label=lemmy"/>
</p>
<p align="center" style="width: 100%;">
<a href="https://www.pikapods.com/pods?run=homebox"><img src="https://www.pikapods.com/static/run-button.svg"/></a>
</p>
## What is HomeBox

View File

@@ -5235,8 +5235,8 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
quansync@0.2.11:
@@ -6289,6 +6289,7 @@ packages:
vue-i18n@11.2.7:
resolution: {integrity: sha512-LPv8bAY5OA0UvFEXl4vBQOBqJzRrlExy92tWgRuwW7tbykHf7CH71G2Y4TM2OwGcIS4+hyqKHS2EVBqaYwPY9Q==}
engines: {node: '>= 16'}
deprecated: This version is NOT deprecated. Previous deprecation was a mistake.
peerDependencies:
vue: ^3.0.0
@@ -11388,7 +11389,7 @@ snapshots:
micro-api-client: 3.3.0
node-fetch: 3.3.2
p-wait-for: 5.0.2
qs: 6.14.0
qs: 6.14.1
optional: true
nitropack@2.12.9(@netlify/blobs@9.1.2):
@@ -12198,7 +12199,7 @@ snapshots:
punycode@2.3.1: {}
qs@6.14.0:
qs@6.14.1:
dependencies:
side-channel: 1.1.0
optional: true

View File

@@ -1,129 +1,182 @@
import type { Page } from "@playwright/test";
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");
const STATUS_ROUTE = "**/api/v1/status";
const WIPE_ROUTE = "**/api/v1/actions/wipe-inventory";
const buildStatusResponse = (demo: boolean) => ({
allowRegistration: true,
build: { buildTime: new Date().toISOString(), commit: "test", version: "v0.0.0" },
demo,
health: true,
labelPrinting: false,
latest: { date: new Date().toISOString(), version: "v0.0.0" },
message: "",
oidc: { allowLocal: true, autoRedirect: false, buttonText: "", enabled: false },
title: "Homebox",
versions: [],
});
async function mockStatus(page: Page, demo: boolean) {
await page.route(STATUS_ROUTE, route => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildStatusResponse(demo)),
});
});
}
async function login(page: Page, email = "demo@example.com", password = "demo") {
await page.goto("/home");
await expect(page).toHaveURL("/");
await page.fill("input[type='text']", email);
await page.fill("input[type='password']", password);
await page.click("button[type='submit']");
await expect(page).toHaveURL("/home");
}
async function openWipeInventory(page: Page) {
await page.goto("/tools");
await page.waitForLoadState("networkidle");
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
const wipeButton = page.getByRole("button", { name: "Wipe Inventory" }).last();
await expect(wipeButton).toBeVisible();
await wipeButton.click();
}
test.describe("Wipe Inventory", () => {
test("shows demo mode warning without wipe options", async ({ page }) => {
await mockStatus(page, true);
await login(page);
await openWipeInventory(page);
await expect(
page.getByText(
"Inventory, labels, locations and maintenance records cannot be wiped whilst Homebox is in demo mode.",
{ exact: false }
)
).toBeVisible();
await expect(page.locator("input#wipe-labels-checkbox")).toHaveCount(0);
await expect(page.locator("input#wipe-locations-checkbox")).toHaveCount(0);
await expect(page.locator("input#wipe-maintenance-checkbox")).toHaveCount(0);
});
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",
test.describe("production mode", () => {
test.beforeEach(async ({ page }) => {
await mockStatus(page, false);
await login(page);
});
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);
test("renders wipe options and submits all flags", async ({ page }) => {
await page.route(WIPE_ROUTE, route => {
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ completed: 0 }) });
});
// 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();
await openWipeInventory(page);
await expect(page.getByText("Wipe Inventory").first()).toBeVisible();
// Take screenshot with all options checked
await page.screenshot({
path: "/tmp/playwright-logs/wipe-inventory-modal-options-checked.png",
const labels = page.locator("input#wipe-labels-checkbox");
const locations = page.locator("input#wipe-locations-checkbox");
const maintenance = page.locator("input#wipe-maintenance-checkbox");
await expect(labels).toBeVisible();
await expect(locations).toBeVisible();
await expect(maintenance).toBeVisible();
await labels.check();
await locations.check();
await maintenance.check();
const requestPromise = page.waitForRequest(WIPE_ROUTE);
await page.getByRole("button", { name: "Confirm" }).last().click();
const request = await requestPromise;
expect(request.postDataJSON()).toEqual({
wipeLabels: true,
wipeLocations: true,
wipeMaintenance: true,
});
await expect(page.locator("[role='status']").first()).toBeVisible();
});
console.log("✅ Screenshot saved: wipe-inventory-modal-options-checked.png");
// Click Confirm button
await confirmButton.click();
await page.waitForTimeout(2000);
test("blocks wipe attempts from non-owners", async ({ page }) => {
await page.route(WIPE_ROUTE, route => {
route.fulfill({
status: 403,
contentType: "application/json",
body: JSON.stringify({ message: "forbidden" }),
});
});
// Wait for the dialog to close (verify button is no longer visible)
await expect(confirmButton).not.toBeVisible({ timeout: 5000 });
await openWipeInventory(page);
// 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 });
const requestPromise = page.waitForRequest(WIPE_ROUTE);
await page.getByRole("button", { name: "Confirm" }).last().click();
await requestPromise;
// Take screenshot of the page after confirmation
await page.screenshot({
path: "/tmp/playwright-logs/after-wipe-confirmation.png",
fullPage: true,
await expect(page.getByText("Failed to wipe inventory.")).toBeVisible();
});
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");
});
const checkboxCases = [
{
name: "labels only",
selection: { labels: true, locations: false, maintenance: false },
},
{
name: "locations only",
selection: { labels: false, locations: true, maintenance: false },
},
{
name: "maintenance only",
selection: { labels: false, locations: false, maintenance: true },
},
];
test("should cancel wipe inventory operation", async ({ page }) => {
// Navigate to Tools page
await page.goto("/tools");
await page.waitForLoadState("networkidle");
for (const scenario of checkboxCases) {
test(`submits correct flags when ${scenario.name} is selected`, async ({ page }) => {
await page.route(WIPE_ROUTE, route => {
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ completed: 0 }) });
});
// Scroll to wipe inventory section
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
await openWipeInventory(page);
await expect(page.getByText("Wipe Inventory").first()).toBeVisible();
// Click Wipe Inventory button
const wipeButton = page.locator("button", { hasText: "Wipe Inventory" }).last();
await wipeButton.click();
await page.waitForTimeout(1000);
const labels = page.locator("input#wipe-labels-checkbox");
const locations = page.locator("input#wipe-locations-checkbox");
const maintenance = page.locator("input#wipe-maintenance-checkbox");
// Verify dialog is open
await expect(page.locator("text=Wipe Inventory").first()).toBeVisible();
if (scenario.selection.labels) {
await labels.check();
} else {
await labels.uncheck();
}
// Click Cancel button
const cancelButton = page.locator("button", { hasText: "Cancel" });
await cancelButton.click();
await page.waitForTimeout(1000);
if (scenario.selection.locations) {
await locations.check();
} else {
await locations.uncheck();
}
// Verify dialog is closed
await expect(page.locator("text=Wipe Inventory").first()).not.toBeVisible({ timeout: 5000 });
if (scenario.selection.maintenance) {
await maintenance.check();
} else {
await maintenance.uncheck();
}
// 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");
const requestPromise = page.waitForRequest(WIPE_ROUTE);
await page.getByRole("button", { name: "Confirm" }).last().click();
const request = await requestPromise;
expect(request.postDataJSON()).toEqual({
wipeLabels: scenario.selection.labels,
wipeLocations: scenario.selection.locations,
wipeMaintenance: scenario.selection.maintenance,
});
});
}
});
});

8
pnpm-lock.yaml generated
View File

@@ -3211,8 +3211,8 @@ packages:
protocols@2.0.2:
resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
quansync@0.2.11:
@@ -6860,7 +6860,7 @@ snapshots:
micro-api-client: 3.3.0
node-fetch: 3.3.2
p-wait-for: 5.0.2
qs: 6.14.0
qs: 6.14.1
optional: true
nitropack@2.12.9(@netlify/blobs@9.1.2):
@@ -7502,7 +7502,7 @@ snapshots:
protocols@2.0.2: {}
qs@6.14.0:
qs@6.14.1:
dependencies:
side-channel: 1.1.0
optional: true