Check if first child renders nothing in Native Testing Library - react-native

Suppose I have the following component:
const MyComponent = () => null;
In React Testing Library (RTL), the following, AFAIK, should work:
const { container } = render(<MyComponent />);
expect(container.firstChild).toBeEmpty();
where .toBeEmpty is a custom matcher from jest-dom. However, in NTL, container.firstChild is not defined so I can't use .toBeEmpty matcher from jest-native. After some experimentation, I got it to work as follows:
expect(container.children[0]).toBeUndefined();
Is there any other, possibly better, way to do this?

In my case I had a situation like so:
function PhotoComponent({policy}) {
if (policy === 'unavailable')
return null;
return (<View />);
}
Tested like this:
it("should not render if policy is 'unavailable'", () => {
const {toJSON} = render(<PhotoComponent policy={'unavailable'} />);
const children = toJSON().children;
expect(children.length).toBe(0);
});
Some times you will want to filter the children like, for instance:
const children = toJSON().children.filter(
(child) => child.type === 'View' || child.type === 'Modal'
);
Keep calm and Happy coding!

Related

Export child components and use individually

I have a case where I need to export two child components and use individually.
Much Desired outcome (Extremely simplified):
Controls.js:
const Controller = ( props ) => {
const ControlBoxes = () => {
return(<Button>Move around!</Button>)
}
const MoveableBox = () => {
return(<View>I will be moved! </View>)
}
return {ControlBoxes, MoveableBox}
}
export default Builder
Canvas.js:
import Controller from './controls'
const boxScaleMove = boxes.map((box, index) => {
return (
<Bulilder.MoveableBox key={box.id} box={box}/>
)
}
const boxController = boxes.map((box, index) => {
return (
<Bulilder.ControlBoxes key={box.id} box={box}/>
)
}
return (
...
{boxController}
...
...
{boxScaleMove}
...
)
Any idea how I can achieve this or am I missing something fundamental? The main issue is that I want to avoid resorting to useContext (due to performance reasons in the case of a lot of boxes rendered) and be able to share variables and states between MoveableBox-component and ControlBoxes-component via Controller -parent.
Any help would be greatly appreciated.
You could use the compound component and use a lower level context to avoid re-rendering of the whole tree and share states across your components that way, below I would ilustrate a basic example of how that would work.
const RandomContext = createContext();
export default function Controller({children, ...rest}) {
const [randomState, setRandom] = useState(0);
return (
<RandomContext.Provider value={{ randomState, setRandom }}>
<div {...rest}>{children}</div>
</RandomContext.Provider>
);
}
Controller.ControlBoxes = function (props) {
const { setRnadom } = useContext(RandomContext);
return (
<Button onClick={() => setRandom(2)} {...props}>Move around!</Button>
);
};
Controller.MoveableBox = function (props) {
const { randomState } = useContext(RandomContext);
return randomState ? <View {...props}>I will be moved!</View> : null;
};
And you would use it as:
<Controller>
<Controller.ControlBoxes />
<Controller.MoveableBox />
<Controller>
In the compound components pattern we are leveraging the fact that in javascript when you declare a function you create a function/object combo. Therefor Controller function is both a function and an object, so we can assign properties the the object part of that combo, properties which are in our case ControlBoxes and MoveableBox which are functions themselves.
NOTE you should probably assign named function the the properties of that object, it's easier to debug if the case needed.
Example.Function = function ExampleFunction(props) {
return "Example";
};

Test case for child components onClick function

I want to test the onClick functionality of MenuPopover.Item id={3} if it was called once or not after clicking on it.
import React from 'react';
import copy from 'copy-to-clipboard';
const TableMenu = ({show, target, onClick, onHide, addedType, disable, readonly, rowId, supportRestore, supportDelete, isRestored}) => (
<MenuPopover
onClick={onClick}
onHide={onHide}>
{!readonly && (addedType ?
<MenuPopover.Item id={1} label='Delete' disabled=true/> :
<MenuPopover.Item id={2} label='Restore' disabled=false/>
)}
<MenuPopover.Item id={3} onClick={() => copy(rowId)} label='Copy'/>
</MenuPopover>
);
Test case written so far
const onCopySpy = sinon.spy();
const props = {
///
onCopy: onCopySpy,
///
};
it('check method onCopy called', () => {
const wrapper = shallow(<TableMenu {...props}/>);
expect(wrapper.find('MenuPopover').children()).to.have.lengthOf(2);
wrapper.find(MenuPopover.Item).... //Test case to call the onClick function
expect(onCopySpy.calledOnce).to.eql(true);
});
copy needs to be mocked in tests:
import copy from 'copy-to-clipboard';
jest.mock('copy-to-clipboard', () => sinon.spy());
...
const wrapper = shallow(<TableMenu {...props}/>);
wrapper.find(MenuPopover.Item).props().onClick();
expect(copy.calledOnce).to.eql(true);
This can be alternatively done with simulate but it does the same thing internally.

react native conditional rendering with section list and flatlist

I have two different components one is CategoryList which is a flatlist and regionlist is a section list. I would like to display the CategoryList first and when item clicked the regionlist will show however I m not sure why it is not working. (I use context to store the state)
{!isToggle ? (
<CategoryList></CategoryList>
) : (
<RegionList style={styles.regionListStyle}></RegionList>
)}
I also create a button to see if it is a problem about the context but it is not.
const ToggleContext = createContext(true);
export const useToggle = () => {
return useContext(ToggleContext);
};
export function ToggleProvideData({children}) {
const [isToggle, setToggle] = useState(true)
return <ToggleContext.Provider value={{isToggle,setToggle}}>
{children}
</ToggleContext.Provider>;
}
I just wonder conditional render is it not working for flatlist?
UPDATE: I tried create a state to store the useContext isToggle but it only appears for like 1 sec
I guess isToggle as a boolean variable, hence you can use the below code for rendering conditionally
{ (isToggle === true) &&
<CategoryList></CategoryList>
}
{ (isToggle === false) &&
<RegionList style={styles.regionListStyle}></RegionList>
}

Can I use multiple or nested elements in the Label of a Picker Item in React Native?

I'm using React Native with NativeBase and would like to make the labels of my Picker more complicated than just one plain string of text.
But is it even possible to pass elements as the label, say multiple child elements wrapped in a single top-level element?
Or do Pickers only support plain text as labels?
As requested by bennygenel, here's a version of what I've tried:
export default class ThingPicker extends React.Component {
render() {
const {
initialThing,
things,
onThingChanged,
} = this.props;
const orderedThings = things.sort();
return (
<Picker
selectedValue={initialThing}
onValueChange={onThingChanged}>
{buildThingItems(orderedThings)}
</Picker>
);
}
}
function buildThingItems(orderedThings) {
let items = orderedThings.map(th => {
const it = th === "BMD" ? (<Text key={th} label={"foo"} value={"bar"}}>Hello</Text>)
: (<Picker.Item key={th} label={th} value={th} />);
return it;
});
}
Yes! It is possible, it just might not look very "right" for React/JSX code. Just create the elements you need and assign them to the label field:
function buildThingItems(orderedThings) {
let items = orderedThings.map(th => {
const it = (<Picker.Item
key={th}
label={currency === "BMD" ? (<Text>Hello</Text>) : th}
value={th} />);
return it;
});
}

Crash with simple history push

just trying come silly stuff and playing around with Cycle.js. and running into problem. Basically I just have a button. When you click it it's suppose to navigate the location to a random hash and display it. Almost like a stupid router w/o predefined routes. Ie. routes are dynamic. Again this isn't anything practical I am just messing with some stuff and trying to learn Cycle.js. But the code below crashes after I click "Add" button. However the location is updated. If I actually just navigate to "#/asdf" it displays the correct content with "Hash: #/asdf". Not sure why the flow is crashing with error:
render-dom.js:242 TypeError: Cannot read property 'subscribe' of undefined(…)
import Rx from 'rx';
import Cycle from '#cycle/core';
import { div, p, button, makeDOMDriver } from '#cycle/dom';
import { createHashHistory } from 'history';
import ranomdstring from 'randomstring';
const history = createHashHistory({ queryKey: false });
function CreateButton({ DOM }) {
const create$ = DOM.select('.create-button').events('click')
.map(() => {
return ranomdstring.generate(10);
}).startWith(null);
const vtree$ = create$.map(rs => rs ?
history.push(`/${rs}`) :
button('.create-button .btn .btn-default', 'Add')
);
return { DOM: vtree$ };
}
function main(sources) {
const hash = location.hash;
const DOM = sources.DOM;
const vtree$ = hash ?
Rx.Observable.of(
div([
p(`Hash: ${hash}`)
])
) :
CreateButton({ DOM }).DOM;
return {
DOM: vtree$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container')
});
Thank you for the help
I would further suggest using #cycle/history to do your route changing
(Only showing relevant parts)
import {makeHistoryDriver} from '#cycle/history'
import {createHashHistory} from 'history'
function main(sources) {
...
return {history: Rx.Observable.just('/some/route') } // a stream of urls
}
const history = createHashHistory({ queryKey: false })
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
history: makeHistoryDriver(history),
})
On your function CreateButton you are mapping your clicks to history.push() instead of mapping it to a vtree which causes the error:
function CreateButton({ DOM }) {
...
const vtree$ = create$.map(rs => rs
? history.push(`/${rs}`) // <-- not a vtree
: button('.create-button .btn .btn-default', 'Add')
);
...
}
Instead you could use the do operator to perform the hashchange:
function CreateButton({ DOM }) {
const create$ =
...
.do(history.push(`/${rs}`)); // <-- here
const vtree$ = Observable.of(
button('.create-button .btn .btn-default', 'Add')
);
...
}
However in functional programming you should not perform side effects on you app logic, every function must remain pure. Instead, all side effects should be handled by drivers. To learn more take a look at the drivers section on Cycle's documentation
To see a working driver jump at the end of the message.
Moreover on your main function you were not using streams to render your vtree. It would have not been reactive to locationHash changes because vtree$ = hash ? ... : ... is only evaluated once on app bootstrapping (when the main function is evaluated and "wires" every streams together).
An improvement will be to declare your main's vtree$ as following while keeping the same logic:
const vtree$ = hash$.map((hash) => hash ? ... : ...)
Here is a complete solution with a small locationHash driver:
import Rx from 'rx';
import Cycle from '#cycle/core';
import { div, p, button, makeDOMDriver } from '#cycle/dom';
import { createHashHistory } from 'history';
import randomstring from 'randomstring';
function makeLocationHashDriver (params) {
const history = createHashHistory(params);
return (routeChange$) => {
routeChange$
.filter(hash => {
const currentHash = location.hash.replace(/^#?\//g, '')
return hash && hash !== currentHash
})
.subscribe(hash => history.push(`/${hash}`));
return Rx.Observable.fromEvent(window, 'hashchange')
.startWith({})
.map(_ => location.hash);
}
}
function CreateButton({ DOM }) {
const create$ = DOM.select('.create-button').events('click')
.map(() => randomstring.generate(10))
.startWith(null);
const vtree$ = Rx.Observable.of(
button('.create-button .btn .btn-default', 'Add')
);
return { DOM: vtree$, routeChange$: create$ };
}
function main({ DOM, hash }) {
const button = CreateButton({ DOM })
const vtree$ = hash.map(hash => hash
? Rx.Observable.of(
div([
p(`Hash: ${hash}`)
])
)
: button.DOM
)
return {
DOM: vtree$,
hash: button.routeChange$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
hash: makeLocationHashDriver({ queryKey: false })
});
PS: there is a typo in your randomstring function name, I fixed it in my example.