diff --git a/components/navigation/NavbarWrapper.tsx b/components/navigation/NavbarWrapper.tsx
index f1a6f1d9..3d68d162 100644
--- a/components/navigation/NavbarWrapper.tsx
+++ b/components/navigation/NavbarWrapper.tsx
@@ -1,6 +1,6 @@
"use client";
-import { NO_NAVBAR_PAGES } from "@/lib/navbar";
+import { isNoNavbarPage } from "@/lib/navbar";
import { usePathname } from "next/navigation";
import React from "react";
@@ -13,7 +13,7 @@ export default function NavbarWrapper({
}) {
const pathname = usePathname();
- if (NO_NAVBAR_PAGES.includes(pathname)) {
+ if (isNoNavbarPage(pathname)) {
return <>{noNavbarChildren}>;
}
diff --git a/lib/navbar.ts b/lib/navbar.ts
index 10a7a0f8..1f28f4a2 100644
--- a/lib/navbar.ts
+++ b/lib/navbar.ts
@@ -47,6 +47,7 @@ export const NAVBAR_ITEMS: NavbarItem[] = [
{ label: "Events", href: "/events", icon: "calendar" },
];
+// Entries ending in "/" also match every path under them (see matchesPage).
export const NO_NAVBAR_PAGES = [
ORGANIZATION_NOT_FOUND_PATH,
"/login",
@@ -56,4 +57,19 @@ export const NO_NAVBAR_PAGES = [
"/forgotpassword",
];
+/**
+ * Whether `pathname` is one of `pages`. An entry ending in "/" matches any
+ * path under it: "/forms/" matches "/forms/abc" but not "/forms" or
+ * "/formsabc". Other entries match only the exact path.
+ */
+export function matchesPage(pathname: string, pages: readonly string[]) {
+ return pages.some((page) =>
+ page.endsWith("/") ? pathname.startsWith(page) : pathname === page,
+ );
+}
+
+export function isNoNavbarPage(pathname: string) {
+ return matchesPage(pathname, NO_NAVBAR_PAGES);
+}
+
export const MEMBERSHIP_REDIRECT_EXCLUDED_PAGES = NO_NAVBAR_PAGES;
diff --git a/styles/globals.css b/styles/globals.css
index acbbad1e..cd27ebfb 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -99,6 +99,16 @@
transparent
);
+ /* Theme tokens that will follow each org's theme (#300). They hold today's
+ values, so using them changes nothing yet. */
+ --color-page-bg: #ffffff;
+ --color-page-text: var(--color-grey-text-strong);
+ /* Text on brand-colored surfaces, e.g. a primary button's label. */
+ --color-brand-foreground: #ffffff;
+ --font-display: var(--font-heading);
+ /* Corner radius for inputs and buttons (Tailwind's `rounded`). */
+ --radius-control: 0.25rem;
+
--text-desktop-display: 48px;
--text-desktop-heading-1: 40px;
--text-desktop-heading-2: 32px;
diff --git a/tests/unit/components/navigation/NavbarWrapper.test.tsx b/tests/unit/components/navigation/NavbarWrapper.test.tsx
new file mode 100644
index 00000000..faa6ade1
--- /dev/null
+++ b/tests/unit/components/navigation/NavbarWrapper.test.tsx
@@ -0,0 +1,53 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import NavbarWrapper from "@/components/navigation/NavbarWrapper";
+
+const pathname = vi.hoisted(() => ({ current: "/" }));
+
+vi.mock("next/navigation", () => ({
+ usePathname: () => pathname.current,
+}));
+
+// No real entry is a prefix yet (#299 and #301 add them), so add one.
+vi.mock("@/lib/navbar", async (importOriginal) => {
+ const actual = await importOriginal
with navbar
+ , + ); +} + +describe("NavbarWrapper", () => { + afterEach(cleanup); + + it("renders the navbar layout on regular pages", () => { + renderAt("/events"); + expect(screen.getByText("with navbar")).toBeTruthy(); + }); + + it("renders without the navbar on excluded pages", () => { + renderAt("/login"); + expect(screen.getByText("without navbar")).toBeTruthy(); + }); + + it("renders without the navbar under a prefix entry", () => { + renderAt("/forms/camp-application-2027"); + expect(screen.getByText("without navbar")).toBeTruthy(); + }); + + it("renders the navbar on a path that only shares the prefix's letters", () => { + renderAt("/formsabc"); + expect(screen.getByText("with navbar")).toBeTruthy(); + }); +}); diff --git a/tests/unit/lib/navbar.test.ts b/tests/unit/lib/navbar.test.ts new file mode 100644 index 00000000..99c54ced --- /dev/null +++ b/tests/unit/lib/navbar.test.ts @@ -0,0 +1,34 @@ +// @vitest-environment node +import { describe, expect, it } from "vitest"; +import { isNoNavbarPage, matchesPage, NO_NAVBAR_PAGES } from "@/lib/navbar"; + +describe(matchesPage, () => { + const pages = ["/login", "/forms/"]; + + it("matches any path under an entry ending in /", () => { + expect(matchesPage("/forms/abc", pages)).toBe(true); + expect(matchesPage("/forms/camp-application-2027/edit", pages)).toBe(true); + }); + + it("doesn't match a path that only shares the prefix's letters", () => { + expect(matchesPage("/formsabc", pages)).toBe(false); + expect(matchesPage("/forms", pages)).toBe(false); + }); + + it("matches other entries exactly", () => { + expect(matchesPage("/login", pages)).toBe(true); + expect(matchesPage("/login/help", pages)).toBe(false); + expect(matchesPage("/loginx", pages)).toBe(false); + }); +}); + +describe(isNoNavbarPage, () => { + it.each(NO_NAVBAR_PAGES)("hides the navbar on %s", (page) => { + expect(isNoNavbarPage(page)).toBe(true); + }); + + it("shows the navbar elsewhere", () => { + expect(isNoNavbarPage("/")).toBe(false); + expect(isNoNavbarPage("/events")).toBe(false); + }); +});