-
-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(test): add some simple jest testing docs
- Loading branch information
Showing
4 changed files
with
422 additions
and
14 deletions.
There are no files selected for viewing
390 changes: 390 additions & 0 deletions
390
apps/docs/src/app/(main)/(markdown)/testing/jest/page.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,390 @@ | ||
# Testing with Jest | ||
|
||
ReactMD provides built-in support for testing with [jest](https://jestjs.io/) with `@jest/globals`. | ||
|
||
# Quickstart | ||
|
||
## Installing Jest Dependencies | ||
|
||
To get started, install the required `jest` dependencies: | ||
|
||
```sh | ||
npm install --save-dev \ | ||
jest \ | ||
@jest/globals \ | ||
@jest/types \ | ||
``` | ||
|
||
It is also recommended to install the following React Testing Library packages to improve the testing experience: | ||
|
||
```sh | ||
npm install --save-dev \ | ||
@testing-library/dom \ | ||
@testing-library/react \ | ||
@testing-library/jest-dom \ | ||
@testing-library/user-event | ||
``` | ||
|
||
For more information, see: | ||
|
||
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) | ||
- [Jest DOM](https://testing-library.com/docs/ecosystem-jest-dom/) | ||
- [User Event Testing](https://testing-library.com/docs/user-event/v13/) | ||
|
||
## Setup Polyfills and Additional Jest Matchers | ||
|
||
Once the jest dependencies have been installed, create or modify a | ||
`jest.config.ts` file that includes a | ||
[setupFilesAfterEnv](https://jestjs.io/docs/configuration#setupfilesafterenv-array): | ||
|
||
```ts fileName="jest.config.ts" | ||
import { type Config } from "jest"; | ||
|
||
const config: Config = { | ||
testEnvironment: "jsdom", | ||
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], | ||
}; | ||
|
||
export default config; | ||
``` | ||
|
||
Next, create or modify the `jest.setup.ts` file to include the test util polyfills and the optional `jest-dom` matchers: | ||
|
||
```ts fileName="jest.setup.ts" | ||
import "@react-md/core/test-utils/polyfills"; | ||
import "@react-md/core/test-utils/jest-setup"; | ||
|
||
// Optional: allow data-testid to be a valid DOM property for typescript | ||
import "@react-md/core/test-utils/data-testid"; | ||
|
||
// Optional: include the jest-dom matchers | ||
import "@testing-library/jest-dom/jest-globals"; | ||
``` | ||
|
||
# Create a custom Test Renderer | ||
|
||
My preferred method of making all the global context providers, data stores, | ||
`react-md` configuration, etc available for each test is to create a utility file | ||
that re-exports everything from `@react-md/core/test-utils`, | ||
`@testing-library/react`, and `@testing-library/user-event`. The example below | ||
shows a possible setup. | ||
|
||
> See [Custom Renderer](https://testing-library.com/docs/react-testing-library/setup#custom-render) for additional context. | ||
```diff fileName="src/__tests__/MyComponent.tsx" | ||
-import { render, userEvent } from "@testing-library/react"; | ||
+import { render, userEvent } from "../test-utils"; | ||
``` | ||
|
||
```tsx fileName="src/rmdConfig.tsx" | ||
import { type ReactMDCoreConfiguration } from "@react-md/core/CoreProviders"; | ||
import { configureIcons } from "@react-md/core/icon/iconConfig"; | ||
|
||
// any icon overrides. Using material icons as an example | ||
configureIcons({ | ||
back: <KeyboardArrowLeftIcon />, | ||
clear: <ClearIcon />, | ||
close: <CloseIcon />, | ||
checkbox: <CheckBoxOutlineBlankIcon />, | ||
checkboxChecked: <CheckBoxIcon />, | ||
checkboxIndeterminate: <IndeterminateCheckBoxIcon />, | ||
dropdown: <ArrowDropDownIcon />, | ||
error: <ErrorOutlineIcon />, | ||
expander: <KeyboardArrowDownIcon />, | ||
forward: <KeyboardArrowRightIcon />, | ||
menu: <MenuIcon />, | ||
notification: <NotificationsIcon />, | ||
password: <RemoveRedEyeIcon />, | ||
radio: <RadioButtonUncheckedIcon />, | ||
radioChecked: <RadioButtonCheckedIcon />, | ||
remove: <CancelIcon />, | ||
selected: <CheckIcon />, | ||
sort: <ArrowUpwardIcon />, | ||
upload: <FileUploadIcon />, | ||
}); | ||
|
||
export const rmdConfig: ReactMDCoreConfiguration = { | ||
// any other global changes | ||
// ssr: true, | ||
// colorSchemeMode: "system", | ||
}; | ||
``` | ||
|
||
```tsx fileName="src/test-utils.tsx" | ||
import { type ReactElement } from "react"; | ||
import { | ||
rmdRender, | ||
type ReactMDRenderOptions, | ||
type RenderOptions, | ||
} from "@react-md/test-utils"; | ||
|
||
import { rmdConfig } from "./rmdConfig.jsx"; | ||
import { MyCustomProviders } from "./MyCustomProviders.jsx"; | ||
|
||
export * from "@react-md/core/test-utils"; | ||
|
||
export const render = ( | ||
ui: ReactElement, | ||
options?: ReactMDRenderOptions | ||
): RenderResult => | ||
rmdRender(ui, { | ||
...options, | ||
rmdConfig: { | ||
...rmdConfig, | ||
...options?.rmdConfig, | ||
}, | ||
wrapper: ({ children }) => ( | ||
<MyCustomProviders>{children}</MyCustomProviders> | ||
), | ||
}); | ||
``` | ||
|
||
# What's Included | ||
|
||
## Polyfills | ||
|
||
By importing the `@react-md/core/test-utils/polyfills`, all the following | ||
polyfills will be applied. You can also manually import the specific polyfills | ||
instead. | ||
|
||
### matchMedia | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/matchMedia"; | ||
``` | ||
|
||
This updates `window.matchMedia` to default to desktop when `typeof window.matchMedia === "undefined"`. | ||
|
||
### ResizeObserver | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/ResizeObserver"; | ||
``` | ||
|
||
This sets the `window.ResizeObserver` to the `ResizeObserverMock` | ||
when it is `undefined`. See the | ||
[ResizeObserverMock](#resize-observer-mock) for more details. | ||
|
||
### IntersectionObserver | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/IntersectionObserver"; | ||
``` | ||
|
||
This sets the `window.IntersectionObserver` to the `IntersectionObserverMock` | ||
when it is `undefined`. See the | ||
[IntersectionObserverMock](#intersection-observer-mock) for more details. | ||
|
||
### TextEncoder | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/TextEncoder"; | ||
``` | ||
|
||
This sets the `global.TextEncoder` to `node:util` | ||
[TextEncoder](https://nodejs.org/docs/latest-v22.x/api/util.html#class-utiltextencoder) | ||
when it is `undefined`. | ||
|
||
### TextDecoder | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/TextDecoder"; | ||
``` | ||
|
||
This sets the `global.TextDecoder` to `node:util` | ||
[TextDecoder](https://nodejs.org/docs/latest-v22.x/api/util.html#class-utiltextdecoder) | ||
when it is `undefined`. | ||
|
||
### offsetParent | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/offsetParent"; | ||
``` | ||
|
||
This polyfill was added for the tree keyboard movement behavior since `offsetParent` might return the incorrect value in jsdom. | ||
|
||
### scrollIntoView | ||
|
||
```ts | ||
import "@react-md/core/test-utils/polyfills/scrollIntoView"; | ||
``` | ||
|
||
This polyfill adds no-op behavior to the `HTMLElement.prototype.scrollIntoView` if it is `undefined`. | ||
|
||
## jest-setup | ||
|
||
The `@react-md/core/test-utils/jest-setup` import just adds the following behavior for all tests: | ||
|
||
```ts | ||
import { beforeEach } from "@jest/globals"; | ||
import { INTERACTION_CONFIG } from "@react-md/core/interaction/config"; | ||
import { TRANSITION_CONFIG } from "@react-md/core/transition/config"; | ||
|
||
beforeEach(() => { | ||
// set the mode to `none` in tests since ripples require | ||
// `getBoundingClientRect()` to create correct CSS. You'll either see warnings | ||
// in the console around invalid css values or `NaN`. | ||
INTERACTION_CONFIG.mode = "none"; | ||
|
||
// disable transitions in tests since it just makes it more difficult | ||
TRANSITION_CONFIG.disabled = true; | ||
}); | ||
``` | ||
|
||
## Match Media Helpers | ||
|
||
Since it can be useful to test different viewport sizes in `jest`, a few `window.matchMedia` helpers have been included. | ||
|
||
### spyOnMatchMedia | ||
|
||
The `spyOnMatchMedia` can be used to change the viewport size for tests and | ||
expect different results. The default behavior is to match the desktop min | ||
width. | ||
|
||
```tsx | ||
import { matchPhone, render, spyOnMatchMedia } from "@react-md/core/test-utils"; | ||
|
||
const matchMedia = spyOnMatchMedia(); | ||
render(<Test />); | ||
|
||
// expect desktop results | ||
|
||
matchMedia.changeViewport(matchPhone); | ||
// expect phone results | ||
``` | ||
|
||
A default matcher can be provided as well: | ||
|
||
```tsx | ||
import { matchPhone, render, spyOnMatchMedia } from "@react-md/core/test-utils"; | ||
|
||
const matchMedia = spyOnMatchMedia(matchPhone); | ||
render(<Test />); | ||
|
||
// expect phone results | ||
``` | ||
|
||
#### Matchers | ||
|
||
The following matchers have been provided out of the box which use the default media query breakpoints: | ||
|
||
- `matchPhone` | ||
- `matchTablet` | ||
- `matchDesktop` | ||
- `matchLargeDesktop` | ||
- `matchAnyDesktop` | ||
|
||
You can also create a custom matcher: | ||
|
||
```ts | ||
const customMatcher: MatchMediaMatcher = (query) => query.includes("20rem"); | ||
|
||
function Example() { | ||
// the `customMatcher` would be called with `"screen and (min-width: 20rem)"` | ||
const matches = useMediaQuery("screen and (min-width: 20rem)"); | ||
|
||
// implementation | ||
} | ||
``` | ||
|
||
## Resize Observer Mock | ||
|
||
The `ResizeObserverMock` can be used to write tests that include the `useResizeObserver` hook. Here is a small example: | ||
|
||
```tsx fileName="src/ExampleComponent.tsx" | ||
import { useResizeObserver } from "@react-md/core/useResizeObserver"; | ||
import { useCallback, useState } from "react"; | ||
|
||
export function ExampleComponent() { | ||
const [size, setSize] = useState({ height: 0, width: 0 }); | ||
const ref = useResizeObserver({ | ||
onUpdate: useCallback((entry) => { | ||
setSize({ | ||
height: entry.contentRect.height, | ||
width: entry.contentRect.width, | ||
}); | ||
}, []), | ||
}); | ||
|
||
return ( | ||
<> | ||
<div data-testid="size">{JSON.stringify(size)}</div> | ||
<div data-testid="resize-target" ref={ref} /> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
```tsx fileName="src/__tests__/ExampleComponent.tsx" | ||
import { | ||
cleanupResizeObserverAfterEach, | ||
render, | ||
screen, | ||
setupResizeObserverMock, | ||
} from "@react-md/core/test-utils"; | ||
import { ExampleComponent } from "../ExampleComponent.jsx"; | ||
|
||
cleanupResizeObserverAfterEach(); | ||
|
||
describe("ExampleComponent", () => { | ||
it("should do stuff", () => { | ||
const observer = setupResizeObserverMock(); | ||
render(<ExampleComponent />); | ||
|
||
const size = screen.getByTestId("size"); | ||
const resizeTarget = screen.getByTestId("resize-target"); | ||
|
||
// jsdom sets all element sizes to 0 by default | ||
expect(size).toHaveTextContent(JSON.stringify({ height: 0, width: 0 })); | ||
|
||
// you can trigger with a custom change | ||
act(() => { | ||
observer.resizeElement(resizeTarget, { height: 100, width: 100 }); | ||
}); | ||
expect(size).toHaveTextContent(JSON.stringify({ height: 100, width: 100 })); | ||
|
||
// or you can mock the `getBoundingClientRect` result | ||
jest.spyOn(resizeTarget, "getBoundingClientRect").mockReturnValue({ | ||
...document.body.getBoundingClientRect(), | ||
height: 200, | ||
width: 200, | ||
}); | ||
|
||
act(() => { | ||
observer.resizeElement(resizeTarget); | ||
}); | ||
expect(size).toHaveTextContent(JSON.stringify({ height: 200, width: 200 })); | ||
}); | ||
}); | ||
``` | ||
|
||
## Timer Helpers | ||
|
||
### testImmediateRaf | ||
|
||
Use this util to make `window.requestAnimationFrame` happen immediately. | ||
|
||
```ts | ||
import { testImmediateRaf } from "@react-md/core/test-utils"; | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
describe("some test suite", () => { | ||
it("should test something with requestAnimationFrame", () => { | ||
const raf = testImmediateRaf(); | ||
|
||
// do some testing with requestAnimationFrame | ||
|
||
// reset to original at the end of the test if not using `jest.restoreAllMocks()` | ||
raf.mockRestore(); | ||
}); | ||
}); | ||
``` | ||
|
||
## Intersection Observer Mock | ||
|
||
This just adds a simple mock that ensures your tests won't crash if the | ||
`useIntersectionObserver` hook is used. This might be updated in the future to | ||
help verify the intersection observer behavior. |
Oops, something went wrong.