diff --git a/packages/ui/app/src/atoms/auth.ts b/packages/ui/app/src/atoms/auth.ts index 739218c564..6f4c08fc20 100644 --- a/packages/ui/app/src/atoms/auth.ts +++ b/packages/ui/app/src/atoms/auth.ts @@ -1,6 +1,6 @@ import type { FernUser } from "@fern-ui/fern-docs-auth"; import { isEqual } from "es-toolkit/predicate"; -import { useAtomValue } from "jotai"; +import { Atom, useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; import { DOCS_ATOM } from "./docs"; @@ -8,6 +8,13 @@ export const FERN_USER_ATOM = selectAtom(DOCS_ATOM, (docs) => docs.user, isEqual FERN_USER_ATOM.debugLabel = "FERN_USER_ATOM"; -export function useFernUser(): FernUser | undefined { - return useAtomValue(FERN_USER_ATOM); +interface UseFernUserOptions { + /** + * A fern user atom for testing purposes only + */ + __test_fern_user_atom?: Atom; +} + +export function useFernUser({ __test_fern_user_atom }: UseFernUserOptions = {}): FernUser | undefined { + return useAtomValue(__test_fern_user_atom ?? FERN_USER_ATOM); } diff --git a/packages/ui/app/src/mdx/components/if/If.test.tsx b/packages/ui/app/src/mdx/components/if/If.test.tsx new file mode 100644 index 0000000000..e1c0848d27 --- /dev/null +++ b/packages/ui/app/src/mdx/components/if/If.test.tsx @@ -0,0 +1,220 @@ +/** + * @vitest-environment jsdom + */ + +import { FernUser } from "@fern-ui/fern-docs-auth"; +import { EVERYONE_ROLE } from "@fern-ui/fern-docs-utils"; +import { render } from "@testing-library/react"; +import { Atom, atom } from "jotai"; +import { freezeAtom } from "jotai/utils"; +import { If } from "./If"; + +function createTestFernUserAtom(roles: string[] | false): Atom { + return freezeAtom(atom(roles ? { roles } : undefined)); +} + +describe("If", () => { + it("renders when the roles=[], and the user is logged with roles=[]", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("renders when user matches role exactly", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("renders when user overlaps with one of the roles", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when the user does not overlap with any of the roles", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + + await expect(findByText("capture_the_flag", { exact: false })).rejects.toThrow(); + }); + + it("renders when the roles=[], and the user exists", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("renders when the roles=[], and the user exists and has a role", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when the roles=[], and the user is not logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("renders when the roles=undefined, and the user exists", async () => { + const { findByText } = render(capture_the_flag); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("renders when the roles=undefined, and the user does not exist", async () => { + const { findByText } = render(capture_the_flag); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when the not=true, and user is not logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("hides when the not=true, and user is logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("renders when loggedIn=true", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when loggedIn=true, and user is not logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("renders when not loggedIn=true, and user is not logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when not loggedIn=true, and user is logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("renders when the role is everyone, including when the user is not logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("renders when the role is everyone, and the user is logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when the user matches role and the not=true", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("renders when the user does not match a role and not=true", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("renders when not=true && roles=[], and the user's roles do not overlap", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); + + it("hides when not=true && roles=[], and the user has role=[], and not=true", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).rejects.toThrow(); + }); + + it("renders when not=true && roles=[], and the user is not logged in", async () => { + const { findByText } = render( + + capture_the_flag + , + ); + await expect(findByText("capture_the_flag")).resolves.toBeDefined(); + }); +}); diff --git a/packages/ui/app/src/mdx/components/if/If.tsx b/packages/ui/app/src/mdx/components/if/If.tsx index a84685033a..dd4de8266d 100644 --- a/packages/ui/app/src/mdx/components/if/If.tsx +++ b/packages/ui/app/src/mdx/components/if/If.tsx @@ -1,4 +1,6 @@ +import { FernUser } from "@fern-ui/fern-docs-auth"; import { EVERYONE_ROLE } from "@fern-ui/fern-docs-utils"; +import { Atom } from "jotai"; import { PropsWithChildren, ReactNode } from "react"; import { useFernUser } from "../../../atoms"; @@ -12,6 +14,16 @@ export interface IfProps { * Invert the role check */ not?: boolean; + + /** + * Whether the user is logged in + */ + loggedIn?: boolean; + + /** + * A fern user atom for testing purposes only + */ + __test_fern_user_atom?: Atom; } /** @@ -27,13 +39,29 @@ export interface IfProps { * some content */ -export function If({ not, roles, children }: PropsWithChildren): ReactNode { - const user = useFernUser(); +export function If({ not, roles, loggedIn, children, __test_fern_user_atom }: PropsWithChildren): ReactNode { + const user = useFernUser({ __test_fern_user_atom }); const userRoles = user?.roles ?? []; - const show = - roles?.some((roles) => userRoles.some((role) => roles.includes(role) || role === EVERYONE_ROLE)) ?? false; + if (not && roles?.length === 0 && userRoles.length > 0) { + return children; + } + + const shouldShow = () => { + if (roles != null) { + if (roles.length === 0) { + return user != null; + } + return roles.some((role) => userRoles.includes(role) || role === EVERYONE_ROLE); + } + if (loggedIn != null) { + return loggedIn === (user != null); + } + return true; + }; + + const show = not ? !shouldShow() : shouldShow(); - return not ? !show && children : show && children; + return show ? children : null; }