Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for '@reach/utils' and compatibility check with react 18 #960

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 23 additions & 1 deletion internal/test/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ import type {
RenderOptions as TLRenderOptions,
RenderResult as TLRenderResult,
} from "@testing-library/react";
import type {
RenderHookOptions as TLLegacyRenderHookOptions,
RenderHookResult as TLLegacyRenderHookResult,
} from "@testing-library/react-hooks";
import type {
RenderHookOptions as TLActualRenderHookOptions,
RenderHookResult as TLActualRenderHookResult,
} from "@testing-library/react-13";

export type RenderOptions = Omit<TLRenderOptions, "queries"> & {
export type RenderOptions = Omit<TLRenderOptions, "queries" | "wrapper"> & {
strict?: boolean;
};

Expand All @@ -15,3 +23,17 @@ export type RenderResult<
setProps(props: P): RenderResult<P, T>;
forceUpdate(): RenderResult<P, T>;
};

export type RenderHookOptions<TProps> = Omit<
TLLegacyRenderHookOptions<TProps> &
TLActualRenderHookOptions<TProps> & {
strict?: boolean;
},
"wrapper"
>;

export type RenderHookResult<TResult, TProps> = TLLegacyRenderHookResult<
TProps,
TResult
> &
TLActualRenderHookResult<TResult, TProps>;
30 changes: 24 additions & 6 deletions internal/test/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as React from "react";
import { act } from "react-dom/test-utils";
import type { MatcherFunction } from "@testing-library/react";
import { render as tlRender, fireEvent } from "@testing-library/react";
import { fireEvent as fireDomEvent } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import type { RenderOptions, RenderResult } from "./types";
import { renderHook as tlRenderHook } from "@testing-library/react-hooks";
import type {
RenderHookOptions,
RenderHookResult,
RenderOptions,
RenderResult,
} from "./types";

/**
* This function is useful if you want to query a DOM element by its text
Expand Down Expand Up @@ -79,6 +82,17 @@ export function render<
return result;
}

export function renderHook<TProps, TResult>(
callback: (props: TProps) => TResult,
options: RenderHookOptions<TProps> = {}
): RenderHookResult<TResult, TProps> {
const { strict = false, ...restOptions } = options;
return tlRenderHook(callback, {
...restOptions,
wrapper: strict ? React.StrictMode : React.Fragment,
});
}

export async function wait(time: number) {
return await new Promise<void>((res) => setTimeout(res, time));
}
Expand Down Expand Up @@ -125,6 +139,10 @@ export function simulateEnterKeyClick(

type Query = (f: MatcherFunction) => HTMLElement | null;

export * from "@testing-library/react";
export { act, userEvent, fireDomEvent };
export {
cleanup as cleanupHooks,
act as actHooks,
} from "@testing-library/react-hooks";
export { cleanup, fireEvent, screen, act } from "@testing-library/react";
export { default as userEvent } from "@testing-library/user-event";
export type { RenderOptions, RenderResult };
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@testing-library/dom": "^8.16.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-13": "npm:@testing-library/react@^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.2.1",
"@types/aria-query": "^5.0.0",
"@types/css": "^0.0.33",
Expand Down
26 changes: 26 additions & 0 deletions packages/utils/__tests__/use-constant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { afterEach, describe, expect, it } from "vitest";
import { renderHook, cleanupHooks } from "@reach-internal/test/utils";
import { useConstant } from "@reach/utils";

afterEach(cleanupHooks);

describe("useConstant", () => {
const renderUseConstant = () =>
renderHook(() => useConstant(() => ({ foo: "bar" })));

it("should return value from callback", () => {
const render = renderUseConstant();

const firstRenderedObject = render.result.current;
expect(firstRenderedObject).toEqual({ foo: "bar" });
});

it("should return the same value after rerender", () => {
const render = renderUseConstant();
const resultFirst = render.result.current;
render.rerender();
const resultSecond = render.result.current;

expect(resultFirst).toBe(resultSecond);
});
});
50 changes: 50 additions & 0 deletions packages/utils/__tests__/use-controlled-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { afterEach, describe, expect, it } from "vitest";
import { renderHook, cleanupHooks, actHooks } from "@reach-internal/test/utils";
import { useControlledState } from "@reach/utils";

afterEach(cleanupHooks);

describe("useControlledState", () => {
const DEFAULT_VALUE = 10;
const CONTROLLED_VALUE = 42;

it("should return value and setter", () => {
const { result } = renderHook(() =>
useControlledState({
defaultValue: DEFAULT_VALUE,
controlledValue: undefined,
})
);

expect(result.current[0]).toBe(DEFAULT_VALUE);
expect(typeof result.current[1]).toBe("function");
});

it("should work as uncontrolled", () => {
const { result } = renderHook(() =>
useControlledState({
defaultValue: DEFAULT_VALUE,
controlledValue: undefined,
})
);
expect(result.current[0]).toBe(DEFAULT_VALUE);
actHooks(() => {
result.current[1](17);
});
expect(result.current[0]).toBe(17);
});

it("should work as controlled", () => {
const { result } = renderHook(() =>
useControlledState({
defaultValue: DEFAULT_VALUE,
controlledValue: CONTROLLED_VALUE,
})
);
expect(result.current[0]).toBe(CONTROLLED_VALUE);
actHooks(() => {
result.current[1](17);
});
expect(result.current[0]).toBe(CONTROLLED_VALUE);
});
});
33 changes: 33 additions & 0 deletions packages/utils/__tests__/use-event-listener.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from "react";
import { render, fireEvent, cleanup } from "@reach-internal/test/utils";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useEventListener } from "@reach/utils";

afterEach(cleanup);

describe("useEventListener", () => {
const Test = ({ onBodyClick }: { onBodyClick: () => void }) => {
useEventListener("click", onBodyClick, document.body);
return null;
};

it("should call event listener when it's need", () => {
const handleBodyClick = vi.fn();
render(<Test onBodyClick={handleBodyClick} />);
fireEvent.click(document.body);
expect(handleBodyClick).toHaveBeenCalledTimes(1);
fireEvent.click(document.body);
expect(handleBodyClick).toHaveBeenCalledTimes(2);
});

it("should can change event listener from args", () => {
const handleBodyClick1 = vi.fn();
const handleBodyClick2 = vi.fn();
const { rerender } = render(<Test onBodyClick={handleBodyClick1} />);
fireEvent.click(document.body);
rerender(<Test onBodyClick={handleBodyClick2} />);
fireEvent.click(document.body);
expect(handleBodyClick1).toHaveBeenCalledOnce();
expect(handleBodyClick2).toHaveBeenCalledOnce();
});
});
106 changes: 106 additions & 0 deletions packages/utils/__tests__/use-focus-change.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from "react";
import { render, cleanup, userEvent } from "@reach-internal/test/utils";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useFocusChange } from "@reach/utils";

afterEach(cleanup);

describe("useFocusChange", () => {
const Test = ({
onChange,
when,
}: {
onChange: () => void;
when?: "focus" | "blur";
}) => {
useFocusChange(onChange, when);
return (
<>
<input type="text" placeholder="first" />
<input type="text" placeholder="second" />
<div>just div</div>
</>
);
};

const renderTest = (when?: "focus" | "blur") => {
const handleChange = vi.fn();
const { getByPlaceholderText, getByText } = render(
<Test onChange={handleChange} when={when} />
);
const firstInput = getByPlaceholderText("first");
const secondInput = getByPlaceholderText("second");
const div = getByText("just div");
return {
firstInput,
secondInput,
div,
handleChange,
};
};

/**
* WARNING: The order of the tests is important:
* the blur test should come first.
* If this is not the case, the activeElement will be dirty
* and the blur event will fire when the input is clicked.
*/

