/* @vitest-environment jsdom */
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { StoreAppDetail } from "@/lib/shared/contracts/apps";
const useStoreAppMock = vi.fn();
const useAppComposeMock = vi.fn();
const useStoreActionsMock = vi.fn();
vi.mock("@/modules/apps/hooks/useStoreApp", () => ({
useStoreApp: (...args: unknown[]) => useStoreAppMock(...args),
}));
vi.mock("@/modules/apps/hooks/useAppCompose", () => ({
useAppCompose: (...args: unknown[]) => useAppComposeMock(...args),
}));
vi.mock("@/modules/apps/hooks/useStoreActions", () => ({
useStoreActions: (...args: unknown[]) => useStoreActionsMock(...args),
}));
import { AppConfiguratorPanel } from "@/modules/apps/components/configurator/app-configurator-panel";
const template: StoreAppDetail = {
id: "home-assistant",
name: "Home Assistant",
description: "Home automation",
platform: "linux",
categories: ["Automation"],
logoUrl: "https://example.com/ha.png",
repositoryUrl: "https://github.com/home-assistant/core",
stackFile: "compose.yml",
status: "not_installed",
webUiPort: 8123,
updateAvailable: false,
localDigest: null,
remoteDigest: null,
note: "Install note",
env: [],
screenshots: [],
installedConfig: null,
};
function setupActions() {
const saveAppSettings = vi.fn().mockResolvedValue(undefined);
const installApp = vi.fn().mockResolvedValue(undefined);
const installCustomApp = vi.fn().mockResolvedValue(undefined);
useStoreActionsMock.mockReturnValue({
saveAppSettings,
installApp,
installCustomApp,
});
return { saveAppSettings, installApp, installCustomApp };
}
describe("AppConfiguratorPanel", () => {
beforeEach(() => {
useStoreAppMock.mockReset();
useAppComposeMock.mockReset();
useStoreActionsMock.mockReset();
useStoreAppMock.mockReturnValue({
data: null,
isLoading: false,
});
useAppComposeMock.mockReturnValue({
data: {
compose:
'services:\n app:\n image: ghcr.io/example/app:latest\n ports:\n - "8123:8123"\n',
primary: {
image: "ghcr.io/example/app:latest",
ports: ["8123:8123"],
},
primaryServiceName: "app",
},
isLoading: false,
isError: false,
error: null,
});
});
it("selects compose source by context", () => {
setupActions();
const installedTarget = {
appId: "home-assistant",
appName: "Home Assistant",
dashboardUrl: "http://homeio.local:8123",
containerName: "home-assistant",
};
const { rerender } = render(
,
);
expect(useAppComposeMock).toHaveBeenCalledWith(
"home-assistant",
true,
"catalog",
);
rerender(
,
);
expect(useAppComposeMock).toHaveBeenCalledWith(
"home-assistant",
true,
"installed",
);
});
it("shows docker run tab only for custom install context", () => {
setupActions();
const { rerender } = render(
,
);
expect(screen.getByRole("button", { name: "Docker Run" })).toBeTruthy();
rerender();
expect(screen.queryByRole("button", { name: "Docker Run" })).toBeNull();
});
it("syncs classic edits into compose view", () => {
setupActions();
render();
fireEvent.change(screen.getByLabelText("Docker Image"), {
target: { value: "nginx:1.27" },
});
fireEvent.click(screen.getByRole("button", { name: "Docker Compose" }));
const composeEditor = screen.getByLabelText("Docker Compose") as HTMLTextAreaElement;
expect(composeEditor.value).toContain("image: nginx:1.27");
});
it("submits install in catalog context", async () => {
const { installApp } = setupActions();
render();
fireEvent.click(screen.getByRole("button", { name: "Install" }));
await waitFor(() => {
expect(installApp).toHaveBeenCalledTimes(1);
});
expect(installApp).toHaveBeenCalledWith(
expect.objectContaining({
appId: "home-assistant",
composeSource: expect.stringContaining("services:"),
}),
);
});
it("prefers injected store actions for catalog installs", async () => {
const { installApp: fallbackInstallApp } = setupActions();
const injectedInstallApp = vi.fn().mockResolvedValue(undefined);
render(
,
);
fireEvent.click(screen.getByRole("button", { name: "Install" }));
await waitFor(() => {
expect(injectedInstallApp).toHaveBeenCalledTimes(1);
});
expect(fallbackInstallApp).not.toHaveBeenCalled();
});
it("submits docker run install for custom context without manual port field", async () => {
const { installCustomApp } = setupActions();
render();
fireEvent.change(screen.getByLabelText("App Name"), {
target: { value: "My Run App" },
});
fireEvent.change(screen.getByLabelText("Docker Run"), {
target: { value: "docker run --name my-run-app -p 8088:80 nginx:latest" },
});
fireEvent.click(screen.getByRole("button", { name: "Install" }));
await waitFor(() => {
expect(installCustomApp).toHaveBeenCalledWith(
expect.objectContaining({
name: "My Run App",
sourceType: "docker-run",
source: "docker run --name my-run-app -p 8088:80 nginx:latest",
}),
);
});
expect(screen.queryByLabelText("Web UI Port")).toBeNull();
});
it("shows the server host instead of localhost for legacy loopback app urls", () => {
setupActions();
useStoreAppMock.mockReturnValue({
data: {
...template,
installedConfig: {
displayName: "Home Assistant",
iconUrl: template.logoUrl,
env: {},
stackName: "home-assistant",
composeSource: "services:\n app:\n image: ghcr.io/example/app:latest\n environment:\n APP_URL: http://localhost:8123\n",
},
},
isLoading: false,
});
useAppComposeMock.mockReturnValue({
data: {
compose:
'services:\n app:\n image: ghcr.io/example/app:latest\n environment:\n APP_URL: "http://localhost:8123"\n ports:\n - "8123:8123"\n',
primary: {
image: "ghcr.io/example/app:latest",
environment: {
APP_URL: "http://localhost:8123",
},
ports: ["8123:8123"],
},
primaryServiceName: "app",
},
isLoading: false,
isError: false,
error: null,
});
render(
,
);
expect(
(screen.getByLabelText("Web UI host") as HTMLInputElement).value,
).toBe("192.168.1.12");
});
});