Skip to content

Commit a9c30b9

Browse files
authored
build: upgrade storybook and add test-helper (#191)
* build: upgrade storybook and add test-helper
1 parent 6b9c09a commit a9c30b9

File tree

13 files changed

+1684
-1204
lines changed

13 files changed

+1684
-1204
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ npm-debug.log*
1616
.tern
1717
.tmp
1818
*.log
19+
test-utils.js
20+
test-utils.d.ts

.storybook/addons.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
import '@storybook/addon-options/register'
21
import '@storybook/addon-actions/register'

.storybook/config.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import React from 'react'
2-
import { addDecorator, configure } from '@storybook/react'
3-
import { withOptions } from '@storybook/addon-options'
4-
import { themes } from '@storybook/components'
5-
import pck from '../package.json'
2+
import { addParameters, configure } from '@storybook/react'
3+
import { create } from '@storybook/theming'
64
import 'intersection-observer'
75
import './base.css'
6+
import pck from '../package'
87

9-
addDecorator(
10-
withOptions({
11-
name: pck.name,
12-
url: pck.repository ? pck.repository.url : null,
13-
theme: themes.dark,
14-
}),
15-
)
8+
addParameters({
9+
options: {
10+
theme: create({
11+
base: 'dark',
12+
brandTitle: pck.name,
13+
brandUrl: pck.repository.url,
14+
}),
15+
isFullscreen: false,
16+
panelPosition: 'bottom',
17+
},
18+
})
1619

1720
/**
1821
* Use require.context to load dynamically: https://webpack.github.io/docs/context.html

.storybook/webpack.config.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
11
// webpack.config.js
2-
const ErrorOverlayPlugin = require('error-overlay-webpack-plugin')
3-
4-
module.exports = (baseConfig, env, defaultConfig) => {
5-
defaultConfig.entry = defaultConfig.entry.map(path => {
6-
if (path.includes('webpack-hot-middleware')) {
7-
return path + '&overlay=false'
8-
}
9-
return path
10-
})
11-
12-
defaultConfig.module.rules.push({
2+
module.exports = ({ config }) => {
3+
config.module.rules.push({
134
test: /\.(ts|tsx)$/,
145
loader: require.resolve('babel-loader'),
156
options: {
167
presets: [['react-app', { flow: false, typescript: true }]],
178
},
189
})
19-
defaultConfig.resolve.extensions.push('.ts', '.tsx')
20-
21-
defaultConfig.plugins.push(new ErrorOverlayPlugin())
10+
config.resolve.extensions.push('.ts', '.tsx')
2211

23-
return defaultConfig
12+
return config
2413
}

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ script:
66
- yarn lint
77
- yarn test --coverage
88
- yarn tsc
9-
- yarn build:storybook
9+
- yarn storybook:build
1010
after_success:
1111
- yarn coveralls
1212
deploy:

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,48 @@ The **`<InView />`** component also accepts the following props:
155155
| **children** | `Function`, `ReactNode` | | true | Children expects a function that receives an object contain an `inView` boolean and `ref` that should be assigned to the element root. Alternately pass a plain child, to have the `<Observer />` deal with the wrapping element. You will also get the `IntersectionObserverEntry` as `entry, giving you more details. |
156156
| **onChange** | `(inView, entry) => void` | | false | Call this function whenever the in view state changes |
157157

158+
## Testing
159+
160+
In order to write meaningful tests, the `IntersectionObserver` needs to be
161+
mocked. If you are writing your tests in Jest, you can use the included
162+
`test-utils.js`. It mocks the `IntersectionObserver`, and includes a few methods
163+
to assist with faking the `inView` state.
164+
165+
### `test-utils.js`
166+
167+
Import the methods from `react-intersection-observer/test-utils`.
168+
169+
**`mockAllIsIntersecting(isIntersecting:boolean)`**
170+
Set the `isIntersecting` on all current IntersectionObserver instances.
171+
172+
**`mockIsIntersecting(element:Element, isIntersecting:boolean)`**
173+
Set the `isIntersecting` for the IntersectionObserver of a specific element.
174+
175+
**`intersectionMockInstance(element:Element): IntersectionObserver`**
176+
Call the `intersectionMockInstance` method with an element, to get the (mocked)
177+
`IntersectionObserver` instance. You can use this to spy on the `observe` and
178+
`unobserve` methods.
179+
180+
### Test Example
181+
182+
```js
183+
import React from 'react'
184+
import { render } from 'react-testing-library'
185+
import { useInView } from 'react-intersection-observer'
186+
import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils'
187+
188+
const HookComponent = ({ options }) => {
189+
const [ref, inView] = useInView(options)
190+
return <div ref={ref}>{inView.toString()}</div>
191+
}
192+
193+
test('should create a hook inView', () => {
194+
const { getByText } = render(<HookComponent />)
195+
mockAllIsIntersecting(true)
196+
getByText('true')
197+
})
198+
```
199+
158200
## Built using `react-intersection-observer`
159201

160202
### [Sticks 'n' Sushi](https://sticksnsushi.com/en)

package.json

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
"main": "dist/react-intersection-observer.cjs.js",
66
"module": "dist/react-intersection-observer.esm.js",
77
"unpkg": "dist/react-intersection-observer.umd.min.js",
8-
"author": "Daniel Schmidt",
98
"typings": "dist/index.d.ts",
9+
"author": "Daniel Schmidt",
1010
"files": [
11-
"dist/*"
11+
"dist/*",
12+
"test-utils.js",
13+
"test-utils.d.ts"
1214
],
1315
"repository": {
1416
"type": "git",
@@ -42,14 +44,16 @@
4244
],
4345
"scripts": {
4446
"coveralls": "cat ./coverage/lcov.info | coveralls",
45-
"build": "rm -rf dist && yarn run build:lib && tsc",
47+
"prebuild": "rm -rf dist lib",
48+
"build": "run-s build:*",
4649
"build:lib": "rollup -c",
47-
"build:storybook": "build-storybook --output-dir example",
50+
"build:ts": "tsc && tsc -p tsconfig.test.json",
4851
"dev": "concurrently -k -r 'jest --watch' 'yarn run storybook'",
4952
"lint": "eslint . --ext js,ts,tsx",
5053
"preversion": "yarn build",
5154
"pretty": "prettier '**/*.{js,ts,tsx,md,json,yml,html}' --write",
5255
"storybook": "start-storybook -p 9000",
56+
"storybook:build": "build-storybook --output-dir example",
5357
"test": "jest"
5458
},
5559
"husky": {
@@ -92,6 +96,7 @@
9296
],
9397
"coveragePathIgnorePatterns": [
9498
"/node_modules/",
99+
"/src/test-utils.ts",
95100
"jest-setup.js"
96101
]
97102
},
@@ -104,31 +109,30 @@
104109
},
105110
"devDependencies": {
106111
"@babel/cli": "^7.2.3",
107-
"@babel/core": "^7.2.2",
108-
"@babel/plugin-proposal-class-properties": "^7.2.3",
109-
"@babel/plugin-transform-runtime": "^7.2.0",
110-
"@babel/preset-env": "^7.2.3",
112+
"@babel/core": "^7.3.4",
113+
"@babel/plugin-proposal-class-properties": "^7.3.4",
114+
"@babel/plugin-transform-runtime": "^7.3.4",
115+
"@babel/preset-env": "^7.3.4",
111116
"@babel/preset-flow": "^7.0.0",
112117
"@babel/preset-react": "^7.0.0",
113118
"@babel/preset-typescript": "^7.1.0",
114-
"@storybook/addon-actions": "^4.1.13",
115-
"@storybook/addon-options": "^4.1.13",
116-
"@storybook/components": "^4.1.13",
117-
"@storybook/react": "^4.1.13",
118-
"@types/jest": "^24.0.4",
119-
"@types/react": "^16.8.4",
119+
"@storybook/addon-actions": "^5.0.0",
120+
"@storybook/components": "^5.0.0",
121+
"@storybook/react": "^5.0.0",
122+
"@types/jest": "^24.0.9",
123+
"@types/react": "^16.8.6",
124+
"@types/react-dom": "^16.8.2",
120125
"@types/storybook__addon-actions": "^3.4.1",
121126
"@types/storybook__react": "^4.0.0",
122-
"@typescript-eslint/eslint-plugin": "^1.4.1",
123-
"@typescript-eslint/parser": "^1.4.1",
127+
"@typescript-eslint/eslint-plugin": "^1.4.2",
128+
"@typescript-eslint/parser": "^1.4.2",
124129
"babel-core": "^7.0.0-bridge.0",
125130
"babel-eslint": "10.0.1",
126131
"babel-jest": "^24.0.0",
127132
"babel-loader": "^8.0.5",
128133
"concurrently": "4.1.0",
129134
"coveralls": "^3.0.3",
130-
"error-overlay-webpack-plugin": "^0.1.6",
131-
"eslint": "5.15.0",
135+
"eslint": "5.15.1",
132136
"eslint-config-react-app": "^3.0.7",
133137
"eslint-plugin-flowtype": "^3.2.1",
134138
"eslint-plugin-import": "2.16.0",
@@ -138,14 +142,14 @@
138142
"intersection-observer": "^0.5.1",
139143
"jest": "^24.0.0",
140144
"jest-dom": "^3.1.1",
141-
"lint-staged": "^8.1.1",
145+
"lint-staged": "^8.1.5",
142146
"npm-run-all": "^4.1.5",
143147
"prettier": "^1.16.2",
144148
"react": "^16.8.3",
145149
"react-dom": "^16.8.3",
146150
"react-test-renderer": "^16.8.3",
147151
"react-testing-library": "^6.0.0",
148-
"rollup": "^1.2.3",
152+
"rollup": "^1.4.1",
149153
"rollup-plugin-babel": "^4.2.0",
150154
"rollup-plugin-commonjs": "^9.2.1",
151155
"rollup-plugin-node-resolve": "^4.0.1",

src/__tests__/hooks.test.js

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import React from 'react'
2-
import { act } from 'react-dom/test-utils'
3-
import { useInView } from '../useInView'
4-
import { observe, unobserve } from '../intersection'
52
import { render } from 'react-testing-library'
6-
7-
jest.mock('../intersection')
8-
9-
afterEach(() => {
10-
observe.mockReset()
11-
})
3+
import { useInView } from '../useInView'
4+
import { intersectionMockInstance, mockAllIsIntersecting } from '../test-utils'
125

136
const HookComponent = ({ options }) => {
147
const [ref, inView] = useInView(options)
15-
return <div ref={ref}>{inView.toString()}</div>
8+
return (
9+
<div data-testid="wrapper" ref={ref}>
10+
{inView.toString()}
11+
</div>
12+
)
1613
}
1714

1815
const LazyHookComponent = ({ options }) => {
@@ -23,42 +20,50 @@ const LazyHookComponent = ({ options }) => {
2320
}, [])
2421
const [ref, inView] = useInView(options)
2522
if (isLoading) return <div>Loading</div>
26-
return <div ref={ref}>{inView.toString()}</div>
23+
return (
24+
<div data-testid="wrapper" ref={ref}>
25+
{inView.toString()}
26+
</div>
27+
)
2728
}
2829

