254 lines · 7.9 KB · TypeScript
RawDownload
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<void> {  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(      <ServerUpdateAction        environmentId={"env-test" as EnvironmentId}        serverLabel="Test server"        selfUpdate="desktop-managed"        targetVersion="0.0.31"      />,    );     expect(markup).toContain("Update the desktop app on that machine to update this server.");    expect(markup).not.toContain("<button");  });   it("updates remote desktop apps through the shared update flow", async () => {    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(      <ServerUpdateProgress        state={{          status: "running",          stage: "resuming",          fromVersion: "0.0.30",          targetVersion: "0.0.31",        }}      />,    );     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(      <ServerUpdateProgress        state={{          status: "running",          stage: "installing",          fromVersion: "0.0.30",          targetVersion: "0.0.31",        }}      />,    );     expect(markup).toContain("Downloading…");    expect(markup).not.toContain("Install");  });   it("keeps the failure visible with its retryable error", () => {    const markup = renderToStaticMarkup(      <ServerUpdateProgress        state={{          status: "failed",          stage: "installing",          fromVersion: "0.0.30",          targetVersion: "0.0.31",          message: "The package could not be verified.",        }}      />,    );     expect(markup).toContain('role="alert"');    expect(markup).toContain("The package could not be verified.");    expect(markup).not.toContain("animate-status-pulse");  });});