Skip to content

Commit

Permalink
fix(types): fix cleanup return type to match async implementation
Browse files Browse the repository at this point in the history
* fix(types): fix `cleanup` return type to match `async` implementation

* test: refactor tests to reduce duplication for different renderers

* test: refactor `hydratedServerRenderer` and added comment for clarity about it
  • Loading branch information
mpeyper committed Jun 30, 2021
1 parent eff2ca6 commit c7a2e97
Show file tree
Hide file tree
Showing 95 changed files with 1,525 additions and 3,674 deletions.
4 changes: 4 additions & 0 deletions jest.config.js
@@ -0,0 +1,4 @@
const { jest: jestConfig } = require('kcd-scripts/config')
module.exports = Object.assign(jestConfig, {
setupFiles: ['<rootDir>/src/__tests__/utils/runForRenderers.ts']
})
258 changes: 258 additions & 0 deletions src/__tests__/asyncHook.test.ts
@@ -0,0 +1,258 @@
import { useState, useRef, useEffect } from 'react'

describe('async hook tests', () => {
const useSequence = (values: string[], intervalMs = 50) => {
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)
}
}, intervalMs)
return () => {
clearInterval(interval)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, otherValues)

return value
}

runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
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 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 not reject when waiting for next update if timeout has been disabled', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100))

expect(result.current).toBe('first')

await waitForNextUpdate({ timeout: false })

expect(result.current).toBe('second')
})

test('should wait for expectation to pass', async () => {
const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))

expect(result.current).toBe('first')

let complete = false
await waitFor(() => {
expect(result.current).toBe('third')
complete = true
})
expect(complete).toBe(true)
})

test('should wait for arbitrary expectation to pass', async () => {
const { waitFor } = renderHook(() => null)

let actual = 0
const expected = 1

setTimeout(() => {
actual = expected
}, 200)

let complete = false
await waitFor(() => {
expect(actual).toBe(expected)
complete = true
})

expect(complete).toBe(true)
})

test('should not hang if expectation is already passing', async () => {
const { result, waitFor } = renderHook(() => useSequence(['first', 'second']))

expect(result.current).toBe('first')

let complete = false
await waitFor(() => {
expect(result.current).toBe('first')
complete = true
})
expect(complete).toBe(true)
})

test('should wait for truthy value', async () => {
const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))

expect(result.current).toBe('first')

await waitFor(() => result.current === 'third')

expect(result.current).toBe('third')
})

test('should wait for arbitrary truthy value', async () => {
const { waitFor } = renderHook(() => null)

let actual = 0
const expected = 1

setTimeout(() => {
actual = expected
}, 200)

await waitFor(() => actual === 1)

expect(actual).toBe(expected)
})

test('should reject if timeout exceeded when waiting for expectation to pass', async () => {
const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))

expect(result.current).toBe('first')

await expect(
waitFor(
() => {
expect(result.current).toBe('third')
},
{ timeout: 75 }
)
).rejects.toThrow(Error('Timed out in waitFor after 75ms.'))
})

test('should not reject when waiting for expectation to pass if timeout has been disabled', async () => {
const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'], 550))

expect(result.current).toBe('first')

await waitFor(
() => {
expect(result.current).toBe('third')
},
{ timeout: false }
)

expect(result.current).toBe('third')
})

test('should check on interval when waiting for expectation to pass', async () => {
const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))

let checks = 0

await waitFor(
() => {
checks++
return result.current === 'third'
},
{ interval: 100 }
)

expect(checks).toBe(3)
})

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 wait for arbitrary value to change', async () => {
const { waitForValueToChange } = renderHook(() => null)

let actual = 0
const expected = 1

setTimeout(() => {
actual = expected
}, 200)

await waitForValueToChange(() => actual)

expect(actual).toBe(expected)
})

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 not reject when waiting for value to change if timeout is disabled', async () => {
const { result, waitForValueToChange } = renderHook(() =>
useSequence(['first', 'second', 'third'], 550)
)

expect(result.current).toBe('first')

await waitForValueToChange(() => result.current === 'third', {
timeout: false
})

expect(result.current).toBe('third')
})

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'))
})
})
})
26 changes: 26 additions & 0 deletions src/__tests__/autoCleanup.disabled.test.ts
@@ -0,0 +1,26 @@
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', () => {
process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'

runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
let cleanupCalled = false

test('first', () => {
const useHookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => useHookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(false)
})
})
})
28 changes: 28 additions & 0 deletions src/__tests__/autoCleanup.noAfterEach.test.ts
@@ -0,0 +1,28 @@
import { useEffect } from 'react'

// This verifies that if afterEach is unavailable
// then we DON'T auto-wire up the afterEach for folks
describe('skip auto cleanup (no afterEach) tests', () => {
// @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type
// eslint-disable-next-line no-global-assign
afterEach = false

runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
let cleanupCalled = false

test('first', () => {
const useHookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => useHookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(false)
})
})
})
31 changes: 31 additions & 0 deletions src/__tests__/autoCleanup.noProcessEnv.test.ts
@@ -0,0 +1,31 @@
import { useEffect } from 'react'

// This verifies that if process.env is unavailable
// then we still auto-wire up the afterEach for folks
describe('auto cleanup (no process.env) tests', () => {
process.env = {
...process.env,
get RHTL_SKIP_AUTO_CLEANUP(): string | undefined {
throw new Error('expected')
}
}

runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
let cleanupCalled = false

test('first', () => {
const useHookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => useHookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(true)
})
})
})
27 changes: 27 additions & 0 deletions src/__tests__/autoCleanup.pure.test.ts
@@ -0,0 +1,27 @@
import { useEffect } from 'react'

// This verifies that if pure imports are used
// then we DON'T auto-wire up the afterEach for folks
describe('skip auto cleanup (pure) tests', () => {
runForRenderers(
['default/pure', 'dom/pure', 'native/pure', 'server/hydrated/pure'],
({ renderHook }) => {
let cleanupCalled = false

test('first', () => {
const useHookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => useHookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(false)
})
}
)
})

0 comments on commit c7a2e97

Please sign in to comment.