diff --git a/apps/web/package.json b/apps/web/package.json
index f8cc79ac..9880ea21 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -7,6 +7,8 @@
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
+ "test": "vitest",
+ "test:run": "vitest run",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"start": "pnpm with-env next start",
@@ -42,6 +44,8 @@
"@cooper/prettier-config": "workspace:*",
"@cooper/tailwind-config": "workspace:*",
"@cooper/tsconfig": "workspace:*",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.0.1",
"@types/node": "^20.14.15",
"@types/react": "catalog:react18",
"@types/react-dom": "catalog:react18",
@@ -49,9 +53,11 @@
"dotenv-cli": "^7.4.2",
"eslint": "catalog:",
"jiti": "^1.21.6",
+ "jsdom": "^25.0.1",
"prettier": "catalog:",
"tailwindcss": "^3.4.4",
- "typescript": "catalog:"
+ "typescript": "catalog:",
+ "vitest": "catalog:"
},
"prettier": "@cooper/prettier-config"
}
diff --git a/apps/web/src/app/_components/auth/actions.test.ts b/apps/web/src/app/_components/auth/actions.test.ts
new file mode 100644
index 00000000..c3969b39
--- /dev/null
+++ b/apps/web/src/app/_components/auth/actions.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, test, vi } from "vitest";
+import { handleGoogleSignIn, handleSignOut } from "./actions";
+
+const mockSignIn = vi.fn();
+const mockSignOut = vi.fn();
+vi.mock("@cooper/auth", () => ({
+ signIn: (...args: unknown[]) => mockSignIn(...args) as unknown,
+ signOut: (...args: unknown[]) => mockSignOut(...args) as unknown,
+}));
+
+describe("auth actions", () => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- vitest beforeEach callback with hoisted mocks
+ beforeEach(() => {
+ mockSignIn.mockReset();
+ mockSignOut.mockReset();
+ });
+
+ describe("handleGoogleSignIn", () => {
+ test("calls signIn with google and redirectTo", async () => {
+ mockSignIn.mockResolvedValue(undefined);
+ await handleGoogleSignIn();
+ expect(mockSignIn).toHaveBeenCalledTimes(1);
+ expect(mockSignIn).toHaveBeenCalledWith("google", { redirectTo: "/" });
+ });
+ });
+
+ describe("handleSignOut", () => {
+ test("calls signOut with redirectTo", async () => {
+ mockSignOut.mockResolvedValue(undefined);
+ await handleSignOut();
+ expect(mockSignOut).toHaveBeenCalledTimes(1);
+ expect(mockSignOut).toHaveBeenCalledWith({ redirectTo: "/" });
+ });
+ });
+});
diff --git a/apps/web/src/utils/companyStatistics.test.ts b/apps/web/src/utils/companyStatistics.test.ts
new file mode 100644
index 00000000..4605b79c
--- /dev/null
+++ b/apps/web/src/utils/companyStatistics.test.ts
@@ -0,0 +1,129 @@
+import { describe, expect, test } from "vitest";
+import {
+ calculatePay,
+ calculatePayRange,
+ calculateWorkModels,
+} from "./companyStatistics";
+import { ReviewType } from "@cooper/db/schema";
+
+describe("companyStatistics", () => {
+ const mockReviews = [
+ {
+ workEnvironment: "REMOTE",
+ hourlyPay: "25",
+ },
+ {
+ workEnvironment: "REMOTE",
+ hourlyPay: "25",
+ },
+ {
+ workEnvironment: "HYBRID",
+ hourlyPay: "30",
+ },
+ {
+ workEnvironment: "INPERSON",
+ hourlyPay: "20",
+ },
+ ] as ReviewType[];
+
+ describe("calculateWorkModels", () => {
+ test("returns empty array for no reviews", () => {
+ expect(calculateWorkModels([])).toEqual([]);
+ });
+
+ test("returns unique work models with count and percentage", () => {
+ const result = calculateWorkModels(mockReviews);
+ expect(result).toHaveLength(3);
+ expect(result).toContainEqual({
+ name: "Remote",
+ percentage: 50,
+ count: 2,
+ });
+ expect(result).toContainEqual({
+ name: "Hybrid",
+ percentage: 25,
+ count: 1,
+ });
+ expect(result).toContainEqual({
+ name: "Inperson",
+ percentage: 25,
+ count: 1,
+ });
+ });
+
+ test("title-cases model names", () => {
+ const result = calculateWorkModels([
+ { workEnvironment: "REMOTE" } as never,
+ ]);
+ expect(result[0]?.name).toBe("Remote");
+ });
+ });
+
+ describe("calculatePay", () => {
+ test("returns empty array for no reviews", () => {
+ expect(calculatePay([])).toEqual([]);
+ });
+
+ test("returns pay breakdown with count and percentage", () => {
+ const result = calculatePay(mockReviews);
+ expect(result).toHaveLength(3);
+ expect(result).toContainEqual({
+ pay: "25",
+ percentage: 50,
+ count: 2,
+ });
+ expect(result).toContainEqual({
+ pay: "30",
+ percentage: 25,
+ count: 1,
+ });
+ expect(result).toContainEqual({
+ pay: "20",
+ percentage: 25,
+ count: 1,
+ });
+ });
+
+ test("treats null hourlyPay as 0", () => {
+ const result = calculatePay([
+ { hourlyPay: null } as never,
+ { hourlyPay: "10" } as never,
+ ]);
+ expect(result).toContainEqual({
+ pay: "0",
+ percentage: 50,
+ count: 1,
+ });
+ });
+ });
+
+ describe("calculatePayRange", () => {
+ test("returns single range with min/max 0 for no reviews", () => {
+ expect(calculatePayRange([])).toEqual([{ min: 0, max: 0 }]);
+ });
+
+ test("splits into Low/Mid/High when 3+ unique pay values", () => {
+ const reviews = [
+ { hourlyPay: "10" },
+ { hourlyPay: "20" },
+ { hourlyPay: "30" },
+ ] as ReviewType[];
+ const result = calculatePayRange(reviews);
+ expect(result).toHaveLength(3);
+ expect(result[0]).toMatchObject({ label: "Low", min: 10 });
+ expect(result[1]).toMatchObject({ label: "Mid" });
+ expect(result[2]).toMatchObject({ label: "High", max: 30 });
+ });
+
+ test("returns Low and Mid ranges when fewer than 3 unique pays", () => {
+ const reviews = [
+ { hourlyPay: "15" },
+ { hourlyPay: "25" },
+ ] as ReviewType[];
+ const result = calculatePayRange(reviews);
+ expect(result).toHaveLength(2);
+ expect(result[0]?.label).toBe("Low");
+ expect(result[1]?.label).toBe("Mid");
+ });
+ });
+});
diff --git a/apps/web/src/utils/dateHelpers.test.ts b/apps/web/src/utils/dateHelpers.test.ts
new file mode 100644
index 00000000..53cf6bd0
--- /dev/null
+++ b/apps/web/src/utils/dateHelpers.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, test } from "vitest";
+import { formatDate } from "./dateHelpers";
+
+describe("dateHelpers", () => {
+ describe("formatDate", () => {
+ test("returns empty string for undefined", () => {
+ expect(formatDate(undefined)).toBe("");
+ });
+
+ test("returns formatted date as dd mth yyyy", () => {
+ expect(formatDate(new Date(2025, 0, 15))).toBe("15 Jan 2025");
+ expect(formatDate(new Date(2024, 11, 1))).toBe("01 Dec 2024");
+ expect(formatDate(new Date(2023, 5, 7))).toBe("07 Jun 2023");
+ });
+
+ test("pads single-digit day with zero", () => {
+ expect(formatDate(new Date(2025, 2, 5))).toBe("05 Mar 2025");
+ });
+ });
+});
diff --git a/apps/web/src/utils/locationHelpers.test.ts b/apps/web/src/utils/locationHelpers.test.ts
new file mode 100644
index 00000000..8be3eac1
--- /dev/null
+++ b/apps/web/src/utils/locationHelpers.test.ts
@@ -0,0 +1,61 @@
+import { describe, expect, test } from "vitest";
+import { abbreviatedStateName, prettyLocationName } from "./locationHelpers";
+
+describe("locationHelpers", () => {
+ describe("prettyLocationName", () => {
+ test("returns N/A for undefined location", () => {
+ expect(prettyLocationName(undefined)).toBe("N/A");
+ });
+
+ test("returns city and abbreviated state when state present (no country)", () => {
+ expect(
+ prettyLocationName({
+ city: "Boston",
+ state: "Massachusetts",
+ country: "USA",
+ } as never),
+ ).toBe("Boston, MA");
+ });
+
+ test("returns city, country when no state", () => {
+ expect(
+ prettyLocationName({
+ city: "Toronto",
+ state: undefined,
+ country: "Canada",
+ } as never),
+ ).toBe("Toronto, Canada");
+ });
+
+ test("abbreviates full state name", () => {
+ expect(
+ prettyLocationName({
+ city: "San Francisco",
+ state: "California",
+ country: "USA",
+ } as never),
+ ).toBe("San Francisco, CA");
+ });
+ });
+
+ describe("abbreviatedStateName", () => {
+ test("uppercases 2-letter state codes", () => {
+ expect(abbreviatedStateName("ca")).toBe("CA");
+ expect(abbreviatedStateName("ny")).toBe("NY");
+ });
+
+ test("converts full state names to abbreviations", () => {
+ expect(abbreviatedStateName("California")).toBe("CA");
+ expect(abbreviatedStateName("New York")).toBe("NY");
+ expect(abbreviatedStateName("Texas")).toBe("TX");
+ expect(abbreviatedStateName("Alabama")).toBe("AL");
+ expect(abbreviatedStateName("District of Columbia")).toBe("DC");
+ expect(abbreviatedStateName("New Hampshire")).toBe("NH");
+ expect(abbreviatedStateName("West Virginia")).toBe("WV");
+ });
+
+ test("returns state unchanged when not in map", () => {
+ expect(abbreviatedStateName("Unknown")).toBe("Unknown");
+ });
+ });
+});
diff --git a/apps/web/src/utils/reviewCountByStars.test.ts b/apps/web/src/utils/reviewCountByStars.test.ts
new file mode 100644
index 00000000..f75c7326
--- /dev/null
+++ b/apps/web/src/utils/reviewCountByStars.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, test } from "vitest";
+import { calculateRatings } from "./reviewCountByStars";
+import { ReviewType } from "@cooper/db/schema";
+
+describe("reviewCountByStars", () => {
+ describe("calculateRatings", () => {
+ test("returns zeros for empty reviews", () => {
+ const result = calculateRatings([]);
+ expect(result).toHaveLength(5);
+ result.forEach((r) => {
+ expect(r.percentage).toBe(0);
+ expect([1, 2, 3, 4, 5]).toContain(r.stars);
+ });
+ });
+
+ test("returns stars 1-5 with percentage", () => {
+ const reviews = [
+ { overallRating: 5 },
+ { overallRating: 5 },
+ { overallRating: 4 },
+ { overallRating: 3 },
+ ] as ReviewType[];
+ const result = calculateRatings(reviews);
+ expect(result).toHaveLength(5);
+ const five = result.find((r) => r.stars === 5);
+ const four = result.find((r) => r.stars === 4);
+ const three = result.find((r) => r.stars === 3);
+ expect(five?.percentage).toBe(50);
+ expect(four?.percentage).toBe(25);
+ expect(three?.percentage).toBe(25);
+ });
+ });
+});
diff --git a/apps/web/src/utils/reviewsAggregationHelpers.test.ts b/apps/web/src/utils/reviewsAggregationHelpers.test.ts
new file mode 100644
index 00000000..62ef5179
--- /dev/null
+++ b/apps/web/src/utils/reviewsAggregationHelpers.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, test } from "vitest";
+import {
+ averageStarRating,
+ listBenefits,
+ mostCommonWorkEnviornment,
+} from "./reviewsAggregationHelpers";
+import { ReviewType } from "@cooper/db/schema";
+
+describe("reviewsAggregationHelpers", () => {
+ describe("mostCommonWorkEnviornment", () => {
+ test("returns In Person when INPERSON is most common", () => {
+ const reviews = [
+ { workEnvironment: "INPERSON" },
+ { workEnvironment: "INPERSON" },
+ { workEnvironment: "REMOTE" },
+ ] as ReviewType[];
+ expect(mostCommonWorkEnviornment(reviews)).toBe("In Person");
+ });
+
+ test("returns Hybrid when HYBRID is most common", () => {
+ const reviews = [
+ { workEnvironment: "HYBRID" },
+ { workEnvironment: "HYBRID" },
+ { workEnvironment: "REMOTE" },
+ ] as ReviewType[];
+ expect(mostCommonWorkEnviornment(reviews)).toBe("Hybrid");
+ });
+
+ test("returns Remote when REMOTE is most common", () => {
+ const reviews = [
+ { workEnvironment: "REMOTE" },
+ { workEnvironment: "REMOTE" },
+ { workEnvironment: "INPERSON" },
+ ] as ReviewType[];
+ expect(mostCommonWorkEnviornment(reviews)).toBe("Remote");
+ });
+
+ test("ties: In Person wins over others", () => {
+ const reviews = [
+ { workEnvironment: "INPERSON" },
+ { workEnvironment: "HYBRID" },
+ { workEnvironment: "REMOTE" },
+ ] as ReviewType[];
+ expect(mostCommonWorkEnviornment(reviews)).toBe("In Person");
+ });
+ });
+
+ describe("averageStarRating", () => {
+ test("returns average of overall ratings", () => {
+ const reviews = [
+ { overallRating: 4 },
+ { overallRating: 5 },
+ { overallRating: 3 },
+ ] as ReviewType[];
+ expect(averageStarRating(reviews)).toBe(4);
+ });
+
+ test("handles decimal average", () => {
+ const reviews = [
+ { overallRating: 4 },
+ { overallRating: 5 },
+ ] as ReviewType[];
+ expect(averageStarRating(reviews)).toBe(4.5);
+ });
+ });
+
+ describe("listBenefits", () => {
+ test("returns empty array when no benefits", () => {
+ const review = {
+ pto: false,
+ federalHolidays: false,
+ freeLunch: false,
+ travelBenefits: false,
+ freeMerch: false,
+ snackBar: false,
+ } as never;
+ expect(listBenefits(review)).toEqual([]);
+ });
+
+ test("returns labels for each benefit that is true", () => {
+ const review = {
+ pto: true,
+ federalHolidays: true,
+ freeLunch: false,
+ travelBenefits: true,
+ freeMerch: false,
+ snackBar: true,
+ } as never;
+ const result = listBenefits(review);
+ expect(result).toContain("Paid Time Off");
+ expect(result).toContain("Federal Holidays Off");
+ expect(result).toContain("Free Transporation");
+ expect(result).toContain("Snack Bar");
+ expect(result).not.toContain("Free Lunch");
+ expect(result).not.toContain("Free Merch");
+ });
+ });
+});
diff --git a/apps/web/src/utils/stringHelpers.test.ts b/apps/web/src/utils/stringHelpers.test.ts
new file mode 100644
index 00000000..1ba158cb
--- /dev/null
+++ b/apps/web/src/utils/stringHelpers.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, test } from "vitest";
+import {
+ abbreviatedWorkTerm,
+ prettyDescription,
+ prettyIndustry,
+ prettyWorkEnviornment,
+ truncateText,
+} from "./stringHelpers";
+
+describe("stringHelpers", () => {
+ describe("truncateText", () => {
+ test("returns text unchanged when shorter than length", () => {
+ expect(truncateText("hi", 10)).toBe("hi");
+ });
+
+ test("returns text with ellipsis when at or over length", () => {
+ const result = truncateText("hello world", 5);
+ expect(result).toContain("hello");
+ expect(result).toContain("...");
+ });
+ });
+
+ describe("abbreviatedWorkTerm", () => {
+ test("abbreviates SPRING, SUMMER, FALL", () => {
+ expect(abbreviatedWorkTerm("SPRING")).toBe("Spr");
+ expect(abbreviatedWorkTerm("SUMMER")).toBe("Sum");
+ expect(abbreviatedWorkTerm("FALL")).toBe("Fall");
+ });
+
+ test("returns other terms unchanged", () => {
+ expect(abbreviatedWorkTerm("WINTER")).toBe("WINTER");
+ });
+ });
+
+ describe("prettyWorkEnviornment", () => {
+ test("formats work environment labels", () => {
+ expect(prettyWorkEnviornment("HYBRID")).toBe("Hybrid");
+ expect(prettyWorkEnviornment("INPERSON")).toBe("In-person");
+ expect(prettyWorkEnviornment("REMOTE")).toBe("Remote");
+ });
+ });
+
+ describe("prettyDescription", () => {
+ test("returns empty string for undefined or null", () => {
+ expect(prettyDescription(undefined)).toBe("");
+ expect(prettyDescription(null)).toBe("");
+ });
+
+ test("returns description when under max length", () => {
+ const short = "Short text";
+ expect(prettyDescription(short)).toBe(short);
+ });
+
+ test("truncates and appends ... when over max length", () => {
+ const long = "a".repeat(250);
+ const result = prettyDescription(long, 200);
+ expect(result).toHaveLength(203);
+ expect(result.endsWith("...")).toBe(true);
+ });
+
+ test("uses default max length 200", () => {
+ const long = "a".repeat(250);
+ expect(prettyDescription(long).length).toBe(203);
+ });
+ });
+
+ describe("prettyIndustry", () => {
+ test("returns Unknown Industry for undefined or empty", () => {
+ expect(prettyIndustry(undefined)).toBe("Unknown Industry");
+ expect(prettyIndustry("")).toBe("Unknown Industry");
+ });
+
+ test("formats known industry codes", () => {
+ expect(prettyIndustry("TECHNOLOGY")).toBe("Technology");
+ expect(prettyIndustry("HEALTHCARE")).toBe("Healthcare");
+ expect(prettyIndustry("FINANCE")).toBe("Finance");
+ expect(prettyIndustry("REALESTATE")).toBe("Real Estate");
+ expect(prettyIndustry("FASHIONANDBEAUTY")).toBe("Fashion & Beauty");
+ expect(prettyIndustry("FOODANDBEVERAGE")).toBe("Food & Beverage");
+ });
+
+ test("returns Unknown Industry for unknown code", () => {
+ expect(prettyIndustry("UNKNOWN")).toBe("Unknown Industry");
+ });
+ });
+});
diff --git a/apps/web/test/app-layout.test.tsx b/apps/web/test/app-layout.test.tsx
new file mode 100644
index 00000000..80fc9a72
--- /dev/null
+++ b/apps/web/test/app-layout.test.tsx
@@ -0,0 +1,57 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import RootLayout from "~/app/layout";
+
+vi.mock("~/env", () => ({
+ env: { VERCEL_ENV: "development", NODE_ENV: "test" },
+}));
+
+vi.mock("~/trpc/react", () => ({
+ TRPCReactProvider: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+}));
+
+vi.mock("~/app/_components/compare/compare-context", () => ({
+ CompareProvider: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+vi.mock("~/app/styles/font", () => ({
+ hankenGroteskFont: { variable: "font-hanken" },
+}));
+
+describe("RootLayout", () => {
+ test("renders children inside providers", () => {
+ render(
+
+ Child content
+ ,
+ );
+ expect(screen.getByTestId("trpc-provider")).toBeInTheDocument();
+ expect(screen.getByTestId("compare-provider")).toBeInTheDocument();
+ expect(screen.getByTestId("child")).toHaveTextContent("Child content");
+ });
+
+ test("renders html with lang and font class", () => {
+ const { container } = render(
+
+ Child
+ ,
+ );
+ const html = container.querySelector("html");
+ expect(html).toHaveAttribute("lang", "en");
+ expect(html).toHaveClass("font-hanken");
+ });
+
+ test("renders body with expected classes", () => {
+ const { container } = render(
+
+ Child
+ ,
+ );
+ const body = container.querySelector("body");
+ expect(body).toHaveClass("bg-cooper-cream-100", "font-sans", "antialiased");
+ });
+});
diff --git a/apps/web/test/auth/login-button-client.test.tsx b/apps/web/test/auth/login-button-client.test.tsx
new file mode 100644
index 00000000..3369dcf7
--- /dev/null
+++ b/apps/web/test/auth/login-button-client.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import LoginButtonClient from "~/app/_components/auth/login-button-client";
+
+vi.mock("next/image", () => ({
+ default: ({ alt }: { alt: string }) => (
+ // eslint-disable-next-line @next/next/no-img-element -- test mock
+
+ ),
+}));
+
+vi.mock("~/app/_components/auth/actions", () => ({
+ handleGoogleSignIn: vi.fn(),
+}));
+
+describe("LoginButtonClient", () => {
+ test("renders form with submit button", () => {
+ render();
+ const form = screen.getByRole("button", { name: /login/i }).closest("form");
+ expect(form).toBeInTheDocument();
+ });
+
+ test("renders login image with correct alt", () => {
+ render();
+ expect(screen.getByRole("img", { name: /login/i })).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/auth/login-button.test.tsx b/apps/web/test/auth/login-button.test.tsx
new file mode 100644
index 00000000..7dfd7f83
--- /dev/null
+++ b/apps/web/test/auth/login-button.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import LoginButton from "~/app/_components/auth/login-button";
+
+vi.mock("next/image", () => ({
+ default: ({ alt }: { alt: string }) => (
+ // eslint-disable-next-line @next/next/no-img-element -- test mock
+
+ ),
+}));
+
+vi.mock("@cooper/auth", () => ({
+ signIn: vi.fn(),
+}));
+
+describe("LoginButton", () => {
+ test("renders mobile form with image button", () => {
+ render();
+ const forms = screen.getAllByRole("button");
+ expect(forms.length).toBeGreaterThanOrEqual(1);
+ expect(screen.getByRole("img", { name: /login/i })).toBeInTheDocument();
+ });
+
+ test("renders Log in button for larger screens", () => {
+ render();
+ expect(screen.getByText("Log in")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/auth/logout-button.test.tsx b/apps/web/test/auth/logout-button.test.tsx
new file mode 100644
index 00000000..ce365383
--- /dev/null
+++ b/apps/web/test/auth/logout-button.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import LogoutButton from "~/app/_components/auth/logout-button";
+
+vi.mock("@cooper/auth", () => ({
+ signOut: vi.fn(),
+}));
+
+describe("LogoutButton", () => {
+ test("renders Sign Out button", () => {
+ render();
+ expect(
+ screen.getByRole("button", { name: /sign out/i }),
+ ).toBeInTheDocument();
+ });
+
+ test("button is inside a form", () => {
+ render();
+ const button = screen.getByRole("button", { name: /sign out/i });
+ expect(button.closest("form")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/back-button.test.tsx b/apps/web/test/back-button.test.tsx
new file mode 100644
index 00000000..743666b0
--- /dev/null
+++ b/apps/web/test/back-button.test.tsx
@@ -0,0 +1,23 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import BackButton from "~/app/_components/back-button";
+
+const mockBack = vi.fn();
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ back: mockBack }),
+}));
+
+describe("BackButton", () => {
+ test("renders Go Back button", () => {
+ render();
+ expect(
+ screen.getByRole("button", { name: /go back/i }),
+ ).toBeInTheDocument();
+ });
+
+ test("calls router.back on click", () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /go back/i }));
+ expect(mockBack).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/apps/web/test/bar-graph.test.tsx b/apps/web/test/bar-graph.test.tsx
new file mode 100644
index 00000000..ca3b6575
--- /dev/null
+++ b/apps/web/test/bar-graph.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import BarGraph from "~/app/_components/reviews/bar-graph";
+
+describe("BarGraph", () => {
+ test("renders title", () => {
+ render();
+ expect(screen.getByText("Culture")).toBeInTheDocument();
+ });
+
+ test("renders value with precision 2", () => {
+ render();
+ expect(screen.getByText("4.2")).toBeInTheDocument();
+ });
+
+ test("renders industry average when provided", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Industry average: 3.5")).toBeInTheDocument();
+ });
+
+ test("does not render industry average when not provided", () => {
+ render();
+ expect(screen.queryByText(/Industry average/)).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/body-logo.test.tsx b/apps/web/test/body-logo.test.tsx
new file mode 100644
index 00000000..04a4c7c5
--- /dev/null
+++ b/apps/web/test/body-logo.test.tsx
@@ -0,0 +1,44 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import BodyLogo from "~/app/_components/body-logo";
+
+vi.mock("next/image", () => ({
+ default: ({
+ src,
+ alt,
+ width,
+ height,
+ }: {
+ src: string;
+ alt: string;
+ width?: number;
+ height?: number;
+ }) => (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ ),
+}));
+
+describe("BodyLogo", () => {
+ test("renders logo with default dimensions when no width", () => {
+ render();
+ const img = screen.getByTestId("body-logo-img");
+ expect(img).toHaveAttribute("src", "/svg/bodyLogo.svg");
+ expect(img).toHaveAttribute("alt", "Logo Picture");
+ expect(img).toHaveAttribute("width", "80");
+ expect(img).toHaveAttribute("height", "70");
+ });
+
+ test("renders logo with custom width", () => {
+ render();
+ const img = screen.getByTestId("body-logo-img");
+ expect(img).toHaveAttribute("width", "200");
+ expect(Number(img.getAttribute("height"))).toBeCloseTo(200 / 1.17);
+ });
+});
diff --git a/apps/web/test/combo-box.test.tsx b/apps/web/test/combo-box.test.tsx
new file mode 100644
index 00000000..970fab96
--- /dev/null
+++ b/apps/web/test/combo-box.test.tsx
@@ -0,0 +1,80 @@
+import { render, screen } from "@testing-library/react";
+import { fireEvent } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import ComboBox from "~/app/_components/combo-box";
+
+const defaultProps = {
+ defaultLabel: "Select option",
+ searchPlaceholder: "Search...",
+ searchEmpty: "No results.",
+ valuesAndLabels: [
+ { value: "a", label: "Option A" },
+ { value: "b", label: "Option B" },
+ { value: "c", label: "Option C" },
+ ],
+ currLabel: "",
+ onSelect: vi.fn(),
+};
+
+describe("ComboBox", () => {
+ test("renders trigger with default label when currLabel is empty", () => {
+ render();
+ expect(screen.getByText("Select option")).toBeInTheDocument();
+ });
+
+ test("renders trigger with currLabel when set", () => {
+ render();
+ expect(screen.getByText("Option B")).toBeInTheDocument();
+ });
+
+ test("opens popover and shows options when trigger clicked", () => {
+ render();
+ fireEvent.click(screen.getByRole("combobox"));
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
+ expect(screen.getByText("Option A")).toBeInTheDocument();
+ expect(screen.getByText("Option B")).toBeInTheDocument();
+ expect(screen.getByText("Option C")).toBeInTheDocument();
+ });
+
+ test("calls onSelect when option clicked", () => {
+ const onSelect = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("combobox"));
+ fireEvent.click(screen.getByText("Option B"));
+ expect(onSelect).toHaveBeenCalledWith("Option B");
+ });
+
+ test("shows search empty message when no match", () => {
+ render();
+ fireEvent.click(screen.getByRole("combobox"));
+ const input = screen.getByPlaceholderText("Search...");
+ fireEvent.change(input, { target: { value: "zzz" } });
+ expect(screen.getByText("No results.")).toBeInTheDocument();
+ });
+
+ test("renders Clear selection button when onClear provided", () => {
+ const onClear = vi.fn();
+ render(
+ ,
+ );
+ expect(screen.getByLabelText("Clear selection")).toBeInTheDocument();
+ });
+
+ test("calls onClear when clear button clicked", () => {
+ const onClear = vi.fn();
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByLabelText("Clear selection"));
+ expect(onClear).toHaveBeenCalled();
+ });
+
+ test("calls onChange when typing in search", () => {
+ const onChange = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("combobox"));
+ const input = screen.getByPlaceholderText("Search...");
+ fireEvent.change(input, { target: { value: "test" } });
+ expect(onChange).toHaveBeenCalledWith("test");
+ });
+});
diff --git a/apps/web/test/companies/all-company-roles-loading.test.tsx b/apps/web/test/companies/all-company-roles-loading.test.tsx
new file mode 100644
index 00000000..aaeedacf
--- /dev/null
+++ b/apps/web/test/companies/all-company-roles-loading.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import RenderAllRoles from "~/app/_components/companies/all-company-roles";
+
+vi.mock("next/navigation", () => ({
+ useRouter: vi.fn(() => ({ push: vi.fn() })),
+}));
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ role: {
+ getByCompany: {
+ useQuery: () => ({
+ isPending: true,
+ isSuccess: false,
+ data: undefined,
+ }),
+ },
+ },
+ },
+}));
+
+vi.mock("~/app/_components/loading-results", () => ({
+ default: () => Loading...
,
+}));
+
+vi.mock("~/app/_components/reviews/role-card-preview", () => ({
+ RoleCardPreview: () => Role
,
+}));
+
+vi.mock("~/app/_components/reviews/new-role-card", () => ({
+ default: () => New Role
,
+}));
+
+describe("RenderAllRoles loading state", () => {
+ test("renders LoadingResults when query is pending", () => {
+ render();
+ expect(screen.getByTestId("loading-results")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/all-company-roles.test.tsx b/apps/web/test/companies/all-company-roles.test.tsx
new file mode 100644
index 00000000..95e77a63
--- /dev/null
+++ b/apps/web/test/companies/all-company-roles.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import RenderAllRoles from "~/app/_components/companies/all-company-roles";
+
+vi.mock("next/navigation", () => ({
+ useRouter: vi.fn(() => ({ push: vi.fn() })),
+}));
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ role: {
+ getByCompany: {
+ useQuery: () => ({
+ isPending: false,
+ isSuccess: true,
+ data: [{ id: "role-1", title: "Engineer", companyName: "Acme" }],
+ }),
+ },
+ },
+ },
+}));
+
+vi.mock("~/app/_components/loading-results", () => ({
+ default: () => Loading...
,
+}));
+
+vi.mock("~/app/_components/reviews/role-card-preview", () => ({
+ RoleCardPreview: () => Role
,
+}));
+
+vi.mock("~/app/_components/reviews/new-role-card", () => ({
+ default: () => New Role
,
+}));
+
+describe("RenderAllRoles", () => {
+ test("renders Roles at company name with count", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/Roles at Acme Corp \(1\)/)).toBeInTheDocument();
+ });
+
+ test("renders NewRoleCard when company is provided", () => {
+ render();
+ expect(screen.getByTestId("new-role-card")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/company-about.test.tsx b/apps/web/test/companies/company-about.test.tsx
new file mode 100644
index 00000000..724e4bf1
--- /dev/null
+++ b/apps/web/test/companies/company-about.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import { CompanyAbout } from "~/app/_components/companies/company-about";
+
+describe("CompanyAbout", () => {
+ test("renders About heading with company name", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("About Acme Corp")).toBeInTheDocument();
+ expect(screen.getByText("We build things.")).toBeInTheDocument();
+ });
+
+ test("handles undefined companyObj", () => {
+ render();
+ expect(screen.getByText(/About/)).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/company-card-preview.test.tsx b/apps/web/test/companies/company-card-preview.test.tsx
new file mode 100644
index 00000000..a2f48b7a
--- /dev/null
+++ b/apps/web/test/companies/company-card-preview.test.tsx
@@ -0,0 +1,65 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import { CompanyCardPreview } from "~/app/_components/companies/company-card-preview";
+
+vi.mock("next/image", () => ({
+ default: ({ alt }: { alt: string }) => (
+ // eslint-disable-next-line @next/next/no-img-element -- test mock
+
+ ),
+}));
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ companyToLocation: {
+ getLocationsByCompanyId: {
+ useQuery: () => ({ data: [] }),
+ },
+ },
+ company: {
+ getAverageById: {
+ useQuery: () => ({ data: { averageOverallRating: 4.2 } }),
+ },
+ },
+ review: {
+ getByCompany: {
+ useQuery: () => ({ data: [{ id: "1" }] }),
+ },
+ },
+ },
+}));
+
+vi.mock("@cooper/ui/logo", () => ({
+ default: () => Logo
,
+}));
+
+vi.mock("~/app/_components/shared/favorite-button", () => ({
+ FavoriteButton: () => ,
+}));
+
+describe("CompanyCardPreview", () => {
+ const companyObj = {
+ id: "c1",
+ name: "Test Company",
+ } as never;
+
+ test("renders company name", () => {
+ render();
+ expect(screen.getByText("Test Company")).toBeInTheDocument();
+ });
+
+ test("renders average rating when present", () => {
+ render();
+ expect(screen.getByText("4.2")).toBeInTheDocument();
+ });
+
+ test("renders review count", () => {
+ render();
+ expect(screen.getByText(/1 review/)).toBeInTheDocument();
+ });
+
+ test("renders Location not specified when no locations", () => {
+ render();
+ expect(screen.getByText("Location not specified")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/company-info.test.tsx b/apps/web/test/companies/company-info.test.tsx
new file mode 100644
index 00000000..031faa2f
--- /dev/null
+++ b/apps/web/test/companies/company-info.test.tsx
@@ -0,0 +1,69 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import CompanyInfo from "~/app/_components/companies/company-info";
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ company: {
+ getById: {
+ useQuery: () => ({
+ isSuccess: true,
+ data: {
+ id: "c1",
+ name: "Acme Corp",
+ },
+ }),
+ },
+ },
+ review: {
+ getByCompany: {
+ useQuery: () => ({ data: [] }),
+ },
+ },
+ useQueries: () => [],
+ },
+}));
+
+vi.mock("@cooper/ui/logo", () => ({
+ default: () => Logo
,
+}));
+
+vi.mock("~/app/_components/shared/favorite-button", () => ({
+ FavoriteButton: () => ,
+}));
+
+vi.mock("~/app/_components/companies/all-company-roles", () => ({
+ default: () => All Roles
,
+}));
+
+vi.mock("~/app/_components/companies/company-about", () => ({
+ CompanyAbout: () => About
,
+}));
+
+vi.mock("~/app/_components/companies/company-reviews", () => ({
+ CompanyReview: () => Reviews
,
+}));
+
+vi.mock("~/app/_components/no-results", () => ({
+ default: () => No results
,
+}));
+
+describe("CompanyInfo", () => {
+ const companyObj = { id: "c1", name: "Acme Corp" } as never;
+
+ test("renders company name when query succeeds", () => {
+ render();
+ expect(screen.getByText("Acme Corp")).toBeInTheDocument();
+ });
+
+ test("renders CompanyAbout and CompanyReview", () => {
+ render();
+ expect(screen.getByTestId("company-about")).toBeInTheDocument();
+ expect(screen.getByTestId("company-review")).toBeInTheDocument();
+ });
+
+ test("renders RenderAllRoles", () => {
+ render();
+ expect(screen.getByTestId("all-roles")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/company-popup.test.tsx b/apps/web/test/companies/company-popup.test.tsx
new file mode 100644
index 00000000..75ec66e5
--- /dev/null
+++ b/apps/web/test/companies/company-popup.test.tsx
@@ -0,0 +1,105 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { fireEvent } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import { CompanyPopup } from "~/app/_components/companies/company-popup";
+
+vi.mock("node_modules/@cooper/ui/src/logo", () => ({
+ default: ({ company }: { company: { name: string } }) => (
+ {company.name}
+ ),
+}));
+
+vi.mock("~/app/_components/shared/favorite-button", () => ({
+ FavoriteButton: () => ,
+}));
+
+vi.mock("~/app/_components/companies/all-company-roles", () => ({
+ default: () => Roles
,
+}));
+
+vi.mock("~/app/_components/companies/company-about", () => ({
+ CompanyAbout: () => About
,
+}));
+
+vi.mock("~/app/_components/companies/company-reviews", () => ({
+ CompanyReview: () => Reviews
,
+}));
+
+const mockCompany = {
+ id: "company-1",
+ name: "Test Company",
+ slug: "test-company",
+} as never;
+
+describe("CompanyPopup", () => {
+ test("renders trigger when provided", () => {
+ render(
+ Open company}
+ company={mockCompany}
+ />,
+ );
+ expect(screen.getByText("Open company")).toBeInTheDocument();
+ });
+
+ test("opens dialog and shows company name when trigger clicked", () => {
+ render(
+ View company}
+ company={mockCompany}
+ />,
+ );
+ fireEvent.click(screen.getByText("View company"));
+ expect(
+ screen.getByRole("heading", { name: "Test Company" }),
+ ).toBeInTheDocument();
+ });
+
+ test("shows single location when locations has one item", () => {
+ render(
+ Open}
+ company={mockCompany}
+ locations={["Boston, MA"]}
+ />,
+ );
+ fireEvent.click(screen.getByText("Open"));
+ expect(screen.getByText("Boston, MA")).toBeInTheDocument();
+ });
+
+ test("shows location count when locations has multiple items", () => {
+ render(
+ Open}
+ company={mockCompany}
+ locations={["Boston, MA", "New York, NY", "Chicago, IL"]}
+ />,
+ );
+ fireEvent.click(screen.getByText("Open"));
+ expect(screen.getByText(/Boston, MA \+2 others/)).toBeInTheDocument();
+ });
+
+ test("shows CompanyAbout and CompanyReview in dialog", () => {
+ render(
+ Open}
+ company={mockCompany}
+ />,
+ );
+ fireEvent.click(screen.getByText("Open"));
+ expect(screen.getByTestId("company-about")).toBeInTheDocument();
+ expect(screen.getByTestId("company-reviews")).toBeInTheDocument();
+ });
+
+ test("shows FavoriteButton in dialog", () => {
+ render(
+ Open}
+ company={mockCompany}
+ />,
+ );
+ fireEvent.click(screen.getByText("Open"));
+ expect(screen.getByTestId("favorite-button")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/company-reviews.test.tsx b/apps/web/test/companies/company-reviews.test.tsx
new file mode 100644
index 00000000..9d77f628
--- /dev/null
+++ b/apps/web/test/companies/company-reviews.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import { CompanyReview } from "~/app/_components/companies/company-reviews";
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ review: {
+ getByCompany: {
+ useQuery: () => ({
+ data: [
+ {
+ id: "r1",
+ overallRating: 4,
+ workEnvironment: "REMOTE",
+ hourlyPay: "25",
+ },
+ ],
+ }),
+ },
+ list: { useQuery: () => ({ data: [{ overallRating: 4 }] }) },
+ },
+ company: {
+ getAverageById: {
+ useQuery: () => ({ data: { averageOverallRating: 4.2 } }),
+ },
+ },
+ },
+}));
+
+vi.mock("~/app/_components/shared/star-graph", () => ({
+ default: () => StarGraph
,
+}));
+
+vi.mock("~/app/_components/companies/company-statistics", () => ({
+ default: () => Statistics
,
+}));
+
+describe("CompanyReview", () => {
+ test("renders StarGraph", () => {
+ render();
+ expect(screen.getByTestId("star-graph")).toBeInTheDocument();
+ });
+
+ test("renders CompanyStatistics when reviews exist", () => {
+ render();
+ expect(screen.getByTestId("company-statistics")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/companies/company-statistics.test.tsx b/apps/web/test/companies/company-statistics.test.tsx
new file mode 100644
index 00000000..d1211005
--- /dev/null
+++ b/apps/web/test/companies/company-statistics.test.tsx
@@ -0,0 +1,60 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import CompanyStatistics from "~/app/_components/companies/company-statistics";
+
+describe("CompanyStatistics", () => {
+ const workModels = [
+ { name: "Remote", percentage: 50, count: 5 },
+ { name: "Hybrid", percentage: 30, count: 3 },
+ { name: "In-person", percentage: 20, count: 2 },
+ ];
+ const payStats = [
+ { pay: "25", percentage: 40, count: 4 },
+ { pay: "30", percentage: 60, count: 6 },
+ ];
+ const payRange = [
+ { label: "Low", min: 0, max: 20 },
+ { label: "Mid", min: 21, max: 35 },
+ { label: "High", min: 36, max: 50 },
+ ];
+
+ test("renders Job type section with Co-op", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Job type")).toBeInTheDocument();
+ expect(screen.getByText("Co-op")).toBeInTheDocument();
+ });
+
+ test("renders Work model section with model names", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Work model")).toBeInTheDocument();
+ expect(screen.getByText("Remote")).toBeInTheDocument();
+ expect(screen.getByText("Hybrid")).toBeInTheDocument();
+ expect(screen.getByText("In-person")).toBeInTheDocument();
+ });
+
+ test("renders Pay section", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Pay")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/compare/compare-context.test.tsx b/apps/web/test/compare/compare-context.test.tsx
new file mode 100644
index 00000000..28d063c1
--- /dev/null
+++ b/apps/web/test/compare/compare-context.test.tsx
@@ -0,0 +1,148 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import {
+ CompareProvider,
+ useCompare,
+} from "~/app/_components/compare/compare-context";
+
+function TestConsumer() {
+ const compare = useCompare();
+ return (
+
+ {String(compare.isCompareMode)}
+ {compare.comparedRoleIds.join(",")}
+
+
+
+
+
+
+
+ );
+}
+
+const mockGetItem = vi.fn((): string | null => null);
+const mockSetItem = vi.fn();
+const mockRemoveItem = vi.fn();
+
+const mockLocalStorage = {
+ getItem: mockGetItem,
+ setItem: mockSetItem,
+ removeItem: mockRemoveItem,
+};
+
+describe("CompareContext", () => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- vitest beforeEach callback with mocks
+ beforeEach(() => {
+ if (typeof window !== "undefined") {
+ Object.defineProperty(window, "localStorage", {
+ value: mockLocalStorage,
+ writable: true,
+ });
+ }
+ });
+
+ test("useCompare throws when outside provider", () => {
+ expect(() => render()).toThrow(
+ "useCompare must be used within CompareProvider",
+ );
+ });
+
+ test("provider exposes initial state", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByTestId("mode")).toHaveTextContent("false");
+ expect(screen.getByTestId("ids")).toHaveTextContent("");
+ });
+
+ test("enterCompareMode sets isCompareMode true", () => {
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByTestId("enter"));
+ expect(screen.getByTestId("mode")).toHaveTextContent("true");
+ });
+
+ test("addRoleId adds id to comparedRoleIds", () => {
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByTestId("enter"));
+ fireEvent.click(screen.getByTestId("add"));
+ expect(screen.getByTestId("ids")).toHaveTextContent("role-1");
+ });
+
+ test("removeRoleId removes id", () => {
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByTestId("enter"));
+ fireEvent.click(screen.getByTestId("add"));
+ fireEvent.click(screen.getByTestId("remove"));
+ expect(screen.getByTestId("ids")).toHaveTextContent("");
+ });
+
+ test("exitCompareMode sets isCompareMode false", () => {
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByTestId("enter"));
+ fireEvent.click(screen.getByTestId("exit"));
+ expect(screen.getByTestId("mode")).toHaveTextContent("false");
+ });
+
+ test("clear resets comparedRoleIds", () => {
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByTestId("enter"));
+ fireEvent.click(screen.getByTestId("add"));
+ fireEvent.click(screen.getByTestId("clear"));
+ expect(screen.getByTestId("ids")).toHaveTextContent("");
+ });
+});
diff --git a/apps/web/test/compare/compare-ui.test.tsx b/apps/web/test/compare/compare-ui.test.tsx
new file mode 100644
index 00000000..b79535a0
--- /dev/null
+++ b/apps/web/test/compare/compare-ui.test.tsx
@@ -0,0 +1,110 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import {
+ CompareControls,
+ CompareColumns,
+} from "~/app/_components/compare/compare-ui";
+
+const mockCompare = {
+ isCompareMode: false,
+ comparedRoleIds: [] as string[],
+ reservedSlots: 0,
+ maxColumns: 3,
+ enterCompareMode: vi.fn(),
+ exitCompareMode: vi.fn(),
+ addRoleId: vi.fn(),
+ removeRoleId: vi.fn(),
+ addSlot: vi.fn(),
+ clear: vi.fn(),
+};
+
+vi.mock("~/app/_components/compare/compare-context", () => ({
+ useCompare: () => mockCompare,
+}));
+
+vi.mock("next/image", () => ({
+ default: ({ alt }: { alt: string }) => (
+ // eslint-disable-next-line @next/next/no-img-element -- test mock
+
+ ),
+}));
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ role: {
+ getManyByIds: {
+ useQuery: () => ({ data: [], isLoading: false }),
+ },
+ },
+ },
+}));
+
+vi.mock("~/app/_components/reviews/role-info", () => ({
+ RoleInfo: () => RoleInfo
,
+}));
+
+describe("CompareControls", () => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- vitest beforeEach callback with mocks
+ beforeEach(() => {
+ mockCompare.isCompareMode = false;
+ mockCompare.comparedRoleIds = [];
+ mockCompare.reservedSlots = 0;
+ mockCompare.enterCompareMode.mockClear();
+ mockCompare.exitCompareMode.mockClear();
+ mockCompare.addSlot.mockClear();
+ });
+
+ test("returns null when anchorRoleId is not provided", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("returns null when inTopBar and not in compare mode", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("renders COMPARE WITH button when not in compare mode", () => {
+ render();
+ expect(
+ screen.getByRole("button", { name: /compare with/i }),
+ ).toBeInTheDocument();
+ });
+
+ test("clicking COMPARE WITH calls enterCompareMode", () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /compare with/i }));
+ expect(mockCompare.enterCompareMode).toHaveBeenCalledTimes(1);
+ });
+
+ test("when in compare mode renders ADD COMPARISON and EXIT", () => {
+ mockCompare.isCompareMode = true;
+ render();
+ expect(
+ screen.getByRole("button", { name: /add comparison/i }),
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /exit/i })).toBeInTheDocument();
+ });
+
+ test("clicking EXIT calls exitCompareMode", () => {
+ mockCompare.isCompareMode = true;
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /exit/i }));
+ expect(mockCompare.exitCompareMode).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe("CompareColumns", () => {
+ const anchorRole = {
+ id: "anchor-1",
+ title: "Engineer",
+ companyName: "Acme",
+ } as never;
+
+ test("renders anchor role column", () => {
+ render();
+ expect(screen.getByTestId("role-info")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/cooper-logo.test.tsx b/apps/web/test/cooper-logo.test.tsx
new file mode 100644
index 00000000..1dbfd7ac
--- /dev/null
+++ b/apps/web/test/cooper-logo.test.tsx
@@ -0,0 +1,44 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import CooperLogo from "~/app/_components/cooper-logo";
+
+vi.mock("next/image", () => ({
+ default: ({
+ src,
+ alt,
+ width,
+ height,
+ }: {
+ src: string;
+ alt: string;
+ width?: number;
+ height?: number;
+ }) => (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ ),
+}));
+
+describe("CooperLogo", () => {
+ test("renders logo with default dimensions when no width", () => {
+ render();
+ const img = screen.getByTestId("cooper-logo-img");
+ expect(img).toHaveAttribute("src", "/svg/logoOutline.svg");
+ expect(img).toHaveAttribute("alt", "Logo Picture");
+ expect(img).toHaveAttribute("width", "80");
+ expect(img).toHaveAttribute("height", "70");
+ });
+
+ test("renders logo with custom width", () => {
+ render();
+ const img = screen.getByTestId("cooper-logo-img");
+ expect(img).toHaveAttribute("width", "120");
+ expect(Number(img.getAttribute("height"))).toBeCloseTo(120 / 1.17);
+ });
+});
diff --git a/apps/web/test/dashboard-layout.test.tsx b/apps/web/test/dashboard-layout.test.tsx
new file mode 100644
index 00000000..735df603
--- /dev/null
+++ b/apps/web/test/dashboard-layout.test.tsx
@@ -0,0 +1,42 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import DashboardLayout from "~/app/(pages)/(dashboard)/layout";
+
+vi.mock("@cooper/ui", () => ({
+ cn: (...args: unknown[]) => args.filter(Boolean).join(" "),
+ CustomToaster: () => Toaster
,
+}));
+
+vi.mock("~/app/_components/header/header-layout", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+vi.mock("~/app/_components/onboarding/onboarding-wrapper", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+describe("DashboardLayout", () => {
+ test("renders children inside HeaderLayout and OnboardingWrapper", () => {
+ render(
+
+ Page content
+ ,
+ );
+ expect(screen.getByTestId("header-layout")).toBeInTheDocument();
+ expect(screen.getByTestId("onboarding-wrapper")).toBeInTheDocument();
+ expect(screen.getByTestId("page-child")).toHaveTextContent("Page content");
+ });
+
+ test("renders CustomToaster", () => {
+ render(
+
+ Child
+ ,
+ );
+ expect(screen.getByTestId("custom-toaster")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/filters/dropdown-filters-bar.test.tsx b/apps/web/test/filters/dropdown-filters-bar.test.tsx
new file mode 100644
index 00000000..12ce85cc
--- /dev/null
+++ b/apps/web/test/filters/dropdown-filters-bar.test.tsx
@@ -0,0 +1,86 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+import DropdownFiltersBar from "~/app/_components/filters/dropdown-filters-bar";
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ location: {
+ getByPrefix: {
+ useQuery: () => ({ data: [], isLoading: false }),
+ },
+ getByPopularity: {
+ useQuery: () => ({ data: [], isLoading: false }),
+ },
+ },
+ useQueries: () => [],
+ },
+}));
+
+vi.mock("~/app/_components/onboarding/constants", () => ({
+ industryOptions: {
+ TECHNOLOGY: { value: "TECHNOLOGY", label: "Technology" },
+ HEALTHCARE: { value: "HEALTHCARE", label: "Healthcare" },
+ },
+ jobTypeOptions: [
+ { value: "CO-OP", label: "Co-op" },
+ { value: "INTERNSHIP", label: "Internship" },
+ ],
+}));
+
+describe("DropdownFiltersBar", () => {
+ const onFilterChange = vi.fn();
+
+ beforeEach(() => {
+ onFilterChange.mockClear();
+ });
+
+ test("renders Industry, Location, Job type, Hourly pay, Overall rating filters", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Industry")).toBeInTheDocument();
+ expect(screen.getByText("Location")).toBeInTheDocument();
+ expect(screen.getByText("Job type")).toBeInTheDocument();
+ expect(screen.getByText("Overall rating")).toBeInTheDocument();
+ });
+
+ test("calls onFilterChange when filter changes", () => {
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Job type"));
+ const coop = screen.queryByText("Co-op");
+ if (coop) {
+ fireEvent.click(coop);
+ expect(onFilterChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ jobTypes: expect.any(Array) as unknown as string[],
+ }),
+ );
+ }
+ });
+});
diff --git a/apps/web/test/form-section.test.tsx b/apps/web/test/form-section.test.tsx
new file mode 100644
index 00000000..f78e41c2
--- /dev/null
+++ b/apps/web/test/form-section.test.tsx
@@ -0,0 +1,24 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import { FormSection } from "~/app/_components/form/form-section";
+
+describe("FormSection", () => {
+ test("renders children", () => {
+ render(
+
+ Section content
+ ,
+ );
+ expect(screen.getByText("Section content")).toBeInTheDocument();
+ });
+
+ test("applies layout classes", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+ const section = container.firstChild as HTMLElement;
+ expect(section).toHaveClass("flex", "flex-col", "rounded-lg", "w-full");
+ });
+});
diff --git a/apps/web/test/form-sections/basic-info-section.test.tsx b/apps/web/test/form-sections/basic-info-section.test.tsx
new file mode 100644
index 00000000..1f2b9681
--- /dev/null
+++ b/apps/web/test/form-sections/basic-info-section.test.tsx
@@ -0,0 +1,147 @@
+import React from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import { BasicInfoSection } from "~/app/_components/form/sections/basic-info-section";
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ location: {
+ getByPrefix: { useQuery: () => ({ data: [] }) },
+ getById: { useQuery: () => ({ data: undefined }) },
+ getByPopularity: {
+ useQuery: () => ({ data: [], isLoading: false }),
+ },
+ },
+ },
+}));
+
+vi.mock(
+ "~/app/_components/reviews/new-review/existing-company-content",
+ () => ({
+ default: () => Company
,
+ }),
+);
+
+vi.mock("~/app/_components/location", () => ({
+ default: () => Location
,
+}));
+
+function Wrapper({
+ children,
+ defaultValues = {},
+}: {
+ children: React.ReactNode;
+ defaultValues?: Record;
+}) {
+ const form = useForm({
+ defaultValues: {
+ jobType: "",
+ workTerm: "",
+ workYear: undefined,
+ locationId: "",
+ industry: "",
+ companyName: "",
+ roleName: "",
+ ...defaultValues,
+ },
+ });
+ return {children};
+}
+
+describe("BasicInfoSection", () => {
+ test("renders ExistingCompanyContent", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByTestId("existing-company-content")).toBeInTheDocument();
+ });
+
+ test("renders Job type label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Job type/)).toBeInTheDocument();
+ });
+
+ test("renders Co-op/internship term label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Co-op\/internship term/)).toBeInTheDocument();
+ });
+
+ test("renders Year of co-op/internship term label", () => {
+ render(
+
+
+ ,
+ );
+ expect(
+ screen.getByText(/Year of co-op\/internship term/),
+ ).toBeInTheDocument();
+ });
+
+ test("renders Industry label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Industry/)).toBeInTheDocument();
+ });
+
+ test("renders Location label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getAllByText(/Location/).length).toBeGreaterThanOrEqual(1);
+ });
+
+ test("renders LocationBox", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByTestId("location-box")).toBeInTheDocument();
+ });
+
+ test("renders with existing jobType value", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Job type/)).toBeInTheDocument();
+ });
+
+ test("renders with existing workTerm and workYear values", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Co-op\/internship term/)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Year of co-op\/internship term/),
+ ).toBeInTheDocument();
+ });
+
+ test("renders with existing industry value", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Industry/)).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/form-sections/company-details-section.test.tsx b/apps/web/test/form-sections/company-details-section.test.tsx
new file mode 100644
index 00000000..e069ccea
--- /dev/null
+++ b/apps/web/test/form-sections/company-details-section.test.tsx
@@ -0,0 +1,125 @@
+import React from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { render, screen } from "@testing-library/react";
+import { fireEvent } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import { CompanyDetailsSection } from "~/app/_components/form/sections/company-details-section";
+
+function Wrapper({
+ children,
+ defaultValues = {},
+}: {
+ children: React.ReactNode;
+ defaultValues?: Record;
+}) {
+ const form = useForm({
+ defaultValues: {
+ workEnvironment: "",
+ drugTest: undefined,
+ cultureRating: 0,
+ supervisorRating: 0,
+ benefits: undefined,
+ ...defaultValues,
+ },
+ });
+ return {children};
+}
+
+describe("CompanyDetailsSection", () => {
+ test("renders Work model label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Work model/)).toBeInTheDocument();
+ });
+
+ test("renders work environment options", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Work model/)).toBeInTheDocument();
+ expect(screen.getByText("In person")).toBeInTheDocument();
+ expect(screen.getByText("Hybrid")).toBeInTheDocument();
+ expect(screen.getByText("Remote")).toBeInTheDocument();
+ });
+
+ test("renders Drug Test label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Drug Test/)).toBeInTheDocument();
+ });
+
+ test("renders Drug Test Yes and No options", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText("Yes")).toBeInTheDocument();
+ expect(screen.getByText("No")).toBeInTheDocument();
+ });
+
+ test("renders Company Culture label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Company Culture/)).toBeInTheDocument();
+ });
+
+ test("renders Supervisor rating label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Supervisor rating/)).toBeInTheDocument();
+ });
+
+ test("renders Benefits label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getAllByText(/Benefits/).length).toBeGreaterThanOrEqual(1);
+ });
+
+ test("renders with existing workEnvironment value", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Work model/)).toBeInTheDocument();
+ });
+
+ test("renders with existing cultureRating and supervisorRating values", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Company Culture/)).toBeInTheDocument();
+ expect(screen.getByText(/Supervisor rating/)).toBeInTheDocument();
+ });
+
+ test("can select drug test Yes", () => {
+ render(
+
+
+ ,
+ );
+ const yesLabel = screen.getByText("Yes");
+ fireEvent.click(yesLabel);
+ expect(yesLabel).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/form-sections/interview-section.test.tsx b/apps/web/test/form-sections/interview-section.test.tsx
new file mode 100644
index 00000000..9ce96801
--- /dev/null
+++ b/apps/web/test/form-sections/interview-section.test.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import { InterviewSection } from "~/app/_components/form/sections/interview-section";
+
+function Wrapper({
+ children,
+ defaultValues = {},
+}: {
+ children: React.ReactNode;
+ defaultValues?: Record;
+}) {
+ const form = useForm({
+ defaultValues: {
+ interviewDifficulty: 0,
+ ...defaultValues,
+ },
+ });
+ return {children};
+}
+
+describe("InterviewSection", () => {
+ test("renders Interview difficulty label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Interview difficulty/)).toBeInTheDocument();
+ });
+
+ test("renders difficulty select with options", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
+ expect(screen.getByText("Select")).toBeInTheDocument();
+ });
+
+ test("renders with existing interviewDifficulty value", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Interview difficulty/)).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/form-sections/pay-section.test.tsx b/apps/web/test/form-sections/pay-section.test.tsx
new file mode 100644
index 00000000..1677e80b
--- /dev/null
+++ b/apps/web/test/form-sections/pay-section.test.tsx
@@ -0,0 +1,125 @@
+import React from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { render, screen } from "@testing-library/react";
+import { fireEvent } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import { PaySection } from "~/app/_components/form/sections/pay-section";
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ location: {
+ getById: { useQuery: () => ({ data: undefined }) },
+ },
+ },
+}));
+
+function Wrapper({
+ children,
+ defaultValues = {},
+}: {
+ children: React.ReactNode;
+ defaultValues?: Record;
+}) {
+ const form = useForm({
+ defaultValues: {
+ hourlyPay: "",
+ locationId: "",
+ overtimeNormal: undefined,
+ pto: undefined,
+ ...defaultValues,
+ },
+ });
+ return {children};
+}
+
+describe("PaySection", () => {
+ test("renders Hourly pay label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Hourly pay/)).toBeInTheDocument();
+ });
+
+ test("renders hourly pay input placeholder", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByPlaceholderText("$")).toBeInTheDocument();
+ });
+
+ test("renders Unpaid position checkbox label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Unpaid position/)).toBeInTheDocument();
+ });
+
+ test("renders Worked overtime label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Worked overtime/)).toBeInTheDocument();
+ });
+
+ test("renders Worked overtime Yes and No options", () => {
+ render(
+
+
+ ,
+ );
+ const yesLabels = screen.getAllByText("Yes");
+ const noLabels = screen.getAllByText("No");
+ expect(yesLabels.length).toBeGreaterThanOrEqual(1);
+ expect(noLabels.length).toBeGreaterThanOrEqual(1);
+ });
+
+ test("renders Received PTO label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Received PTO/)).toBeInTheDocument();
+ });
+
+ test("toggling Unpaid position disables hourly pay input", () => {
+ render(
+
+
+ ,
+ );
+ const unpaidLabel = screen.getByText(/Unpaid position/);
+ const input = screen.getByPlaceholderText("$");
+ expect(input).not.toBeDisabled();
+ fireEvent.click(unpaidLabel);
+ expect(input).toBeDisabled();
+ });
+
+ test("renders with existing hourlyPay value", () => {
+ render(
+
+
+ ,
+ );
+ const input = screen.getByPlaceholderText("$");
+ expect(input).toHaveValue("25");
+ });
+
+ test("renders with overtimeNormal and pto values", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Worked overtime/)).toBeInTheDocument();
+ expect(screen.getByText(/Received PTO/)).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/form-sections/review-section.test.tsx b/apps/web/test/form-sections/review-section.test.tsx
new file mode 100644
index 00000000..68f6f4ff
--- /dev/null
+++ b/apps/web/test/form-sections/review-section.test.tsx
@@ -0,0 +1,58 @@
+import { FormProvider, useForm } from "react-hook-form";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test } from "vitest";
+import { ReviewSection } from "~/app/_components/form/sections/review-section";
+
+function Wrapper({ children }: { children: React.ReactNode }) {
+ const form = useForm({
+ defaultValues: {
+ overallRating: 0,
+ textReview: "",
+ },
+ });
+ return {children};
+}
+
+describe("ReviewSection", () => {
+ test("renders Overall rating label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Overall rating/)).toBeInTheDocument();
+ });
+
+ test("renders Review text label", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/Review text/)).toBeInTheDocument();
+ });
+
+ test("renders review textarea placeholder", () => {
+ render(
+
+
+ ,
+ );
+ expect(
+ screen.getByPlaceholderText(/job duties not mentioned/),
+ ).toBeInTheDocument();
+ });
+
+ test("renders respectful hint text", () => {
+ render(
+
+
+ ,
+ );
+ expect(
+ screen.getByText(
+ /Please be respectful and do not mention any specific names/,
+ ),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/header/header-layout.test.tsx b/apps/web/test/header/header-layout.test.tsx
new file mode 100644
index 00000000..4c5b4f15
--- /dev/null
+++ b/apps/web/test/header/header-layout.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+
+vi.mock("@cooper/auth", () => ({
+ auth: vi.fn().mockResolvedValue(null),
+}));
+
+vi.mock("~/app/_components/header/header", () => ({
+ default: ({ auth }: { auth: React.ReactNode }) => (
+ {auth}
+ ),
+}));
+
+vi.mock("~/app/_components/auth/login-button", () => ({
+ default: () => ,
+}));
+
+vi.mock("~/app/_components/profile/profile-button", () => ({
+ default: () => ,
+}));
+
+describe("HeaderLayout", () => {
+ test("renders children and Header when session is null", async () => {
+ const HeaderLayout = (
+ await import("~/app/_components/header/header-layout")
+ ).default;
+ const element = await HeaderLayout({
+ children: Page content,
+ });
+ render(element);
+ expect(screen.getByTestId("header")).toBeInTheDocument();
+ expect(screen.getByTestId("child")).toHaveTextContent("Page content");
+ expect(screen.getByRole("button", { name: /login/i })).toBeInTheDocument();
+ });
+
+ test("renders article wrapper for children", async () => {
+ const HeaderLayout = (
+ await import("~/app/_components/header/header-layout")
+ ).default;
+ const element = await HeaderLayout({
+ children: Content,
+ });
+ const { container } = render(element);
+ const article = container.querySelector("article");
+ expect(article).toBeInTheDocument();
+ expect(screen.getByTestId("child")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/test/header/header.test.tsx b/apps/web/test/header/header.test.tsx
new file mode 100644
index 00000000..d90bd7eb
--- /dev/null
+++ b/apps/web/test/header/header.test.tsx
@@ -0,0 +1,89 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, test, vi } from "vitest";
+import Header from "~/app/_components/header/header";
+
+vi.mock("next/image", () => ({
+ default: ({ alt }: { alt: string }) => (
+ // eslint-disable-next-line @next/next/no-img-element -- test mock
+
+ ),
+}));
+
+vi.mock("next/link", () => ({
+ default: ({
+ children,
+ href,
+ onClick,
+ }: {
+ children: React.ReactNode;
+ href: string;
+ onClick?: (e: React.MouseEvent) => void;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("~/trpc/react", () => ({
+ api: {
+ auth: {
+ getSession: {
+ useQuery: () => ({ data: null, isLoading: false }),
+ },
+ },
+ },
+}));
+
+vi.mock("~/app/_components/cooper-logo", () => ({
+ default: () => Logo
,
+}));
+
+vi.mock("~/app/_components/header/mobile-header-button", () => ({
+ default: ({
+ label,
+ children,
+ href: _href,
+ }: {
+ href?: string;
+ label?: string;
+ children?: React.ReactNode;
+ }) => (
+
+ {label}
+ {children}
+
+ ),
+}));
+
+describe("Header", () => {
+ test("renders Cooper title", () => {
+ render(Auth} />);
+ expect(
+ screen.getByRole("heading", { name: /cooper/i }),
+ ).toBeInTheDocument();
+ });
+
+ test("renders Submit Feedback link", () => {
+ render(Auth} />);
+ expect(
+ screen.getByRole("link", { name: /submit feedback or bug reports/i }),
+ ).toBeInTheDocument();
+ });
+
+ test("opens mobile menu when burger is clicked", () => {
+ render(