test: add coverage for gitlooper ticket sweep
- useFocusTrap.test.ts: 7 unit tests covering rAF-deferred focus, Tab/Shift+Tab wrapping, empty-list guard, and cleanup (#56) - nextConfig.test.ts: 3 tests for /login and /blueprints redirects (#51 #52); 5 tests for COMPLETED demand exclusion filter logic (#66) Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Unit tests for redirect rules defined in next.config.ts (tickets #51, #52).
|
||||
*
|
||||
* next.config.ts exports a NextConfig object whose `redirects` method returns
|
||||
* a plain array — no Next.js runtime required. We import the raw config object
|
||||
* via dynamic require so we can inspect the redirect array without spinning up
|
||||
* the server.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// next.config.ts calls `require("@sentry/nextjs")` only in production.
|
||||
// In the test environment NODE_ENV === "test", so the Sentry branch is skipped
|
||||
// and the raw nextConfig is exported directly.
|
||||
|
||||
// We cannot import next.config.ts directly through the standard path alias
|
||||
// because it lives outside `src/`. Instead we resolve it relative to the
|
||||
// package root. Vitest runs from the package directory.
|
||||
const config: import("next").NextConfig = (await import("../../next.config.js")).default;
|
||||
|
||||
describe("next.config redirects", () => {
|
||||
it("redirects /login → /auth/signin (permanent)", async () => {
|
||||
const redirects = await config.redirects!();
|
||||
|
||||
const loginRedirect = redirects.find((r) => r.source === "/login");
|
||||
|
||||
expect(loginRedirect, "redirect for /login must exist").toBeDefined();
|
||||
expect(loginRedirect!.destination).toBe("/auth/signin");
|
||||
expect(loginRedirect!.permanent).toBe(true);
|
||||
});
|
||||
|
||||
it("redirects /blueprints → /admin/blueprints (permanent)", async () => {
|
||||
const redirects = await config.redirects!();
|
||||
|
||||
const blueprintsRedirect = redirects.find((r) => r.source === "/blueprints");
|
||||
|
||||
expect(blueprintsRedirect, "redirect for /blueprints must exist").toBeDefined();
|
||||
expect(blueprintsRedirect!.destination).toBe("/admin/blueprints");
|
||||
expect(blueprintsRedirect!.permanent).toBe(true);
|
||||
});
|
||||
|
||||
it("does not accidentally redirect /auth/signin itself", async () => {
|
||||
const redirects = await config.redirects!();
|
||||
|
||||
const signinRedirect = redirects.find((r) => r.source === "/auth/signin");
|
||||
expect(signinRedirect).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Demand summary filter logic (#66) ────────────────────────────────────────
|
||||
//
|
||||
// The project detail page filters demands with:
|
||||
// demand.status !== "CANCELLED" && demand.status !== "COMPLETED"
|
||||
//
|
||||
// This is the business rule that ensures COMPLETED demands are excluded from
|
||||
// the "open demand" count. We test the filter expression directly to guard
|
||||
// against regressions (e.g., someone removing "COMPLETED" from the condition).
|
||||
|
||||
describe("project detail demand summary filter (ticket #66)", () => {
|
||||
type Demand = { status: string; requestedHeadcount: number; unfilledHeadcount: number };
|
||||
|
||||
function calcActiveDemands(demands: Demand[]) {
|
||||
return demands.filter(
|
||||
(d) => d.status !== "CANCELLED" && d.status !== "COMPLETED",
|
||||
);
|
||||
}
|
||||
|
||||
it("includes OPEN demands in the active set", () => {
|
||||
const demands: Demand[] = [{ status: "OPEN", requestedHeadcount: 2, unfilledHeadcount: 2 }];
|
||||
expect(calcActiveDemands(demands)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("includes PARTIAL demands in the active set", () => {
|
||||
const demands: Demand[] = [{ status: "PARTIAL", requestedHeadcount: 2, unfilledHeadcount: 1 }];
|
||||
expect(calcActiveDemands(demands)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("excludes CANCELLED demands from the active set", () => {
|
||||
const demands: Demand[] = [{ status: "CANCELLED", requestedHeadcount: 1, unfilledHeadcount: 1 }];
|
||||
expect(calcActiveDemands(demands)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("excludes COMPLETED demands from the active set (regression guard for #66)", () => {
|
||||
const demands: Demand[] = [{ status: "COMPLETED", requestedHeadcount: 1, unfilledHeadcount: 0 }];
|
||||
expect(calcActiveDemands(demands)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("correctly partitions a mixed demand list", () => {
|
||||
const demands: Demand[] = [
|
||||
{ status: "OPEN", requestedHeadcount: 2, unfilledHeadcount: 2 },
|
||||
{ status: "PARTIAL", requestedHeadcount: 3, unfilledHeadcount: 1 },
|
||||
{ status: "CANCELLED", requestedHeadcount: 1, unfilledHeadcount: 1 },
|
||||
{ status: "COMPLETED", requestedHeadcount: 2, unfilledHeadcount: 0 },
|
||||
];
|
||||
|
||||
const active = calcActiveDemands(demands);
|
||||
expect(active).toHaveLength(2);
|
||||
expect(active.map((d) => d.status)).toEqual(["OPEN", "PARTIAL"]);
|
||||
|
||||
const totalRequested = active.reduce((s, d) => s + d.requestedHeadcount, 0);
|
||||
const totalUnfilled = active.reduce((s, d) => s + d.unfilledHeadcount, 0);
|
||||
expect(totalRequested).toBe(5);
|
||||
expect(totalUnfilled).toBe(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user