81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render } from "@testing-library/react";
|
|
import { cleanup } from "@testing-library/react";
|
|
|
|
vi.mock("./useAuth", () => ({
|
|
useAuth: vi.fn(),
|
|
}));
|
|
|
|
import { useAuth } from "./useAuth";
|
|
import { RoleGate } from "./RoleGate";
|
|
|
|
function mockUser(role: "admin" | "user" | "viewer" | null) {
|
|
(useAuth as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
user: role ? { id: 1, username: "x", role } : null,
|
|
status: role ? "authenticated" : "unauthenticated",
|
|
});
|
|
}
|
|
|
|
describe("RoleGate", () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
cleanup();
|
|
});
|
|
|
|
it("renders children when role is allowed", () => {
|
|
mockUser("admin");
|
|
const { container } = render(
|
|
<RoleGate allow={["admin", "user"]}>
|
|
<button>Do thing</button>
|
|
</RoleGate>
|
|
);
|
|
expect(container.querySelector("button")).not.toBeNull();
|
|
expect(container.textContent).toContain("Do thing");
|
|
});
|
|
|
|
it("renders disabled (via span wrap) when role is NOT allowed", () => {
|
|
mockUser("viewer");
|
|
const { container } = render(
|
|
<RoleGate allow={["admin", "user"]}>
|
|
<button>Do thing</button>
|
|
</RoleGate>
|
|
);
|
|
const buttons = container.querySelectorAll("button");
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
// The single child element is wrapped in a span that carries the
|
|
// disabled visual signal — opacity-50 plus pointer-events-none so
|
|
// the inner control can't be clicked.
|
|
const wrapper = container.querySelector("span");
|
|
expect(wrapper).not.toBeNull();
|
|
expect(wrapper?.className).toContain("opacity-50");
|
|
expect(wrapper?.className).toContain("pointer-events-none");
|
|
// The wrapper also carries a native `title` attribute that
|
|
// explains why the affordance is disabled.
|
|
expect(wrapper?.getAttribute("title")).toMatch(/role \(viewer\) cannot perform this action/);
|
|
});
|
|
|
|
it("renders fallback when provided and role not allowed", () => {
|
|
mockUser("viewer");
|
|
const { container } = render(
|
|
<RoleGate allow={["admin"]} fallback={<span>NO</span>}>
|
|
<button>Do thing</button>
|
|
</RoleGate>
|
|
);
|
|
expect(container.textContent).toContain("NO");
|
|
// Fallback path does NOT wrap children — original button is absent.
|
|
expect(container.querySelector("button")).toBeNull();
|
|
});
|
|
|
|
it("renders nothing when user is null", () => {
|
|
mockUser(null);
|
|
const { container } = render(
|
|
<RoleGate allow={["admin", "user"]}>
|
|
<button>Do thing</button>
|
|
</RoleGate>
|
|
);
|
|
expect(container.querySelector("button")).toBeNull();
|
|
expect(container.textContent).not.toContain("Do thing");
|
|
});
|
|
});
|