Skip to content

Commit 2513fab

Browse files
QuiiBzTom Lienard
and
Tom Lienard
authored
feat(jest-helpers): add jest-helpers package (#432)
* feat(jest-helpers): add jest-helpers package * chore(deps): update deps * docs: add readme * fix(jest-helpers): eslint no-extraenous-dependencies * chore(deps): add @testing-library/react to peerDeps * chore(jest-helpers): add doc to methods fields in readme * chore(deps): update deps * refactor(jest-helpers): remove initializeIfNeeded * feat(jest-helpers): add tests Co-authored-by: Tom Lienard <[email protected]>
1 parent b59a375 commit 2513fab

File tree

11 files changed

+456
-2
lines changed

11 files changed

+456
-2
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ scaleway-lib is a set of NPM packages used at Scaleway.
7878
![npm bundle size](https://img.shields.io/bundlephobia/min/@scaleway/regex)
7979
![npm](https://img.shields.io/npm/v/@scaleway/regex)
8080

81+
- [`@scaleway/jest-helpers`](./packages/jest-helpers/README.md): utilities jest functions.
82+
83+
![npm](https://img.shields.io/npm/dm/@scaleway/jest-helpers)
84+
![npm bundle size](https://img.shields.io/bundlephobia/min/@scaleway/jest-helpers)
85+
![npm](https://img.shields.io/npm/v/@scaleway/jest-helpers)
86+
8187
## Development
8288

8389
### Locally

packages/jest-helpers/.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/__tests__/**
2+
src
3+
!.npmignore

packages/jest-helpers/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# `@scaleway/jest-helpers`
2+
3+
## A package for utilities jest functions
4+
5+
## Install
6+
7+
```bash
8+
$ yarn add @scaleway/jest-functions
9+
```
10+
11+
## How to use
12+
13+
### Create the helpers functions
14+
15+
```tsx
16+
import makeHelpers from '@scaleway/jest-helpers'
17+
18+
const Wrapper = ({ children }) => (
19+
<ThemeProvider>{children}</ThemeProvider>
20+
)
21+
22+
export const {
23+
renderWithTheme,
24+
shouldMatchEmotionSnapshot,
25+
shouldMatchEmotionSnapshotWithPortal,
26+
} = makeHelpers(Wrapper)
27+
```
28+
29+
#### With a theme prop
30+
31+
```tsx
32+
import makeHelpers from '@scaleway/jest-helpers'
33+
import defaultTheme from '..'
34+
35+
interface WrapperProps {
36+
theme?: typeof defaultTheme
37+
}
38+
39+
const Wrapper = ({ theme, children }) => (
40+
<ThemeProvider theme={theme}>{children}</ThemeProvider>
41+
)
42+
43+
export const {
44+
renderWithTheme,
45+
shouldMatchEmotionSnapshot,
46+
shouldMatchEmotionSnapshotWithPortal,
47+
} = makeHelpers(Wrapper)
48+
```
49+
50+
#### With CreateSerializerOptions
51+
52+
```tsx
53+
import makeHelpers from '@scaleway/jest-helpers'
54+
55+
const Wrapper = ({ children }) => (
56+
<ThemeProvider>{children}</ThemeProvider>
57+
)
58+
59+
export const {
60+
renderWithTheme,
61+
shouldMatchEmotionSnapshot,
62+
shouldMatchEmotionSnapshotWithPortal,
63+
} = makeHelpers(Wrapper, { classNameReplacer: className => className })
64+
```
65+
66+
### renderWithTheme
67+
68+
Automatically uses `CacheProvider` from `@emotion/cache`. Use it with a component, optional options & optional theme.
69+
70+
```tsx
71+
const renderWithTheme = (
72+
component: ReactNode, // The component to render
73+
options?: RenderOptions, // RenderOptions from @testing-library/react
74+
theme?: Theme, // Optional theme to use which will be passed to the Wrapper above
75+
) => ReturnType<typeof render>
76+
```
77+
78+
### shouldMatchEmotionSnapshot / shouldMatchEmotionSnapshotWithPortal
79+
80+
Internally it uses the `renderWithTheme` generated from above.
81+
82+
```tsx
83+
const shouldMatchEmotionSnapshot = (
84+
component: ReactNode, // The component to render
85+
options: { // In an object to make it backward-compatible and don't introduce any API breaking changes
86+
options?: RenderOptions // RenderOptions from @testing-library/react
87+
transform?: (node: ReturnType<typeof render>) => Promise<void> | void // (a)sync function execute between the render and the expect. You can use this if you need mockAllIsIntersecting
88+
theme?: Theme // Optional theme to use which will be passed to the Wrapper above
89+
},
90+
) => Promise<void>
91+
```

packages/jest-helpers/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@scaleway/jest-helpers",
3+
"version": "1.0.0",
4+
"description": "A package for utilities jest functions",
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"module": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"publishConfig": {
10+
"access": "public"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/scaleway/scaleway-lib",
15+
"directory": "packages/jest-helpers"
16+
},
17+
"license": "MIT",
18+
"dependencies": {
19+
"@emotion/cache": "^11.1.3",
20+
"@emotion/jest": "^11.3.0",
21+
"@emotion/react": "^11.1.4",
22+
"@testing-library/react": "^12.1.2"
23+
},
24+
"devDependencies": {
25+
"@types/react": "^17.0.27"
26+
},
27+
"peerDependencies": {
28+
"react": "^17.0.1"
29+
}
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`@jest-helpers should call tranform with shouldMatchEmotionSnapshot 1`] = `
4+
<DocumentFragment>
5+
<div
6+
id="wrapper"
7+
>
8+
<div
9+
id="test"
10+
/>
11+
</div>
12+
</DocumentFragment>
13+
`;
14+
15+
exports[`@jest-helpers should call transform with shouldMatchEmotionSnapshot 1`] = `
16+
<DocumentFragment>
17+
<div
18+
id="wrapper"
19+
>
20+
<div
21+
id="test"
22+
/>
23+
</div>
24+
</DocumentFragment>
25+
`;
26+
27+
exports[`@jest-helpers should render with renderWithTheme 1`] = `
28+
<div
29+
data-testid="test"
30+
/>
31+
`;
32+
33+
exports[`@jest-helpers should render with shouldMatchEmotionSnapshot 1`] = `
34+
<DocumentFragment>
35+
<div
36+
id="wrapper"
37+
>
38+
<div
39+
id="test"
40+
/>
41+
</div>
42+
</DocumentFragment>
43+
`;
44+
45+
exports[`@jest-helpers should render with shouldMatchEmotionSnapshotWithPortal 1`] = `
46+
<DocumentFragment>
47+
<div
48+
id="wrapper"
49+
>
50+
<div
51+
id="test"
52+
/>
53+
</div>
54+
</DocumentFragment>
55+
`;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react'
2+
import makeHelpers from ".."
3+
import { RenderWithThemeFn } from "../helpers/renderWithTheme"
4+
import { ShouldMatchEmotionSnapshotFn } from '../helpers/shouldMatchEmotionSnapshot'
5+
import { ShouldMatchEmotionSnapshotWithPortalFn } from '../helpers/shouldMatchEmotionSnapshotWithPortal'
6+
7+
describe('@jest-helpers', () => {
8+
let renderWithTheme: RenderWithThemeFn<unknown>
9+
let shouldMatchEmotionSnapshot: ShouldMatchEmotionSnapshotFn<unknown>
10+
let shouldMatchEmotionSnapshotWithPortal: ShouldMatchEmotionSnapshotWithPortalFn<unknown>
11+
12+
beforeAll(() => {
13+
const helpers = makeHelpers(({ children }) => (
14+
<div id="wrapper">
15+
{children}
16+
</div>
17+
))
18+
19+
renderWithTheme = helpers.renderWithTheme
20+
shouldMatchEmotionSnapshot = helpers.shouldMatchEmotionSnapshot
21+
shouldMatchEmotionSnapshotWithPortal = helpers.shouldMatchEmotionSnapshotWithPortal
22+
})
23+
24+
test('should render with renderWithTheme', () => {
25+
const node = renderWithTheme(<div data-testid="test" />)
26+
const element = node.getByTestId("test")
27+
28+
expect(element).toMatchSnapshot()
29+
})
30+
31+
test('should render with shouldMatchEmotionSnapshot', async () => {
32+
await shouldMatchEmotionSnapshot(<div id="test" />)
33+
})
34+
35+
test('should call tranform with shouldMatchEmotionSnapshot', async () => {
36+
const transform = jest.fn()
37+
await shouldMatchEmotionSnapshot(<div id="test" />, { transform })
38+
39+
expect(transform).toHaveBeenCalledTimes(1)
40+
})
41+
42+
test('should render with shouldMatchEmotionSnapshotWithPortal', async () => {
43+
await shouldMatchEmotionSnapshotWithPortal(<div id="test" />)
44+
})
45+
46+
it('should call transform with shouldMatchEmotionSnapshot', async () => {
47+
const transform = jest.fn()
48+
await shouldMatchEmotionSnapshotWithPortal(<div id="test" />, { transform })
49+
50+
expect(transform).toHaveBeenCalledTimes(1)
51+
})
52+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import createCache from '@emotion/cache'
2+
import { CacheProvider } from '@emotion/react'
3+
import { RenderOptions, render } from '@testing-library/react'
4+
import React, { FC, ReactNode } from 'react'
5+
6+
const emotionCache = createCache({
7+
key: 'cache',
8+
})
9+
10+
emotionCache.compat = true
11+
12+
export type RenderWithThemeFn<Theme> = (component: ReactNode, options?: RenderOptions, theme?: Theme) => ReturnType<typeof render>
13+
14+
export default function makeRenderWithTheme<Theme>(Wrapper: FC<{ theme?: Theme }>): RenderWithThemeFn<Theme> {
15+
return (component, options, theme) => render(
16+
<CacheProvider value={emotionCache}>
17+
<Wrapper theme={theme}>
18+
{component}
19+
</Wrapper>
20+
</CacheProvider>,
21+
options,
22+
)
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { RenderOptions, render } from '@testing-library/react'
2+
import { ReactNode } from 'react'
3+
import { RenderWithThemeFn } from './renderWithTheme'
4+
5+
interface Options<Theme> {
6+
options?: RenderOptions
7+
transform?: (node: ReturnType<typeof render>) => Promise<void> | void
8+
theme?: Theme
9+
}
10+
11+
export type ShouldMatchEmotionSnapshotFn<Theme> = (component: ReactNode, options?: Options<Theme>) => Promise<void>
12+
13+
export default function makeShouldMatchEmotionSnapshot<Theme>(renderWithTheme: RenderWithThemeFn<Theme>): ShouldMatchEmotionSnapshotFn<Theme> {
14+
return async (component, { options, transform, theme } = {}) => {
15+
const node = renderWithTheme(component, options, theme)
16+
if (transform) await transform(node)
17+
18+
expect(node.asFragment()).toMatchSnapshot()
19+
node.unmount()
20+
}
21+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { RenderOptions, render } from '@testing-library/react'
2+
import { ReactNode } from 'react'
3+
import { RenderWithThemeFn } from './renderWithTheme'
4+
5+
interface Options<Theme> {
6+
options?: RenderOptions
7+
transform?: (node: ReturnType<typeof render>) => Promise<void> | void
8+
theme?: Theme
9+
}
10+
11+
export type ShouldMatchEmotionSnapshotWithPortalFn<Theme> = (component: ReactNode, options?: Options<Theme>) => Promise<void>
12+
13+
export default function makeShouldMatchEmotionSnapshotWithPortal<Theme>(renderWithTheme: RenderWithThemeFn<Theme>): ShouldMatchEmotionSnapshotWithPortalFn<Theme> {
14+
return async (component, { options, transform, theme } = {}) => {
15+
// Save the instance of console (disable warning about adding element directly to document.body which is necessary when testing portal components)
16+
const { console } = global
17+
global.console = { ...console, error: jest.fn() }
18+
19+
const node = renderWithTheme(
20+
component,
21+
{
22+
container: document.body,
23+
...options,
24+
},
25+
theme,
26+
)
27+
if (transform) await transform(node)
28+
expect(node.asFragment()).toMatchSnapshot()
29+
30+
// Unmounting to don't see the warning message described above
31+
node.unmount()
32+
global.console = console
33+
}
34+
}

packages/jest-helpers/src/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { CreateSerializerOptions, createSerializer } from '@emotion/jest'
2+
import { FC } from 'react'
3+
import makeRenderWithTheme, { RenderWithThemeFn } from './helpers/renderWithTheme'
4+
import makeShouldMatchEmotionSnapshot, { ShouldMatchEmotionSnapshotFn } from './helpers/shouldMatchEmotionSnapshot'
5+
import makeShouldMatchEmotionSnapshotWithPortal, { ShouldMatchEmotionSnapshotWithPortalFn } from './helpers/shouldMatchEmotionSnapshotWithPortal'
6+
7+
export { default as makeRenderWithTheme } from './helpers/renderWithTheme'
8+
9+
type Helpers<Theme> = {
10+
renderWithTheme: RenderWithThemeFn<Theme>
11+
shouldMatchEmotionSnapshot: ShouldMatchEmotionSnapshotFn<Theme>
12+
shouldMatchEmotionSnapshotWithPortal: ShouldMatchEmotionSnapshotWithPortalFn<Theme>
13+
}
14+
15+
export default function makeHelpers<Theme>(Wrapper: FC<{ theme?: Theme }>, createSerializerOptions?: CreateSerializerOptions): Helpers<Theme> {
16+
expect.addSnapshotSerializer(
17+
createSerializer(createSerializerOptions),
18+
)
19+
20+
const renderWithTheme = makeRenderWithTheme(Wrapper)
21+
const shouldMatchEmotionSnapshot = makeShouldMatchEmotionSnapshot(renderWithTheme)
22+
const shouldMatchEmotionSnapshotWithPortal = makeShouldMatchEmotionSnapshotWithPortal(renderWithTheme)
23+
24+
return {
25+
renderWithTheme,
26+
shouldMatchEmotionSnapshot,
27+
shouldMatchEmotionSnapshotWithPortal,
28+
}
29+
}

0 commit comments

Comments
 (0)