Skip to content

Commit

Permalink
✨ handle events shallow configuration for react
Browse files Browse the repository at this point in the history
  • Loading branch information
FBerthelot committed Jun 21, 2019
1 parent 8ec125d commit f2d4ca3
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const {shallow} = require('../shallow');
const React = require('react');

describe('shallow - event', () => {
const ComponentParent = ({product, onProductAddedToBasket}) => {
return (
<button type="button" onClick={() => onProductAddedToBasket(product)}/>
);
};

it('it should call productAddedToBasket event', () => {
const productAddedToBasketSpy = jest.fn();
const product = {price: 20};
const cmp = shallow(<ComponentParent product={product}/>, {
events: {
productAddedToBasket: productAddedToBasketSpy
}
});

expect(productAddedToBasketSpy).not.toHaveBeenCalled();

cmp.dispatchEvent('click');

expect(productAddedToBasketSpy).toHaveBeenCalledWith(product);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,37 @@ const {shallow} = require('../shallow');
const React = require('react');

describe('shallow - querySelector', () => {
it('should work with the useReducer hooks', () => {
const initialState = {count: 0};
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
default:
return {count: state.count - 1};
}
};

const Counter = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<>
Total : {state.count}
<button type="button" onClick={() => dispatch({type: 'increment'})}>
+
</button>
<button
id="desc"
type="button"
onClick={() => dispatch({type: 'decrement'})}
>
-
</button>
</>
);
};
const initialState = {count: 0};
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
default:
return {count: state.count - 1};
}
};

const Counter = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<>
Total : {state.count}
<button type="button" onClick={() => dispatch({type: 'increment'})}>
+
</button>
<button
id="desc"
type="button"
onClick={() => dispatch({type: 'decrement'})}
>
-
</button>
</>
);
};

