Skip to content

Commit

Permalink
Merge pull request #8 from FBerthelot/finilizeHooks
Browse files Browse the repository at this point in the history
Finilize hooks
  • Loading branch information
FBerthelot authored Apr 19, 2019
2 parents fb37c48 + 266fbb4 commit b201338
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 57 deletions.
2 changes: 0 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ workflows:
requires:
- check
filters:
branches:
only: master
tags:
only: /.*/
- publish-doc:
Expand Down
88 changes: 71 additions & 17 deletions packages/component-test-utils-react/src/dispatcher/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
// https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js

class Dispatcher {
/* ReadContext: () => {},
useCallback: () => {},
useDebugValue: () => {},
useImperativeHandle: () => {},
useLayoutEffect: () => {},
useMemo: () => {},
useRef: () => {}, */
/* ReadContext: () => {} */

constructor(shallowedComponent) {
this._hookStorage = [];
Expand All @@ -34,24 +28,29 @@ class Dispatcher {
return hookIndex;
}

_isSameMemo(memo, hookIndex) {
const haveMemo =
this._hookStorage[hookIndex] && this._hookStorage[hookIndex].memo;

return (
haveMemo &&
(this._hookStorage[hookIndex].memo === memo ||
!this._hookStorage[hookIndex].memo.find(
(runningMemo, i) => runningMemo !== memo[i]
))
);
}

useState(initialState) {
return this.useReducer((_, arg) => arg, initialState);
}

useEffect(fn, memo) {
const hookIndex = this._getHookIndex();

const haveMemo = this._hookStorage[hookIndex];
// If effect have no memo, consider memo have changed
const haveSameMemo =
haveMemo &&
(this._hookStorage[hookIndex] === memo ||
!this._hookStorage[hookIndex].find(
(runningMemo, i) => runningMemo !== memo[i]
));
const haveSameMemo = this._isSameMemo(memo, hookIndex);

if (!haveSameMemo) {
this._hookStorage[hookIndex] = memo;
this._hookStorage[hookIndex] = {memo};
fn();
}
}
Expand Down Expand Up @@ -81,6 +80,61 @@ class Dispatcher {
}
];
}

useCallback(fn, memo) {
return this.useMemo(() => fn, memo);
}

useMemo(fn, memo) {
const hookIndex = this._getHookIndex();
const haveSameMemo = this._isSameMemo(memo, hookIndex);

if (haveSameMemo) {
return this._hookStorage[hookIndex].value;
}

const value = fn();
this._hookStorage[hookIndex] = {memo, value};
return value;
}

useRef(initialValue) {
const hookIndex = this._getHookIndex();

if (!this._hookStorage[hookIndex]) {
this._hookStorage[hookIndex] = {
current: initialValue
};
}

return this._hookStorage[hookIndex];
}

useImperativeHandle(ref, objectBuilder, memo) {
const hookIndex = this._getHookIndex();
const haveSameMemo = this._isSameMemo(memo, hookIndex);

if (haveSameMemo) {
return;
}

this._hookStorage[hookIndex] = {memo};

ref.current = {
...ref.current,
...objectBuilder()
};
}

useLayoutEffect(...args) {
return this.useEffect(...args);
}

useDebugValue(...args) {
if (this.debug) {
console.debug(...args);
}
}
}

exports.createDispatcher = shallowedComponent => {
Expand Down
195 changes: 171 additions & 24 deletions packages/component-test-utils-react/src/dispatcher/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe('dispatcher', () => {
expect(contextValue).toBe(42);
});
});

describe('useReducer', () => {
it('should return the initialState at the first render', () => {
const initialState = {count: 0};
Expand Down Expand Up @@ -122,28 +123,174 @@ describe('dispatcher', () => {
expect(state2.count).toBe(45);
});
});
});

/* Const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Total : {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
} */
describe('useCallback', () => {
it('should generate the callback each time when no memo is given', () => {
const countFn = jest.fn();

const memoizedCallback1 = dispatcher.useCallback(
jest.fn(() => countFn())
);

dispatcher._informDipatcherRenderIsComming();
const memoizedCallback2 = dispatcher.useCallback(
jest.fn(() => countFn())
);

expect(memoizedCallback1).not.toBe(memoizedCallback2);
});

it('should generate the callback each time when memo given not the same', () => {
const countFn = jest.fn();

const memoizedCallback1 = dispatcher.useCallback(
jest.fn(() => countFn()),
[1]
);

dispatcher._informDipatcherRenderIsDone();
const memoizedCallback2 = dispatcher.useCallback(
jest.fn(() => countFn()),
[2]
);

expect(memoizedCallback1).not.toBe(memoizedCallback2);
});

it('should not generate the callback each time when memo is given', () => {
const countFn = jest.fn();

const memoizedCallback1 = dispatcher.useCallback(
jest.fn(() => countFn()),
[1]
);

dispatcher._informDipatcherRenderIsDone();
const memoizedCallback2 = dispatcher.useCallback(
jest.fn(() => countFn()),
[1]
);

expect(memoizedCallback1).toBe(memoizedCallback2);
});
});

