mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-17 08:39:47 +10:00
📦 v5.0.15 - https://docs.rxresu.me/changelog
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { act, render, renderHook, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { afterEach, describe, expect, it } from "vite-plus/test";
|
||||
|
||||
import { ConfirmDialogProvider, useConfirm } from "./use-confirm";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function wrapper({ children }: { children: React.ReactNode }) {
|
||||
return <ConfirmDialogProvider>{children}</ConfirmDialogProvider>;
|
||||
}
|
||||
|
||||
/** A test component that triggers confirm and stores the result */
|
||||
function ConfirmTester() {
|
||||
const confirm = useConfirm();
|
||||
const [result, setResult] = React.useState<boolean | null>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const confirmed = await confirm("Delete this?", {
|
||||
description: "This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Keep",
|
||||
});
|
||||
setResult(confirmed);
|
||||
}}
|
||||
>
|
||||
Trigger
|
||||
</button>
|
||||
{result !== null && <span data-testid="result">{String(result)}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useConfirm", () => {
|
||||
it("throws when used outside provider", () => {
|
||||
expect(() => {
|
||||
renderHook(() => useConfirm());
|
||||
}).toThrow("useConfirm must be used within a <ConfirmDialogProvider />");
|
||||
});
|
||||
|
||||
it("returns a function when used inside provider", () => {
|
||||
const { result } = renderHook(() => useConfirm(), { wrapper });
|
||||
expect(typeof result.current).toBe("function");
|
||||
});
|
||||
|
||||
it("confirm() returns a promise", () => {
|
||||
const { result } = renderHook(() => useConfirm(), { wrapper });
|
||||
|
||||
let promise: Promise<boolean> | undefined;
|
||||
act(() => {
|
||||
promise = result.current("Are you sure?");
|
||||
});
|
||||
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
it("opens dialog with title and description when confirm is called", async () => {
|
||||
render(
|
||||
<ConfirmDialogProvider>
|
||||
<ConfirmTester />
|
||||
</ConfirmDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Trigger").click();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Delete this?")).toBeDefined();
|
||||
expect(screen.getByText("This action cannot be undone.")).toBeDefined();
|
||||
expect(screen.getByText("Delete")).toBeDefined();
|
||||
expect(screen.getByText("Keep")).toBeDefined();
|
||||
});
|
||||
|
||||
it("resolves true when confirm button is clicked", async () => {
|
||||
render(
|
||||
<ConfirmDialogProvider>
|
||||
<ConfirmTester />
|
||||
</ConfirmDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Trigger").click();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Delete").click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("result").textContent).toBe("true");
|
||||
});
|
||||
|
||||
it("resolves false when cancel button is clicked", async () => {
|
||||
render(
|
||||
<ConfirmDialogProvider>
|
||||
<ConfirmTester />
|
||||
</ConfirmDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Trigger").click();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Keep").click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("result").textContent).toBe("false");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vite-plus/test";
|
||||
|
||||
import { useControlledState } from "./use-controlled-state";
|
||||
|
||||
describe("useControlledState", () => {
|
||||
describe("uncontrolled mode (no value prop)", () => {
|
||||
it("uses defaultValue as initial state", () => {
|
||||
const { result } = renderHook(() => useControlledState({ defaultValue: "hello" }));
|
||||
expect(result.current[0]).toBe("hello");
|
||||
});
|
||||
|
||||
it("updates internal state when setter is called", () => {
|
||||
const { result } = renderHook(() => useControlledState({ defaultValue: 0 }));
|
||||
|
||||
act(() => {
|
||||
result.current[1](42);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(42);
|
||||
});
|
||||
|
||||
it("calls onChange when setter is called", () => {
|
||||
const onChange = vi.fn();
|
||||
const { result } = renderHook(() => useControlledState({ defaultValue: "a", onChange }));
|
||||
|
||||
act(() => {
|
||||
result.current[1]("b");
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("b");
|
||||
});
|
||||
|
||||
it("passes extra args to onChange", () => {
|
||||
const onChange = vi.fn();
|
||||
const { result } = renderHook(() => useControlledState<string, [number]>({ defaultValue: "a", onChange }));
|
||||
|
||||
act(() => {
|
||||
result.current[1]("b", 99);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("b", 99);
|
||||
});
|
||||
});
|
||||
|
||||
describe("controlled mode (value prop provided)", () => {
|
||||
it("uses value prop as initial state", () => {
|
||||
const { result } = renderHook(() => useControlledState({ value: "controlled" }));
|
||||
expect(result.current[0]).toBe("controlled");
|
||||
});
|
||||
|
||||
it("syncs internal state when value prop changes", () => {
|
||||
let value = "first";
|
||||
const { result, rerender } = renderHook(() => useControlledState({ value }));
|
||||
|
||||
expect(result.current[0]).toBe("first");
|
||||
|
||||
value = "second";
|
||||
rerender();
|
||||
|
||||
expect(result.current[0]).toBe("second");
|
||||
});
|
||||
|
||||
it("still calls onChange when setter is called in controlled mode", () => {
|
||||
const onChange = vi.fn();
|
||||
const { result } = renderHook(() => useControlledState({ value: "x", onChange }));
|
||||
|
||||
act(() => {
|
||||
result.current[1]("y");
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("y");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles undefined defaultValue", () => {
|
||||
const { result } = renderHook(() => useControlledState({}));
|
||||
expect(result.current[0]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles boolean values", () => {
|
||||
const { result } = renderHook(() => useControlledState({ defaultValue: false }));
|
||||
expect(result.current[0]).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current[1](true);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
it("handles object values", () => {
|
||||
const obj = { key: "value" };
|
||||
const { result } = renderHook(() => useControlledState({ defaultValue: obj }));
|
||||
expect(result.current[0]).toBe(obj);
|
||||
});
|
||||
|
||||
it("works without onChange callback", () => {
|
||||
const { result } = renderHook(() => useControlledState({ defaultValue: 5 }));
|
||||
|
||||
act(() => {
|
||||
result.current[1](10);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
|
||||
|
||||
import { useIsMobile } from "./use-mobile";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock window.matchMedia
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type MockMatchMedia = {
|
||||
matches: boolean;
|
||||
listeners: Array<(e: { matches: boolean }) => void>;
|
||||
trigger: (matches: boolean) => void;
|
||||
};
|
||||
|
||||
let mockMql: MockMatchMedia;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMql = {
|
||||
matches: false,
|
||||
listeners: [],
|
||||
trigger(matches: boolean) {
|
||||
this.matches = matches;
|
||||
for (const listener of this.listeners) {
|
||||
listener({ matches });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
vi.stubGlobal(
|
||||
"matchMedia",
|
||||
vi.fn(() => ({
|
||||
get matches() {
|
||||
return mockMql.matches;
|
||||
},
|
||||
addEventListener: (_event: string, cb: (e: { matches: boolean }) => void) => {
|
||||
mockMql.listeners.push(cb);
|
||||
},
|
||||
removeEventListener: (_event: string, cb: (e: { matches: boolean }) => void) => {
|
||||
mockMql.listeners = mockMql.listeners.filter((l) => l !== cb);
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useIsMobile", () => {
|
||||
it("returns false on desktop-sized screens", () => {
|
||||
mockMql.matches = false;
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true on mobile-sized screens", () => {
|
||||
mockMql.matches = true;
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("updates when screen size changes to mobile", () => {
|
||||
mockMql.matches = false;
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
act(() => {
|
||||
mockMql.trigger(true);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("updates when screen size changes to desktop", () => {
|
||||
mockMql.matches = true;
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
act(() => {
|
||||
mockMql.trigger(false);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("cleans up event listener on unmount", () => {
|
||||
mockMql.matches = false;
|
||||
const { unmount } = renderHook(() => useIsMobile());
|
||||
|
||||
expect(mockMql.listeners).toHaveLength(1);
|
||||
unmount();
|
||||
expect(mockMql.listeners).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("uses 768px breakpoint (max-width: 767px)", () => {
|
||||
renderHook(() => useIsMobile());
|
||||
expect(window.matchMedia).toHaveBeenCalledWith("(max-width: 767px)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import { act, fireEvent, render, renderHook, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
|
||||
|
||||
vi.mock("@lingui/core/macro", () => ({
|
||||
t: (strings: TemplateStringsArray) => strings[0],
|
||||
}));
|
||||
|
||||
import { PromptDialogProvider, usePrompt } from "./use-prompt";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function wrapper({ children }: { children: React.ReactNode }) {
|
||||
return <PromptDialogProvider>{children}</PromptDialogProvider>;
|
||||
}
|
||||
|
||||
function PromptTester() {
|
||||
const prompt = usePrompt();
|
||||
const [result, setResult] = React.useState<string | null | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const value = await prompt("Enter name", {
|
||||
description: "Provide your full name",
|
||||
defaultValue: "John",
|
||||
confirmText: "Submit",
|
||||
cancelText: "Skip",
|
||||
});
|
||||
setResult(value);
|
||||
}}
|
||||
>
|
||||
Open Prompt
|
||||
</button>
|
||||
{result !== undefined && <span data-testid="result">{result === null ? "null" : result}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("usePrompt", () => {
|
||||
it("throws when used outside provider", () => {
|
||||
expect(() => {
|
||||
renderHook(() => usePrompt());
|
||||
}).toThrow("usePrompt must be used within a <PromptDialogProvider />");
|
||||
});
|
||||
|
||||
it("returns a function when used inside provider", () => {
|
||||
const { result } = renderHook(() => usePrompt(), { wrapper });
|
||||
expect(typeof result.current).toBe("function");
|
||||
});
|
||||
|
||||
it("prompt() returns a promise", () => {
|
||||
const { result } = renderHook(() => usePrompt(), { wrapper });
|
||||
|
||||
let promise: Promise<string | null> | undefined;
|
||||
act(() => {
|
||||
promise = result.current("Enter value");
|
||||
});
|
||||
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
it("opens dialog with title, description, and default value", async () => {
|
||||
render(
|
||||
<PromptDialogProvider>
|
||||
<PromptTester />
|
||||
</PromptDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Open Prompt").click();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Enter name")).toBeDefined();
|
||||
expect(screen.getByText("Provide your full name")).toBeDefined();
|
||||
expect(screen.getByText("Submit")).toBeDefined();
|
||||
expect(screen.getByText("Skip")).toBeDefined();
|
||||
|
||||
// Check default value in input
|
||||
const input = screen.getByDisplayValue("John");
|
||||
expect(input).toBeDefined();
|
||||
});
|
||||
|
||||
it("resolves with input value when confirm is clicked", async () => {
|
||||
render(
|
||||
<PromptDialogProvider>
|
||||
<PromptTester />
|
||||
</PromptDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Open Prompt").click();
|
||||
});
|
||||
|
||||
// Change the input value
|
||||
const input = screen.getByDisplayValue("John");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Jane Doe" } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Submit").click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("result").textContent).toBe("Jane Doe");
|
||||
});
|
||||
|
||||
it("resolves with null when cancel is clicked", async () => {
|
||||
render(
|
||||
<PromptDialogProvider>
|
||||
<PromptTester />
|
||||
</PromptDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Open Prompt").click();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Skip").click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("result").textContent).toBe("null");
|
||||
});
|
||||
|
||||
it("resolves with value when Enter key is pressed", async () => {
|
||||
render(
|
||||
<PromptDialogProvider>
|
||||
<PromptTester />
|
||||
</PromptDialogProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
screen.getByText("Open Prompt").click();
|
||||
});
|
||||
|
||||
const input = screen.getByDisplayValue("John");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Enter User" } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("result").textContent).toBe("Enter User");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user