Part of the tea shop's site, like the order history, is only for signed-in customers. Who's signed in is needed all over the page (the header greets them, the order history needs their name), so keep it in context, and build a wrapper that guards the private parts.
Export AuthProvider by name. It takes children and an optional initialUser, which is null (signed out, the default) or a user like { name: "Ana" }.
Export useAuth() by name. Inside an AuthProvider it returns { user, login, logout }: user is null or { name }, login(name) signs in as { name }, and logout() signs out. Every component under the same provider sees the same user. Outside any provider, useAuth() throws an Error with the message useAuth must be used inside an AuthProvider.
Export RequireAuth by name. It takes children. When someone is signed in, it renders its children and nothing else. When no one is, it renders a sign-in form instead of the children: a <form> with a text box labelled Your name and a Sign in submit button, so clicking the button and pressing Enter both submit it.
Submitting signs in with the name, trimmed of spaces at both ends. If the trimmed name is empty, stay signed out and show Enter your name to sign in. in an element with role="alert". Every RequireAuth on the page follows the same sign-in: signing in through one opens them all, and signing out anywhere closes them all.
<AuthProvider>
<Header /> {/* useAuth() to greet the user and sign out */}
<RequireAuth>
<OrderHistory /> {/* only shown when signed in */}
</RequireAuth>
</AuthProvider>
The default export, App, wires these together for the Preview.
render(
<AuthProvider>
<RequireAuth>
<h2>Your orders</h2>
</RequireAuth>
</AuthProvider>,
);
expect(screen.queryByRole("heading", { name: "Your orders" })).not.toBeInTheDocument();
expect(screen.queryByText("Your orders")).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Your name" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Sign in" })).toBeInTheDocument();
render(
<AuthProvider>
<RequireAuth>
<h2>Your orders</h2>
</RequireAuth>
</AuthProvider>,
);
await user.type(screen.getByRole("textbox", { name: "Your name" }), "Ana");
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByRole("heading", { name: "Your orders" })).toBeInTheDocument();
expect(screen.queryByRole("textbox", { name: "Your name" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Sign in" })).not.toBeInTheDocument();
function Greeting() {
const { user } = useAuth();
return <p>Hello, {user.name}!</p>;
}
render(
<AuthProvider>
<RequireAuth>
<Greeting />
</RequireAuth>
</AuthProvider>,
);
await user.type(screen.getByRole("textbox", { name: "Your name" }), " Ana {Enter}");
expect(screen.getByText("Hello, Ana!")).toBeInTheDocument();
render(
<AuthProvider>
<RequireAuth>
<h2>Your orders</h2>
</RequireAuth>
</AuthProvider>,
);
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByRole("alert")).toHaveTextContent("Enter your name to sign in.");
await user.type(screen.getByRole("textbox", { name: "Your name" }), " ");
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByRole("alert")).toHaveTextContent("Enter your name to sign in.");
expect(screen.queryByRole("heading", { name: "Your orders" })).not.toBeInTheDocument();
function Header() {
const { user, logout } = useAuth();
return <header>{user && <button onClick={logout}>Sign out</button>}</header>;
}
render(
<AuthProvider>
<Header />
<main>
<RequireAuth>
<h2>Your orders</h2>
</RequireAuth>
</main>
</AuthProvider>,
);
await user.type(screen.getByRole("textbox", { name: "Your name" }), "Ana");
await user.click(screen.getByRole("button", { name: "Sign in" }));
await user.click(screen.getByRole("button", { name: "Sign out" }));
expect(screen.queryByRole("heading", { name: "Your orders" })).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Your name" })).toBeInTheDocument();
render(
<AuthProvider>
<section aria-label="Orders">
<RequireAuth>
<h2>Your orders</h2>
</RequireAuth>
</section>
<section aria-label="Favourites">
<RequireAuth>
<h2>Your favourites</h2>
</RequireAuth>
</section>
</AuthProvider>,
);
const orders = within(screen.getByRole("region", { name: "Orders" }));
await user.type(orders.getByRole("textbox", { name: "Your name" }), "Ana");
await user.click(orders.getByRole("button", { name: "Sign in" }));
expect(screen.getByRole("heading", { name: "Your orders" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Your favourites" })).toBeInTheDocument();
expect(screen.queryByRole("textbox", { name: "Your name" })).not.toBeInTheDocument();
function Greeting() {
const { user } = useAuth();
return <p>Hello, {user.name}!</p>;
}
render(
<AuthProvider initialUser={{ name: "Bo" }}>
<RequireAuth>
<Greeting />
</RequireAuth>
</AuthProvider>,
);
expect(screen.getByText("Hello, Bo!")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Sign in" })).not.toBeInTheDocument();
function Account() {
const { user, login, logout } = useAuth();
return (
<div>
<p>{user ? "Signed in as " + user.name : "Signed out"}</p>
<button onClick={() => login("Kai")}>Quick sign in</button>
<button onClick={logout}>Sign out</button>
</div>
);
}
render(
<AuthProvider>
<Account />
</AuthProvider>,
);
expect(screen.getByText("Signed out")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Quick sign in" }));
expect(screen.getByText("Signed in as Kai")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Sign out" }));
expect(screen.getByText("Signed out")).toBeInTheDocument();
expect(() =>
render(
<RequireAuth>
<h2>Your orders</h2>
</RequireAuth>,
),
).toThrow("useAuth must be used inside an AuthProvider");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Sign-in gate. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.