mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: port the demo redesign with Manage and Settings tabs
This commit is contained in:
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", () => {
|
||||
|
||||
Reference in new issue
Block a user