2930
test('should create a hook', () => {
30-
render(<HookComponent />)
31-
expect(observe).toHaveBeenCalled()
31+
const { getByTestId } = render(<HookComponent />)
32+
const wrapper = getByTestId('wrapper')
33+
const instance = intersectionMockInstance(wrapper)
34+
35+
expect(instance.observe).toHaveBeenCalledWith(wrapper)
3236
})
3337

3438
test('should create a lazy hook', () => {
35-
render(<LazyHookComponent />)
36-
expect(observe).toHaveBeenCalled()
39+
const { getByTestId } = render(<LazyHookComponent />)
40+
const wrapper = getByTestId('wrapper')
41+
const instance = intersectionMockInstance(wrapper)
42+
43+
expect(instance.observe).toHaveBeenCalledWith(wrapper)
3744
})
3845

3946
test('should create a hook inView', () => {
40-
observe.mockImplementation((el, callback, options) => {
41-
if (callback) callback(true, {})
42-
})
4347
const { getByText } = render(<HookComponent />)
44-
expect(observe).toHaveBeenCalled()
48+
mockAllIsIntersecting(true)
49+
4550
getByText('true')
4651
})
4752

4853
test('should respect trigger once', () => {
49-
observe.mockImplementation((el, callback) => {
50-
if (callback) callback(true, {})
51-
})
52-
render(<HookComponent options={{ triggerOnce: true }} />)
53-
expect(observe).toHaveBeenCalled()
54+
const { getByText } = render(
55+
<HookComponent options={{ triggerOnce: true }} />,
56+
)
57+
mockAllIsIntersecting(true)
58+
mockAllIsIntersecting(false)
5459

55-
act(() => {
56-
expect(unobserve).toHaveBeenCalled()
57-
})
60+
getByText('true')
5861
})
5962

