Extract custom hooks for reusable logic

This commit is contained in:
adilallo
2026-01-26 12:51:27 -07:00
parent f513aecacc
commit 86d7cff5d4
21 changed files with 1590 additions and 141 deletions
+94
View File
@@ -0,0 +1,94 @@
import { renderHook, act } from "@testing-library/react";
import { vi, describe, test, expect, beforeEach, afterEach } from "vitest";
import { useClickOutside } from "../../../app/hooks/useClickOutside";
import { useRef } from "react";
describe("useClickOutside", () => {
let handler: ReturnType<typeof vi.fn>;
beforeEach(() => {
handler = vi.fn();
});
afterEach(() => {
vi.clearAllMocks();
});
test("calls handler when clicking outside element", () => {
const { result } = renderHook(() => {
const ref = useRef(null);
useClickOutside([ref], handler, true);
return ref;
});
const div = document.createElement("div");
document.body.appendChild(div);
result.current.current = div;
act(() => {
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
});
expect(handler).toHaveBeenCalledTimes(1);
document.body.removeChild(div);
});
test("does not call handler when clicking inside element", () => {
const { result } = renderHook(() => {
const ref = useRef(null);
useClickOutside([ref], handler, true);
return ref;
});
const div = document.createElement("div");
const innerDiv = document.createElement("div");
div.appendChild(innerDiv);
document.body.appendChild(div);
result.current.current = div;
act(() => {
innerDiv.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
});
expect(handler).not.toHaveBeenCalled();
document.body.removeChild(div);
});
test("does not call handler when disabled", () => {
const { result } = renderHook(() => {
const ref = useRef(null);
useClickOutside([ref], handler, false);
return ref;
});
act(() => {
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
});
expect(handler).not.toHaveBeenCalled();
});
test("handles multiple refs", () => {
const { result } = renderHook(() => {
const ref1 = useRef(null);
const ref2 = useRef(null);
useClickOutside([ref1, ref2], handler, true);
return { ref1, ref2 };
});
const div1 = document.createElement("div");
const div2 = document.createElement("div");
document.body.appendChild(div1);
document.body.appendChild(div2);
result.current.ref1.current = div1;
result.current.ref2.current = div2;
act(() => {
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
});
expect(handler).toHaveBeenCalledTimes(1);
document.body.removeChild(div1);
document.body.removeChild(div2);
});
});
+26
View File
@@ -0,0 +1,26 @@
import { renderHook } from "@testing-library/react";
import { describe, test, expect } from "vitest";
import { useComponentId } from "../../../app/hooks/useComponentId";
describe("useComponentId", () => {
test("generates unique IDs with prefix", () => {
const { result } = renderHook(() => useComponentId("input"));
expect(result.current.id).toMatch(/^input-/);
expect(result.current.labelId).toMatch(/^input-.*-label$/);
});
test("uses provided ID when given", () => {
const { result } = renderHook(() => useComponentId("input", "custom-id"));
expect(result.current.id).toBe("custom-id");
expect(result.current.labelId).toBe("custom-id-label");
});
test("generates different IDs for different prefixes", () => {
const { result: result1 } = renderHook(() => useComponentId("input"));
const { result: result2 } = renderHook(() => useComponentId("select"));
expect(result1.current.id).not.toBe(result2.current.id);
expect(result1.current.id).toMatch(/^input-/);
expect(result2.current.id).toMatch(/^select-/);
});
});
+118
View File
@@ -0,0 +1,118 @@
import { renderHook } from "@testing-library/react";
import { describe, test, expect } from "vitest";
import { useSchemaData } from "../../../app/hooks/useSchemaData";
describe("useSchemaData", () => {
test("generates Organization schema", () => {
const { result } = renderHook(() =>
useSchemaData({
type: "Organization",
name: "Test Org",
url: "https://example.com",
email: "test@example.com",
sameAs: ["https://twitter.com/test"],
}),
);
expect(result.current).toEqual({
"@context": "https://schema.org",
"@type": "Organization",
name: "Test Org",
url: "https://example.com",
email: "test@example.com",
sameAs: ["https://twitter.com/test"],
});
});
test("generates WebSite schema", () => {
const { result } = renderHook(() =>
useSchemaData({
type: "WebSite",
name: "Test Site",
url: "https://example.com",
potentialAction: {
target: "https://example.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
}),
);
expect(result.current).toEqual({
"@context": "https://schema.org",
"@type": "WebSite",
name: "Test Site",
url: "https://example.com",
potentialAction: {
"@type": "SearchAction",
target: "https://example.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
});
});
test("generates HowTo schema", () => {
const { result } = renderHook(() =>
useSchemaData({
type: "HowTo",
name: "How to test",
description: "Test description",
steps: [
{ name: "Step 1", text: "Do this" },
{ name: "Step 2", text: "Do that" },
],
}),
);
expect(result.current).toEqual({
"@context": "https://schema.org",
"@type": "HowTo",
name: "How to test",
description: "Test description",
step: [
{
"@type": "HowToStep",
position: 1,
name: "Step 1",
text: "Do this",
},
{
"@type": "HowToStep",
position: 2,
name: "Step 2",
text: "Do that",
},
],
});
});
test("generates BreadcrumbList schema", () => {
const { result } = renderHook(() =>
useSchemaData({
type: "BreadcrumbList",
items: [
{ name: "Home", url: "https://example.com" },
{ name: "Page", url: "https://example.com/page" },
],
}),
);
expect(result.current).toEqual({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: "https://example.com",
},
{
"@type": "ListItem",
position: 2,
name: "Page",
item: "https://example.com/page",
},
],
});
});
});