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(),})); vi.mock("~/hooks/useCopyToClipboard", () => ({ useCopyToClipboard: () => ({ copyToClipboard: vi.fn() }),}));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(); }); 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(); });}); 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"); });});
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(),
}));
vi.mock("~/hooks/useCopyToClipboard", () => ({
useCopyToClipboard: () => ({ copyToClipboard: vi.fn() }),
}));
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();
});
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();
});
});
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");
});
});