diff --git a/custom/index.js b/custom/index.js
new file mode 100644
index 00000000..11d50e45
--- /dev/null
+++ b/custom/index.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/custom'
+module.exports = require('../lib/custom')
diff --git a/custom/pure.js b/custom/pure.js
new file mode 100644
index 00000000..c98aee61
--- /dev/null
+++ b/custom/pure.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/custom/pure'
+module.exports = require('../lib/custom/pure')
diff --git a/dom/index.js b/dom/index.js
new file mode 100644
index 00000000..5b8693bf
--- /dev/null
+++ b/dom/index.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/dom'
+module.exports = require('../lib/dom')
diff --git a/dom/pure.js b/dom/pure.js
new file mode 100644
index 00000000..c6e171cc
--- /dev/null
+++ b/dom/pure.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/dom/pure'
+module.exports = require('../lib/dom/pure')
diff --git a/native/index.js b/native/index.js
new file mode 100644
index 00000000..bc2242a6
--- /dev/null
+++ b/native/index.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/native'
+module.exports = require('../lib/native')
diff --git a/native/pure.js b/native/pure.js
new file mode 100644
index 00000000..10dc143f
--- /dev/null
+++ b/native/pure.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/native/pure'
+module.exports = require('../lib/native/pure')
diff --git a/package.json b/package.json
index 6fe490d9..5bf2527a 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,9 @@
"files": [
"lib",
"src",
+ "dom",
+ "native",
+ "server",
"pure.js",
"dont-cleanup-after-each.js"
],
@@ -61,12 +64,25 @@
"prettier-eslint": "11.0.0",
"prettier-eslint-cli": "5.0.0",
"react": "16.13.1",
+ "react-dom": "^16.13.1",
"react-test-renderer": "16.13.1"
},
"peerDependencies": {
"react": ">=16.9.0",
+ "react-dom": ">=16.9.0",
"react-test-renderer": ">=16.9.0"
},
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-test-renderer": {
+ "optional": true
+ }
+ },
"jest": {
"collectCoverage": true,
"coverageDirectory": "./coverage/",
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 00000000..21b80f69
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/server'
+module.exports = require('../lib/server')
diff --git a/server/pure.js b/server/pure.js
new file mode 100644
index 00000000..a64c1790
--- /dev/null
+++ b/server/pure.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react-hooks/server/pure'
+module.exports = require('../lib/server/pure')
diff --git a/src/asyncUtils.js b/src/core/async-utils.js
similarity index 96%
rename from src/asyncUtils.js
rename to src/core/async-utils.js
index c3cf7ab9..ab673d76 100644
--- a/src/asyncUtils.js
+++ b/src/core/async-utils.js
@@ -1,12 +1,10 @@
-import { act } from 'react-test-renderer'
-
function createTimeoutError(utilName, { timeout }) {
const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`)
timeoutError.timeout = true
return timeoutError
}
-function asyncUtils(addResolver) {
+function asyncUtils(act, addResolver) {
let nextUpdatePromise = null
const waitForNextUpdate = async (options = {}) => {
diff --git a/src/cleanup.js b/src/core/cleanup.js
similarity index 62%
rename from src/cleanup.js
rename to src/core/cleanup.js
index c240b5e1..2ed4a75a 100644
--- a/src/cleanup.js
+++ b/src/core/cleanup.js
@@ -16,4 +16,13 @@ function removeCleanup(callback) {
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
}
+cleanup.autoRegister = function () {
+ // Automatically registers cleanup in supported testing frameworks
+ if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
+ afterEach(async () => {
+ await cleanup()
+ })
+ }
+}
+
export { cleanup, addCleanup, removeCleanup }
diff --git a/src/flush-microtasks.js b/src/core/flush-microtasks.js
similarity index 100%
rename from src/flush-microtasks.js
rename to src/core/flush-microtasks.js
diff --git a/src/core/index.js b/src/core/index.js
new file mode 100644
index 00000000..114e5448
--- /dev/null
+++ b/src/core/index.js
@@ -0,0 +1,91 @@
+import asyncUtils from './async-utils'
+import { cleanup, addCleanup, removeCleanup } from './cleanup'
+
+function TestHook({ callback, setValue, setError, ...props }) {
+ try {
+ setValue(callback(props))
+ } catch (err) {
+ if (err.then) {
+ throw err
+ } else {
+ setError(err)
+ }
+ }
+ return null
+}
+
+function resultContainer() {
+ let value = null
+ let error = null
+ const resolvers = []
+
+ const result = {
+ get current() {
+ if (error) {
+ throw error
+ }
+ return value
+ },
+ get error() {
+ return error
+ }
+ }
+
+ const updateResult = (val, err) => {
+ value = val
+ error = err
+ resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
+ }
+
+ return {
+ result,
+ addResolver: (resolver) => {
+ resolvers.push(resolver)
+ },
+ setValue: (val) => updateResult(val),
+ setError: (err) => updateResult(undefined, err)
+ }
+}
+
+function defaultWrapper({ children }) {
+ return children
+}
+
+function createRenderHook(createRenderer) {
+ return function renderHook(callback, { initialProps, wrapper = defaultWrapper } = {}) {
+ const { result, setValue, setError, addResolver } = resultContainer()
+ const hookProps = { current: initialProps }
+ const props = { callback, setValue, setError }
+ const options = { initialProps, wrapper }
+
+ const { render, rerender, unmount, act, ...rendererUtils } = createRenderer(
+ TestHook,
+ props,
+ options
+ )
+
+ render(hookProps.current)
+
+ function rerenderHook(newProps = hookProps.current) {
+ hookProps.current = newProps
+ rerender(hookProps.current)
+ }
+
+ function unmountHook() {
+ removeCleanup(unmountHook)
+ unmount()
+ }
+
+ addCleanup(unmountHook)
+
+ return {
+ result,
+ rerender: rerenderHook,
+ unmount: unmountHook,
+ ...asyncUtils(act, addResolver),
+ ...rendererUtils
+ }
+ }
+}
+
+export { createRenderHook, cleanup }
diff --git a/src/custom/index.js b/src/custom/index.js
new file mode 100644
index 00000000..ca4f465a
--- /dev/null
+++ b/src/custom/index.js
@@ -0,0 +1,5 @@
+import { createCustomRenderer, cleanup } from './pure'
+
+cleanup.autoRegister()
+
+export { createCustomRenderer, cleanup }
diff --git a/src/custom/pure.js b/src/custom/pure.js
new file mode 100644
index 00000000..d6010705
--- /dev/null
+++ b/src/custom/pure.js
@@ -0,0 +1,8 @@
+import { createRenderHook, cleanup } from '../core'
+
+function createCustomRenderer(createRenderer) {
+ const renderHook = createRenderHook(createRenderer)
+ return { renderHook }
+}
+
+export { createCustomRenderer, cleanup }
diff --git a/src/dom/index.js b/src/dom/index.js
new file mode 100644
index 00000000..d18e042c
--- /dev/null
+++ b/src/dom/index.js
@@ -0,0 +1,5 @@
+import { renderHook, act, cleanup } from './pure'
+
+cleanup.autoRegister()
+
+export { renderHook, act, cleanup }
diff --git a/src/dom/pure.js b/src/dom/pure.js
new file mode 100644
index 00000000..9d6689d4
--- /dev/null
+++ b/src/dom/pure.js
@@ -0,0 +1,44 @@
+import React, { Suspense } from 'react'
+import ReactDOM from 'react-dom'
+import { act } from 'react-dom/test-utils'
+import { createRenderHook, cleanup } from '../core'
+
+function Fallback() {
+ return null
+}
+
+function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) {
+ const container = document.createElement('div')
+
+ const toRender = (props) =>
+ console.log(props) || (
+
+
+
+ )
+
+ return {
+ render(props) {
+ document.body.appendChild(container)
+ act(() => {
+ ReactDOM.render(}>{toRender(props)}, container)
+ })
+ },
+ rerender(props) {
+ act(() => {
+ ReactDOM.render(}>{toRender(props)}, container)
+ })
+ },
+ unmount() {
+ act(() => {
+ ReactDOM.unmountComponentAtNode(container)
+ })
+ document.body.removeChild(container)
+ },
+ act
+ }
+}
+
+const renderHook = createRenderHook(createRenderer)
+
+export { renderHook, act, cleanup }
diff --git a/src/index.js b/src/index.js
index c1abc074..d18e042c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,10 +1,5 @@
-import { cleanup } from './pure'
+import { renderHook, act, cleanup } from './pure'
-// Automatically registers cleanup in supported testing frameworks
-if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
- afterEach(async () => {
- await cleanup()
- })
-}
+cleanup.autoRegister()
-export * from './pure'
+export { renderHook, act, cleanup }
diff --git a/src/native/index.js b/src/native/index.js
new file mode 100644
index 00000000..d18e042c
--- /dev/null
+++ b/src/native/index.js
@@ -0,0 +1,5 @@
+import { renderHook, act, cleanup } from './pure'
+
+cleanup.autoRegister()
+
+export { renderHook, act, cleanup }
diff --git a/src/native/pure.js b/src/native/pure.js
new file mode 100644
index 00000000..c57cad5d
--- /dev/null
+++ b/src/native/pure.js
@@ -0,0 +1,39 @@
+import React, { Suspense } from 'react'
+import { act, create } from 'react-test-renderer'
+import { createRenderHook, cleanup } from '../core'
+
+function Fallback() {
+ return null
+}
+function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) {
+ let container
+
+ const toRender = (props) => (
+
+
+
+ )
+
+ return {
+ render(props) {
+ act(() => {
+ container = create(}>{toRender(props)})
+ })
+ },
+ rerender(props) {
+ act(() => {
+ container.update(}>{toRender(props)})
+ })
+ },
+ unmount() {
+ act(() => {
+ container.unmount()
+ })
+ },
+ act
+ }
+}
+
+const renderHook = createRenderHook(createRenderer)
+
+export { renderHook, act, cleanup }
diff --git a/src/pure.js b/src/pure.js
index 3b4a475d..34450820 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -1,102 +1,22 @@
-import React, { Suspense } from 'react'
-import { act, create } from 'react-test-renderer'
-import asyncUtils from './asyncUtils'
-import { cleanup, addCleanup, removeCleanup } from './cleanup'
-
-function TestHook({ callback, hookProps, onError, children }) {
+function hasDependency(name) {
try {
- children(callback(hookProps))
- } catch (err) {
- if (err.then) {
- throw err
- } else {
- onError(err)
- }
+ require(name)
+ return true
+ } catch {
+ return false
}
- return null
-}
-
-function Fallback() {
- return null
}
-function resultContainer() {
- let value = null
- let error = null
- const resolvers = []
-
- const result = {
- get current() {
- if (error) {
- throw error
- }
- return value
- },
- get error() {
- return error
- }
- }
-
- const updateResult = (val, err) => {
- value = val
- error = err
- resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
- }
-
- return {
- result,
- addResolver: (resolver) => {
- resolvers.push(resolver)
- },
- setValue: (val) => updateResult(val),
- setError: (err) => updateResult(undefined, err)
- }
-}
+const autoDetectableRenderers = [
+ { required: 'react-dom', renderer: './dom/pure' },
+ { required: 'react-test-renderer', renderer: './native/pure' }
+]
-function renderHook(callback, { initialProps, wrapper } = {}) {
- const { result, setValue, setError, addResolver } = resultContainer()
- const hookProps = { current: initialProps }
+const validRenderers = autoDetectableRenderers.filter(({ required }) => hasDependency(required))
- const wrapUiIfNeeded = (innerElement) =>
- wrapper ? React.createElement(wrapper, hookProps.current, innerElement) : innerElement
-
- const toRender = () =>
- wrapUiIfNeeded(
- }>
-
- {setValue}
-
-
- )
-
- let testRenderer
- act(() => {
- testRenderer = create(toRender())
- })
- const { unmount, update } = testRenderer
-
- function rerenderHook(newProps = hookProps.current) {
- hookProps.current = newProps
- act(() => {
- update(toRender())
- })
- }
-
- function unmountHook() {
- act(() => {
- removeCleanup(unmountHook)
- unmount()
- })
- }
-
- addCleanup(unmountHook)
-
- return {
- result,
- rerender: rerenderHook,
- unmount: unmountHook,
- ...asyncUtils(addResolver)
- }
+if (validRenderers.length === 0) {
+ const options = autoDetectableRenderers.map(({ option }) => ` - ${option}`).join('\n')
+ throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`)
}
-export { renderHook, cleanup, act }
+module.exports = require(validRenderers[0].renderer)
diff --git a/src/server/index.js b/src/server/index.js
new file mode 100644
index 00000000..d18e042c
--- /dev/null
+++ b/src/server/index.js
@@ -0,0 +1,5 @@
+import { renderHook, act, cleanup } from './pure'
+
+cleanup.autoRegister()
+
+export { renderHook, act, cleanup }
diff --git a/src/server/pure.js b/src/server/pure.js
new file mode 100644
index 00000000..07fed150
--- /dev/null
+++ b/src/server/pure.js
@@ -0,0 +1,70 @@
+import React from 'react'
+import ReactDOMServer from 'react-dom/server'
+import ReactDOM from 'react-dom'
+import { act as baseAct } from 'react-dom/test-utils'
+import { createRenderHook, cleanup } from '../core'
+
+let act
+
+function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) {
+ const container = document.createElement('div')
+
+ const toRender = (props) => (
+
+
+
+ )
+
+ let renderProps
+ let hydrated = false
+
+ act = function act(...args) {
+ if (!hydrated) {
+ throw new Error('You must hydrate the component before you can act')
+ }
+ return baseAct(...args)
+ }
+
+ return {
+ render(props) {
+ renderProps = props
+ baseAct(() => {
+ const serverOutput = ReactDOMServer.renderToString(toRender(props))
+ container.innerHTML = serverOutput
+ })
+ },
+ hydrate() {
+ if (hydrated) {
+ throw new Error('The component can only be hydrated once')
+ }
+ if (!hydrated) {
+ document.body.appendChild(container)
+ baseAct(() => {
+ ReactDOM.hydrate(toRender(renderProps), container)
+ })
+ hydrated = true
+ }
+ },
+ rerender(props) {
+ if (!hydrated) {
+ throw new Error('You must hydrate the component before you can rerender')
+ }
+ baseAct(() => {
+ ReactDOM.render(toRender(props), container)
+ })
+ },
+ unmount() {
+ if (hydrated) {
+ baseAct(() => {
+ ReactDOM.unmountComponentAtNode(container)
+ document.body.removeChild(container)
+ })
+ }
+ },
+ act
+ }
+}
+
+const renderHook = createRenderHook(createRenderer)
+
+export { renderHook, act, cleanup }
diff --git a/test/asyncHook.test.js b/test/dom/asyncHook.test.js
similarity index 99%
rename from test/asyncHook.test.js
rename to test/dom/asyncHook.test.js
index 61a58cb6..20fe9153 100644
--- a/test/asyncHook.test.js
+++ b/test/dom/asyncHook.test.js
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('async hook tests', () => {
const useSequence = (...values) => {
diff --git a/test/autoCleanup.disabled.test.js b/test/dom/autoCleanup.disabled.test.js
similarity index 92%
rename from test/autoCleanup.disabled.test.js
rename to test/dom/autoCleanup.disabled.test.js
index 7da342d5..533a0f7f 100644
--- a/test/autoCleanup.disabled.test.js
+++ b/test/dom/autoCleanup.disabled.test.js
@@ -8,7 +8,7 @@ describe('skip auto cleanup (disabled) tests', () => {
beforeAll(() => {
process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
- renderHook = require('src').renderHook
+ renderHook = require('src/dom').renderHook
})
test('first', () => {
diff --git a/test/autoCleanup.noAfterEach.test.js b/test/dom/autoCleanup.noAfterEach.test.js
similarity index 92%
rename from test/autoCleanup.noAfterEach.test.js
rename to test/dom/autoCleanup.noAfterEach.test.js
index c1f51eea..7dc1a237 100644
--- a/test/autoCleanup.noAfterEach.test.js
+++ b/test/dom/autoCleanup.noAfterEach.test.js
@@ -8,7 +8,7 @@ describe('skip auto cleanup (no afterEach) tests', () => {
beforeAll(() => {
afterEach = false
- renderHook = require('src').renderHook
+ renderHook = require('src/dom').renderHook
})
test('first', () => {
diff --git a/test/autoCleanup.test.js b/test/dom/autoCleanup.test.js
similarity index 93%
rename from test/autoCleanup.test.js
rename to test/dom/autoCleanup.test.js
index d644fe79..fcb599a2 100644
--- a/test/autoCleanup.test.js
+++ b/test/dom/autoCleanup.test.js
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
// This verifies that by importing RHTL in an
// environment which supports afterEach (like Jest)
diff --git a/test/cleanup.test.js b/test/dom/cleanup.test.js
similarity index 94%
rename from test/cleanup.test.js
rename to test/dom/cleanup.test.js
index a8c3bbba..9dbabb1b 100644
--- a/test/cleanup.test.js
+++ b/test/dom/cleanup.test.js
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
-import { renderHook, cleanup } from 'src'
+import { renderHook, cleanup } from 'src/dom'
describe('cleanup tests', () => {
test('should flush effects on cleanup', async () => {
diff --git a/test/customHook.test.js b/test/dom/customHook.test.js
similarity index 94%
rename from test/customHook.test.js
rename to test/dom/customHook.test.js
index 72dd1bac..df156f7c 100644
--- a/test/customHook.test.js
+++ b/test/dom/customHook.test.js
@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react'
-import { renderHook, act } from 'src'
+import { renderHook, act } from 'src/dom'
describe('custom hook tests', () => {
function useCounter() {
diff --git a/test/errorHook.test.js b/test/dom/errorHook.test.js
similarity index 87%
rename from test/errorHook.test.js
rename to test/dom/errorHook.test.js
index dbfb21fa..e7ced74b 100644
--- a/test/errorHook.test.js
+++ b/test/dom/errorHook.test.js
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('error hook tests', () => {
function useError(throwError) {
@@ -48,13 +48,13 @@ describe('error hook tests', () => {
})
test('should reset error', () => {
- const { result, rerender } = renderHook((throwError) => useError(throwError), {
- initialProps: true
+ const { result, rerender } = renderHook(({ throwError }) => useError(throwError), {
+ initialProps: { throwError: true }
})
expect(result.error).not.toBe(undefined)
- rerender(false)
+ rerender({ throwError: false })
expect(result.current).not.toBe(undefined)
expect(result.error).toBe(undefined)
@@ -91,17 +91,15 @@ describe('error hook tests', () => {
test('should reset async error', async () => {
const { result, waitForNextUpdate, rerender } = renderHook(
- (throwError) => useAsyncError(throwError),
- {
- initialProps: true
- }
+ ({ throwError }) => useAsyncError(throwError),
+ { initialProps: { throwError: true } }
)
await waitForNextUpdate()
expect(result.error).not.toBe(undefined)
- rerender(false)
+ rerender({ throwError: false })
await waitForNextUpdate()
@@ -137,16 +135,13 @@ describe('error hook tests', () => {
})
test('should reset effect error', () => {
- const { result, waitForNextUpdate, rerender } = renderHook(
- (throwError) => useEffectError(throwError),
- {
- initialProps: true
- }
- )
+ const { result, rerender } = renderHook(({ throwError }) => useEffectError(throwError), {
+ initialProps: { throwError: true }
+ })
expect(result.error).not.toBe(undefined)
- rerender(false)
+ rerender({ throwError: false })
expect(result.current).not.toBe(undefined)
expect(result.error).toBe(undefined)
diff --git a/test/suspenseHook.test.js b/test/dom/suspenseHook.test.js
similarity index 97%
rename from test/suspenseHook.test.js
rename to test/dom/suspenseHook.test.js
index f7ece119..2cbdf4f0 100644
--- a/test/suspenseHook.test.js
+++ b/test/dom/suspenseHook.test.js
@@ -1,4 +1,4 @@
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('suspense hook tests', () => {
const cache = {}
diff --git a/test/useContext.test.js b/test/dom/useContext.test.js
similarity index 57%
rename from test/useContext.test.js
rename to test/dom/useContext.test.js
index 2c22caca..1ffd7aad 100644
--- a/test/useContext.test.js
+++ b/test/dom/useContext.test.js
@@ -1,5 +1,5 @@
import React, { createContext, useContext } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('useContext tests', () => {
test('should get default value from context', () => {
@@ -24,39 +24,19 @@ describe('useContext tests', () => {
expect(result.current).toBe('bar')
})
- test('should update mutated value in context', () => {
- const TestContext = createContext('foo')
-
- const value = { current: 'bar' }
-
- const wrapper = ({ children }) => (
- {children}
- )
-
- const { result, rerender } = renderHook(() => useContext(TestContext), { wrapper })
-
- value.current = 'baz'
-
- rerender()
-
- expect(result.current).toBe('baz')
- })
-
test('should update value in context when props are updated', () => {
const TestContext = createContext('foo')
- const wrapper = ({ current, children }) => (
- {children}
+ const wrapper = ({ contextValue, children }) => (
+ {children}
)
const { result, rerender } = renderHook(() => useContext(TestContext), {
wrapper,
- initialProps: {
- current: 'bar'
- }
+ initialProps: { contextValue: 'bar' }
})
- rerender({ current: 'baz' })
+ rerender({ contextValue: 'baz' })
expect(result.current).toBe('baz')
})
diff --git a/test/useEffect.test.js b/test/dom/useEffect.test.js
similarity index 97%
rename from test/useEffect.test.js
rename to test/dom/useEffect.test.js
index d5c96d2f..1b609ff3 100644
--- a/test/useEffect.test.js
+++ b/test/dom/useEffect.test.js
@@ -1,5 +1,5 @@
import { useEffect, useLayoutEffect } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('useEffect tests', () => {
test('should handle useEffect hook', () => {
diff --git a/test/useMemo.test.js b/test/dom/useMemo.test.js
similarity index 97%
rename from test/useMemo.test.js
rename to test/dom/useMemo.test.js
index 3abeb855..080d8356 100644
--- a/test/useMemo.test.js
+++ b/test/dom/useMemo.test.js
@@ -1,5 +1,5 @@
import { useMemo, useCallback } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('useCallback tests', () => {
test('should handle useMemo hook', () => {
diff --git a/test/dom/useReducer.test.js b/test/dom/useReducer.test.js
new file mode 100644
index 00000000..99929026
--- /dev/null
+++ b/test/dom/useReducer.test.js
@@ -0,0 +1,18 @@
+import { useReducer } from 'react'
+import { renderHook, act } from 'src/dom'
+
+describe('useReducer tests', () => {
+ test('should handle useReducer hook', () => {
+ const reducer = (state, action) => (action.type === 'inc' ? state + 1 : state)
+ const { result } = renderHook(() => {
+ const [state, dispatch] = useReducer(reducer, 0)
+ return { state, dispatch }
+ })
+
+ expect(result.current.state).toBe(0)
+
+ act(() => result.current.dispatch({ type: 'inc' }))
+
+ expect(result.current.state).toBe(1)
+ })
+})
diff --git a/test/useRef.test.js b/test/dom/useRef.test.js
similarity index 94%
rename from test/useRef.test.js
rename to test/dom/useRef.test.js
index 63dd241d..37878417 100644
--- a/test/useRef.test.js
+++ b/test/dom/useRef.test.js
@@ -1,5 +1,5 @@
import { useRef, useImperativeHandle } from 'react'
-import { renderHook } from 'src'
+import { renderHook } from 'src/dom'
describe('useHook tests', () => {
test('should handle useRef hook', () => {
diff --git a/test/dom/useState.test.js b/test/dom/useState.test.js
new file mode 100644
index 00000000..ae61d0bc
--- /dev/null
+++ b/test/dom/useState.test.js
@@ -0,0 +1,24 @@
+import { useState } from 'react'
+import { renderHook, act } from 'src/dom'
+
+describe('useState tests', () => {
+ test('should use state value', () => {
+ const { result } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ expect(result.current.value).toBe('foo')
+ })
+
+ test('should update state value using setter', () => {
+ const { result } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ act(() => result.current.setValue('bar'))
+
+ expect(result.current.value).toBe('bar')
+ })
+})
diff --git a/test/native/asyncHook.test.js b/test/native/asyncHook.test.js
new file mode 100644
index 00000000..1a9f298c
--- /dev/null
+++ b/test/native/asyncHook.test.js
@@ -0,0 +1,217 @@
+import { useState, useRef, useEffect } from 'react'
+import { renderHook } from 'src/native'
+
+describe('async hook tests', () => {
+ const useSequence = (...values) => {
+ const [first, ...otherValues] = values
+ const [value, setValue] = useState(first)
+ const index = useRef(0)
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setValue(otherValues[index.current++])
+ if (index.current === otherValues.length) {
+ clearInterval(interval)
+ }
+ }, 50)
+ return () => {
+ clearInterval(interval)
+ }
+ }, [...values])
+
+ return value
+ }
+
+ test('should wait for next update', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('second')
+ })
+
+ test('should wait for multiple updates', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('second')
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('third')
+ })
+
+ test('should resolve all when updating', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()])
+
+ expect(result.current).toBe('second')
+ })
+
+ test('should reject if timeout exceeded when waiting for next update', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow(
+ Error('Timed out in waitForNextUpdate after 10ms.')
+ )
+ })
+
+ test('should wait for expectation to pass', async () => {
+ const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ let complete = false
+ await wait(() => {
+ expect(result.current).toBe('third')
+ complete = true
+ })
+ expect(complete).toBe(true)
+ })
+
+ test('should not hang if expectation is already passing', async () => {
+ const { result, wait } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ let complete = false
+ await wait(() => {
+ expect(result.current).toBe('first')
+ complete = true
+ })
+ expect(complete).toBe(true)
+ })
+
+ test('should reject if callback throws error', async () => {
+ const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ wait(
+ () => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current === 'third'
+ },
+ {
+ suppressErrors: false
+ }
+ )
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
+
+ test('should reject if callback immediately throws error', async () => {
+ const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ wait(
+ () => {
+ throw new Error('Something Unexpected')
+ },
+ {
+ suppressErrors: false
+ }
+ )
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
+
+ test('should wait for truthy value', async () => {
+ const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ await wait(() => result.current === 'third')
+
+ expect(result.current).toBe('third')
+ })
+
+ test('should reject if timeout exceeded when waiting for expectation to pass', async () => {
+ const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ wait(
+ () => {
+ expect(result.current).toBe('third')
+ },
+ { timeout: 75 }
+ )
+ ).rejects.toThrow(Error('Timed out in wait after 75ms.'))
+ })
+
+ test('should wait for value to change', async () => {
+ const { result, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ await waitForValueToChange(() => result.current === 'third')
+
+ expect(result.current).toBe('third')
+ })
+
+ test('should reject if timeout exceeded when waiting for value to change', async () => {
+ const { result, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ waitForValueToChange(() => result.current === 'third', {
+ timeout: 75
+ })
+ ).rejects.toThrow(Error('Timed out in waitForValueToChange after 75ms.'))
+ })
+
+ test('should reject if selector throws error', async () => {
+ const { result, waitForValueToChange } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ waitForValueToChange(() => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current
+ })
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
+
+ test('should not reject if selector throws error and suppress errors option is enabled', async () => {
+ const { result, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ await waitForValueToChange(
+ () => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current === 'third'
+ },
+ { suppressErrors: true }
+ )
+
+ expect(result.current).toBe('third')
+ })
+})
diff --git a/test/native/autoCleanup.disabled.test.js b/test/native/autoCleanup.disabled.test.js
new file mode 100644
index 00000000..a3498f3b
--- /dev/null
+++ b/test/native/autoCleanup.disabled.test.js
@@ -0,0 +1,28 @@
+import { useEffect } from 'react'
+
+// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
+// then we DON'T auto-wire up the afterEach for folks
+describe('skip auto cleanup (disabled) tests', () => {
+ let cleanupCalled = false
+ let renderHook
+
+ beforeAll(() => {
+ process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
+ renderHook = require('src/native').renderHook
+ })
+
+ test('first', () => {
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+ renderHook(() => hookWithCleanup())
+ })
+
+ test('second', () => {
+ expect(cleanupCalled).toBe(false)
+ })
+})
diff --git a/test/native/autoCleanup.noAfterEach.test.js b/test/native/autoCleanup.noAfterEach.test.js
new file mode 100644
index 00000000..363e02c9
--- /dev/null
+++ b/test/native/autoCleanup.noAfterEach.test.js
@@ -0,0 +1,28 @@
+import { useEffect } from 'react'
+
+// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
+// then we DON'T auto-wire up the afterEach for folks
+describe('skip auto cleanup (no afterEach) tests', () => {
+ let cleanupCalled = false
+ let renderHook
+
+ beforeAll(() => {
+ afterEach = false
+ renderHook = require('src/native').renderHook
+ })
+
+ test('first', () => {
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+ renderHook(() => hookWithCleanup())
+ })
+
+ test('second', () => {
+ expect(cleanupCalled).toBe(false)
+ })
+})
diff --git a/test/native/autoCleanup.test.js b/test/native/autoCleanup.test.js
new file mode 100644
index 00000000..8511b966
--- /dev/null
+++ b/test/native/autoCleanup.test.js
@@ -0,0 +1,24 @@
+import { useEffect } from 'react'
+import { renderHook } from 'src/native'
+
+// This verifies that by importing RHTL in an
+// environment which supports afterEach (like Jest)
+// we'll get automatic cleanup between tests.
+describe('auto cleanup tests', () => {
+ let cleanupCalled = false
+
+ test('first', () => {
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+ renderHook(() => hookWithCleanup())
+ })
+
+ test('second', () => {
+ expect(cleanupCalled).toBe(true)
+ })
+})
diff --git a/test/native/cleanup.test.js b/test/native/cleanup.test.js
new file mode 100644
index 00000000..42481f42
--- /dev/null
+++ b/test/native/cleanup.test.js
@@ -0,0 +1,41 @@
+import { useEffect } from 'react'
+import { renderHook, cleanup } from 'src/native'
+
+describe('cleanup tests', () => {
+ test('should flush effects on cleanup', async () => {
+ let cleanupCalled = false
+
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+
+ renderHook(() => hookWithCleanup())
+
+ await cleanup()
+
+ expect(cleanupCalled).toBe(true)
+ })
+
+ test('should cleanup all rendered hooks', async () => {
+ let cleanupCalled = []
+ const hookWithCleanup = (id) => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled[id] = true
+ }
+ })
+ }
+
+ renderHook(() => hookWithCleanup(1))
+ renderHook(() => hookWithCleanup(2))
+
+ await cleanup()
+
+ expect(cleanupCalled[1]).toBe(true)
+ expect(cleanupCalled[2]).toBe(true)
+ })
+})
diff --git a/test/native/customHook.test.js b/test/native/customHook.test.js
new file mode 100644
index 00000000..ddf2106f
--- /dev/null
+++ b/test/native/customHook.test.js
@@ -0,0 +1,29 @@
+import { useState, useCallback } from 'react'
+import { renderHook, act } from 'src/native'
+
+describe('custom hook tests', () => {
+ function useCounter() {
+ const [count, setCount] = useState(0)
+
+ const increment = useCallback(() => setCount(count + 1), [count])
+ const decrement = useCallback(() => setCount(count - 1), [count])
+
+ return { count, increment, decrement }
+ }
+
+ test('should increment counter', () => {
+ const { result } = renderHook(() => useCounter())
+
+ act(() => result.current.increment())
+
+ expect(result.current.count).toBe(1)
+ })
+
+ test('should decrement counter', () => {
+ const { result } = renderHook(() => useCounter())
+
+ act(() => result.current.decrement())
+
+ expect(result.current.count).toBe(-1)
+ })
+})
diff --git a/test/native/errorHook.test.js b/test/native/errorHook.test.js
new file mode 100644
index 00000000..a465a8e2
--- /dev/null
+++ b/test/native/errorHook.test.js
@@ -0,0 +1,150 @@
+import { useState, useEffect } from 'react'
+import { renderHook } from 'src/native'
+
+describe('error hook tests', () => {
+ function useError(throwError) {
+ if (throwError) {
+ throw new Error('expected')
+ }
+ return true
+ }
+
+ function useAsyncError(throwError) {
+ const [value, setValue] = useState()
+ useEffect(() => {
+ const timeout = setTimeout(() => setValue(throwError), 100)
+ return () => clearTimeout(timeout)
+ }, [throwError])
+ return useError(value)
+ }
+
+ function useEffectError(throwError) {
+ useEffect(() => {
+ useError(throwError)
+ }, [])
+ return true
+ }
+
+ describe('synchronous', () => {
+ test('should raise error', () => {
+ const { result } = renderHook(() => useError(true))
+
+ expect(() => {
+ expect(result.current).not.toBe(undefined)
+ }).toThrow(Error('expected'))
+ })
+
+ test('should capture error', () => {
+ const { result } = renderHook(() => useError(true))
+
+ expect(result.error).toEqual(Error('expected'))
+ })
+
+ test('should not capture error', () => {
+ const { result } = renderHook(() => useError(false))
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+
+ test('should reset error', () => {
+ const { result, rerender } = renderHook(({ throwError }) => useError(throwError), {
+ initialProps: { throwError: true }
+ })
+
+ expect(result.error).not.toBe(undefined)
+
+ rerender({ throwError: false })
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+ })
+
+ describe('asynchronous', () => {
+ test('should raise async error', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true))
+
+ await waitForNextUpdate()
+
+ expect(() => {
+ expect(result.current).not.toBe(undefined)
+ }).toThrow(Error('expected'))
+ })
+
+ test('should capture async error', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true))
+
+ await waitForNextUpdate()
+
+ expect(result.error).toEqual(Error('expected'))
+ })
+
+ test('should not capture async error', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useAsyncError(false))
+
+ await waitForNextUpdate()
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+
+ test('should reset async error', async () => {
+ const { result, waitForNextUpdate, rerender } = renderHook(
+ ({ throwError }) => useAsyncError(throwError),
+ { initialProps: { throwError: true } }
+ )
+
+ await waitForNextUpdate()
+
+ expect(result.error).not.toBe(undefined)
+
+ rerender({ throwError: false })
+
+ await waitForNextUpdate()
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+ })
+
+ /*
+ These tests capture error cases that are not currently being caught successfully.
+ Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308
+ for more details.
+ */
+ describe.skip('effect', () => {
+ test('should raise effect error', () => {
+ const { result } = renderHook(() => useEffectError(true))
+
+ expect(() => {
+ expect(result.current).not.toBe(undefined)
+ }).toThrow(Error('expected'))
+ })
+
+ test('should capture effect error', () => {
+ const { result } = renderHook(() => useEffectError(true))
+ expect(result.error).toEqual(Error('expected'))
+ })
+
+ test('should not capture effect error', () => {
+ const { result } = renderHook(() => useEffectError(false))
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+
+ test('should reset effect error', () => {
+ const { result, rerender } = renderHook(({ throwError }) => useEffectError(throwError), {
+ initialProps: { throwError: true }
+ })
+
+ expect(result.error).not.toBe(undefined)
+
+ rerender({ throwError: false })
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+ })
+})
diff --git a/test/native/suspenseHook.test.js b/test/native/suspenseHook.test.js
new file mode 100644
index 00000000..25059b50
--- /dev/null
+++ b/test/native/suspenseHook.test.js
@@ -0,0 +1,49 @@
+import { renderHook } from 'src/native'
+
+describe('suspense hook tests', () => {
+ const cache = {}
+ const fetchName = (isSuccessful) => {
+ if (!cache.value) {
+ cache.value = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ if (isSuccessful) {
+ resolve('Bob')
+ } else {
+ reject(new Error('Failed to fetch name'))
+ }
+ }, 50)
+ })
+ .then((value) => (cache.value = value))
+ .catch((e) => (cache.value = e))
+ }
+ return cache.value
+ }
+
+ const useFetchName = (isSuccessful = true) => {
+ const name = fetchName(isSuccessful)
+ if (typeof name.then === 'function' || name instanceof Error) {
+ throw name
+ }
+ return name
+ }
+
+ beforeEach(() => {
+ delete cache.value
+ })
+
+ test('should allow rendering to be suspended', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useFetchName(true))
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('Bob')
+ })
+
+ test('should set error if suspense promise rejects', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useFetchName(false))
+
+ await waitForNextUpdate()
+
+ expect(result.error).toEqual(new Error('Failed to fetch name'))
+ })
+})
diff --git a/test/native/useContext.test.js b/test/native/useContext.test.js
new file mode 100644
index 00000000..3ecf1e7f
--- /dev/null
+++ b/test/native/useContext.test.js
@@ -0,0 +1,43 @@
+import React, { createContext, useContext } from 'react'
+import { renderHook } from 'src/native'
+
+describe('useContext tests', () => {
+ test('should get default value from context', () => {
+ const TestContext = createContext('foo')
+
+ const { result } = renderHook(() => useContext(TestContext))
+
+ const value = result.current
+
+ expect(value).toBe('foo')
+ })
+
+ test('should get value from context provider', () => {
+ const TestContext = createContext('foo')
+
+ const wrapper = ({ children }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useContext(TestContext), { wrapper })
+
+ expect(result.current).toBe('bar')
+ })
+
+ test('should update value in context when props are updated', () => {
+ const TestContext = createContext('foo')
+
+ const wrapper = ({ contextValue, children }) => (
+ {children}
+ )
+
+ const { result, rerender } = renderHook(() => useContext(TestContext), {
+ wrapper,
+ initialProps: { contextValue: 'bar' }
+ })
+
+ rerender({ contextValue: 'baz' })
+
+ expect(result.current).toBe('baz')
+ })
+})
diff --git a/test/native/useEffect.test.js b/test/native/useEffect.test.js
new file mode 100644
index 00000000..5714b146
--- /dev/null
+++ b/test/native/useEffect.test.js
@@ -0,0 +1,62 @@
+import { useEffect, useLayoutEffect } from 'react'
+import { renderHook } from 'src/native'
+
+describe('useEffect tests', () => {
+ test('should handle useEffect hook', () => {
+ const sideEffect = { [1]: false, [2]: false }
+
+ const { rerender, unmount } = renderHook(
+ ({ id }) => {
+ useEffect(() => {
+ sideEffect[id] = true
+ return () => {
+ sideEffect[id] = false
+ }
+ }, [id])
+ },
+ { initialProps: { id: 1 } }
+ )
+
+ expect(sideEffect[1]).toBe(true)
+ expect(sideEffect[2]).toBe(false)
+
+ rerender({ id: 2 })
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(true)
+
+ unmount()
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(false)
+ })
+
+ test('should handle useLayoutEffect hook', () => {
+ const sideEffect = { [1]: false, [2]: false }
+
+ const { rerender, unmount } = renderHook(
+ ({ id }) => {
+ useLayoutEffect(() => {
+ sideEffect[id] = true
+ return () => {
+ sideEffect[id] = false
+ }
+ }, [id])
+ },
+ { initialProps: { id: 1 } }
+ )
+
+ expect(sideEffect[1]).toBe(true)
+ expect(sideEffect[2]).toBe(false)
+
+ rerender({ id: 2 })
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(true)
+
+ unmount()
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(false)
+ })
+})
diff --git a/test/native/useMemo.test.js b/test/native/useMemo.test.js
new file mode 100644
index 00000000..ce043ea5
--- /dev/null
+++ b/test/native/useMemo.test.js
@@ -0,0 +1,64 @@
+import { useMemo, useCallback } from 'react'
+import { renderHook } from 'src/native'
+
+describe('useCallback tests', () => {
+ test('should handle useMemo hook', () => {
+ const { result, rerender } = renderHook(({ value }) => useMemo(() => ({ value }), [value]), {
+ initialProps: { value: 1 }
+ })
+
+ const value1 = result.current
+
+ expect(value1).toEqual({ value: 1 })
+
+ rerender()
+
+ const value2 = result.current
+
+ expect(value2).toEqual({ value: 1 })
+
+ expect(value2).toBe(value1)
+
+ rerender({ value: 2 })
+
+ const value3 = result.current
+
+ expect(value3).toEqual({ value: 2 })
+
+ expect(value3).not.toBe(value1)
+ })
+
+ test('should handle useCallback hook', () => {
+ const { result, rerender } = renderHook(
+ ({ value }) => {
+ const callback = () => ({ value })
+ return useCallback(callback, [value])
+ },
+ { initialProps: { value: 1 } }
+ )
+
+ const callback1 = result.current
+
+ const calbackValue1 = callback1()
+
+ expect(calbackValue1).toEqual({ value: 1 })
+
+ const callback2 = result.current
+
+ const calbackValue2 = callback2()
+
+ expect(calbackValue2).toEqual({ value: 1 })
+
+ expect(callback2).toBe(callback1)
+
+ rerender({ value: 2 })
+
+ const callback3 = result.current
+
+ const calbackValue3 = callback3()
+
+ expect(calbackValue3).toEqual({ value: 2 })
+
+ expect(callback3).not.toBe(callback1)
+ })
+})
diff --git a/test/native/useReducer.test.js b/test/native/useReducer.test.js
new file mode 100644
index 00000000..305ae656
--- /dev/null
+++ b/test/native/useReducer.test.js
@@ -0,0 +1,18 @@
+import { useReducer } from 'react'
+import { renderHook, act } from 'src/native'
+
+describe('useReducer tests', () => {
+ test('should handle useReducer hook', () => {
+ const reducer = (state, action) => (action.type === 'inc' ? state + 1 : state)
+ const { result } = renderHook(() => {
+ const [state, dispatch] = useReducer(reducer, 0)
+ return { state, dispatch }
+ })
+
+ expect(result.current.state).toBe(0)
+
+ act(() => result.current.dispatch({ type: 'inc' }))
+
+ expect(result.current.state).toBe(1)
+ })
+})
diff --git a/test/native/useRef.test.js b/test/native/useRef.test.js
new file mode 100644
index 00000000..9955dfda
--- /dev/null
+++ b/test/native/useRef.test.js
@@ -0,0 +1,27 @@
+import { useRef, useImperativeHandle } from 'react'
+import { renderHook } from 'src/native'
+
+describe('useHook tests', () => {
+ test('should handle useRef hook', () => {
+ const { result } = renderHook(() => useRef())
+
+ const refContainer = result.current
+
+ expect(Object.keys(refContainer)).toEqual(['current'])
+ expect(refContainer.current).toBeUndefined()
+ })
+
+ test('should handle useImperativeHandle hook', () => {
+ const { result } = renderHook(() => {
+ const ref = useRef()
+ useImperativeHandle(ref, () => ({
+ fakeImperativeMethod: () => true
+ }))
+ return ref
+ })
+
+ const refContainer = result.current
+
+ expect(refContainer.current.fakeImperativeMethod()).toBe(true)
+ })
+})
diff --git a/test/native/useState.test.js b/test/native/useState.test.js
new file mode 100644
index 00000000..b59e64cd
--- /dev/null
+++ b/test/native/useState.test.js
@@ -0,0 +1,24 @@
+import { useState } from 'react'
+import { renderHook, act } from 'src/native'
+
+describe('useState tests', () => {
+ test('should use state value', () => {
+ const { result } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ expect(result.current.value).toBe('foo')
+ })
+
+ test('should update state value using setter', () => {
+ const { result } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ act(() => result.current.setValue('bar'))
+
+ expect(result.current.value).toBe('bar')
+ })
+})
diff --git a/test/server/asyncHook.test.js b/test/server/asyncHook.test.js
new file mode 100644
index 00000000..a8d4a7e9
--- /dev/null
+++ b/test/server/asyncHook.test.js
@@ -0,0 +1,277 @@
+import { useState, useRef, useEffect } from 'react'
+import { renderHook } from 'src/server'
+
+describe('async hook tests', () => {
+ const useSequence = (...values) => {
+ const [first, ...otherValues] = values
+ const [value, setValue] = useState(first)
+ const index = useRef(0)
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setValue(otherValues[index.current++])
+ if (index.current === otherValues.length) {
+ clearInterval(interval)
+ }
+ }, 50)
+ return () => {
+ clearInterval(interval)
+ }
+ }, [...values])
+
+ return value
+ }
+
+ test('should wait for next update', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('second')
+ })
+
+ test('should wait for multiple updates', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('second')
+
+ await waitForNextUpdate()
+
+ expect(result.current).toBe('third')
+ })
+
+ test('should resolve all when updating', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()])
+
+ expect(result.current).toBe('second')
+ })
+
+ test('should reject if timeout exceeded when waiting for next update', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow(
+ Error('Timed out in waitForNextUpdate after 10ms.')
+ )
+ })
+
+ test('should wait for expectation to pass', async () => {
+ const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ let complete = false
+ await wait(() => {
+ expect(result.current).toBe('third')
+ complete = true
+ })
+ expect(complete).toBe(true)
+ })
+
+ test('should not hang if expectation is already passing', async () => {
+ const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ let complete = false
+ await wait(() => {
+ expect(result.current).toBe('first')
+ complete = true
+ })
+ expect(complete).toBe(true)
+ })
+
+ test('should reject if callback throws error', async () => {
+ const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ wait(
+ () => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current === 'third'
+ },
+ {
+ suppressErrors: false
+ }
+ )
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
+
+ test('should reject if callback immediately throws error', async () => {
+ const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ wait(
+ () => {
+ throw new Error('Something Unexpected')
+ },
+ {
+ suppressErrors: false
+ }
+ )
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
+
+ test('should wait for truthy value', async () => {
+ const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await wait(() => result.current === 'third')
+
+ expect(result.current).toBe('third')
+ })
+
+ test('should reject if timeout exceeded when waiting for expectation to pass', async () => {
+ const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third'))
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ wait(
+ () => {
+ expect(result.current).toBe('third')
+ },
+ { timeout: 75 }
+ )
+ ).rejects.toThrow(Error('Timed out in wait after 75ms.'))
+ })
+
+ test('should wait for value to change', async () => {
+ const { result, hydrate, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await waitForValueToChange(() => result.current === 'third')
+
+ expect(result.current).toBe('third')
+ })
+
+ test('should reject if timeout exceeded when waiting for value to change', async () => {
+ const { result, hydrate, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ waitForValueToChange(() => result.current === 'third', {
+ timeout: 75
+ })
+ ).rejects.toThrow(Error('Timed out in waitForValueToChange after 75ms.'))
+ })
+
+ test('should reject if selector throws error', async () => {
+ const { result, hydrate, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second')
+ )
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await expect(
+ waitForValueToChange(() => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current
+ })
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
+
+ test('should not reject if selector throws error and suppress errors option is enabled', async () => {
+ const { result, hydrate, waitForValueToChange } = renderHook(() =>
+ useSequence('first', 'second', 'third')
+ )
+
+ expect(result.current).toBe('first')
+
+ hydrate()
+
+ expect(result.current).toBe('first')
+
+ await waitForValueToChange(
+ () => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current === 'third'
+ },
+ { suppressErrors: true }
+ )
+
+ expect(result.current).toBe('third')
+ })
+})
diff --git a/test/server/autoCleanup.disabled.test.js b/test/server/autoCleanup.disabled.test.js
new file mode 100644
index 00000000..d94e643c
--- /dev/null
+++ b/test/server/autoCleanup.disabled.test.js
@@ -0,0 +1,28 @@
+import { useEffect } from 'react'
+
+// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
+// then we DON'T auto-wire up the afterEach for folks
+describe('skip auto cleanup (disabled) tests', () => {
+ let cleanupCalled = false
+ let renderHook
+
+ beforeAll(() => {
+ process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
+ renderHook = require('src/server').renderHook
+ })
+
+ test('first', () => {
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+ renderHook(() => hookWithCleanup())
+ })
+
+ test('second', () => {
+ expect(cleanupCalled).toBe(false)
+ })
+})
diff --git a/test/server/autoCleanup.noAfterEach.test.js b/test/server/autoCleanup.noAfterEach.test.js
new file mode 100644
index 00000000..d4f30a80
--- /dev/null
+++ b/test/server/autoCleanup.noAfterEach.test.js
@@ -0,0 +1,28 @@
+import { useEffect } from 'react'
+
+// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
+// then we DON'T auto-wire up the afterEach for folks
+describe('skip auto cleanup (no afterEach) tests', () => {
+ let cleanupCalled = false
+ let renderHook
+
+ beforeAll(() => {
+ afterEach = false
+ renderHook = require('src/server').renderHook
+ })
+
+ test('first', () => {
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+ renderHook(() => hookWithCleanup())
+ })
+
+ test('second', () => {
+ expect(cleanupCalled).toBe(false)
+ })
+})
diff --git a/test/server/autoCleanup.test.js b/test/server/autoCleanup.test.js
new file mode 100644
index 00000000..67428422
--- /dev/null
+++ b/test/server/autoCleanup.test.js
@@ -0,0 +1,32 @@
+import { useEffect } from 'react'
+import { renderHook } from 'src/server'
+
+// This verifies that by importing RHTL in an
+// environment which supports afterEach (like Jest)
+// we'll get automatic cleanup between tests.
+describe('auto cleanup tests', () => {
+ const cleanups = {
+ ssr: false,
+ hydrated: false
+ }
+
+ test('first', () => {
+ const hookWithCleanup = (name) => {
+ useEffect(() => {
+ return () => {
+ cleanups[name] = true
+ }
+ })
+ }
+
+ renderHook(() => hookWithCleanup('ssr'))
+
+ const { hydrate } = renderHook(() => hookWithCleanup('hydrated'))
+ hydrate()
+ })
+
+ test('second', () => {
+ expect(cleanups.ssr).toBe(false)
+ expect(cleanups.hydrated).toBe(true)
+ })
+})
diff --git a/test/server/cleanup.test.js b/test/server/cleanup.test.js
new file mode 100644
index 00000000..fb7f6d7d
--- /dev/null
+++ b/test/server/cleanup.test.js
@@ -0,0 +1,67 @@
+import { useEffect } from 'react'
+import { renderHook, cleanup } from 'src/server'
+
+describe('cleanup tests', () => {
+ test('should flush effects on cleanup', async () => {
+ let cleanupCalled = false
+
+ const hookWithCleanup = () => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled = true
+ }
+ })
+ }
+
+ const { hydrate } = renderHook(() => hookWithCleanup())
+
+ hydrate()
+
+ await cleanup()
+
+ expect(cleanupCalled).toBe(true)
+ })
+
+ test('should cleanup all rendered hooks', async () => {
+ let cleanupCalled = []
+ const hookWithCleanup = (id) => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled[id] = true
+ }
+ })
+ }
+
+ const { hydrate: hydrate1 } = renderHook(() => hookWithCleanup(1))
+ const { hydrate: hydrate2 } = renderHook(() => hookWithCleanup(2))
+
+ hydrate1()
+ hydrate2()
+
+ await cleanup()
+
+ expect(cleanupCalled[1]).toBe(true)
+ expect(cleanupCalled[2]).toBe(true)
+ })
+
+ test('should only cleanup hydrated hooks', async () => {
+ let cleanupCalled = [false, false]
+ const hookWithCleanup = (id) => {
+ useEffect(() => {
+ return () => {
+ cleanupCalled[id] = true
+ }
+ })
+ }
+
+ renderHook(() => hookWithCleanup(1))
+ const { hydrate } = renderHook(() => hookWithCleanup(2))
+
+ hydrate()
+
+ await cleanup()
+
+ expect(cleanupCalled[1]).toBe(false)
+ expect(cleanupCalled[2]).toBe(true)
+ })
+})
diff --git a/test/server/customHook.test.js b/test/server/customHook.test.js
new file mode 100644
index 00000000..b699b7d8
--- /dev/null
+++ b/test/server/customHook.test.js
@@ -0,0 +1,33 @@
+import { useState, useCallback } from 'react'
+import { renderHook, act } from 'src/server'
+
+describe('custom hook tests', () => {
+ function useCounter() {
+ const [count, setCount] = useState(0)
+
+ const increment = useCallback(() => setCount(count + 1), [count])
+ const decrement = useCallback(() => setCount(count - 1), [count])
+
+ return { count, increment, decrement }
+ }
+
+ test('should increment counter', () => {
+ const { result, hydrate } = renderHook(() => useCounter())
+
+ hydrate()
+
+ act(() => result.current.increment())
+
+ expect(result.current.count).toBe(1)
+ })
+
+ test('should decrement counter', () => {
+ const { result, hydrate } = renderHook(() => useCounter())
+
+ hydrate()
+
+ act(() => result.current.decrement())
+
+ expect(result.current.count).toBe(-1)
+ })
+})
diff --git a/test/server/errorHook.test.js b/test/server/errorHook.test.js
new file mode 100644
index 00000000..616a17c4
--- /dev/null
+++ b/test/server/errorHook.test.js
@@ -0,0 +1,170 @@
+import { useState, useEffect } from 'react'
+import { renderHook } from 'src/server'
+
+describe('error hook tests', () => {
+ function useError(throwError) {
+ if (throwError) {
+ throw new Error('expected')
+ }
+ return true
+ }
+
+ function useAsyncError(throwError) {
+ const [value, setValue] = useState()
+ useEffect(() => {
+ const timeout = setTimeout(() => setValue(throwError), 100)
+ return () => clearTimeout(timeout)
+ }, [throwError])
+ return useError(value)
+ }
+
+ function useEffectError(throwError) {
+ useEffect(() => {
+ useError(throwError)
+ }, [])
+ return true
+ }
+
+ describe('synchronous', () => {
+ test('should raise error', () => {
+ const { result } = renderHook(() => useError(true))
+
+ expect(() => {
+ expect(result.current).not.toBe(undefined)
+ }).toThrow(Error('expected'))
+ })
+
+ test('should capture error', () => {
+ const { result } = renderHook(() => useError(true))
+
+ expect(result.error).toEqual(Error('expected'))
+ })
+
+ test('should not capture error', () => {
+ const { result } = renderHook(() => useError(false))
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+
+ test('should reset error', () => {
+ const { result, hydrate, rerender } = renderHook(({ throwError }) => useError(throwError), {
+ initialProps: { throwError: true }
+ })
+
+ expect(result.error).not.toBe(undefined)
+
+ hydrate()
+
+ rerender({ throwError: false })
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+ })
+
+ describe('asynchronous', () => {
+ test('should raise async error', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncError(true))
+
+ hydrate()
+
+ await waitForNextUpdate()
+
+ expect(() => {
+ expect(result.current).not.toBe(undefined)
+ }).toThrow(Error('expected'))
+ })
+
+ test('should capture async error', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncError(true))
+
+ hydrate()
+
+ await waitForNextUpdate()
+
+ expect(result.error).toEqual(Error('expected'))
+ })
+
+ test('should not capture async error', async () => {
+ const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncError(false))
+
+ hydrate()
+
+ await waitForNextUpdate()
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+
+ test('should reset async error', async () => {
+ const { result, hydrate, waitForNextUpdate, rerender } = renderHook(
+ ({ throwError }) => useAsyncError(throwError),
+ { initialProps: { throwError: true } }
+ )
+
+ hydrate()
+
+ await waitForNextUpdate()
+
+ expect(result.error).not.toBe(undefined)
+
+ rerender({ throwError: false })
+
+ await waitForNextUpdate()
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+ })
+
+ /*
+ These tests capture error cases that are not currently being caught successfully.
+ Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308
+ for more details.
+ */
+ describe.skip('effect', () => {
+ test('should raise effect error', () => {
+ const { result, hydrate } = renderHook(() => useEffectError(true))
+
+ hydrate()
+
+ expect(() => {
+ expect(result.current).not.toBe(undefined)
+ }).toThrow(Error('expected'))
+ })
+
+ test('should capture effect error', () => {
+ const { result, hydrate } = renderHook(() => useEffectError(true))
+
+ hydrate()
+
+ expect(result.error).toEqual(Error('expected'))
+ })
+
+ test('should not capture effect error', () => {
+ const { result, hydrate } = renderHook(() => useEffectError(false))
+
+ hydrate()
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+
+ test('should reset effect error', () => {
+ const { result, hydrate, rerender } = renderHook(
+ ({ throwError }) => useEffectError(throwError),
+ { initialProps: { throwError: true } }
+ )
+
+ hydrate()
+
+ expect(result.error).not.toBe(undefined)
+
+ rerender({ throwError: false })
+
+ expect(result.current).not.toBe(undefined)
+ expect(result.error).toBe(undefined)
+ })
+ })
+})
diff --git a/test/server/useContext.test.js b/test/server/useContext.test.js
new file mode 100644
index 00000000..2934d7f0
--- /dev/null
+++ b/test/server/useContext.test.js
@@ -0,0 +1,45 @@
+import React, { createContext, useContext } from 'react'
+import { renderHook } from 'src/server'
+
+describe('useContext tests', () => {
+ test('should get default value from context', () => {
+ const TestContext = createContext('foo')
+
+ const { result } = renderHook(() => useContext(TestContext))
+
+ const value = result.current
+
+ expect(value).toBe('foo')
+ })
+
+ test('should get value from context provider', () => {
+ const TestContext = createContext('foo')
+
+ const wrapper = ({ children }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useContext(TestContext), { wrapper })
+
+ expect(result.current).toBe('bar')
+ })
+
+ test('should update value in context when props are updated', () => {
+ const TestContext = createContext('foo')
+
+ const wrapper = ({ contextValue, children }) => (
+ {children}
+ )
+
+ const { result, hydrate, rerender } = renderHook(() => useContext(TestContext), {
+ wrapper,
+ initialProps: { contextValue: 'bar' }
+ })
+
+ hydrate()
+
+ rerender({ contextValue: 'baz' })
+
+ expect(result.current).toBe('baz')
+ })
+})
diff --git a/test/server/useEffect.test.js b/test/server/useEffect.test.js
new file mode 100644
index 00000000..ff2387cd
--- /dev/null
+++ b/test/server/useEffect.test.js
@@ -0,0 +1,38 @@
+import { useEffect, useLayoutEffect } from 'react'
+import { renderHook } from 'src/server'
+
+describe('useEffect tests', () => {
+ test('should handle useEffect hook', () => {
+ const sideEffect = { [1]: false, [2]: false }
+
+ const { hydrate, rerender, unmount } = renderHook(
+ ({ id }) => {
+ useEffect(() => {
+ sideEffect[id] = true
+ return () => {
+ sideEffect[id] = false
+ }
+ }, [id])
+ },
+ { initialProps: { id: 1 } }
+ )
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(false)
+
+ hydrate()
+
+ expect(sideEffect[1]).toBe(true)
+ expect(sideEffect[2]).toBe(false)
+
+ rerender({ id: 2 })
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(true)
+
+ unmount()
+
+ expect(sideEffect[1]).toBe(false)
+ expect(sideEffect[2]).toBe(false)
+ })
+})
diff --git a/test/server/useMemo.test.js b/test/server/useMemo.test.js
new file mode 100644
index 00000000..19bc1960
--- /dev/null
+++ b/test/server/useMemo.test.js
@@ -0,0 +1,87 @@
+import { useMemo, useCallback } from 'react'
+import { renderHook } from 'src/server'
+
+describe('useCallback tests', () => {
+ test('should handle useMemo hook', () => {
+ const { result, hydrate, rerender } = renderHook(
+ ({ value }) => useMemo(() => ({ value }), [value]),
+ {
+ initialProps: { value: 1 }
+ }
+ )
+
+ const value1 = result.current
+
+ expect(value1).toEqual({ value: 1 })
+
+ hydrate()
+
+ const value2 = result.current
+
+ expect(value2).toEqual({ value: 1 })
+
+ expect(value2).not.toBe(value1)
+
+ rerender()
+
+ const value3 = result.current
+
+ expect(value3).toEqual({ value: 1 })
+
+ expect(value3).toBe(value2)
+
+ rerender({ value: 2 })
+
+ const value4 = result.current
+
+ expect(value4).toEqual({ value: 2 })
+
+ expect(value4).not.toBe(value2)
+ })
+
+ test('should handle useCallback hook', () => {
+ const { result, hydrate, rerender } = renderHook(
+ ({ value }) => {
+ const callback = () => ({ value })
+ return useCallback(callback, [value])
+ },
+ { initialProps: { value: 1 } }
+ )
+
+ const callback1 = result.current
+
+ const calbackValue1 = callback1()
+
+ expect(calbackValue1).toEqual({ value: 1 })
+
+ hydrate()
+
+ const callback2 = result.current
+
+ const calbackValue2 = callback2()
+
+ expect(calbackValue2).toEqual({ value: 1 })
+
+ expect(callback2).not.toBe(callback1)
+
+ rerender()
+
+ const callback3 = result.current
+
+ const calbackValue3 = callback3()
+
+ expect(calbackValue3).toEqual({ value: 1 })
+
+ expect(callback3).toBe(callback2)
+
+ rerender({ value: 2 })
+
+ const callback4 = result.current
+
+ const calbackValue4 = callback4()
+
+ expect(calbackValue4).toEqual({ value: 2 })
+
+ expect(callback4).not.toBe(callback2)
+ })
+})
diff --git a/test/server/useReducer.test.js b/test/server/useReducer.test.js
new file mode 100644
index 00000000..ea3724ab
--- /dev/null
+++ b/test/server/useReducer.test.js
@@ -0,0 +1,21 @@
+import { useReducer } from 'react'
+import { renderHook, act } from 'src/server'
+
+describe('useReducer tests', () => {
+ test('should handle useReducer hook', () => {
+ const reducer = (state, action) => (action.type === 'inc' ? state + 1 : state)
+
+ const { result, hydrate } = renderHook(() => {
+ const [state, dispatch] = useReducer(reducer, 0)
+ return { state, dispatch }
+ })
+
+ hydrate()
+
+ expect(result.current.state).toBe(0)
+
+ act(() => result.current.dispatch({ type: 'inc' }))
+
+ expect(result.current.state).toBe(1)
+ })
+})
diff --git a/test/server/useRef.test.js b/test/server/useRef.test.js
new file mode 100644
index 00000000..1327e604
--- /dev/null
+++ b/test/server/useRef.test.js
@@ -0,0 +1,29 @@
+import { useRef, useImperativeHandle } from 'react'
+import { renderHook } from 'src/server'
+
+describe('useHook tests', () => {
+ test('should handle useRef hook', () => {
+ const { result } = renderHook(() => useRef('foo'))
+
+ const refContainer = result.current
+
+ expect(Object.keys(refContainer)).toEqual(['current'])
+ expect(refContainer.current).toBe('foo')
+ })
+
+ test('should handle useImperativeHandle hook', () => {
+ const { result, hydrate } = renderHook(() => {
+ const ref = useRef()
+ useImperativeHandle(ref, () => ({
+ fakeImperativeMethod: () => true
+ }))
+ return ref
+ })
+
+ expect(result.current.current).toBeUndefined()
+
+ hydrate()
+
+ expect(result.current.current.fakeImperativeMethod()).toBe(true)
+ })
+})
diff --git a/test/server/useState.test.js b/test/server/useState.test.js
new file mode 100644
index 00000000..48af8903
--- /dev/null
+++ b/test/server/useState.test.js
@@ -0,0 +1,39 @@
+import { useState } from 'react'
+import { renderHook, act } from 'src/server'
+
+describe('useState tests', () => {
+ test('should use state value', () => {
+ const { result } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ expect(result.current.value).toBe('foo')
+ })
+
+ test('should retain state value after hydration', () => {
+ const { result, hydrate } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ hydrate()
+
+ expect(result.current.value).toBe('foo')
+ })
+
+ test('should update state value using setter', () => {
+ const { result, hydrate } = renderHook(() => {
+ const [value, setValue] = useState('foo')
+ return { value, setValue }
+ })
+
+ hydrate()
+
+ act(() => {
+ result.current.setValue('bar')
+ })
+
+ expect(result.current.value).toBe('bar')
+ })
+})
diff --git a/test/useReducer.test.js b/test/useReducer.test.js
deleted file mode 100644
index 092d585d..00000000
--- a/test/useReducer.test.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useReducer } from 'react'
-import { renderHook, act } from 'src'
-
-describe('useReducer tests', () => {
- test('should handle useReducer hook', () => {
- const reducer = (state, action) => (action.type === 'inc' ? state + 1 : state)
- const { result } = renderHook(() => useReducer(reducer, 0))
-
- const [initialState, dispatch] = result.current
-
- expect(initialState).toBe(0)
-
- act(() => dispatch({ type: 'inc' }))
-
- const [state] = result.current
-
- expect(state).toBe(1)
- })
-})
diff --git a/test/useState.test.js b/test/useState.test.js
deleted file mode 100644
index ebb943f8..00000000
--- a/test/useState.test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useState } from 'react'
-import { renderHook, act } from 'src'
-
-describe('useState tests', () => {
- test('should use setState value', () => {
- const { result } = renderHook(() => useState('foo'))
-
- const [value] = result.current
-
- expect(value).toBe('foo')
- })
-
- test('should update setState value using setter', () => {
- const { result } = renderHook(() => useState('foo'))
-
- const [_, setValue] = result.current
-
- act(() => setValue('bar'))
-
- const [value] = result.current
-
- expect(value).toBe('bar')
- })
-})