Jest snapshot testing of FlatList renderItem - react-native

I have a wrapper for Flatlist called FlatListShadow but for this post FlatListShadow and FlatList is the same thing
I need to test the renderItem function in FlatListShadow which looks like this
renderItem={({ item }) => (
<Device
title={item.deviceName}
platform={item.platform}
updatedAt={item.updatedAt}
status={item.status}
selectDevice={() => selectDevice(item.deviceId)}
isSelected={selectedDeviceIdList.includes(item.deviceId)}
/>
)}
Unfortuanately in the snapshot it only gives this information
renderItem={[Function]}

If you're using enzyme you can achieve it like this
// prepare a mock item to render the renderItem with
const mockItem = {
deviceName: 'mock device name',
platform: 'mock platform',
updatedAt: 123,
status: 'mock status',
deviceId: '1-2-3-4',
}
describe('YourComponent', () => {
let shallowWrapper
beforeAll(() => {
shallowWraper = shallow(<YourComponent />);
});
it('should match the snapshot', () => {
// will generate snapshot for your component
expect(shallowWrapper).toMatchSnapshot();
});
describe('.renderItem', () => {
let renderItemShallowWrapper;
beforeAll(() => {
// find the component whose property is rendered as renderItem={[Function]}
// if we presume it's imported as ComponentWithRenderItemProp
// find it and get it's renderItem property
RenderItem = shallowWraper.find(ComponentWithRenderItemProp).prop('renderItem');
// and since it's a component render it as such
// with mockItem
renderItemShallowWrapper = shallow(<RenderItem item={mockItem} />);
});
it('should match the snapshot', () => {
// generate snapshot for the renderItem
expect(renderItemShallowWrapper).toMatchSnapshot();
});
});
});

If you are using jest :
describe('EasyToUseSection', () => {
it.only('flatlist should return renderItem correctly', () => {
const itemData = {
name: 'Name',
desc: 'Description',
};
const { getByTestId } = renderComponent();
expect(getByTestId('flatlist')).toBeDefined();
const element = getByTestId('flatlist').props.renderItem(itemData);
expect(element.props.data).toStrictEqual(itemData);
expect(element.type).toBe(Device);
});
});
This way the data that is sent is checked and also the component rendered type can be checked

Related

Flatlist is very slow in using big data in react native

i have a big data list of products thats paginate, in every page it load 10 item, but when i add new items to itemlist,flatlist gets very slow,As the number of pages increases, so does the loading time of new products,The function of the choose button is also slowed down.
How to speed up loading I tried all the best methods but it still did not work. Did not React Native really solve this problem?
export default function Products(props) {
const toast = useToast();
const [isLoading, setSetIsLoading] = useState(true);
const [items, setItems] = useState([]);
const [fetchStatus, setFetchStatus] = useState(false);
const [page, setPage] = useState(1);
const [sending, setSending] = useState(false);
async function getProducts() {
let token = await AsyncStorage.getItem('#token');
let data = {
token: token,
page: page,
};
await get_products(data)
.then(res => {
setItems([...items, ...res.data.data.docs]);
setPage(res.data.data.nextPage);
})
.catch(err => {
console.log(err);
});
}
async function getNextPage() {
let token = await AsyncStorage.getItem('#token');
let data = {
token: token,
page: page,
};
await get_products(data)
.then(res => {
setItems([...items, ...res.data.data.docs]);
setPage(res.data.data.nextPage);
})
.catch(err => {
console.log(err);
});
}
async function selectProduct(id) {
setSending(true);
console.log({id});
let token = await AsyncStorage.getItem('#token');
let data = {
product_id: id
};
await select_products(data,token).then(res => {
toast.show({
description:res.data.message
})
setSending(false);
}).catch(rej => {
console.log({rej})
toast.show({
description:rej?.response?.data.message,
})
setSending(false);
})
}
useFocusEffect(
React.useCallback(() => {
getProducts();
return () => {
setItems([]);
setPage();
};
}, []),
);
renderItem =({item}) => (
<Card
selectProduct={id => selectProduct(id)}
sending={sending}
obj={item}
/>
)
return (
<View mb={20}>
<FlatList
data={items}
extraData={items}
removeClippedSubviews={true}
renderItem={renderItem}
keyExtractor={(item) => `${item._id}-item`}
onEndReached={getNextPage}
maxToRenderPerBatch="13"
ListFooterComponent={() => {
return <ActivityIndicator color="orange" size="large" />;
}}></FlatList>
</View>
);
}
Did you use **map method **?
It can help you for more easily loading data

how to test component with setState hook inside async api call in useEffect(func, [])