it("should call handler on blur", async () => {
const {
firstInput,
secondInput,
div,
handleChange: handleBlur,
} = renderTest("blur");

await userEvent.click(firstInput);
expect(handleBlur).not.toHaveBeenCalled();

await userEvent.click(secondInput);
expect(handleBlur).toHaveBeenCalledTimes(1);
expect(handleBlur).toHaveBeenCalledWith(
document.body,
document.body,
expect.any(FocusEvent)
);

await userEvent.click(div);
expect(handleBlur).toHaveBeenCalledTimes(2);
expect(handleBlur).toHaveBeenCalledWith(
document.body,
document.body,
expect.any(FocusEvent)
);
});

it("should call handler on focus", async () => {
const { firstInput, secondInput, handleChange: handleFocus } = renderTest();

await userEvent.click(firstInput);
expect(handleFocus).toHaveBeenCalledTimes(1);
expect(handleFocus).toHaveBeenCalledWith(
firstInput,
document.body,
expect.any(FocusEvent)
);

await userEvent.click(secondInput);
expect(handleFocus).toHaveBeenCalledTimes(2);
expect(handleFocus).toHaveBeenCalledWith(
secondInput,
firstInput,
expect.any(FocusEvent)
);
});

it("should do not call handler on focus at the same node", async () => {
const { firstInput, handleChange: handleFocus } = renderTest();

await userEvent.click(firstInput);
expect(handleFocus).toHaveBeenCalledOnce();

await userEvent.click(firstInput);
expect(handleFocus).toHaveBeenCalledOnce();
});
});
20 changes: 12 additions & 8 deletions packages/utils/src/use-focus-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,24 @@ export function useFocusChange(
lastActiveElement.current = ownerDocument.activeElement;

function onChange(event: FocusEvent) {
if (lastActiveElement.current !== ownerDocument.activeElement) {
handleChange(
ownerDocument.activeElement,
lastActiveElement.current,
event
);
lastActiveElement.current = ownerDocument.activeElement;
if (
when === "focus" &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If do not check it and try use this hook with blur, handleChange will never be called, because ownerDocument.activeElement will be always ownerDocument.body.

lastActiveElement.current === ownerDocument.activeElement
) {
return;
}
handleChange(
ownerDocument.activeElement,
lastActiveElement.current,
event
);
lastActiveElement.current = ownerDocument.activeElement;
}

ownerDocument.addEventListener(when, onChange, true);

return () => {
ownerDocument.removeEventListener(when, onChange);
ownerDocument.removeEventListener(when, onChange, true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing the handler didn't work before.
demo: https://codepen.io/dartess/pen/rNdgWEj

};
}, [when, handleChange, ownerDocument]);
}