FN-8134: add update banner restart action
Offer a supervised Fusion restart directly after an in-app update succeeds. - Add restart capability detection, scheduling feedback, and manual-restart guidance to the update banner - Cover supported, unsupported, failed, loading, and mobile restart states - Document the update flow and add a minor CLI changeset Files changed: .changeset/fn-8134-restart-after-update.md | 7 ++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/UpdateAvailableBanner.css | 10 +- packages/dashboard/app/components/UpdateAvailableBanner.tsx | 100 +++++++++++++-- packages/dashboard/app/components/__tests__/UpdateAvailableBanner.test.tsx | 139 ++++++++++++++++++--- packages/i18n/locales/en/app.json | 4 + 6 files changed, 236 insertions(+), 26 deletions(-) Fusion-Task-Id: FN-8134 Fusion-Task-Lineage: 84f9290e-aa87-4144-a9db-da445dcb76e3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8134-restart-after-update.md
Normal file
7
.changeset/fn-8134-restart-after-update.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Add a one-click "Restart Fusion" button to the update banner after an in-app update.
|
||||||
|
category: feature
|
||||||
|
dev: Reuses POST /api/system/restart via requestSystemRestart and the SystemInfoResponse.restartSupported capability flag; button degrades to a disabled state with a manual-restart note when unsupervised.
|
||||||
@@ -6,7 +6,7 @@ The Fusion dashboard is the main control plane for tasks, agents, missions, sett
|
|||||||
|
|
||||||
## Dashboard Updates
|
## Dashboard Updates
|
||||||
|
|
||||||
When Fusion detects a newer `@runfusion/fusion` release, the Settings modal footer shows the available version with **Learn more** and **Update now** actions. **Update now** installs the latest global package with npm; after it succeeds, restart Fusion to apply the new version because the already-running dashboard server is unchanged until restart.
|
When Fusion detects a newer `@runfusion/fusion` release, the Settings modal footer shows the available version with **Learn more** and **Update now** actions. **Update now** installs the latest global package with npm; after it succeeds, the dashboard update banner offers a one-click **Restart Fusion** action because the already-running dashboard server is unchanged until restart. When Fusion is unsupervised (for example, started with `--no-supervise`), that banner action is disabled and explains that Fusion must be restarted manually.
|
||||||
|
|
||||||
## Settings discovery
|
## Settings discovery
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-available-banner__update-btn {
|
.update-available-banner__update-btn,
|
||||||
|
.update-available-banner__restart-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
@@ -63,7 +64,8 @@
|
|||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-available-banner__update-btn svg.spinning {
|
.update-available-banner__update-btn svg.spinning,
|
||||||
|
.update-available-banner__restart-btn svg.spinning {
|
||||||
animation: update-available-banner-spin 1s linear infinite;
|
animation: update-available-banner-spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +115,10 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-available-banner__actions {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.update-available-banner__dismiss {
|
.update-available-banner__dismiss {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import "./UpdateAvailableBanner.css";
|
import "./UpdateAvailableBanner.css";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RefreshCw, X } from "lucide-react";
|
import { Power, RefreshCw, X } from "lucide-react";
|
||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import { getErrorMessage } from "@fusion/core";
|
import { getErrorMessage } from "@fusion/core";
|
||||||
import { installUpdate } from "../api";
|
import { fetchSystemInfo, installUpdate, requestSystemRestart } from "../api";
|
||||||
import type { UpdateInstallResponse } from "../api";
|
import type { UpdateInstallResponse } from "../api";
|
||||||
|
|
||||||
interface UpdateAvailableBannerProps {
|
interface UpdateAvailableBannerProps {
|
||||||
@@ -16,6 +16,27 @@ export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss
|
|||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const [installLoading, setInstallLoading] = useState(false);
|
const [installLoading, setInstallLoading] = useState(false);
|
||||||
const [installResult, setInstallResult] = useState<UpdateInstallResponse | null>(null);
|
const [installResult, setInstallResult] = useState<UpdateInstallResponse | null>(null);
|
||||||
|
const [restartSupported, setRestartSupported] = useState<boolean | undefined>();
|
||||||
|
const [restartLoading, setRestartLoading] = useState(false);
|
||||||
|
const [restartScheduled, setRestartScheduled] = useState(false);
|
||||||
|
const [restartError, setRestartError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
void fetchSystemInfo()
|
||||||
|
.then((info) => {
|
||||||
|
if (active) setRestartSupported(info.restartSupported);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Fail closed: an unavailable capability response must not offer a restart that cannot run.
|
||||||
|
if (active) setRestartSupported(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleInstallUpdate = async () => {
|
const handleInstallUpdate = async () => {
|
||||||
setInstallLoading(true);
|
setInstallLoading(true);
|
||||||
@@ -35,8 +56,33 @@ export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:UpdateBanner 2026-07-16-00:00:
|
||||||
|
Issue #1799 requires a successful in-app update to offer the supervised restart hook in-place.
|
||||||
|
Hosts without restart support keep the control visible but disabled with manual-restart guidance.
|
||||||
|
*/
|
||||||
|
const handleRestart = async () => {
|
||||||
|
if (restartLoading || restartSupported !== true) return;
|
||||||
|
|
||||||
|
setRestartLoading(true);
|
||||||
|
setRestartError(null);
|
||||||
|
try {
|
||||||
|
const result = await requestSystemRestart("update-banner");
|
||||||
|
if (result.scheduled) {
|
||||||
|
setRestartScheduled(true);
|
||||||
|
} else {
|
||||||
|
setRestartError(t("updateBanner.restartFailed", "Restart could not be scheduled. Try restarting Fusion manually."));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setRestartError(getErrorMessage(error) || t("updateBanner.restartFailed", "Restart could not be scheduled. Try restarting Fusion manually."));
|
||||||
|
} finally {
|
||||||
|
setRestartLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const installSucceeded = installResult?.updated === true;
|
const installSucceeded = installResult?.updated === true;
|
||||||
const installError = installResult?.error;
|
const installError = installResult?.error;
|
||||||
|
const restartUnavailable = restartSupported !== true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="update-available-banner" role="status" aria-live="polite">
|
<div className="update-available-banner" role="status" aria-live="polite">
|
||||||
@@ -63,11 +109,49 @@ export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss
|
|||||||
</p>
|
</p>
|
||||||
<div className="update-available-banner__actions">
|
<div className="update-available-banner__actions">
|
||||||
{installSucceeded ? (
|
{installSucceeded ? (
|
||||||
<span className="update-available-banner__install-status update-available-banner__install-status--success" aria-live="polite">
|
<>
|
||||||
{t("updateBanner.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", {
|
<span className="update-available-banner__install-status update-available-banner__install-status--success" aria-live="polite">
|
||||||
version: installResult.latestVersion ?? latestVersion,
|
{t("updateBanner.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", {
|
||||||
})}
|
version: installResult.latestVersion ?? latestVersion,
|
||||||
</span>
|
})}
|
||||||
|
</span>
|
||||||
|
{restartScheduled ? (
|
||||||
|
<span className="update-available-banner__install-status" aria-live="polite">
|
||||||
|
{t("updateBanner.restarting", "Restarting… Your connection will close shortly.")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm update-available-banner__restart-btn"
|
||||||
|
onClick={() => {
|
||||||
|
void handleRestart();
|
||||||
|
}}
|
||||||
|
disabled={restartUnavailable || restartLoading}
|
||||||
|
>
|
||||||
|
{restartLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw size={12} className="spinning" aria-hidden="true" />
|
||||||
|
{t("updateBanner.restarting", "Restarting…")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Power size={12} aria-hidden="true" />
|
||||||
|
{t("updateBanner.restartNow", "Restart Fusion")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{restartUnavailable && (
|
||||||
|
<span className="update-available-banner__install-status" aria-live="polite">
|
||||||
|
{t("updateBanner.restartUnavailable", "Needs a supervising parent — restart Fusion manually without --no-supervise.")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{restartError && (
|
||||||
|
<span className="update-available-banner__install-status update-available-banner__install-status--error" aria-live="polite">
|
||||||
|
{restartError}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { UpdateAvailableBanner } from "../UpdateAvailableBanner";
|
import { UpdateAvailableBanner } from "../UpdateAvailableBanner";
|
||||||
|
|
||||||
|
const mockFetchSystemInfo = vi.hoisted(() => vi.fn());
|
||||||
const mockInstallUpdate = vi.hoisted(() => vi.fn());
|
const mockInstallUpdate = vi.hoisted(() => vi.fn());
|
||||||
|
const mockRequestSystemRestart = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("../../api", () => ({
|
||||||
|
fetchSystemInfo: (...args: unknown[]) => mockFetchSystemInfo(...args),
|
||||||
installUpdate: (...args: unknown[]) => mockInstallUpdate(...args),
|
installUpdate: (...args: unknown[]) => mockInstallUpdate(...args),
|
||||||
|
requestSystemRestart: (...args: unknown[]) => mockRequestSystemRestart(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("lucide-react", async (importOriginal) => {
|
vi.mock("lucide-react", async (importOriginal) => {
|
||||||
@@ -17,15 +21,29 @@ vi.mock("lucide-react", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const successfulInstall = { currentVersion: "0.6.0", latestVersion: "0.7.0", updated: true };
|
||||||
|
|
||||||
|
function renderBanner() {
|
||||||
|
return render(<UpdateAvailableBanner latestVersion="0.7.0" currentVersion="0.6.0" onDismiss={vi.fn()} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeInstall() {
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Update now" }));
|
||||||
|
await screen.findByText("Updated to v0.7.0 — restart Fusion to apply");
|
||||||
|
}
|
||||||
|
|
||||||
describe("UpdateAvailableBanner", () => {
|
describe("UpdateAvailableBanner", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockFetchSystemInfo.mockReset();
|
||||||
mockInstallUpdate.mockReset();
|
mockInstallUpdate.mockReset();
|
||||||
mockInstallUpdate.mockResolvedValue({ currentVersion: "0.6.0", latestVersion: "0.7.0", updated: true });
|
mockRequestSystemRestart.mockReset();
|
||||||
|
mockFetchSystemInfo.mockResolvedValue({ restartSupported: true });
|
||||||
|
mockInstallUpdate.mockResolvedValue(successfulInstall);
|
||||||
|
mockRequestSystemRestart.mockResolvedValue({ scheduled: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders version information with release notes and learn more links", () => {
|
it("renders version information with release notes and learn more links", () => {
|
||||||
render(
|
renderBanner();
|
||||||
<UpdateAvailableBanner latestVersion="0.7.0" currentVersion="0.6.0" onDismiss={vi.fn()} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(/Update available: v0.7.0 \(current: v0.6.0\)/)).toBeInTheDocument();
|
expect(screen.getByText(/Update available: v0.7.0 \(current: v0.6.0\)/)).toBeInTheDocument();
|
||||||
expect(screen.getByText("fn update")).toBeInTheDocument();
|
expect(screen.getByText("fn update")).toBeInTheDocument();
|
||||||
@@ -40,9 +58,7 @@ describe("UpdateAvailableBanner", () => {
|
|||||||
it("dismiss button calls onDismiss", () => {
|
it("dismiss button calls onDismiss", () => {
|
||||||
const onDismiss = vi.fn();
|
const onDismiss = vi.fn();
|
||||||
|
|
||||||
render(
|
render(<UpdateAvailableBanner latestVersion="0.7.0" currentVersion="0.6.0" onDismiss={onDismiss} />);
|
||||||
<UpdateAvailableBanner latestVersion="0.7.0" currentVersion="0.6.0" onDismiss={onDismiss} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Dismiss update notice" }));
|
fireEvent.click(screen.getByRole("button", { name: "Dismiss update notice" }));
|
||||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||||
@@ -68,25 +84,95 @@ describe("UpdateAvailableBanner", () => {
|
|||||||
expect(screen.queryByRole("status")).toBeNull();
|
expect(screen.queryByRole("status")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables update-now while installing and then shows restart hint", async () => {
|
it("disables update-now while installing and then renders an enabled restart button", async () => {
|
||||||
let resolveInstall: ((result: { currentVersion: string; latestVersion: string; updated: boolean }) => void) | undefined;
|
let resolveInstall: ((result: typeof successfulInstall) => void) | undefined;
|
||||||
mockInstallUpdate.mockReturnValueOnce(new Promise((resolve) => {
|
mockInstallUpdate.mockReturnValueOnce(new Promise((resolve) => {
|
||||||
resolveInstall = resolve;
|
resolveInstall = resolve;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
render(<UpdateAvailableBanner latestVersion="0.7.0" currentVersion="0.6.0" onDismiss={vi.fn()} />);
|
renderBanner();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Update now" }));
|
fireEvent.click(screen.getByRole("button", { name: "Update now" }));
|
||||||
expect(screen.getByRole("button", { name: "Updating…" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: "Updating…" })).toBeDisabled();
|
||||||
expect(screen.getByTestId("icon-refresh")).toHaveClass("spinning");
|
expect(screen.getByTestId("icon-refresh")).toHaveClass("spinning");
|
||||||
|
|
||||||
resolveInstall?.({ currentVersion: "0.6.0", latestVersion: "0.7.0", updated: true });
|
resolveInstall?.(successfulInstall);
|
||||||
|
|
||||||
expect(await screen.findByText("Updated to v0.7.0 — restart Fusion to apply")).toBeInTheDocument();
|
await screen.findByText("Updated to v0.7.0 — restart Fusion to apply");
|
||||||
|
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeEnabled();
|
||||||
expect(screen.queryByRole("button", { name: "Update now" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Update now" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows install errors inline without removing retry button", async () => {
|
it("restarts a supervised host with the update-banner reason", async () => {
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Restart Fusion" }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockRequestSystemRestart).toHaveBeenCalledWith("update-banner"));
|
||||||
|
expect(await screen.findByText("Restarting… Your connection will close shortly.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the restart button disabled with manual guidance when unsupported", async () => {
|
||||||
|
mockFetchSystemInfo.mockResolvedValueOnce({ restartSupported: false });
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeDisabled();
|
||||||
|
expect(screen.getByText(/Needs a supervising parent/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps restart disabled while system info is loading", async () => {
|
||||||
|
mockFetchSystemInfo.mockReturnValueOnce(new Promise(() => {}));
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails closed with manual guidance when system info cannot be loaded", async () => {
|
||||||
|
mockFetchSystemInfo.mockRejectedValueOnce(new Error("network unavailable"));
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeDisabled());
|
||||||
|
expect(screen.getByText(/Needs a supervising parent/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a disabled spinning restart action while a restart request is in flight", async () => {
|
||||||
|
mockRequestSystemRestart.mockReturnValueOnce(new Promise(() => {}));
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Restart Fusion" }));
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /Restarting/ })).toBeDisabled();
|
||||||
|
expect(screen.getByTestId("icon-refresh")).toHaveClass("spinning");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a re-clickable inline error when restart rejects", async () => {
|
||||||
|
mockRequestSystemRestart.mockRejectedValueOnce(new Error("restart conflict"));
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Restart Fusion" }));
|
||||||
|
|
||||||
|
expect(await screen.findByText("restart conflict")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a re-clickable inline error when restart is not scheduled", async () => {
|
||||||
|
mockRequestSystemRestart.mockResolvedValueOnce({ scheduled: false });
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Restart Fusion" }));
|
||||||
|
|
||||||
|
expect(await screen.findByText("Restart could not be scheduled. Try restarting Fusion manually.")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows install errors inline without rendering a restart button or removing retry", async () => {
|
||||||
mockInstallUpdate.mockResolvedValueOnce({
|
mockInstallUpdate.mockResolvedValueOnce({
|
||||||
currentVersion: "0.6.0",
|
currentVersion: "0.6.0",
|
||||||
latestVersion: "0.7.0",
|
latestVersion: "0.7.0",
|
||||||
@@ -94,12 +180,35 @@ describe("UpdateAvailableBanner", () => {
|
|||||||
error: "permission denied",
|
error: "permission denied",
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<UpdateAvailableBanner latestVersion="0.7.0" currentVersion="0.6.0" onDismiss={vi.fn()} />);
|
renderBanner();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Update now" }));
|
fireEvent.click(screen.getByRole("button", { name: "Update now" }));
|
||||||
|
|
||||||
await waitFor(() => expect(mockInstallUpdate).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(mockInstallUpdate).toHaveBeenCalledTimes(1));
|
||||||
expect(await screen.findByText("Update failed: permission denied")).toBeInTheDocument();
|
expect(await screen.findByText("Update failed: permission denied")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "Update now" })).not.toBeDisabled();
|
expect(screen.getByRole("button", { name: "Update now" })).toBeEnabled();
|
||||||
|
expect(screen.queryByRole("button", { name: "Restart Fusion" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["supported", true],
|
||||||
|
["unsupported", false],
|
||||||
|
])("keeps the mobile action row and %s restart control in the document", async (_state, restartSupported) => {
|
||||||
|
const previousWidth = window.innerWidth;
|
||||||
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: 480 });
|
||||||
|
mockFetchSystemInfo.mockResolvedValueOnce({ restartSupported });
|
||||||
|
|
||||||
|
renderBanner();
|
||||||
|
await completeInstall();
|
||||||
|
|
||||||
|
const actions = document.querySelector(".update-available-banner__actions");
|
||||||
|
const restartButton = screen.getByRole("button", { name: "Restart Fusion" });
|
||||||
|
expect(actions).toBeInTheDocument();
|
||||||
|
expect(actions).toContainElement(restartButton);
|
||||||
|
expect(restartButton).toBeInTheDocument();
|
||||||
|
expect(restartButton).toHaveProperty("disabled", !restartSupported);
|
||||||
|
if (!restartSupported) expect(screen.getByText(/Needs a supervising parent/)).toBeInTheDocument();
|
||||||
|
|
||||||
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: previousWidth });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8487,6 +8487,10 @@
|
|||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
"message": "Update available: v{{latestVersion}} (current: v{{currentVersion}}). Run fn update for an installed CLI, or pull this source checkout.",
|
"message": "Update available: v{{latestVersion}} (current: v{{currentVersion}}). Run fn update for an installed CLI, or pull this source checkout.",
|
||||||
"releaseNotes": "Release notes",
|
"releaseNotes": "Release notes",
|
||||||
|
"restartFailed": "Restart could not be scheduled. Try restarting Fusion manually.",
|
||||||
|
"restartNow": "Restart Fusion",
|
||||||
|
"restartUnavailable": "Needs a supervising parent — restart Fusion manually without --no-supervise.",
|
||||||
|
"restarting": "Restarting… Your connection will close shortly.",
|
||||||
"updateFailed": "Update failed",
|
"updateFailed": "Update failed",
|
||||||
"updateFailedWithMessage": "Update failed: {{message}}",
|
"updateFailedWithMessage": "Update failed: {{message}}",
|
||||||
"updateNow": "Update now",
|
"updateNow": "Update now",
|
||||||
|
|||||||
Reference in New Issue
Block a user