I'm facing a problem unit-testing a component with react-native-testing-library.
I have a component like this:
// components/TestComponent.js
function TestComponent() {
const [data, setData] = useState();
useEffect(() => {
clientLibrary.getData()
.then((result) => { setData(result.data); } )
.catch((err) => { //handle error here } )
}, []);
render (
<ListComponent
testID={"comp"}
data={data})
renderItem={(item) => <ListItem testID={'item'} data={item} />}
/>
);
}
And I test it like this:
// components/TestComponent.test.js
it('should render 10 list item', async () => {
const data = new Array(10).fill({}).map((v, idx) => ({
id: `v_${idx}`,
}));
const req = jest.spyOn(clientLibrary, 'getData').mockImplementation(() => {
return Promise.resolve(data);
});
const {queryByTestId, queryAllByTestId} = render(
<TestComponent />,
);
expect(await queryByTestId('comp')).toBeTruthy(); // this will pass
expect(await queryAllByTestId('item').length).toEqual(10); // this will fail with result: 0 expected: 10
}); // this failed
The test will fail/pass with
Attempted to log "Warning: An update to TestComponent inside a test was not wrapped in act(...). pointing to setData in useEffect.
I've tried wrapping the render with act(), the assertion with act(), not mocking the api call, wrapping the whole test in act(), but the error won't go away.
I have tried looking at testing-library docs/git/q&a for this case, scoured stackoverflow questions too, but I still can't make this test works.
Can anyone point me to the right direction to solve this?
A note: I'm not trying to test implementation detail. I just want to test that given a fetch result X, the component would render as expected, which is rendering 10 list item.
Your component is performing an asynchronous state update during mounting inside useEffect so the act of rendering has an asynchronous side effect that needs to be wrapped in an await act(async()) call. See the testing recipes documentation on data fetching.
You can try something like this in your test:
it('should render 10 list item', async () => {
// Get these from `screen` now instead of `render`
const { queryByTestId, queryAllByTestId } = screen
const data = new Array(10).fill({}).map((v, idx) => ({
id: `v_${idx}`,
}));
const req = jest.spyOn(clientLibrary, 'getData').mockImplementation(() => {
return Promise.resolve(data);
});
await act(async () => {
render(
<TestComponent />
);
})
expect(await queryByTestId('comp')).toBeTruthy();
expect(await queryAllByTestId('item').length).toEqual(10);
});

React Admin Change field based on related record field

Let's say I have a record called 'assets' which has a column called deducitble. An asset can have one Insurer. The insurer has a boolean field 'allowOtherDeductible'.
When editing the asset, I want the ability to first check if the associated insurer has allowOtherDeductible set to true. If so I'll allow a TextInput for deductible, if false, a SelectInput.
How can I achieve this? I cannot see a way to fetch related record's fields when conditionally rendering fields.
I ended up pulling in all the insurers and loading the asset when the component loaded. Seems a bit inefficient, but I couldn't come up with a better way:
export const AssetEdit = props => {
const dataProvider = useDataProvider();
const [insurers, setInsurers] = useState([]);
const [asset, setAsset] = useState(null);
const [otherDeductible, setOtherDeductible] = useState(false);
useEffect(() => {
dataProvider.getOne('assets', { id: props.id })
.then(({ data }) => {
setAsset(data);
return dataProvider.getList('insurers', { pagination: { page: 1, perPage: 100 } });
})
.then(({ data }) => setInsurers(data))
.catch(error => {
console.log(error)
})
}, [])
useEffect(() => {
if (asset && insurers) {
const selectedInsurer = insurers.find(insurer => insurer.id === asset?.insurer_id);
setOtherDeductible(selectedInsurer?.other_deductible);
}
}, [insurers])
return (
<Edit {...props}>
<SimpleForm>
{otherDeductible &&
<NumberInput
source={'deductible'}
parse={val => dollarsToCents(val)}
format={val => centsToDollars(val)}
/>
}
<FormDataConsumer>
{({ formData, ...rest }) => {
if(!otherDeductible){
return <SelectInput
source="deductible"
parse={val => dollarsToCents(val)}
format={val => centsToDollars(val)}
choices={getDeductibleChoices(formData.insured_value)}
/>
}
}}
</FormDataConsumer>
</SimpleForm>
</Edit>
)
}
I'd write a custom Input taking advantage of the fact that SimpleForm injects the record to all its children:
const DeductibleInput = ({ record }) => {
if (!record) return null;
const { data, loaded } = useGetOne('insurers', record.insured_id);
if (!loaded) return null; // or a loader component
return data.otherDeductible
? <NumberInput
source="deductible"
parse={val => dollarsToCents(val)}
format={val => centsToDollars(val)}
/>
: <SelectInput
source="deductible"
parse={val => dollarsToCents(val)}
format={val => centsToDollars(val)}
choices={getDeductibleChoices(record.insured_value)}
/>
}

Possible unhandled promise rejection on hardware back press

I have set up a store function
export const storeData = async text => {
try {
await AsyncStorage.getItem("notes")
.then((notes) => {
const noteList = notes ? JSON.parse(notes) : [];
noteList.push(text);
AsyncStorage.setItem('notes', JSON.stringify(noteList));
});
} catch (error) {
console.log("error saving" + error);
}
};
When calling from the header back button it works as intended
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
storeData(text).then(() => {
navigation.goBack();
}
}} />
)
});
But when using it from the hardware back button it gives me an "unhandled promise rejection, undefined is not an object. evaluating _this.navigation".
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
storeData(text).then(() => {
this.navigation.goBack();
});
});
return () => backHandler.remove();
}, [text]);
Can anyone see what might cause this behaviour?
replace this by props. thiskey word is used mainly in class components here i its a functional components so navigation is reached by props.navigation
The full code would look like
function EditNoteScreen({ navigation }) {
const [text, setText] = useState("");
const backAction = () => {
storeData(text).then(() => {
Keyboard.dismiss();
navigation.goBack();
});
}
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
backAction();
});
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
backAction();
}} />
)
});
return () => backHandler.remove();
}, [text]);
If I simply have my storage function run with the hardware back press the code will work and the hardware back buttons default behavior will take me back, but then the new item will not show up until refreshed, which is why i want the back behavior delayed until saving is done.
One way to ignore this would simply be to update the flatlist again on state change, but I would rather have the information there from the refresh rather then popping in.