it('should work with the useReducer hooks', () => {
const cmp = shallow(<Counter/>);

expect(cmp.html()).toBe(
Expand All @@ -52,4 +52,14 @@ describe('shallow - querySelector', () => {
'<>Total : 0<button type="button" onClick="[onClick]">+</button><button id="desc" type="button" onClick="[onClick]">-</button></>'
);
});

it('should display the view of the component when cannot find the element', () => {
const cmp = shallow(<Counter/>);

expect(() => {
cmp.querySelector('yolo').dispatchEvent('yolo2');
}).toThrow(
'<>Total : 0<button type="button" onClick="[onClick]">+</button><button id="desc" type="button" onClick="[onClick]">-</button></>'
);
});
});
9 changes: 7 additions & 2 deletions packages/component-test-utils-react/src/emptyShallow.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
exports.EmptyShallowedComponent = class EmptyShallowedComponent {
constructor(selector) {
constructor(selector, view) {
this.nodeNotExistError = new Error(
`The node you try to access with "${selector}" selector does not exist`
`
The node you try to access with "${selector}" selector does not exist
${view ? view : 'Your component render falsy value'}
`
);
}

Expand Down
12 changes: 12 additions & 0 deletions packages/component-test-utils-react/src/emptyShallow.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@ describe('EmptyShallowedComponent', () => {
new EmptyShallowedComponent('selector1234').setProps('toto')
).toThrow('selector1234');
});

it('should show the component view', () => {
expect(() =>
new EmptyShallowedComponent('selector1234', 'view4506').setProps('toto')
).toThrow('view4506');
});

it('should show a special message when the component view is falsy', () => {
expect(() =>
new EmptyShallowedComponent('selector1234').setProps('toto')
).toThrow('Your component render falsy value');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const isSelectedObject = (elem, selector) => {
return elem.type === selector;
};

exports.querySelector = (shallowedComponent, selector, ShallowRender) => {
exports.querySelector = (shallowedComponent, selector, ShallowRender, getView) => {
// When the children is not an array nor an object, impossible to target it !
if (
!shallowedComponent.props ||
Expand All @@ -34,7 +34,7 @@ exports.querySelector = (shallowedComponent, selector, ShallowRender) => {
isSelectedObject(children, selector) && children;

if (!targetedComponent) {
return new EmptyShallowedComponent(selector);
return new EmptyShallowedComponent(selector, getView());
}

const WrapperComponent = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,27 @@ describe('querySelector', () => {

it('Should return an EmptyShallowedComponent when component have no props', () => {
const component = {};
const result = querySelector(component, 'tagname', ShallowRender);
const result = querySelector(component, 'tagname', ShallowRender, () => {});

expect(result instanceof EmptyShallowedComponent).toBe(true);
});

it('Should return null if component have no children', () => {
const component = {props: {}};
const result = querySelector(component, 'tagname', ShallowRender);
const result = querySelector(component, 'tagname', ShallowRender, () => {});

expect(result instanceof EmptyShallowedComponent).toBe(true);
});

it('Should return null when children is not an element but a string', () => {
const component = {props: {children: 'toto'}};
const result = querySelector(component, 'tagname', ShallowRender);
const result = querySelector(component, 'tagname', ShallowRender, () => {});
expect(result instanceof EmptyShallowedComponent).toBe(true);
});

it('Should return null when children not correspond', () => {
const component = {props: {children: {type: 'toto'}}};
const result = querySelector(component, 'tagname', ShallowRender);
const result = querySelector(component, 'tagname', ShallowRender, () => {});
expect(result instanceof EmptyShallowedComponent).toBe(true);
});

Expand Down
27 changes: 18 additions & 9 deletions packages/component-test-utils-react/src/shallow.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ const defaultConfig = {
};

class ShallowRender {
constructor(component, config = defaultConfig) {
constructor(component, config = defaultConfig, isRoot) {
this._unmounted = false;
this._component = component;
this._config = config;
this._dispatcher = createDispatcher(this);
this._isRoot = Boolean(isRoot);

this._render();
}
Expand All @@ -42,11 +43,19 @@ class ShallowRender {
(this._instance && this._instance.props) ||
this._component.props;

const enhancedProps = this._isRoot && this._config.events ? {
...props,
...Object.keys(this._config.events).reduce((addedProps, eventKey) => {
addedProps[`on${eventKey[0].toUpperCase()}${eventKey.slice(1)}`] = this._config.events[eventKey];
return addedProps;
}, {})
} : props;

if (isClassComponent(this._component.type)) {
if (!this._instance) {
const updater = new Updater(this);
// eslint-disable-next-line new-cap
this._instance = new this._component.type(props, {}, updater);
this._instance = new this._component.type(enhancedProps, {}, updater);
this._instance.state = this._instance.state || null;
this._instance.updater = updater;
firstRender = true;
Expand All @@ -56,13 +65,13 @@ class ShallowRender {
this._instance.state = {
...this._instance.state,
...this._component.type.getDerivedStateFromProps(
props,
enhancedProps,
this._instance.state
)
};
}

this._instance.props = props;
this._instance.props = enhancedProps;

if (
!forceUpdate &&
Expand All @@ -80,11 +89,11 @@ class ShallowRender {
} else if (ReactIs.isForwardRef(this._component)) {
reactEl = this._component.type.render.call(
undefined,
props,
enhancedProps,
this._component.ref
);
} else {
reactEl = this._component.type.call(undefined, props);
reactEl = this._component.type.call(undefined, enhancedProps);
}

try {
Expand All @@ -103,7 +112,7 @@ class ShallowRender {
this._handleClassLAfterRenderLifeCycle(firstRender);
}

this._prevProps = props;
this._prevProps = enhancedProps;
}

_handleErrorInRender(error) {
Expand Down Expand Up @@ -199,7 +208,7 @@ class ShallowRender {
querySelector(selector) {
this._throwIfUnmounted('querySelector');

return querySelector(this._rendered, selector, ShallowRender);
return querySelector(this._rendered, selector, ShallowRender, this.html.bind(this));
}

unmount() {
Expand All @@ -214,7 +223,7 @@ class ShallowRender {
}

exports.shallow = (component, config) => {
return new ShallowRender(component, config);
return new ShallowRender(component, config, true);
};

function isClassComponent(Component) {
Expand Down
3 changes: 3 additions & 0 deletions website/docs/shallow/constructor.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Here is the list of available options:

- {Object} `mocks` The key is the property name, and the value his value.
- {Boolean} `blackList` If true each component will not be mocked as in default mode (whiteList). In this mode you will have to explicitly give to shallow configuration object the list of components to not mock.
- {Object} `events` The key is the event name, the value should be a spy

### Examples

Expand Down Expand Up @@ -87,3 +88,5 @@ it('it should only mock ChildrenComponent2', () => {
expect(cmp.html()).toBe('<div><ChildrenComponent><div><ChildrenComponent2/></div></ChildrenComponent></div>');
});
```

##### Event listening (with jest)

0 comments on commit f2d4ca3

Please sign in to comment.