1
mirror of https://github.com/jakejarvis/domainstack.io.git synced 2025-12-02 19:33:48 -05:00

refactor: update test imports to use custom test-utils for consistent testing setup

This commit is contained in:
2025-11-30 15:07:31 -05:00
parent d187b29ba6
commit 67ce984d9b
23 changed files with 277 additions and 160 deletions

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { CreateIssueButton } from "@/components/create-issue-button";
import { render, screen } from "@/lib/test-utils";
describe("CreateIssueButton", () => {
it("renders with icon and label", () => {

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { CertificatesSection, equalHostname } from "./certificates-section";
vi.mock("@/components/domain/favicon", () => ({

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { DnsRecordList } from "@/components/domain/dns/dns-record-list";
import { render, screen } from "@/lib/test-utils";
vi.mock("@/components/domain/favicon", () => ({
Favicon: ({ domain }: { domain: string }) => <div>icon:{domain}</div>,

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { DnsSection } from "./dns-section";
vi.mock("@/components/domain/dns/dns-group", () => ({

View File

@@ -1,8 +1,8 @@
/* @vitest-environment jsdom */
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DomainSearch } from "@/components/domain/domain-search";
import { render, screen, waitFor } from "@/lib/test-utils";
const nav = vi.hoisted(() => ({
push: vi.fn(),

View File

@@ -1,10 +1,10 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createElement } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DomainSuggestionsClient } from "@/components/domain/domain-suggestions-client";
import { HomeSearchProvider } from "@/components/layout/home-search-context";
import { render, screen } from "@/lib/test-utils";
vi.mock("@/hooks/use-router", () => ({
useRouter: () => ({ push: vi.fn() }),

View File

@@ -1,8 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { createElement } from "react";
import type { Mock } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@/lib/test-utils";
import { Favicon } from "./favicon";
vi.mock("next/image", () => ({
@@ -27,64 +26,65 @@ vi.mock("next/image", () => ({
}),
}));
// Mock the tRPC client to return a mock queryOptions function
const mockQueryOptions = vi.fn();
vi.mock("@/lib/trpc/client", () => ({
useTRPC: () => ({
domain: {
getFavicon: {
queryOptions: (vars: unknown) => ({
queryKey: ["getFavicon", vars],
}),
queryOptions: mockQueryOptions,
},
},
}),
}));
vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
"@tanstack/react-query",
);
return {
...actual,
useQuery: vi.fn(),
};
});
const { useQuery } = await import("@tanstack/react-query");
describe("Favicon", () => {
beforeEach(() => {
(useQuery as unknown as Mock).mockReset();
mockQueryOptions.mockClear();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("shows skeleton while loading (after mount)", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: undefined,
isPending: true,
isPlaceholderData: false,
});
it("shows skeleton while loading (after mount)", async () => {
// Configure mock to return queryOptions that React Query will use
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getFavicon", { domain }],
queryFn: () => new Promise(() => {}), // Never resolves to keep loading state
}));
render(<Favicon domain="example.com" size={16} />);
// While loading, should show skeleton
const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
// Initial render shows skeleton due to not mounted
expect(
document.querySelectorAll('[data-slot="skeleton"]').length,
).toBeGreaterThan(0);
// Wait for mount effect
await waitFor(() => {
const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
});
it("shows letter avatar when no url (after mount)", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: { url: null },
isPending: false,
isPlaceholderData: false,
});
it("shows letter avatar when no url (after mount)", async () => {
// Configure mock to return queryOptions with null URL
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getFavicon", { domain }],
queryFn: async () => ({ url: null }),
}));
render(<Favicon domain="example.com" size={16} />);
// After mount, when no favicon URL, should show letter avatar
// Wait for query to complete and component to mount
await waitFor(() => {
const avatar = screen.queryByText("E");
expect(avatar).toBeInTheDocument();
});
const avatar = screen.getByText("E");
expect(avatar).toBeInTheDocument();
expect(avatar).toHaveAttribute("data-favicon", "example.com");
expect(avatar).toHaveAttribute("role", "img");
@@ -92,14 +92,19 @@ describe("Favicon", () => {
expect(document.querySelector('[data-slot="image"]')).toBeNull();
});
it("shows domain letter avatar when no url and not loading", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: { url: null },
isPending: false,
isPlaceholderData: false,
});
it("shows domain letter avatar when no url and not loading", async () => {
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getFavicon", { domain }],
queryFn: async () => ({ url: null }),
}));
render(<Favicon domain="example.com" size={16} />);
// Wait for query to complete
await waitFor(() => {
expect(screen.queryByText("E")).toBeInTheDocument();
});
// Should show letter 'E' for example.com
const avatar = screen.getByText("E");
expect(avatar).toBeInTheDocument();
@@ -110,36 +115,52 @@ describe("Favicon", () => {
expect(document.querySelector('[data-slot="image"]')).toBeNull();
});
it("generates consistent colors for same domain", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: { url: null },
isPending: false,
isPlaceholderData: false,
});
it("generates consistent colors for same domain", async () => {
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getFavicon", { domain }],
queryFn: async () => ({ url: null }),
}));
const { unmount } = render(<Favicon domain="example.com" size={16} />);
await waitFor(() => {
expect(screen.queryByText("E")).toBeInTheDocument();
});
const firstAvatar = screen.getByText("E");
const firstClass = firstAvatar.className;
const firstStyle = firstAvatar.getAttribute("style");
unmount();
render(<Favicon domain="example.com" size={16} />);
await waitFor(() => {
expect(screen.queryByText("E")).toBeInTheDocument();
});
const secondAvatar = screen.getByText("E");
expect(secondAvatar.className).toBe(firstClass);
expect(secondAvatar.getAttribute("style")).toBe(firstStyle);
});
it("renders Image when url present", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: {
url: "https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/32x32.webp",
},
isPending: false,
isPlaceholderData: false,
});
it("renders Image when url present", async () => {
const faviconUrl =
"https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/32x32.webp";
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getFavicon", { domain }],
queryFn: async () => ({ url: faviconUrl }),
}));
render(<Favicon domain="example.com" size={16} />);
// Wait for the image to be rendered
await waitFor(() => {
const img = screen.queryByRole("img", { name: /icon/i });
expect(img).toBeInTheDocument();
});
const img = screen.getByRole("img", { name: /icon/i });
expect(img).toHaveAttribute(
"src",
"https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/32x32.webp",
);
expect(img).toHaveAttribute("src", faviconUrl);
});
});

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { HeadersSection } from "./headers-section";
// Keep TooltipContent empty in unit tests to avoid text duplication issues.

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { HostingSection } from "./hosting-section";
vi.mock("next/dynamic", () => ({

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { KeyValue } from "./key-value";
// Mock CopyButton - we're testing KeyValue, not clipboard functionality

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { ProviderValue } from "./provider-value";
vi.mock("@/components/domain/favicon", () => ({

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { RegistrationSection } from "./registration-section";
vi.mock("@/components/domain/favicon", () => ({

View File

@@ -1,7 +1,6 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from "@testing-library/react";
import type { Mock } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@/lib/test-utils";
import { ScreenshotTooltip } from "./screenshot-tooltip";
vi.mock("@/components/ui/tooltip", () => ({
@@ -26,74 +25,78 @@ vi.mock("next/image", () => ({
),
}));
// Mock the tRPC client to return a mock queryOptions function
const mockQueryOptions = vi.fn();
vi.mock("@/lib/trpc/client", () => ({
useTRPC: () => ({
domain: {
getScreenshot: {
queryOptions: (vars: unknown) => ({
queryKey: ["getScreenshot", vars],
}),
queryOptions: mockQueryOptions,
},
},
}),
}));
vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
"@tanstack/react-query",
);
return {
...actual,
useQuery: vi.fn(),
};
});
const { useQuery } = await import("@tanstack/react-query");
describe("ScreenshotTooltip", () => {
beforeEach(() => {
(useQuery as unknown as Mock).mockReset();
mockQueryOptions.mockClear();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("fetches on open and shows loading UI", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: undefined,
isLoading: true,
isFetching: false,
});
it("fetches on open and shows loading UI", async () => {
// Configure mock to return queryOptions that keep loading state
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getScreenshot", { domain }],
queryFn: () => new Promise(() => {}), // Never resolves to keep loading state
}));
render(
<ScreenshotTooltip domain="example.com">
<span>hover me</span>
</ScreenshotTooltip>,
);
// Simulate open by clicking the trigger
fireEvent.click(screen.getByText("hover me"));
expect(screen.getByText(/taking screenshot/i)).toBeInTheDocument();
// Should show loading state
await waitFor(() => {
expect(screen.getByText(/taking screenshot/i)).toBeInTheDocument();
});
});
it("renders image when loaded", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: {
url: "https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/1200x630.webp",
},
isLoading: false,
isFetching: false,
});
it("renders image when loaded", async () => {
const screenshotUrl =
"https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/1200x630.webp";
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getScreenshot", { domain }],
queryFn: async () => ({ url: screenshotUrl }),
}));
render(
<ScreenshotTooltip domain="example.com">
<span>hover me</span>
</ScreenshotTooltip>,
);
fireEvent.click(screen.getByText("hover me"));
// Wait for the image to be rendered
await waitFor(() => {
const img = screen.queryByRole("img", {
name: /homepage preview of example.com/i,
});
expect(img).toBeInTheDocument();
});
const img = screen.getByRole("img", {
name: /homepage preview of example.com/i,
});
expect(img).toHaveAttribute(
"src",
"https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/1200x630.webp",
);
expect(img).toHaveAttribute("src", screenshotUrl);
});
});

View File

@@ -1,8 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { createElement } from "react";
import type { Mock } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@/lib/test-utils";
import { Screenshot } from "./screenshot";
vi.mock("next/image", () => ({
@@ -27,73 +26,79 @@ vi.mock("next/image", () => ({
}),
}));
// Mock the tRPC client to return a mock queryOptions function
const mockQueryOptions = vi.fn();
vi.mock("@/lib/trpc/client", () => ({
useTRPC: () => ({
domain: {
getScreenshot: {
queryOptions: (vars: unknown) => ({
queryKey: ["getScreenshot", vars],
}),
queryOptions: mockQueryOptions,
},
},
}),
}));
vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
"@tanstack/react-query",
);
return {
...actual,
useQuery: vi.fn(),
};
});
const { useQuery } = await import("@tanstack/react-query");
describe("Screenshot", () => {
beforeEach(() => {
(useQuery as unknown as Mock).mockReset();
mockQueryOptions.mockClear();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("shows loading UI during fetch", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: undefined,
isLoading: true,
isFetching: false,
});
it("shows loading UI during fetch", async () => {
// Configure mock to return queryOptions that keep loading state
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getScreenshot", { domain }],
queryFn: () => new Promise(() => {}), // Never resolves to keep loading state
}));
render(<Screenshot domain="example.com" />);
expect(screen.getByText(/taking screenshot/i)).toBeInTheDocument();
// Should show loading state
await waitFor(() => {
expect(screen.getByText(/taking screenshot/i)).toBeInTheDocument();
});
});
it("renders image when url present", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: {
url: "https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/1200x630.webp",
},
isLoading: false,
isFetching: false,
});
it("renders image when url present", async () => {
const screenshotUrl =
"https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/1200x630.webp";
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getScreenshot", { domain }],
queryFn: async () => ({ url: screenshotUrl }),
}));
render(<Screenshot domain="example.com" />);
// Wait for the image to be rendered
await waitFor(() => {
const img = screen.queryByRole("img", {
name: /homepage preview of example.com/i,
});
expect(img).toBeInTheDocument();
});
const img = screen.getByRole("img", {
name: /homepage preview of example.com/i,
});
expect(img).toHaveAttribute(
"src",
"https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/1200x630.webp",
);
expect(img).toHaveAttribute("src", screenshotUrl);
});
it("shows fallback when no url and not loading", () => {
(useQuery as unknown as Mock).mockReturnValue({
data: { url: null },
isLoading: false,
isFetching: false,
});
it("shows fallback when no url and not loading", async () => {
mockQueryOptions.mockImplementation(({ domain }: { domain: string }) => ({
queryKey: ["getScreenshot", { domain }],
queryFn: async () => ({ url: null }),
}));
render(<Screenshot domain="example.com" />);
expect(screen.getByText(/unable to take/i)).toBeInTheDocument();
// Wait for query to complete
await waitFor(() => {
expect(screen.getByText(/unable to take/i)).toBeInTheDocument();
});
});
});

