feat(auth): RoleGate component
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// @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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user