-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite of useLayout to avoid a delayed rendering. Adding render-debo…
…uncer. (#15) * Rewriting useLayout to avoid a delayed rendering. Adding render-debouncer to speed up the rendering * updating react to use 16.9.0-alpha to be able to await an act (pure test func) * Fixing review comments
- Loading branch information
Helene Rignér
committed
May 3, 2019
1 parent
8f0123a
commit 3d9e25f
Showing
6 changed files
with
159 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import ReactDOM from 'react-dom'; | ||
|
||
let timer = null; | ||
let pendingStateMutators = []; | ||
let startTime = null; | ||
function fireAllPendingMutators() { | ||
ReactDOM.unstable_batchedUpdates(() => { | ||
pendingStateMutators.forEach(mutator => mutator()); | ||
pendingStateMutators = []; | ||
}); | ||
} | ||
|
||
function debounce(stateMutator) { | ||
pendingStateMutators.push(stateMutator); | ||
if (timer != null) { | ||
// Timer already pending | ||
clearTimeout(timer); | ||
} else { | ||
// No timer pending, set start time | ||
startTime = new Date().getTime(); | ||
} | ||
const now = new Date().getTime(); | ||
const averageInterMutateInterval = (now - startTime) / pendingStateMutators.length; | ||
const timerInterval = (averageInterMutateInterval < 32) ? averageInterMutateInterval * 3 + 4 : 100; // Never wait more than 100 ms | ||
timer = setTimeout(() => { | ||
fireAllPendingMutators(); | ||
timer = null; | ||
}, timerInterval); | ||
} | ||
|
||
export default debounce; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,103 +1,98 @@ | ||
import { renderHook, act } from 'react-hooks-testing-library'; | ||
import { useLayout } from '../../src/index'; | ||
import TestPromise from './test-promise'; | ||
|
||
describe('useLayout', () => { | ||
let promise; | ||
let model; | ||
|
||
const mockLayout = { | ||
id: 'myLayout', | ||
}; | ||
|
||
beforeEach(() => { | ||
promise = new TestPromise(); | ||
model = { | ||
id: 'myModel', | ||
on: jest.fn(), | ||
removeListener: jest.fn(), | ||
getLayout: jest.fn(() => promise), | ||
getLayout: () => jest.fn().mockReturnValue(mockLayout), | ||
}; | ||
}); | ||
|
||
test('should return undefined when no model is present', () => { | ||
test('should return null when no model is present', () => { | ||
const { result } = renderHook(() => useLayout(null)); | ||
const [layout, error] = result.current; | ||
expect(layout).toBeUndefined(); | ||
expect(error).toBeUndefined(); | ||
expect(layout).toBeNull(); | ||
expect(error).toBeNull(); | ||
}); | ||
|
||
test('should return undefined on pending promise', () => { | ||
test('should catch and return error', async () => { | ||
const rejectedError = new Error('Error occurred'); | ||
model.getLayout = () => { throw rejectedError; }; | ||
const { result } = renderHook(() => useLayout(model)); | ||
const [layout, error] = result.current; | ||
expect(layout).toBeUndefined(); | ||
expect(error).toBeUndefined(); | ||
}); | ||
|
||
test('should throw error on rejected promise', () => { | ||
const { result } = renderHook(() => useLayout(model)); | ||
const rejectedError = 'Error occurred'; | ||
act(() => promise.reject(rejectedError)); | ||
const [layout, error] = result.current; | ||
expect(layout).toBeUndefined(); | ||
expect(layout).toBeNull(); | ||
expect(error).toEqual(rejectedError); | ||
}); | ||
|
||
test('should return layout object', () => { | ||
const { result } = renderHook(() => useLayout(model)); | ||
act(() => promise.resolve(mockLayout)); | ||
test('should return layout object', async () => { | ||
const { result, waitForNextUpdate } = renderHook(() => useLayout(model)); | ||
await act(async () => waitForNextUpdate()); | ||
const [layout, error] = result.current; | ||
expect(layout).toEqual(mockLayout); | ||
expect(error).toBeUndefined(); | ||
expect(error).toBeNull(); | ||
}); | ||
|
||
test('should add listener for model changes', () => { | ||
test('should add listener for model changes', async () => { | ||
model.getLayout = jest.fn(); | ||
renderHook(() => useLayout(model)); | ||
act(() => promise.resolve(mockLayout)); | ||
expect(model.getLayout).toHaveBeenCalledTimes(1); | ||
expect(model.on).toHaveBeenCalledTimes(1); | ||
expect(model.on).toHaveBeenCalledWith('changed', expect.any(Function)); | ||
// fake a changed event | ||
const modelChangedHandler = model.on.mock.calls[0][1]; | ||
modelChangedHandler(); | ||
expect(model.getLayout).toHaveBeenCalledTimes(2); | ||
expect(model.removeListener).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
test('should remove event listener for model changes on unmount', () => { | ||
const { unmount } = renderHook(() => useLayout(model)); | ||
act(() => promise.resolve(mockLayout)); | ||
test('should remove event listener for model changes on unmount', async () => { | ||
const { waitForNextUpdate, unmount } = renderHook(() => useLayout(model)); | ||
await act(async () => waitForNextUpdate()); | ||
unmount(); | ||
expect(model.removeListener).toHaveBeenCalledTimes(1); | ||
expect(model.removeListener).toHaveBeenCalledWith('changed', expect.any(Function)); | ||
}); | ||
|
||
test('side effect should run when model is updated', () => { | ||
const { rerender } = renderHook(() => useLayout(model)); | ||
act(() => promise.resolve(mockLayout)); | ||
test('side effect should run when model is updated', async () => { | ||
const { rerender, waitForNextUpdate } = renderHook(() => useLayout(model)); | ||
await act(async () => waitForNextUpdate()); | ||
rerender(); | ||
expect(model.on).toHaveBeenCalledTimes(1); | ||
model.id = 'myNewId'; | ||
rerender(); | ||
expect(model.on).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
test('if model object is a Doc, getAppLayout should be called', () => { | ||
test('if model object is a Doc, getAppLayout should be called', async () => { | ||
model = { | ||
id: 'myApp', | ||
on: jest.fn(), | ||
removeListener: jest.fn(), | ||
getAppLayout: jest.fn(() => promise), | ||
getAppLayout: jest.fn().mockReturnValue(mockLayout), | ||
}; | ||
const { result } = renderHook(() => useLayout(model)); | ||
act(() => promise.resolve(mockLayout)); | ||
const { result, waitForNextUpdate } = renderHook(() => useLayout(model)); | ||
await act(async () => waitForNextUpdate()); | ||
const [layout, error] = result.current; | ||
expect(model.getAppLayout).toBeCalledTimes(1); | ||
expect(layout).toEqual(mockLayout); | ||
expect(error).toBeUndefined(); | ||
expect(error).toBeNull(); | ||
}); | ||
|
||
test('layout should not be updated if component has been unmounted when promise resolves', () => { | ||
test('layout should not be updated if component has been unmounted when promise resolves', async () => { | ||
model.getLayout = jest.fn(); | ||
const { result, unmount } = renderHook(() => useLayout(model)); | ||
unmount(); | ||
act(() => promise.resolve(mockLayout)); | ||
const [layout, error] = result.current; | ||
expect(layout).toBeUndefined(); | ||
expect(error).toBeUndefined(); | ||
expect(layout).toBeNull(); | ||
expect(error).toBeNull(); | ||
}); | ||
}); |