Files
kandev/apps/web/components/task/panel-primitives.test.tsx
T
2026-09-20 11:33:30 +01:00

140 lines
4.7 KiB
TypeScript

import { createRef } from "react";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
PanelFooterBar,
PanelHeaderBar,
PanelHeaderBarSplit,
PanelHeaderOverflowMenu,
shouldUsePanelHeaderOverflow,
} from "./panel-primitives";
afterEach(cleanup);
const SPLIT_TITLE = "Split title";
const SPLIT_ACTION = "Split action";
const RESIZE_TITLE = "Resize title";
const RESIZE_ACTION = "Resize action";
const MORE_ACTIONS = "More actions";
const SPLIT_HEADER_TEST_ID = "split-header";
const PANEL_FOOTER_TEST_ID = "panel-footer";
describe("panel header primitives", () => {
it("passes standard div attributes and refs through the shared header shell", () => {
const ref = createRef<HTMLDivElement>();
render(
<PanelHeaderBar ref={ref} data-testid="panel-header" aria-label="Panel actions">
Header
</PanelHeaderBar>,
);
const header = screen.getByTestId("panel-header");
expect(header.getAttribute("aria-label")).toBe("Panel actions");
expect(header.textContent).toContain("Header");
expect(ref.current).toBe(header);
expect(header.className).toContain("h-[1.875rem]");
expect(header.className).toContain("[@media(max-width:47.999rem)]:h-12");
expect(header.className).toContain("[@media(pointer:coarse)]:h-12");
expect(header.className).toContain("[@media(max-width:47.999rem)]:[&_button]:min-h-11");
expect(header.className).toContain("[@media(pointer:coarse)]:[&_a]:min-w-11");
});
it.each([
[239, true],
[480, true],
[520, false],
])("resolves width-aware overflow at %ipx", (width, expected) => {
expect(shouldUsePanelHeaderOverflow(width, 520)).toBe(expected);
});
it("keeps split actions in the shared header and leaves footer geometry separate", () => {
render(
<>
<PanelHeaderBarSplit
data-testid={SPLIT_HEADER_TEST_ID}
left={<span>{SPLIT_TITLE}</span>}
right={<button type="button">{SPLIT_ACTION}</button>}
rightWhenOverflow={<button type="button">Primary action</button>}
overflow={<button type="button">{MORE_ACTIONS}</button>}
overflowAt={520}
/>
<PanelFooterBar data-testid={PANEL_FOOTER_TEST_ID}>Footer</PanelFooterBar>
</>,
);
expect(screen.getByTestId(SPLIT_HEADER_TEST_ID).textContent).toContain(SPLIT_TITLE);
expect(screen.getByTestId(SPLIT_HEADER_TEST_ID).textContent).toContain(SPLIT_ACTION);
expect(screen.getByTestId(SPLIT_HEADER_TEST_ID).textContent).not.toContain("Primary action");
expect(screen.getByTestId(PANEL_FOOTER_TEST_ID).className).not.toContain("h-[30px]");
});
it("exposes a keyboard-addressable overflow trigger for secondary actions", () => {
render(
<PanelHeaderOverflowMenu label="More panel actions">
<button type="button">Open editor</button>
</PanelHeaderOverflowMenu>,
);
const trigger = screen.getByRole("button", { name: "More panel actions" });
expect(trigger.getAttribute("data-testid")).toBe("panel-header-overflow");
expect(trigger.className).toContain("max-md:size-11");
});
});
describe("panel header overflow state", () => {
it("clears a stale overflow state when overflow detection is disabled", async () => {
class ResizeObserverMock {
constructor(private readonly callback: ResizeObserverCallback) {}
observe(target: Element) {
Object.defineProperty(target, "getBoundingClientRect", {
configurable: true,
value: () => ({ width: 240 }),
});
this.callback([{ target } as ResizeObserverEntry], this as unknown as ResizeObserver);
}
disconnect() {}
}
vi.stubGlobal("ResizeObserver", ResizeObserverMock);
const view = render(
<PanelHeaderBarSplit
data-testid={SPLIT_HEADER_TEST_ID}
left={<span>{RESIZE_TITLE}</span>}
right={<button type="button">{RESIZE_ACTION}</button>}
overflow={<button type="button">{MORE_ACTIONS}</button>}
overflowAt={520}
/>,
);
await waitFor(() =>
expect(screen.getByTestId(SPLIT_HEADER_TEST_ID).getAttribute("data-panel-overflow")).toBe(
"true",
),
);
view.rerender(
<PanelHeaderBarSplit
data-testid={SPLIT_HEADER_TEST_ID}
left={<span>{RESIZE_TITLE}</span>}
right={<button type="button">{RESIZE_ACTION}</button>}
/>,
);
await waitFor(() =>
expect(
screen.getByTestId(SPLIT_HEADER_TEST_ID).getAttribute("data-panel-overflow"),
).toBeNull(),
);
expect(
screen
.getByRole("button", { name: RESIZE_ACTION })
.parentElement?.className.split(/\s+/)
.includes("hidden"),
).toBe(false);
vi.unstubAllGlobals();
});
});