import type { ReactElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import type { EnvironmentId } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import { AsyncResult } from "effect/unstable/reactivity"; import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; const testState = vi.hoisted(() => ({ updateServer: vi.fn(), toast: vi.fn(), continueThreadsAfterServerUpdate: false, })); vi.mock("~/hooks/useCopyToClipboard", () => ({ useCopyToClipboard: () => ({ copyToClipboard: vi.fn() }), })); vi.mock("~/hooks/useSettings", () => ({ useEnvironmentSettings: ( _environmentId: EnvironmentId, selector: (settings: { continueThreadsAfterServerUpdate: boolean }) => unknown, ) => selector({ continueThreadsAfterServerUpdate: testState.continueThreadsAfterServerUpdate }), })); vi.mock("~/state/server", () => ({ serverEnvironment: { updateServer: Symbol("updateServer") }, })); vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: () => testState.updateServer, })); vi.mock("./ui/toast", () => ({ toastManager: { add: testState.toast }, })); import { ServerUpdateAction, ServerUpdateProgress } from "./ServerUpdateAction"; type ActionElement = ReactElement<{ readonly onClick?: () => void; }>; function renderAction(): ActionElement { return ServerUpdateAction({ environmentId: "env-test" as EnvironmentId, serverLabel: "Test server", selfUpdate: "boot-service", targetVersion: "0.0.31", }) as ActionElement; } async function flushPromises(): Promise { await Promise.resolve(); await Promise.resolve(); } describe("ServerUpdateAction", () => { beforeEach(() => { testState.updateServer.mockReset(); testState.toast.mockReset(); testState.continueThreadsAfterServerUpdate = false; }); it("reports success only after the shared update flow reconnects", async () => { testState.updateServer.mockResolvedValue( AsyncResult.success({ targetVersion: "0.0.31", method: "boot-service" as const }), ); renderAction().props.onClick?.(); await flushPromises(); expect(testState.updateServer).toHaveBeenCalledWith({ environmentId: "env-test", input: { targetVersion: "0.0.31" }, }); expect(testState.toast).toHaveBeenCalledWith({ type: "success", title: "Test server updated", description: "Reconnected on t3@0.0.31.", }); }); it("reports one result when the update action is double-clicked", async () => { let finishUpdate: (() => void) | undefined; testState.updateServer.mockImplementation( () => new Promise((resolve) => { finishUpdate = () => resolve( AsyncResult.success({ targetVersion: "0.0.31", method: "boot-service" as const }), ); }), ); const action = renderAction(); action.props.onClick?.(); action.props.onClick?.(); expect(testState.updateServer).toHaveBeenCalledTimes(1); finishUpdate?.(); await flushPromises(); expect(testState.toast).toHaveBeenCalledTimes(1); }); it("quietly releases the action when the operation is interrupted", async () => { testState.updateServer.mockResolvedValue(AsyncResult.failure(Cause.interrupt())); renderAction().props.onClick?.(); await flushPromises(); expect(testState.toast).not.toHaveBeenCalled(); }); it("keeps the manual instruction for desktop servers without remote update support", () => { const markup = renderToStaticMarkup( , ); expect(markup).toContain("Update the desktop app on that machine to update this server."); expect(markup).not.toContain(" { testState.updateServer.mockResolvedValue( AsyncResult.success({ targetVersion: "0.0.34", method: "desktop-app" as const }), ); const action = ServerUpdateAction({ environmentId: "env-test" as EnvironmentId, serverLabel: "Test server", selfUpdate: "desktop-managed", desktopAppUpdate: true, targetVersion: "0.0.31", }) as ActionElement; // No confirm-dialog host is mounted in this test, which the component // treats as consent: the click itself was the request. action.props.onClick?.(); await flushPromises(); expect(testState.updateServer).toHaveBeenCalledWith({ environmentId: "env-test", input: { targetVersion: "0.0.31" }, }); expect(testState.toast).toHaveBeenCalledWith({ type: "success", title: "Test server updated", description: "Desktop app relaunched on 0.0.34.", }); }); it("leaves thread continuation off by default", async () => { testState.updateServer.mockResolvedValue( AsyncResult.success({ targetVersion: "0.0.31", method: "boot-service" as const }), ); const action = ServerUpdateAction({ environmentId: "env-test" as EnvironmentId, serverLabel: "Test server", selfUpdate: "boot-service", threadContinuation: true, targetVersion: "0.0.31", }) as ActionElement; action.props.onClick?.(); await flushPromises(); expect(testState.updateServer).toHaveBeenCalledWith({ environmentId: "env-test", input: { targetVersion: "0.0.31" }, }); }); it("applies the saved thread continuation preference automatically", async () => { testState.updateServer.mockResolvedValue( AsyncResult.success({ targetVersion: "0.0.31", method: "boot-service" as const }), ); testState.continueThreadsAfterServerUpdate = true; const action = ServerUpdateAction({ environmentId: "env-test" as EnvironmentId, serverLabel: "Test server", selfUpdate: "boot-service", threadContinuation: true, targetVersion: "0.0.31", }) as ActionElement; action.props.onClick?.(); await flushPromises(); expect(testState.updateServer).toHaveBeenCalledWith({ environmentId: "env-test", input: { targetVersion: "0.0.31", continueRunningThreads: true }, }); }); }); describe("ServerUpdateProgress", () => { it("shows one calm status row for the restart wait", () => { const markup = renderToStaticMarkup( , ); expect(markup).toContain("Restarting…"); // The wait state is monochrome and calm: no versions, no step rail, no // success/warning colors, one duty-cycled pulse on the dot. expect(markup).not.toContain("0.0.30"); expect(markup).not.toContain("Resum"); expect(markup).not.toContain("text-success"); expect(markup).not.toContain("text-primary"); expect(markup).toContain("animate-status-pulse"); expect(markup).not.toContain("animate-spin"); }); it("folds the sub-second installing handoff into the download phase", () => { const markup = renderToStaticMarkup( , ); expect(markup).toContain("Downloading…"); expect(markup).not.toContain("Install"); }); it("keeps the failure visible with its retryable error", () => { const markup = renderToStaticMarkup( , ); expect(markup).toContain('role="alert"'); expect(markup).toContain("The package could not be verified."); expect(markup).not.toContain("animate-status-pulse"); }); });