Testing a component after state has been updated in componentDidMount using enzyme jest react-native

I have a component in react-native
export default class SearchResultsScreen extends Component {
constructor(props){
super(props);
this.state = {
centres: [],
};
};
componentDidMount() {
let searchUrl =`${hostname}centres_search/`;
let lat = this.props.origin_lat;
let long = this.props.origin_long;
let distance = this.props.distance;
let url = `${searchUrl}origin_lat=${lat}&origin_long=${long}&distance=${distance}`
console.log('before fetch called');
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
console.log('before state updated '+responseJson[0].name);
this.setState({
centres: responseJson,
});
console.log('after fetch called '+responseJson[0].name);
})
.catch((error)=>{
console.log('error '+error);
this.setState({
centres: [],
});
});
}
static navigationOptions = {
title: 'Centers',
headerTitleStyle: navigationHeaderStyle
};
render() {
let centresList;
if(typeof this.state.centres == 'undefined' ||
this.state.centres.length == 0) {
centresList = <Text>No results found</Text>
} else {
centresList = <FlatList
data={this.state.centres}
keyExtractor={(item, index) => item._centre_id}
renderItem={({item}) => <CentreComponent Centre={item}/>}
ItemSeparatorComponent={() => <ListSeperator />}
/>
}
return(
<View>
{centresList}
<ListSeperator />
</View>
);
};
}
So basically it loads CentreComponent(s) based on the response from fetch (i.e the number of results it gets).
In my test with Jest I am trying to assert two Components of type CentreComponent exist. My test looks like this:
jest.disableAutomock();
import 'react-native';
import React from 'react';
import {shallow, mount, render} from 'enzyme';
import SearchResultsScreen from '../../src/SearchResultsScreen/index';
import CentreComponent from
'../../src/SearchResultsScreen/CentreComponent';
import renderer from 'react-test-renderer';
describe('SearchResultsScreen', () => {
test.only('renders more than one CentreComponent', () => {
jest.useFakeTimers();
global.fetch = jest.fn().mockImplementation(() => {
let kc = [{
_centre_id: 1,
name: 'test',
address_1: 'abc',
address_2: 'def',
city: 'so',
postcode: 'tt',
}, {
_centre_id: 2,
name: 'testee',
address_1: 'abc',
address_2: 'def',
city: 'so',
postcode: 'tt',
}]
console.log('mock fetch called');
return new Promise((resolve, reject) => {
process.nextTick(
() => resolve({ok: true, json: function(){return kc}})
);
});
});
const checkbox = mount(<SearchResultsScreen origin_long='5' origin_lat='2' distance='5'/>);
jest.runAllTicks();
checkbox.update();
expect(checkbox.find(KarCentreComponent)).toHaveLength(2);
});
});
When I run the test
jest.js --env=jsdom
I see the following console.log being printed in order:
console.log('before fetch called');
console.log('mock fetch called');
console.log('before state updated '+responseJson[0].name);
Error at line:
console.log('error '+error);
The error which I get is:
{ Invariant Violation: Element type is invalid: expected a string (for
built-in components) or a class/function (for composite components)
but got: undefined. You likely forgot to export your component from
the file it's defined in. Check the render method of
`SearchResultsScreen`
So my suspicion is that, when componentDidUpdate is calling
this.setState({
centres: responseJson,
});
It is throwing an exception which is being caught by the "catch".