6063
test('should unmount the hook', () => {
61-
const { unmount } = render(<HookComponent />)
64+
const { unmount, getByTestId } = render(<HookComponent />)
65+
const wrapper = getByTestId('wrapper')
66+
const instance = intersectionMockInstance(wrapper)
6267
unmount()
63-
expect(unobserve).toHaveBeenCalled()
68+
expect(instance.unobserve).toHaveBeenCalledWith(wrapper)
6469
})

src/intersection.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function observe(
5555
"react-intersection-observer: Trying to observe %s, but it's already being observed by another instance.\nMake sure the `ref` is only used by a single <Observer /> instance.\n\n%s",
5656
element,
5757
)
58-
58+
/* istanbul ignore if */
5959
if (!element) return
6060
// Create a unique ID for this observer instance, based on the root, root margin and threshold.
6161
// An observer with the same options can be reused, so lets use this fact
@@ -105,10 +105,13 @@ export function unobserve(element: Element | null) {
105105
let itemsLeft = false
106106
// Check if we still have observers configured with the same root.
107107
let rootObserved = false
108+
/* istanbul ignore else */
108109
if (observerId) {
109110
INSTANCE_MAP.forEach((item, key) => {
110111
if (item && key !== element) {
112+
/* istanbul ignore else */
111113
if (item.observerId === observerId) itemsLeft = true
114+
/* istanbul ignore else */
112115
if (item.observer.root === root) rootObserved = true
113116
}
114117
})
@@ -145,6 +148,7 @@ function onChange(changes: IntersectionObserverEntry[]) {
145148
const instance = INSTANCE_MAP.get(target)
146149

147150
// Firefox can report a negative intersectionRatio when scrolling.
151+
/* istanbul ignore else */
148152
if (instance && intersectionRatio >= 0) {
149153
const thresholds = instance.observer.thresholds
150154

0 commit comments

Comments
 (0)