Skip to the code
cf.

Sign-in gate

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.

What the tests check

  1. shows the sign-in form instead of the children when signed out
    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();
    
  2. signing in shows the children and hides the form
    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();
    
  3. Enter submits, and the name is trimmed
    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();
    
  4. an empty name shows an alert and stays signed out
    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();
    
  5. signing out from elsewhere in the tree brings the form back
    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();
    
  6. signing in through one gate opens every gate
    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();
    
  7. starts signed in as initialUser
    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();
    
  8. login and logout work from any component
    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();
    
  9. useAuth outside an AuthProvider throws a helpful error
    expect(() =>
      render(
        <RequireAuth>
          <h2>Your orders</h2>
        </RequireAuth>,
      ),
    ).toThrow("useAuth must be used inside an AuthProvider");
    

Learn more

App.jsxSaved in this browser

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).