FE: Add tests to install/uninstall modals (#31992)

This commit is contained in:
RachelElysia
2025-08-19 09:04:07 -04:00
committed by GitHub
parent 977768d642
commit dae35c887b
8 changed files with 629 additions and 105 deletions
+27
View File
@@ -9,6 +9,7 @@ import {
IAppStoreApp,
IFleetMaintainedApp,
IFleetMaintainedAppDetails,
ISoftwareInstallResult,
} from "interfaces/software";
import {
ISoftwareTitlesResponse,
@@ -305,3 +306,29 @@ export const createMockFleetMaintainedAppDetails = (
) => {
return { ...DEFAULT_FLEET_MAINTAINED_APP_DETAILS_MOCK, ...overrides };
};
const DEFAULT_SOFTWARE_INSTALL_RESULT: ISoftwareInstallResult = {
host_display_name: "Test Host",
install_uuid: "uuid-123",
software_title: "CoolApp",
software_title_id: 1,
software_package: "com.cool.app",
host_id: 42,
status: "installed",
detail: "",
output: "",
pre_install_query_output: "",
post_install_script_output: "",
created_at: "2025-08-10T12:00:00Z",
updated_at: "2025-08-10T12:05:00Z",
self_service: false,
};
export const createMockSoftwareInstallResult = (
overrides?: Partial<ISoftwareInstallResult>
) => {
return {
...DEFAULT_SOFTWARE_INSTALL_RESULT,
...overrides,
};
};
@@ -0,0 +1,163 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import { createMockSoftwareInstallResult } from "__mocks__/softwareMock";
import { StatusMessage, ModalButtons } from "./SoftwareInstallDetailsModal";
describe("SoftwareInstallDetailsModal - StatusMessage component", () => {
it("renders basic 'is installed' message when not installed by fleet (no installResult provided)", () => {
render(<StatusMessage softwareName="CoolApp" isDUP={false} />);
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
expect(screen.getByText(/is installed/)).toBeInTheDocument();
});
it("on software library page/pending activity, renders pending install message with host and package name", () => {
render(
<StatusMessage
softwareName="CoolApp"
installResult={createMockSoftwareInstallResult({
status: "pending_install",
})}
isDUP={false}
/>
);
expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument();
expect(
screen.getByText(/is installing or will install/)
).toBeInTheDocument();
expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument();
expect(screen.getByText(/Test Host/)).toBeInTheDocument();
expect(screen.getByText(/when it comes online/)).toBeInTheDocument();
});
it("on device user page, renders failed install with retry option with contact link", () => {
render(
<StatusMessage
softwareName="CoolApp"
installResult={createMockSoftwareInstallResult({
status: "failed_install",
})}
isDUP
contactUrl="http://support"
/>
);
expect(screen.queryByTestId("error-icon")).toBeInTheDocument();
expect(screen.getByText(/failed to install/)).toBeInTheDocument();
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
// Host name should not be rendered for device user page
expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument();
expect(screen.getByText(/You can retry/)).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /contact your IT admin/ })
).toHaveAttribute("href", "http://support");
});
it("on device user page, renders failed install with retry option without contact link", () => {
render(
<StatusMessage
softwareName="CoolApp"
installResult={createMockSoftwareInstallResult({
status: "failed_install",
})}
isDUP
/>
);
expect(screen.queryByTestId("error-icon")).toBeInTheDocument();
expect(screen.getByText(/failed to install/)).toBeInTheDocument();
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
// Host name should not be rendered for device user page
expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument();
expect(screen.getByText(/You can retry/)).toBeInTheDocument();
// Don't show link of not provided
expect(
screen.queryByRole("link", { name: /contact your IT admin/ })
).not.toBeInTheDocument();
});
it("on host details page, renders failed install without retry", () => {
render(
<StatusMessage
softwareName="CoolApp"
installResult={createMockSoftwareInstallResult({
status: "failed_install",
})}
isDUP={false}
contactUrl="http://support"
/>
);
expect(screen.queryByTestId("error-icon")).toBeInTheDocument();
expect(screen.getByText(/failed to install/)).toBeInTheDocument();
expect(screen.getByText(/Test Host/)).toBeInTheDocument();
expect(screen.queryByText(/You can retry/)).not.toBeInTheDocument();
});
it("on host details page/install activity, renders installed message with timestamp", () => {
render(
<StatusMessage
softwareName="CoolApp"
installResult={createMockSoftwareInstallResult({
status: "installed",
})}
isDUP={false}
/>
);
expect(screen.queryByTestId("success-icon")).toBeInTheDocument();
expect(screen.getByText(/Fleet installed/)).toBeInTheDocument();
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
expect(screen.getByText(/Test Host/)).toBeInTheDocument();
expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument();
expect(screen.getByText(/\d+.*ago/)).toBeInTheDocument();
});
});
describe("SoftwareInstallDetailsModal - ModalButtons component", () => {
it("on device user page, shows Retry/Cancel for failed install and triggers handlers", async () => {
const onCancel = jest.fn();
const onRetry = jest.fn();
const { user } = renderWithSetup(
<ModalButtons
deviceAuthToken="token123"
status="failed_install"
hostSoftwareId={99}
onCancel={onCancel}
onRetry={onRetry}
/>
);
expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Retry" }));
expect(onRetry).toHaveBeenCalledWith(99);
expect(onCancel).toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(2);
});
it("shows Done button for pending install", () => {
const onCancel = jest.fn();
render(<ModalButtons status="pending_install" onCancel={onCancel} />);
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Retry" })
).not.toBeInTheDocument();
});
it("on device user page, shows Done button for installed software", () => {
const onCancel = jest.fn();
render(
<ModalButtons
deviceAuthToken="token123"
status="installed"
onCancel={onCancel}
/>
);
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
});
});
@@ -16,6 +16,7 @@ import deviceUserAPI from "services/entities/device_user";
import InventoryVersions from "pages/hosts/details/components/InventoryVersions";
import Modal from "components/Modal";
import ModalFooter from "components/ModalFooter";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import Textarea from "components/Textarea";
@@ -49,19 +50,21 @@ export const renderContactOption = (url?: string) => (
</>
);
// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases
// present there
const StatusMessage = ({
softwareName,
installResult,
isDUP,
contactUrl,
}: {
interface IInstallStatusMessage {
softwareName: string;
installResult?: ISoftwareInstallResult;
isDUP: boolean;
contactUrl?: string;
}) => {
}
// TODO - match VppInstallDetailsModal status to this, still accounting for MDM-specific cases
// present there
export const StatusMessage = ({
softwareName,
installResult,
isDUP,
contactUrl,
}: IInstallStatusMessage) => {
// the case when software is installed by the user and not by Fleet
if (!installResult) {
return (
@@ -142,6 +145,51 @@ const StatusMessage = ({
);
};
interface IModalButtonsProps {
deviceAuthToken?: string;
status?: string;
hostSoftwareId?: number;
onRetry?: (id: number) => void;
onCancel: () => void;
}
export const ModalButtons = ({
deviceAuthToken,
status,
hostSoftwareId,
onRetry,
onCancel,
}: IModalButtonsProps) => {
if (deviceAuthToken && status === "failed_install") {
const onClickRetry = () => {
// on DUP, where this is relevant, both will be defined
if (onRetry && hostSoftwareId) {
onRetry(hostSoftwareId);
}
onCancel();
};
return (
<ModalFooter
primaryButtons={
<>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
</>
}
/>
);
}
return (
<ModalFooter primaryButtons={<Button onClick={onCancel}>Done</Button>} />
);
};
interface ISoftwareInstallDetailsProps {
/** note that details.install_uuid is present in hostSoftware, but since it is always needed for
this modal while hostSoftware is not, as in the case of the activity feeds, it is specifically
@@ -170,14 +218,6 @@ export const SoftwareInstallDetailsModal = ({
setShowInstallDetails((prev) => !prev);
};
const onClickRetry = () => {
// on DUP, where this is relevant, both will be defined
if (onRetry && hostSoftware?.id) {
onRetry(hostSoftware.id);
}
onCancel();
};
const isInstalledByFleet = hostSoftware
? !!hostSoftware.software_package?.last_install
: true; // if no hostSoftware passed in, can assume this is the activity feed, meaning this can only refer to a Fleet-handled install
@@ -304,26 +344,6 @@ export const SoftwareInstallDetailsModal = ({
);
};
const renderCta = () => {
if (deviceAuthToken && swInstallResult?.status === "failed_install") {
return (
<div className="modal-cta-wrap">
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
</div>
);
}
return (
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Done</Button>
</div>
);
};
return (
<Modal
title="Install details"
@@ -333,7 +353,13 @@ export const SoftwareInstallDetailsModal = ({
>
<>
{renderContent()}
{renderCta()}
<ModalButtons
deviceAuthToken={deviceAuthToken}
status={swInstallResult?.status}
hostSoftwareId={hostSoftware?.id}
onRetry={onRetry}
onCancel={onCancel}
/>
</>
</Modal>
);
@@ -0,0 +1,155 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import { createMockHostSoftware } from "__mocks__/hostMock";
import { IHostSoftwareUiStatus } from "interfaces/software";
import { StatusMessage, ModalButtons } from "./SoftwareUninstallDetailsModal";
describe("SoftwareUninstallDetailsModal - StatusMessage component", () => {
it("from activity or host details page of offline host, renders pending uninstall message with package name and host", () => {
render(
<StatusMessage
hostDisplayName="Offline Host"
status="pending_uninstall"
softwareName="CoolApp"
softwarePackageName="com.cool.app"
isDUP={false}
/>
);
expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument();
expect(
screen.getByText(/is uninstalling or will uninstall/)
).toBeInTheDocument();
expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument(); // Show package name
expect(screen.getByText(/Offline Host/)).toBeInTheDocument(); // Show host name
expect(screen.getByText(/when it comes online/)).toBeInTheDocument(); // Only reach modal if host offline
});
// from device user page, cannot reach pending uninstall modal
it("from device user page, renders failed uninstall message with retry text", () => {
render(
<StatusMessage
hostDisplayName="Test Host"
status="failed_uninstall"
softwareName="CoolApp"
isDUP
contactUrl="http://support"
/>
);
expect(screen.queryByTestId("error-icon")).toBeInTheDocument();
expect(screen.getByText(/failed to uninstall/)).toBeInTheDocument();
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
expect(screen.queryByText(/Test Host/)).not.toBeInTheDocument(); // Do not render host name
expect(screen.getByText(/You can retry/)).toBeInTheDocument(); // Render retry message
});
it("from host details page/failed uninstall activity, renders failed uninstall message for with no retry text", () => {
render(
<StatusMessage
hostDisplayName="Test Host"
status="failed_uninstall"
softwareName="CoolApp"
isDUP={false}
contactUrl="http://support"
/>
);
expect(screen.queryByTestId("error-icon")).toBeInTheDocument();
expect(screen.getByText(/failed to uninstall/)).toBeInTheDocument();
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
expect(screen.getByText(/Test Host/)).toBeInTheDocument(); // Render host name
expect(screen.queryByText(/You can retry/)).not.toBeInTheDocument(); // Do not render retry message
});
it("from successful uninstall activity, renders uninstalled message with timestamp", () => {
render(
<StatusMessage
hostDisplayName="Test Host"
status="uninstalled"
softwareName="CoolApp"
softwarePackageName="com.cool.app"
timestamp="2025-08-10T12:00:00Z"
isDUP={false}
/>
);
expect(screen.queryByTestId("success-icon")).toBeInTheDocument();
expect(screen.getByText(/Fleet uninstalled/)).toBeInTheDocument();
expect(screen.getByText(/CoolApp/)).toBeInTheDocument();
expect(screen.getByText(/Test Host/)).toBeInTheDocument();
expect(screen.getByText(/\(com\.cool\.app\)/)).toBeInTheDocument();
expect(screen.getByText(/\d+.*ago/)).toBeInTheDocument(); // timestamp relative
});
// from device user page, cannot reach successful uninstall modal
});
describe("SoftwareUninstallDetailsModal - ModalButtons component", () => {
it("from failed uninstall on a device user page, shows Retry/Cancel and calls handlers", async () => {
const onCancel = jest.fn();
const onRetry = jest.fn();
const hostSoftware = {
...createMockHostSoftware({ status: "failed_uninstall" }),
ui_status: "failed_uninstall" as IHostSoftwareUiStatus,
};
const { user } = renderWithSetup(
<ModalButtons
uninstallStatus="failed_uninstall"
deviceAuthToken="token123"
onCancel={onCancel}
onRetry={onRetry}
hostSoftware={hostSoftware}
/>
);
expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Retry" }));
expect(onRetry).toHaveBeenCalledWith(hostSoftware);
expect(onCancel).toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(2); // first from retry, second from cancel
});
it("from pending uninstall activity or software library of an offline host, shows only Done button", () => {
const onCancel = jest.fn();
render(
<ModalButtons uninstallStatus="pending_uninstall" onCancel={onCancel} />
);
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Retry" })
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Cancel" })
).not.toBeInTheDocument();
});
// from device user page, cannot reach pending uninstall modal
it("from successful uninstall activity, shows Done", () => {
const onCancel = jest.fn();
render(
<ModalButtons
uninstallStatus="uninstalled"
deviceAuthToken="token123"
onCancel={onCancel}
/>
);
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
});
// from device user page, cannot reach successful uninstall modal
});
@@ -18,6 +18,7 @@ import Button from "components/buttons/Button";
import DataError from "components/DataError";
import Icon from "components/Icon";
import Modal from "components/Modal";
import ModalFooter from "components/ModalFooter";
import Spinner from "components/Spinner";
import Textarea from "components/Textarea";
import RevealButton from "components/buttons/RevealButton";
@@ -30,7 +31,7 @@ import { renderContactOption } from "../SoftwareInstallDetailsModal/SoftwareInst
const baseClass = "software-uninstall-details-modal";
interface IUninstallStatusMessage {
host_display_name: string;
hostDisplayName: string;
status: SoftwareUninstallStatus;
softwareName: string;
softwarePackageName?: string;
@@ -39,8 +40,8 @@ interface IUninstallStatusMessage {
contactUrl?: string;
}
const StatusMessage = ({
host_display_name,
export const StatusMessage = ({
hostDisplayName,
status,
softwareName,
softwarePackageName,
@@ -48,11 +49,7 @@ const StatusMessage = ({
isDUP,
contactUrl,
}: IUninstallStatusMessage) => {
const formattedHost = host_display_name ? (
<b>{host_display_name}</b>
) : (
"the host"
);
const formattedHost = hostDisplayName ? <b>{hostDisplayName}</b> : "the host";
const isPending = isPendingStatus(status);
const displayTimeStamp =
@@ -100,6 +97,51 @@ const StatusMessage = ({
);
};
interface IModalButtonsProps {
uninstallStatus: SoftwareUninstallStatus;
deviceAuthToken?: string;
onCancel: () => void;
onRetry?: (s: IHostSoftwareWithUiStatus) => void;
hostSoftware?: IHostSoftwareWithUiStatus;
}
export const ModalButtons = ({
uninstallStatus,
deviceAuthToken,
onCancel,
onRetry,
hostSoftware,
}: IModalButtonsProps) => {
const onClickRetry = () => {
if (onRetry && hostSoftware) {
onRetry(hostSoftware);
}
onCancel();
};
if (deviceAuthToken && uninstallStatus === "failed_uninstall") {
return (
<ModalFooter
primaryButtons={
<>
{" "}
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
</>
}
/>
);
}
return (
<ModalFooter primaryButtons={<Button onClick={onCancel}>Done</Button>} />
);
};
export interface ISWUninstallDetailsParentState {
softwareName: string;
uninstallStatus: SoftwareUninstallStatus;
@@ -142,14 +184,6 @@ const SoftwareUninstallDetailsModal = ({
const toggleDetails = () => setShowDetails((prev) => !prev);
const onClickRetry = () => {
// on DUP, where this is relevant, both will be defined
if (onRetry && hostSoftware) {
onRetry(hostSoftware);
}
onCancel();
};
const { data: uninstallResult, isLoading, isError, error } = useQuery<
IScriptResultResponse,
AxiosError
@@ -192,7 +226,7 @@ const SoftwareUninstallDetailsModal = ({
return (
<div className={`${baseClass}__modal-content`}>
<StatusMessage
host_display_name={hostDisplayName || ""}
hostDisplayName={hostDisplayName || ""}
status={
(uninstallStatus || "pending_uninstall") as SoftwareUninstallStatus
}
@@ -225,26 +259,6 @@ const SoftwareUninstallDetailsModal = ({
);
};
const renderCta = () => {
if (deviceAuthToken && uninstallStatus === "failed_uninstall") {
return (
<div className="modal-cta-wrap">
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
</div>
);
}
return (
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Done</Button>
</div>
);
};
return (
<Modal
title="Uninstall details"
@@ -254,7 +268,13 @@ const SoftwareUninstallDetailsModal = ({
>
<>
{renderContent()}
{renderCta()}
<ModalButtons
uninstallStatus={uninstallStatus}
deviceAuthToken={deviceAuthToken}
onCancel={onCancel}
onRetry={onRetry}
hostSoftware={hostSoftware}
/>
</>
</Modal>
);
@@ -1,5 +1,8 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { getStatusMessage } from "./VppInstallDetailsModal";
import { renderWithSetup } from "test/test-utils";
import { getStatusMessage, ModalButtons } from "./VppInstallDetailsModal";
describe("getStatusMessage helper function", () => {
it("shows NotNow message when isStatusNotNow is true", () => {
@@ -77,6 +80,20 @@ describe("getStatusMessage helper function", () => {
).toBeInTheDocument();
});
it("shows pending install on host when it comes online", () => {
render(
getStatusMessage({
displayStatus: "pending_install",
isMDMStatusNotNow: false,
isMDMStatusAcknowledged: false,
appName: "Logic Pro",
hostDisplayName: "Marko's MacBook Pro",
commandUpdatedAt: "2025-07-29T22:49:52Z",
})
);
expect(screen.getByText(/when it comes online/i)).toBeInTheDocument();
});
it("shows default message for installed status", () => {
render(
getStatusMessage({
@@ -93,6 +110,22 @@ describe("getStatusMessage helper function", () => {
expect(screen.getByText(/Marko's MacBook Pro/i)).toBeInTheDocument();
});
it("shows manual install message when installed not through Fleet", () => {
render(
getStatusMessage({
displayStatus: "installed",
isMDMStatusNotNow: false,
isMDMStatusAcknowledged: false,
appName: "Logic Pro",
hostDisplayName: "Marko's MacBook Pro",
commandUpdatedAt: "", // <-- empty
})
);
expect(screen.getByText(/Logic Pro/i)).toBeInTheDocument();
expect(screen.getByText(/is installed\./i)).toBeInTheDocument();
});
it("shows default message with 'the host' if host_display_name is empty", () => {
render(
getStatusMessage({
@@ -107,4 +140,75 @@ describe("getStatusMessage helper function", () => {
expect(screen.getByText(/Fleet installed/i)).toBeInTheDocument();
expect(screen.getByText(/the host/i)).toBeInTheDocument();
});
it("shows relative timestamp when available", () => {
render(
getStatusMessage({
displayStatus: "failed_install",
isMDMStatusNotNow: false,
isMDMStatusAcknowledged: false,
appName: "Logic Pro",
hostDisplayName: "Marko's MacBook Pro",
commandUpdatedAt: new Date().toISOString(),
})
);
expect(screen.getByText(/\(.*ago\)/i)).toBeInTheDocument();
});
it("on the device user page, does not show host info", () => {
render(
getStatusMessage({
isDUP: true,
displayStatus: "installed",
isMDMStatusNotNow: false,
isMDMStatusAcknowledged: false,
appName: "Logic Pro",
hostDisplayName: "Marko's MacBook Pro",
commandUpdatedAt: "2025-07-29T22:49:52Z",
})
);
expect(screen.queryByText(/Marko's MacBook Pro/i)).not.toBeInTheDocument();
});
});
describe("VPP Install Details Modal - ModalButtons component", () => {
it("renders Done button by default", async () => {
const onCancel = jest.fn();
const { user } = renderWithSetup(
<ModalButtons displayStatus="installed" onCancel={onCancel} />
);
const doneButton = screen.getByRole("button", { name: /done/i });
expect(doneButton).toBeInTheDocument();
await user.click(doneButton);
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("renders Cancel + Retry when failed_install with deviceAuthToken", async () => {
const onCancel = jest.fn();
const onRetry = jest.fn();
const { user } = renderWithSetup(
<ModalButtons
displayStatus="failed_install"
deviceAuthToken="fake_token"
onCancel={onCancel}
onRetry={onRetry}
hostSoftwareId={123}
/>
);
const cancelButton = screen.getByRole("button", { name: /cancel/i });
const retryButton = screen.getByRole("button", { name: /retry/i });
expect(cancelButton).toBeInTheDocument();
expect(retryButton).toBeInTheDocument();
// Retry should trigger onRetry + onCancel
await user.click(retryButton);
expect(onRetry).toHaveBeenCalledWith(123);
expect(onCancel).toHaveBeenCalled();
});
});
@@ -14,6 +14,7 @@ import { IMdmCommandResult } from "interfaces/mdm";
import InventoryVersions from "pages/hosts/details/components/InventoryVersions";
import Modal from "components/Modal";
import ModalFooter from "components/ModalFooter";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import Textarea from "components/Textarea";
@@ -148,6 +149,50 @@ export const getStatusMessage = ({
);
};
interface IModalButtonsProps {
displayStatus: SoftwareInstallStatus | "pending";
deviceAuthToken?: string;
onCancel: () => void;
onRetry?: (id: number) => void;
hostSoftwareId?: number;
}
export const ModalButtons = ({
displayStatus,
deviceAuthToken,
onCancel,
onRetry,
hostSoftwareId,
}: IModalButtonsProps) => {
const onClickRetry = () => {
// on DUP, where this is relevant, both will be defined
if (onRetry && hostSoftwareId) {
onRetry(hostSoftwareId);
}
onCancel();
};
if (deviceAuthToken && displayStatus === "failed_install") {
return (
<ModalFooter
primaryButtons={
<>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
</>
}
/>
);
}
return (
<ModalFooter primaryButtons={<Button onClick={onCancel}>Done</Button>} />
);
};
const baseClass = "vpp-install-details-modal";
export type IVppInstallDetails = {
@@ -206,13 +251,6 @@ export const VppInstallDetailsModal = ({
};
};
const onClickRetry = () => {
// on DUP, where this is relevant, both will be defined
if (onRetry && hostSoftware?.id) {
onRetry(hostSoftware.id);
}
onCancel();
};
const {
data: vppCommandResult,
isLoading: isLoadingVPPCommandResult,
@@ -349,26 +387,6 @@ export const VppInstallDetailsModal = ({
);
};
const renderCta = () => {
if (deviceAuthToken && displayStatus === "failed_install") {
return (
<div className="modal-cta-wrap">
<Button type="submit" onClick={onClickRetry}>
Retry
</Button>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
</div>
);
}
return (
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Done</Button>
</div>
);
};
return (
<Modal
title="Install details"
@@ -378,7 +396,13 @@ export const VppInstallDetailsModal = ({
>
<>
{renderContent()}
{renderCta()}
<ModalButtons
deviceAuthToken={deviceAuthToken}
hostSoftwareId={hostSoftware?.id}
onRetry={onRetry}
onCancel={onCancel}
displayStatus={displayStatus}
/>
</>
</Modal>
);
@@ -69,6 +69,8 @@ describe("SoftwareUpdateModal", () => {
onUpdate={noop}
/>
);
expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument();
expect(
screen.getByText(/Fleet is updating or will update/i)
).toBeInTheDocument();
@@ -89,6 +91,8 @@ describe("SoftwareUpdateModal", () => {
onUpdate={noop}
/>
);
expect(screen.queryByTestId("pending-outline-icon")).toBeInTheDocument();
expect(
screen.getByText(/Fleet is updating or will update/i)
).toBeInTheDocument();
@@ -108,6 +112,7 @@ describe("SoftwareUpdateModal", () => {
/>
);
expect(screen.queryByTestId("error-outline-icon")).toBeInTheDocument();
expect(screen.getByText(/New version of/i)).toBeInTheDocument();
expect(screen.getByText(/mock software.app/i)).toBeInTheDocument();
expect(