Files
kandev/apps/web/components/task/remote-cloud-tooltip.test.tsx
T

409 lines
13 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RemoteCloudTooltip } from "./remote-cloud-tooltip";
const mocks = vi.hoisted(() => ({
getKubernetesTaskSession: vi.fn(),
wsRequest: vi.fn(),
touch: false,
}));
const STATUS_TRIGGER_ID = "remote-executor-status-trigger";
const EAGER_TASK_ID = "task-eager";
const EAGER_SESSION_ID = "session-eager";
vi.mock("@/lib/api/domains/kubernetes-api", () => ({
getKubernetesTaskSession: mocks.getKubernetesTaskSession,
}));
vi.mock("@/lib/ws/connection", () => ({
getWebSocketClient: () => ({ request: mocks.wsRequest, getStatus: () => "connected" }),
}));
beforeEach(() => {
mocks.getKubernetesTaskSession.mockReset();
mocks.wsRequest.mockReset();
mocks.touch = false;
});
afterEach(() => cleanup());
vi.mock("@kandev/ui/tooltip", () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipContent: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div data-testid="tooltip-content" {...props}>
{children}
</div>
),
}));
vi.mock("@/hooks/use-compact-task-chrome", () => ({
useTouchDrawer: () => mocks.touch,
}));
vi.mock("@kandev/ui/drawer", () => ({
Drawer: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DrawerTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DrawerContent: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div {...props}>{children}</div>
),
DrawerHeader: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div {...props}>{children}</div>
),
DrawerTitle: ({ children, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h2 {...props}>{children}</h2>
),
DrawerDescription: ({ children, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p {...props}>{children}</p>
),
DrawerClose: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
function openTooltip() {
fireEvent.pointerEnter(screen.getByTestId(STATUS_TRIGGER_ID), {
pointerType: "mouse",
});
}
function reopenTooltip() {
fireEvent.pointerLeave(screen.getByTestId(STATUS_TRIGGER_ID), {
pointerType: "mouse",
});
openTooltip();
}
describe("RemoteCloudTooltip executor icon", () => {
it("shows a container icon for Docker executors", () => {
render(
<RemoteCloudTooltip
taskId="task-1"
executorType="local_docker"
fallbackName="Docker"
status={{ remote_checked_at: new Date().toISOString() }}
/>,
);
expect(screen.getByTestId("executor-status-container-icon")).toBeTruthy();
expect(screen.queryByTestId("executor-status-cloud-icon")).toBeNull();
});
it("keeps the cloud icon for Sprites executors", () => {
render(
<RemoteCloudTooltip
taskId="task-1"
executorType="sprites"
fallbackName="Sprites.dev"
status={{ remote_checked_at: new Date().toISOString() }}
/>,
);
expect(screen.getByTestId("executor-status-cloud-icon")).toBeTruthy();
});
it("uses a keyboard-focusable semantic status trigger", () => {
render(
<RemoteCloudTooltip
taskId="task-trigger"
executorType="k8s"
fallbackName="Cluster executor"
status={{ remote_checked_at: new Date().toISOString() }}
/>,
);
const trigger = screen.getByTestId(STATUS_TRIGGER_ID);
expect(trigger.getAttribute("role")).toBe("img");
expect(trigger.getAttribute("tabindex")).toBe("0");
expect(trigger.getAttribute("aria-label")).toContain("Cluster executor");
});
});
describe("RemoteCloudTooltip relative timestamps", () => {
it("renders created and last-check times as relative values", () => {
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
render(
<RemoteCloudTooltip
taskId="task-1"
executorType="sprites"
fallbackName="Sprites.dev"
status={{
remote_name: "kandev-da7d150f-585",
remote_state: "running",
remote_created_at: twoHoursAgo,
remote_checked_at: fiveMinutesAgo,
}}
/>,
);
expect(screen.getByTestId("remote-executor-status-created").textContent).toContain("2h ago");
expect(screen.getByTestId("remote-executor-status-checked").textContent).toContain("5m ago");
});
});
describe("RemoteCloudTooltip live status", () => {
it("eagerly uses exact Kubernetes status and refetches after each reopen", async () => {
mocks.getKubernetesTaskSession
.mockResolvedValueOnce({
task_id: EAGER_TASK_ID,
session_id: EAGER_SESSION_ID,
pod_name: "pod-1",
pod_phase: "Running",
restarts: 0,
failure_reason: "Unauthorized",
})
.mockResolvedValueOnce({
task_id: EAGER_TASK_ID,
session_id: EAGER_SESSION_ID,
pod_name: "pod-1",
pod_phase: "Running",
restarts: 0,
})
.mockResolvedValueOnce({
task_id: EAGER_TASK_ID,
session_id: EAGER_SESSION_ID,
pod_name: "pod-1",
pod_phase: "Running",
restarts: 0,
});
render(
<RemoteCloudTooltip
taskId={EAGER_TASK_ID}
sessionId={EAGER_SESSION_ID}
executorId="executor-eager"
executorType="k8s"
/>,
);
await waitFor(() =>
expect(mocks.getKubernetesTaskSession).toHaveBeenCalledWith(
"executor-eager",
EAGER_TASK_ID,
EAGER_SESSION_ID,
),
);
expect((await screen.findByTestId("remote-executor-status-error")).textContent).toContain(
"Unauthorized",
);
expect(mocks.wsRequest).not.toHaveBeenCalled();
openTooltip();
await waitFor(() => expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(2));
await waitFor(() => expect(screen.queryByTestId("remote-executor-status-error")).toBeNull());
expect(screen.getByTestId("remote-executor-status-state").textContent).toContain("Running");
reopenTooltip();
await waitFor(() => expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(3));
});
it("keeps the newest exact Kubernetes response when scope changes", async () => {
let resolveFirst: ((value: unknown) => void) | undefined;
mocks.getKubernetesTaskSession
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveFirst = resolve;
}),
)
.mockResolvedValueOnce({
task_id: "task-new",
session_id: "session-new",
pod_name: "new-pod",
pod_phase: "Running",
restarts: 0,
});
const { rerender } = render(
<RemoteCloudTooltip
taskId="task-old"
sessionId="session-old"
executorId="executor-old"
executorType="k8s"
/>,
);
openTooltip();
await waitFor(() => expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(1));
rerender(
<RemoteCloudTooltip
taskId="task-new"
sessionId="session-new"
executorId="executor-new"
executorType="k8s"
/>,
);
await waitFor(() => expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(2));
expect(await screen.findByText("new-pod")).toBeTruthy();
resolveFirst?.({
task_id: "task-1",
session_id: "session-1",
pod_name: "stale-pod",
pod_phase: "Failed",
restarts: 0,
});
await waitFor(() => expect(screen.queryByText("stale-pod")).toBeNull());
expect(screen.getByText("new-pod")).toBeTruthy();
});
});
describe("RemoteCloudTooltip shared status presentation", () => {
it("uses the pending tone for a waiting Kubernetes container", () => {
render(
<RemoteCloudTooltip
taskId="task-waiting"
executorType="k8s"
fallbackName="pod-waiting"
status={{ remote_state: "waiting", remote_checked_at: new Date().toISOString() }}
/>,
);
const row = screen.getByTestId("remote-executor-status-state");
expect(row.querySelector("dd")?.className).toContain("text-amber-600");
});
it("shares one eager request across duplicate exact-scope consumers", async () => {
mocks.getKubernetesTaskSession.mockResolvedValue({
task_id: "task-duplicate",
session_id: "session-duplicate",
pod_name: "pod-1",
pod_phase: "Running",
restarts: 0,
});
render(
<>
<RemoteCloudTooltip
taskId="task-duplicate"
sessionId="session-duplicate"
executorId="executor-duplicate"
executorType="k8s"
/>
<RemoteCloudTooltip
taskId="task-duplicate"
sessionId="session-duplicate"
executorId="executor-duplicate"
executorType="k8s"
/>
</>,
);
await waitFor(() => expect(screen.getAllByText("pod-1")).toHaveLength(2));
expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(1);
});
it("renders a structured Kubernetes task status summary", async () => {
mocks.getKubernetesTaskSession.mockResolvedValue({
task_id: "task-structured",
session_id: "session-structured",
pod_name: "pod-structured",
pod_phase: "Running",
container_state: "running",
restarts: 4,
workspace_kind: "managed_pvc",
created_at: "2026-09-01T09:00:00Z",
});
render(
<RemoteCloudTooltip
taskId="task-structured"
sessionId="session-structured"
executorId="executor-structured"
executorType="k8s"
/>,
);
const summary = await screen.findByTestId("remote-executor-status-summary");
expect(summary.className).toContain("w-full");
expect(screen.getByTestId("remote-executor-status-identity").textContent).toBe(
"pod-structured",
);
expect(screen.getByTestId("remote-executor-status-state").textContent).toContain("running");
expect(screen.getByTestId("remote-executor-status-restarts").textContent).toContain("4");
expect(screen.getByTestId("remote-executor-status-workspace").textContent).toContain(
"managed_pvc",
);
});
});
describe("RemoteCloudTooltip compatibility status", () => {
it("keeps WebSocket status for other remote executors", async () => {
mocks.wsRequest.mockResolvedValue({
remote_name: "sprite-1",
remote_state: "running",
remote_checked_at: new Date().toISOString(),
});
render(
<RemoteCloudTooltip
taskId="task-compatible"
sessionId="session-compatible"
executorId="executor-compatible"
executorType="sprites"
/>,
);
openTooltip();
await waitFor(() => expect(mocks.wsRequest).toHaveBeenCalledTimes(1));
expect(mocks.getKubernetesTaskSession).not.toHaveBeenCalled();
reopenTooltip();
await waitFor(() => expect(mocks.wsRequest).toHaveBeenCalledTimes(2));
});
});
describe("RemoteCloudTooltip coarse-pointer disclosure", () => {
it("opens the shared summary from a 44px task-safe Drawer trigger", () => {
mocks.touch = true;
const onParentClick = vi.fn();
render(
<div onClick={onParentClick}>
<RemoteCloudTooltip
taskId="task-touch"
executorType="k8s"
fallbackName="pod-touch"
status={{
remote_name: "pod-touch",
remote_state: "running",
remote_checked_at: new Date().toISOString(),
remote_restarts: 2,
remote_workspace_kind: "empty_dir",
}}
/>
</div>,
);
const trigger = screen.getByTestId(STATUS_TRIGGER_ID);
expect(trigger.getAttribute("role")).toBe("button");
expect(trigger.getAttribute("aria-haspopup")).toBe("dialog");
expect(trigger.className).toContain("after:size-11");
fireEvent.click(trigger);
expect(onParentClick).not.toHaveBeenCalled();
expect(screen.getByTestId("remote-executor-status-drawer")).toBeTruthy();
expect(screen.getByTestId("remote-executor-status-workspace").textContent).toContain(
"empty_dir",
);
fireEvent.click(screen.getByRole("button", { name: "Close" }));
expect(onParentClick).not.toHaveBeenCalled();
});
it("does not refresh repeatedly for auto-repeated activation keys", async () => {
mocks.touch = true;
mocks.getKubernetesTaskSession.mockResolvedValue({
task_id: "task-touch-repeat",
session_id: "session-touch-repeat",
pod_name: "pod-touch",
pod_phase: "Running",
restarts: 0,
});
render(
<RemoteCloudTooltip
taskId="task-touch-repeat"
sessionId="session-touch-repeat"
executorId="executor-touch-repeat"
executorType="k8s"
/>,
);
await waitFor(() => expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(1));
const trigger = screen.getByTestId(STATUS_TRIGGER_ID);
fireEvent.keyDown(trigger, { key: "Enter", repeat: true });
expect(mocks.getKubernetesTaskSession).toHaveBeenCalledTimes(1);
});
});