describe('useMemo', () => {
let computeExpensiveValue;
beforeEach(() => {
let i = 0;
computeExpensiveValue = () => {
i++;
return i;
};
});

it('should computeValue each time when no memo is send', () => {
dispatcher.useMemo(() => computeExpensiveValue());
dispatcher._informDipatcherRenderIsDone();
const computedNTime = dispatcher.useMemo(() => computeExpensiveValue());

expect(computedNTime).toBe(2);
});

it('should computeValue each time when different memo is send', () => {
dispatcher.useMemo(() => computeExpensiveValue(), [1]);
dispatcher._informDipatcherRenderIsDone();
const computedNTime = dispatcher.useMemo(() => computeExpensiveValue(), [
2
]);

expect(computedNTime).toBe(2);
});

it('should computeValue one time when same memo is send', () => {
dispatcher.useMemo(() => computeExpensiveValue(), [1]);
dispatcher._informDipatcherRenderIsDone();
const computedNTime = dispatcher.useMemo(() => computeExpensiveValue(), [
1
]);

expect(computedNTime).toBe(1);
});
});

describe('useRef', () => {
it('should return the initial value into the current', () => {
const result = dispatcher.useRef(42);
expect(result.current).toEqual(42);
});

it('should return the same object each render', () => {
const object = {};
dispatcher.useRef(object);
object.focus = true;
dispatcher._informDipatcherRenderIsDone();
const result = dispatcher.useRef(object);

expect(result.current).toBe(object);
});
});

describe('useImperativeHandle', () => {
it('should enhance the reference given in params', () => {
const ref = dispatcher.useRef({});
const focusFn = jest.fn();
dispatcher.useImperativeHandle(ref, () => ({
focus: focusFn
}));

ref.current.focus();

expect(focusFn).toHaveBeenCalled();
});

it('should memoize the function given', () => {
const objectBuilder = jest.fn(() => ({
focus: jest.fn()
}));

const ref = dispatcher.useRef({});
dispatcher.useImperativeHandle(ref, objectBuilder, [1]);
dispatcher._informDipatcherRenderIsDone();

dispatcher.useRef({});
dispatcher.useImperativeHandle(ref, objectBuilder, [1]);
dispatcher._informDipatcherRenderIsDone();

expect(objectBuilder).toHaveBeenCalledTimes(1);

dispatcher.useRef({});
dispatcher.useImperativeHandle(ref, objectBuilder, [2]);

expect(objectBuilder).toHaveBeenCalledTimes(2);
});
});

describe('useLayoutEffect', () => {
it('should call useEffect', () => {
dispatcher.useEffect = jest.fn(() => 4);

const args = [jest.fn(), [1, 4]];

const res = dispatcher.useLayoutEffect(...args);

expect(dispatcher.useEffect).toHaveBeenCalledWith(...args);
expect(res).toBe(4);
});
});

describe('useDebugValue', () => {
it('should do nothing when dispatcher configuration is not debug', () => {
console.debug = jest.fn();
dispatcher.useDebugValue('yolo');
expect(console.debug).not.toHaveBeenCalled();
});

it('should call console.debug when dispatcher configuration is debug', () => {
dispatcher.debug = true;
console.debug = jest.fn();
dispatcher.useDebugValue('yolo');
expect(console.debug).toHaveBeenCalledWith('yolo');
});
});
});
22 changes: 17 additions & 5 deletions packages/component-test-utils-react/src/render/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,28 @@ const render = (reactEl, config, ShallowRender) => {
return reactEl;
}

// When rendering modify the context value
if (ReactIs.isContextProvider(reactEl) && reactEl.props.value) {
reactEl.type._context._currentValue = reactEl.props.value;
}

const isAlreadyMocked = Boolean(reactEl._mock);
if (isAlreadyMocked) {
reactEl._mock._render();
}

if (!isAlreadyMocked && ReactIs.isForwardRef(reactEl)) {
const shallowRender = new ShallowRender(
reactEl,
config
);

return {
...shallowRender._rendered,
_mock: shallowRender
};
}

// When rendering modify the context value
if (ReactIs.isContextProvider(reactEl) && reactEl.props.value) {
reactEl.type._context._currentValue = reactEl.props.value;
}

const shouldBeMocked =
typeof reactEl.type === 'function' &&
Object.keys(config.mocks).includes(reactEl.type.name);
Expand Down
Loading

0 comments on commit b201338

Please sign in to comment.