feat: port the demo redesign with Manage and Settings tabs

This commit is contained in:
LukeGus committed 2026-10-05 16:23:21 -05:00
1 parent f8151cb885
commit 35c954b79a
128 files changed
+13262 -8143

No files matched your search

@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { masonryColumns } from "@/components/card-masonry";
describe("masonryColumns", () => {
it("stacks into one column when narrow", () => {
expect(masonryColumns(400, 4)).toBe(1);
});
it("grows with the container up to the preference", () => {
expect(masonryColumns(700, 3)).toBe(2);
expect(masonryColumns(1000, 3)).toBe(3);
expect(masonryColumns(1300, 3)).toBe(3);
expect(masonryColumns(1300, 4)).toBe(4);
});
it("falls back to three for an unknown preference", () => {
expect(masonryColumns(1300, 9)).toBe(3);
});
});
@@ -0,0 +1,93 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
import {
ConnectionGate,
type ConnectionGateController,
} from "@/components/connection/ConnectionGate";
beforeEach(() => vi.useFakeTimers());
afterEach(() => {
cleanup();
vi.useRealTimers();
});
describe("ConnectionGate", () => {
it("shows the children once connected", async () => {
render(
<ConnectionGate
message="docker.connecting"
connect={(gate) => gate.markConnected()}
>
<span>containers</span>
</ConnectionGate>,
);
await act(async () => {});
expect(screen.getByText("containers")).toBeTruthy();
expect(screen.queryByRole("status")).toBeNull();
});
it("keeps the screen up with the reason and a retry when connect throws", async () => {
const connect = vi
.fn<(gate: ConnectionGateController) => Promise<void> | void>()
.mockRejectedValueOnce(new Error("No route to host"))
.mockImplementation((gate) => gate.markConnected());
render(
<ConnectionGate message="files.connecting" connect={connect}>
<span>tree</span>
</ConnectionGate>,
);
await act(async () => {});
expect(screen.queryByText("tree")).toBeNull();
expect(screen.getAllByText("No route to host").length).toBeGreaterThan(0);
expect(screen.getByText("connection.failedRetrying")).toBeTruthy();
await act(async () => {
fireEvent.click(screen.getByText("connection.retryNow"));
});
expect(screen.getByText("tree")).toBeTruthy();
});
it("does not retry on its own when unavailable", async () => {
const connect = vi.fn((gate: ConnectionGateController) =>
gate.markUnavailable({ title: "docker.notEnabled" }),
);
render(
<ConnectionGate message="docker.connecting" connect={connect}>
<span>containers</span>
</ConnectionGate>,
);
await act(async () => {});
expect(screen.getByText("docker.notEnabled")).toBeTruthy();
await act(async () => {
vi.advanceTimersByTime(60_000);
});
expect(connect).toHaveBeenCalledTimes(1);
expect(screen.queryByText("containers")).toBeNull();
});
it("keeps children mounted under the screen when asked", async () => {
render(
<ConnectionGate
message="serial.connecting"
keepMounted
connect={() => new Promise(() => {})}
>
<span>xterm</span>
</ConnectionGate>,
);
await act(async () => {});
expect(screen.getByText("xterm")).toBeTruthy();
expect(screen.getByRole("status")).toBeTruthy();
});
});
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/react";
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
@@ -25,16 +25,16 @@ describe("ConnectionScreen", () => {
});
it("renders the disconnected state without a provider", () => {
expect(() =>
render(
<ConnectionScreen
status="disconnected"
message="remoteDesktop.hostNotFound"
/>,
),
).not.toThrow();
render(
<ConnectionScreen
status="disconnected"
message="common.loading"
disconnectedMessage="terminal.connectionLost"
/>,
);
expect(screen.getByText("remoteDesktop.hostNotFound")).toBeTruthy();
expect(screen.getByText("terminal.connectionLost")).toBeTruthy();
expect(screen.queryByText("common.loading")).toBeNull();
expect(screen.getByRole("status").getAttribute("data-status")).toBe(
"disconnected",
);
@@ -49,4 +49,66 @@ describe("ConnectionScreen", () => {
expect(screen.getByText(/sshAuth\.connectionLogTitle/)).toBeTruthy();
});
it("offers retry and the reason once a connect fails", () => {
const retry = vi.fn();
render(
<ConnectionScreen
status="error"
message="common.loading"
errorDetail="Authentication failed"
onManualRetry={retry}
/>,
);
expect(screen.getByText("connection.failed")).toBeTruthy();
expect(screen.getByText("Authentication failed")).toBeTruthy();
fireEvent.click(screen.getByText("connection.reconnect"));
expect(retry).toHaveBeenCalledTimes(1);
});
it("lets the user skip the wait while it retries on its own", () => {
const retry = vi.fn();
render(
<ConnectionScreen
status="error"
attempt={2}
maxAttempts={8}
nextRetryInMs={4000}
onManualRetry={retry}
/>,
);
expect(screen.getByText("connection.failedRetrying")).toBeTruthy();
expect(screen.getByText("connection.retryingIn")).toBeTruthy();
fireEvent.click(screen.getByText("connection.retryNow"));
expect(retry).toHaveBeenCalled();
});
it("shows an unavailable state with its hint, action and retry", () => {
render(
<ConnectionScreen
status="connected"
unavailable={{
title: "docker.notEnabled",
hint: "docker.notEnabledHint",
action: <button>open editor</button>,
}}
onManualRetry={() => {}}
/>,
);
expect(screen.getByRole("status").getAttribute("data-status")).toBe(
"unavailable",
);
expect(screen.getByText("docker.notEnabled")).toBeTruthy();
expect(screen.getByText("docker.notEnabledHint")).toBeTruthy();
expect(screen.getByText("open editor")).toBeTruthy();
expect(screen.getByText("connection.reconnect")).toBeTruthy();
});
it("renders nothing once connected", () => {
const { container } = render(<ConnectionScreen status="connected" />);
expect(container.innerHTML).toBe("");
});
});
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { DataView } from "@/components/data-view";
afterEach(cleanup);
const items = [
{ id: "a", name: "alpha" },
{ id: "b", name: "beta" },
];
function renderView(view: "grid" | "list", onRowClick = vi.fn()) {
return render(
<DataView
items={items}
view={view}
density="compact"
getKey={(item) => item.id}
renderCard={(item) => <div>card {item.name}</div>}
columns={{ base: 1, md: 2 }}
listColumns={[
{ key: "name", header: "Name", width: "1fr", cell: (i) => i.name },
]}
onRowClick={onRowClick}
empty={<span>nothing</span>}
/>,
);
}
describe("DataView", () => {
it("renders cards in grid mode", () => {
renderView("grid");
expect(screen.getByText("card alpha")).toBeTruthy();
expect(screen.queryByText("Name")).toBeNull();
});
it("renders a header and clickable rows in list mode", () => {
const onRowClick = vi.fn();
renderView("list", onRowClick);
expect(screen.getByText("Name")).toBeTruthy();
fireEvent.click(screen.getByText("beta"));
expect(onRowClick).toHaveBeenCalledWith(items[1]);
});
it("shows the empty state with no items", () => {
render(
<DataView
items={[]}
view="grid"
getKey={() => ""}
renderCard={() => null}
columns={{}}
listColumns={[]}
empty={<span>nothing</span>}
/>,
);
expect(screen.getByText("nothing")).toBeTruthy();
});
});
@@ -0,0 +1,107 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
import {
Facts,
PanelSearch,
PanelShell,
Segmented,
ViewToggle,
} from "@/components/panel-layout";
afterEach(cleanup);
describe("PanelSearch", () => {
it("clears with the button and with Esc", () => {
const onChange = vi.fn();
const { rerender } = render(
<PanelSearch value="web" onChange={onChange} />,
);
fireEvent.click(screen.getByLabelText("panel.clearSearch"));
expect(onChange).toHaveBeenLastCalledWith("");
rerender(<PanelSearch value="db" onChange={onChange} />);
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
expect(onChange).toHaveBeenCalledTimes(2);
});
it("hides the clear button while empty", () => {
render(<PanelSearch value="" onChange={() => {}} />);
expect(screen.queryByLabelText("panel.clearSearch")).toBeNull();
});
});
describe("Segmented", () => {
it("marks the active option and reports a pick", () => {
const onChange = vi.fn();
render(
<Segmented
value="hosts"
onChange={onChange}
options={[
{ value: "hosts", label: "Hosts", count: 3 },
{ value: "credentials", label: "Credentials" },
]}
/>,
);
const radios = screen.getAllByRole("radio");
expect(radios[0].getAttribute("aria-checked")).toBe("true");
fireEvent.click(radios[1]);
expect(onChange).toHaveBeenCalledWith("credentials");
});
});
describe("ViewToggle", () => {
it("switches view and density", () => {
const onView = vi.fn();
const onDensity = vi.fn();
render(
<ViewToggle
view="grid"
onView={onView}
density="comfortable"
onDensity={onDensity}
/>,
);
fireEvent.click(screen.getByLabelText("panel.listView"));
expect(onView).toHaveBeenCalledWith("list");
fireEvent.click(screen.getByLabelText("panel.compactRows"));
expect(onDensity).toHaveBeenCalledWith("compact");
});
});
describe("Facts", () => {
it("puts a rule only between facts", () => {
const { container } = render(
<Facts>
<span>one</span>
{null}
<span>two</span>
</Facts>,
);
expect(container.querySelectorAll("[aria-hidden]")).toHaveLength(1);
});
});
describe("PanelShell", () => {
it("draws the header, toolbar and footer", () => {
render(
<PanelShell
title="Docker"
status="2 running"
toolbar={<span>toolbar</span>}
footer={<span>footer</span>}
>
body
</PanelShell>,
);
expect(screen.getByText("Docker")).toBeTruthy();
expect(screen.getByText("2 running")).toBeTruthy();
expect(screen.getByText("toolbar")).toBeTruthy();
expect(screen.getByText("footer")).toBeTruthy();
});
});
@@ -0,0 +1,218 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import { useState } from "react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
import {
InlineView,
PanePrompt,
SurfaceScope,
useConfirm,
type ConfirmOptions,
} from "@/components/surface/surface-scope";
afterEach(cleanup);
function Asker({
options,
onResult,
}: {
options: ConfirmOptions;
onResult: (ok: boolean) => void;
}) {
const confirm = useConfirm();
return (
<button onClick={() => void confirm(options).then(onResult)}>ask</button>
);
}
describe("useConfirm", () => {
it("resolves true when confirmed with Enter", async () => {
const onResult = vi.fn();
render(
<SurfaceScope>
<Asker options={{ title: "Delete web-01?" }} onResult={onResult} />
</SurfaceScope>,
);
fireEvent.click(screen.getByText("ask"));
expect(screen.getByRole("alertdialog")).toBeTruthy();
expect(screen.getByText("Delete web-01?")).toBeTruthy();
await act(async () => {
fireEvent.keyDown(window, { key: "Enter" });
});
expect(onResult).toHaveBeenCalledWith(true);
expect(screen.queryByRole("alertdialog")).toBeNull();
});
it("resolves false on Esc and on Cancel", async () => {
const onResult = vi.fn();
render(
<SurfaceScope>
<Asker options={{ title: "Remove?" }} onResult={onResult} />
</SurfaceScope>,
);
fireEvent.click(screen.getByText("ask"));
await act(async () => {
fireEvent.keyDown(window, { key: "Escape" });
});
expect(onResult).toHaveBeenLastCalledWith(false);
fireEvent.click(screen.getByText("ask"));
await act(async () => {
fireEvent.click(screen.getByText("common.cancel"));
});
expect(onResult).toHaveBeenCalledTimes(2);
expect(onResult).toHaveBeenLastCalledWith(false);
});
it("stops Enter from reaching anything listening underneath", async () => {
const underneath = vi.fn();
window.addEventListener("keydown", underneath);
render(
<SurfaceScope>
<Asker options={{ title: "Close?" }} onResult={() => {}} />
</SurfaceScope>,
);
fireEvent.click(screen.getByText("ask"));
await act(async () => {
fireEvent.keyDown(window, { key: "Enter" });
});
expect(underneath).not.toHaveBeenCalled();
window.removeEventListener("keydown", underneath);
});
it("uses a neutral confirm button when not destructive", () => {
render(
<SurfaceScope>
<Asker
options={{ title: "Apply?", destructive: false }}
onResult={() => {}}
/>
</SurfaceScope>,
);
fireEvent.click(screen.getByText("ask"));
expect(screen.getByText("common.confirm")).toBeTruthy();
});
});
function Editor({ onBeforeClose }: { onBeforeClose?: () => boolean }) {
const [open, setOpen] = useState(false);
const [nested, setNested] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>open</button>
<InlineView
open={open}
onOpenChange={setOpen}
title="Edit folder"
onBeforeClose={onBeforeClose}
>
<span>folder form</span>
<button onClick={() => setNested(true)}>nested</button>
<InlineView
open={nested}
onOpenChange={setNested}
title="Pick icon"
width="wide"
>
<span>icon grid</span>
</InlineView>
</InlineView>
</>
);
}
describe("InlineView", () => {
it("takes over the surface and reports the widening", () => {
const onEditingChange = vi.fn();
render(
<SurfaceScope onEditingChange={onEditingChange}>
<Editor />
</SurfaceScope>,
);
expect(onEditingChange).toHaveBeenLastCalledWith(false);
fireEvent.click(screen.getByText("open"));
expect(screen.getByText("folder form")).toBeTruthy();
expect(onEditingChange).toHaveBeenLastCalledWith(true);
fireEvent.click(screen.getByText("nested"));
expect(screen.getByText("icon grid")).toBeTruthy();
expect(onEditingChange).toHaveBeenLastCalledWith("wide");
});
it("goes back one view at a time", async () => {
render(
<SurfaceScope>
<Editor />
</SurfaceScope>,
);
fireEvent.click(screen.getByText("open"));
fireEvent.click(screen.getByText("nested"));
const backs = screen.getAllByLabelText("common.back");
await act(async () => {
fireEvent.click(backs[backs.length - 1]);
});
expect(screen.queryByText("icon grid")).toBeNull();
expect(screen.getByText("folder form")).toBeTruthy();
});
it("stays open when the close guard says no", async () => {
render(
<SurfaceScope>
<Editor onBeforeClose={() => false} />
</SurfaceScope>,
);
fireEvent.click(screen.getByText("open"));
await act(async () => {
fireEvent.click(screen.getByLabelText("common.back"));
});
expect(screen.getByText("folder form")).toBeTruthy();
});
});
describe("PanePrompt", () => {
it("dims the pane by default and closes on a backdrop click", () => {
const onCancel = vi.fn();
render(
<PanePrompt open title="Title" onCancel={onCancel}>
<p>body</p>
</PanePrompt>,
);
const card = screen.getByRole("dialog", { name: "Title" });
const layer = card.parentElement!;
expect(layer.className).toContain("z-50");
fireEvent.mouseDown(card);
expect(onCancel).not.toHaveBeenCalled();
fireEvent.mouseDown(layer);
expect(onCancel).toHaveBeenCalledOnce();
});
it("covers a connecting surface with an opaque layer", () => {
render(
<PanePrompt
open
title="Login"
layer="connection"
backgroundColor="rgb(1, 2, 3)"
/>,
);
const layer = screen.getByRole("dialog", { name: "Login" }).parentElement!;
expect(layer.className).toContain("z-500");
expect(layer.style.backgroundColor).toBe("rgb(1, 2, 3)");
});
it("renders nothing when closed", () => {
render(<PanePrompt open={false} title="Hidden" />);
expect(screen.queryByRole("dialog")).toBeNull();
});
});
@@ -0,0 +1,70 @@
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import { TabStrip } from "@/components/tab-strip";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
afterEach(() => {
cleanup();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("reveals clipped tabs and updates the scroll controls after scrolling and resizing", () => {
let resize = () => {};
vi.stubGlobal(
"ResizeObserver",
class {
constructor(callback: () => void) {
resize = callback;
}
observe() {}
disconnect() {}
},
);
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(200);
const width = vi
.spyOn(HTMLElement.prototype, "scrollWidth", "get")
.mockReturnValue(600);
const scrollBy = vi.fn();
HTMLElement.prototype.scrollBy = scrollBy;
const onTabChange = vi.fn();
const view = render(
<TabStrip
tabs={[
{ id: "general", label: "General", icon: null },
{ id: "metrics", label: "Metrics", icon: null },
]}
activeTab="general"
onTabChange={onTabChange}
/>,
);
const left = screen.getByLabelText(
"common.scrollTabsLeft",
) as HTMLButtonElement;
const right = screen.getByLabelText(
"common.scrollTabsRight",
) as HTMLButtonElement;
expect(left.disabled).toBe(true);
expect(right.disabled).toBe(false);
fireEvent.click(right);
expect(scrollBy).toHaveBeenCalledWith({ left: 150, behavior: "smooth" });
const strip = view.container.querySelector(".overflow-x-auto") as HTMLElement;
strip.scrollLeft = 400;
fireEvent.scroll(strip);
expect(left.disabled).toBe(false);
expect(right.disabled).toBe(true);
fireEvent.click(screen.getByText("Metrics"));
expect(onTabChange).toHaveBeenCalledWith("metrics");
strip.scrollLeft = 0;
width.mockReturnValue(200);
act(resize);
expect(screen.queryByLabelText("common.scrollTabsRight")).toBeNull();
});
@@ -29,12 +29,12 @@ describe("VersionBadge", () => {
expect(link.textContent).toBe(UPDATE_TEXT);
});
it("keeps the yellow update treatment on the link", () => {
it("keeps the warning update treatment on the link", () => {
render(<VersionBadge status="requires_update" releaseUrl={RELEASE_URL} />);
const className = screen.getByRole("link").className;
expect(className).toContain("bg-yellow-500/20");
expect(className).toContain("text-yellow-400");
expect(className).toContain("bg-warning/20");
expect(className).toContain("text-warning");
expect(className).toContain("hover:underline");
});
@@ -74,7 +74,7 @@ describe("VersionBadge", () => {
const className = screen.getByRole("link").className;
expect(className).toContain("w-fit");
expect(className).toContain("bg-yellow-500/20");
expect(className).toContain("bg-warning/20");
});
it("applies the className on the inert span too", () => {