View File

@@ -2,14 +2,13 @@
import { Ban } from "lucide-react";
import posthog from "posthog-js";
import type { ReactNode } from "react";
import { Component } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { createLogger } from "@/lib/logger/client";
interface Props {
children: ReactNode;
children: React.ReactNode;
sectionName: string;
}

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { MetaTagsGrid } from "./meta-tags-grid";
describe("MetaTagsGrid", () => {

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { SeoResponse } from "@/lib/schemas";
import { render, screen } from "@/lib/test-utils";
import { RobotsSummary } from "./robots-summary";
vi.mock("@/components/ui/tooltip", () => ({

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { SeoResponse } from "@/lib/schemas";
import { render, screen } from "@/lib/test-utils";
// Mock child components to isolate main component testing
vi.mock("@/components/domain/seo/meta-tags-grid", () => ({

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen, within } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { render, screen, within } from "@/lib/test-utils";
import { SocialPreviewTabs } from "./social-preview-tabs";
describe("SocialPreviewTabs", () => {

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import { createElement } from "react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import { SocialPreview } from "./social-preview";
// Mock next/image with a plain img for JSDOM

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
// Mock the video player components to avoid media-chrome dependencies
vi.mock("@/components/ui/video-player", () => ({

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@/lib/test-utils";
import { HeaderSearch } from "./header-search";
import { HeaderSearchProvider } from "./header-search-context";

89
lib/test-utils.tsx Normal file
View File

@@ -0,0 +1,89 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type RenderOptions, render } from "@testing-library/react";
/**
* Creates a QueryClient configured for testing.
*
* Configuration follows TanStack Query testing best practices:
* - retry: false - Prevents test timeouts on failed queries
* - gcTime: Infinity - Prevents "Jest did not exit" warnings
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/testing
*/
export function createTestQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: {
// Turn off retries to prevent test timeouts
retry: false,
// Set gcTime to Infinity to prevent cleanup warnings
gcTime: Number.POSITIVE_INFINITY,
},
mutations: {
// Turn off retries for mutations too
retry: false,
},
},
});
}
/**
* Test wrapper that provides QueryClientProvider with a fresh client for each test.
* Ensures test isolation by creating a new QueryClient instance per render.
*/
function createWrapper(queryClient: QueryClient) {
return function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
}
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
/**
* Optional QueryClient instance. If not provided, a new one will be created
* using createTestQueryClient().
*/
queryClient?: QueryClient;
}
/**
* Custom render function that wraps components with QueryClientProvider.
*
* Usage:
* ```tsx
* import { render, screen } from '@/lib/test-utils'
*
* it('renders component with React Query', () => {
* render(<MyComponent />)
* expect(screen.getByText('Hello')).toBeInTheDocument()
* })
*
* // With custom QueryClient
* it('uses prefilled cache', () => {
* const queryClient = createTestQueryClient()
* queryClient.setQueryData(['key'], { data: 'value' })
* render(<MyComponent />, { queryClient })
* })
* ```
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/testing
*/
function customRender(ui: React.ReactElement, options?: CustomRenderOptions) {
const { queryClient = createTestQueryClient(), ...renderOptions } =
options ?? {};
return {
...render(ui, {
wrapper: createWrapper(queryClient),
...renderOptions,
}),
queryClient,
};
}
// Re-export everything from @testing-library/react
export * from "@testing-library/react";
// Override render with our custom version
export { customRender as render };