Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -42,16 +44,20 @@
"@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",
"@types/react-scroll": "^1.8.10",
"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"
}
35 changes: 35 additions & 0 deletions apps/web/src/app/_components/auth/actions.test.ts
Original file line number Diff line number Diff line change
@@ -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: "/" });
});
});
});
129 changes: 129 additions & 0 deletions apps/web/src/utils/companyStatistics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe, expect, test } from "vitest";
import {
calculatePay,
calculatePayRange,
calculateWorkModels,
} from "./companyStatistics";
import { ReviewType } from "@cooper/db/schema";

Check warning on line 7 in apps/web/src/utils/companyStatistics.test.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

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");
});
});
});
20 changes: 20 additions & 0 deletions apps/web/src/utils/dateHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
61 changes: 61 additions & 0 deletions apps/web/src/utils/locationHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
33 changes: 33 additions & 0 deletions apps/web/src/utils/reviewCountByStars.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test } from "vitest";
import { calculateRatings } from "./reviewCountByStars";
import { ReviewType } from "@cooper/db/schema";

Check warning on line 3 in apps/web/src/utils/reviewCountByStars.test.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

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);
});
});
});
Loading