Files
prowler/ui/vitest.integration.setup.ts

112 lines
3.9 KiB
TypeScript

// Global stylesheet (Tailwind + design tokens) is imported by the Next.js
// layouts in the real app. Tests render the page in isolation, bypassing the
// layout, so without this import Tailwind classes resolve to nothing — the
// page collapses to unstyled HTML and stacked elements end up overlapping
// the graph nodes, blocking Playwright clicks. Pull the stylesheet directly
// so the test bundle gets the same CSS the production page receives.
import "@/styles/globals.css";
import { afterAll, afterEach, beforeAll, vi } from "vitest";
import { resetToasts } from "@/components/shadcn/toast/use-toast";
import { worker } from "./__tests__/msw/worker";
// Server Actions ("use server") are bundled by Vite as plain async functions
// — the directive is a Next.js compiler concept, not part of Vite. When the
// page invokes one, it runs in the browser and reaches `auth()` from
// next-auth, which calls `next/headers` (request-scoped AsyncLocalStorage
// only set up by Next's request handler) and throws "headers was called
// outside a request scope". That kills every action before it can hit
// MSW. Stub `auth.config` with a fake session so the action proceeds to
// `fetch()` and MSW takes over.
vi.mock("@/auth.config", () => ({
auth: vi.fn(() => Promise.resolve({ accessToken: "test-access-token" })),
signIn: vi.fn(),
signOut: vi.fn(),
handlers: {},
}));
// Server Actions call `revalidatePath`/`revalidateTag` from `next/cache` after
// mutations. Those read Next's static-generation store, which only exists
// inside a Next request scope — in the browser test they throw "Invariant:
// static generation store missing", aborting the action before the UI can
// react. Stub them as no-ops; the tests assert on UI state, not cache
// invalidation.
vi.mock("next/cache", () => ({
revalidatePath: vi.fn(),
revalidateTag: vi.fn(),
unstable_cache: <T>(fn: T) => fn,
}));
// Next.js's App Router context (`useRouter`, `useSearchParams`, `usePathname`)
// is not available in vitest browser — there's no Next runtime mounting the
// providers. We back the hooks with the real `window.location` so navigating
// via `history.replaceState` in tests is enough to drive the page.
vi.mock("next/navigation", () => {
const router = {
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
prefetch: vi.fn(() => Promise.resolve()),
};
return {
useSearchParams: () => new URLSearchParams(window.location.search),
useRouter: () => router,
usePathname: () => window.location.pathname,
useParams: () => ({}),
redirect: vi.fn(),
notFound: vi.fn(),
};
});
beforeAll(async () => {
await worker.start({
serviceWorker: { url: "/mockServiceWorker.js" },
onUnhandledRequest: "error",
});
});
afterEach(() => {
worker.resetHandlers();
// Module-level store: a toast raised here would re-render on the next test's
// mount and intercept clicks aimed at the page.
resetToasts();
});
afterAll(() => {
worker.stop();
});
// React Flow's pan/drag handlers dispatch pointer events that access
// `event.view.document` on the node. When user-event synthesises these
// events the `view` property can be null, producing harmless
// "Cannot read properties of null (reading 'document')" errors.
// Swallow only that specific unhandled error; everything else propagates.
const isReactFlowNullViewError = (reason: unknown): boolean => {
const message =
reason instanceof Error
? reason.message
: typeof reason === "string"
? reason
: "";
return message.includes(
"Cannot read properties of null (reading 'document')",
);
};
window.addEventListener("error", (event) => {
if (isReactFlowNullViewError(event.error)) {
event.preventDefault();
event.stopImmediatePropagation();
}
});
window.addEventListener("unhandledrejection", (event) => {
if (isReactFlowNullViewError(event.reason)) {
event.preventDefault();
}
});