Testing UI updates caused by state changes - reactjs

I'm having a create-react-app and am trying to test a functional component. The component makes a service call on initial mount and sets the state and populates a material ui select based on the results.
Now, the user can pick one of those options on the select and I make another service call to retrieve more data based on that.
I am able to mock the initial service call, return mock values and prove that the select got populated but I am having trouble with finding the mocked option text in the select. I get an error - Unable to find an element with the text. This could be because the text is broken up by multiple elements.
Here is how my test looks like:
const fetchMock = jest.spyOn(window, 'fetch').mockImplementation(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve([{itemName:
'Color', itemId: 23},
{itemName: 'Car', itemId: 34}])}));
const localhostURL = "http://localhost:8080";
let retrieveByLabelTextFunction;
let retrieveByRole;
act(() => { const { getByLabelText } =
render(<ItemSelection/>);
retrieveByLabelTextFunction = getByLabelText;
retrieveByRole = getByRole;
});
expect(fetchMock).toBeCalledWith(localhostURL + '/items/');
findByDisplayValue("Color");
findByDisplayValue("Car");
const items = retrieveByLabelTextFunction("Items");
fireEvent.mouseDown(items);
const itemListbox = within(retrieveByRole("listbox"));
fireEvent.click(itemListbox.getByText("I34 - Car"));
Any thoughts on how to test this scenario? I34 in the text is added by the code under test appending the id basically.

Related

Component-testing #lexical/react editor: How to assert against HTML rendered by Cypress?

I want to component-test a controlled #lexical/react based input in Cypress. An example scenario:
Given I have a controlled input with initial state "Hello world"
When I click into the input
And I press "! key" + "Home key" + "Arrow right key" + "Arrow right key" + "n key"
Then input content is "Henlo world!"
How can I get the editor's text content, so I can assert against it?
I was hoping I could attach a newly-created editor instance to the rendered content to gain access to native lexical methods:
it('Works as intended', () => {
const Test: FC = () => {
const [state, setState] = useState('Hello world')
return <MyInput value={state} setValue={setState} />
}
mount(<Test />)
cy.get('[contenteditable="true"]').should(editorRoot => {
const editorHTML = editorRoot.get()[0]
const editorHtmlClone = editorHTML.cloneNode(true)
const editor = createEditor()
editor.setRootElement(editorHtmlClone as any)
editor.getEditorState().read(() => {
const textValue = $getRoot().getTextContent()
// Now I can access the editor's value
// but unfortunately it's been overwritten
})
})
})
But unfortunately this fails, because editor.setRootElement clears RootElement's contents.
I would really love to find a way to tap into Lexical's API, so I can test my functionality via the API without having to mirror it's exact implementation in my tests (for example defining exact expected html - it can change, doesn't mean much outside of Lexical and is a worse at describing my desired state).
Are there any options/workarounds to achieve this?
You can write the instance on the element or the window itself on your test environment. That's what we do for the TreeView (debugger)!
useEffect(() => {
const element = treeElementRef.current;
if (element !== null) {
element.__lexicalEditor = editor;
return () => {
element.__lexicalEditor = null;
};
}
}, [editor]);
That said, I would argue that your expectations fit a unit test more than an E2E test. An E2E test should validate the DOM, the HTML (via innerHTML) and selection (via getDOMSelection()).
If you want to learn more about how we do E2E testing you can check Lexical's.
ℹ️ We're planning to ship a separate testing package for users to be able to leverage all the utilities we have built on top of Playwright over the time rather than having to reimplement them on their own.

Trying to reset a list before filtering, but having trouble with the asynchronous states

So i have a const with an array of objects, named "data", a state that receive that data so I can update the screen when I change it, and I have a function that filter that data.
Every time that the user change the state of an HTML select, I trigger a function named handleChange that calls the filter function, so the user user can see the filtered content.
The problem is that I want to reset the state that receives the data, with the original data before filtering it, so if the user change one of the selects, it filter based on the original data, not the previous changed one, but when I try to update the state with the const data value, it doesn't work.
Here is my code
const [list, setList] = useState<IData[]>([...data]);
const [filter, setFilter] = useState<IFilter>({
name: "",
color: "",
date: "",
});
function handleChange(
key: keyof IData,
event: React.ChangeEvent<{ value: string }>
): void {
const newFilter = { ...filter };
newFilter[key as keyof IData] = event.target.value;
setList([...data]); // reset the data
setFilter({ ...newFilter }); // set the filter value
filterList();
}
function filterList(): void {
const keys = Object.keys(filter) as Array<keyof IData>;
keys.forEach((key: keyof IData) => {
if (filter[key]) {
const newList = list.filter((item: IData) => item[key] === filter[key]);
setList([...newList]);
}
});
}
the problem is here
setList([...data]); // reset the data
setFilter({ ...newFilter }); // set the filter value
filterList();
apparently, when the filterList happens, the list state is not yet restarted with the data value, since if I console log it inside filterList(), it return me only the list that was previous filtered. Is there a way for me to make sure that the setList happens before filtering, so I'm sure that the list that is filtered is always based on the initial value?
You can access the updated values inside useEffect (more about useEffect), so instead of calling filterList() directly inside the handler you move it inside the hook:
React.useEffect(()=>{filterList();},[list,filter])
Alternatively, you can pass the values as parameters to the function, after you updated the state with them
filterList(newList, newFilter)
UPDATE
As you update list inside filterList you'll need some sort of a flag to indicate if you need to filter of not (I'd use useRef). Note that it would be preferable to pass parameters into filterList instead, because this way there will be one less rendering cycle. Here is a simplified example of how both can work, let me know if they make sense : https://jsfiddle.net/6xoeqkr3/

React Native for each item in JSON object returned by AsyncStorage create a component on screen

I have a page currently that stores data into AsyncStorage as a stringified JSON object with the keys: "title", "description" and "rating". For each JSON object stored in Async, I would like to create a new component in a specific using the component
I have got this being read and converted back into JSON objects in a different page. My issue is I would like to have this data used to populate the screen with premade component called "calendarEntryBox" for each JSON object.
Currently I am able to have one component made for each item in an array, this array has items from Async pushed to it. For testing purposes it is stored with premade entries before Async does anything. However, since this AsyncStorage is async, when I try to put data from Async and store it in this array, the map function within the screens return view has already done it.
How can I take JSON objects retrieved asynchronously, use them to create a component and put this component into a on a page?
Thanks
Here is the code I have for pushing items to an array from
AsyncStorage
const getData = async () => {
try{
var allDaysAsString = await AsyncStorage.getItem("Days");
var allDaysAsJSON = JSON.parse(allDaysAsString);
Object.keys(allDaysAsJSON).forEach(function(key) {
// console.log(Object.keys(allDaysAsJSON));
calendarEntries.push({
title: allDaysAsJSON[key].title,
description: allDaysAsJSON[key].description,
rating: allDaysAsJSON[key].rating
});
});
Here is the code I have for creating a view from each of the items in the array:
<View style={styles.calendarContainer}>
{calendarEntries.map((item, key)=>(
<CalendarEntryBox key = {key} style = {styles.smallBox} />)
)}
</View>
This works for test items already stored into the calendarEntries array:
var calendarEntries = [{rating: "bad", description: "hello I am a descrip", title: "what a title this is"},{rating: "bad", description: "hello I am a descrip", title: "what a title this is"}];
But when items are added to this array using the getData function, this is not rendered on the users view.
The codes which are provided didn't give enough info how you implemented what you wanted. here is an approach i would take for your case
in your component state i add a property named calendarEntries
state = {
calendarEntries: []
}
and change getData function to return an array containing the parsed values when resolved(its async)
and in componentDidMount i would call getData
async componentDidMount() {
const parsedValuesArray = await this.getData()
this.setState({ calendarEntries: parsedValuesArray )} // will make page re-render
}
and in your render function you use the state property:
{
this.state.calendarEntries.map((item, key)=>(
<CalendarEntryBox key = {key} style = {styles.smallBox} />)
)
}
the codes shown are simplified as much as i could just to show you an approach. hope it helps

GraphQL Automatic refetch on empty responses

I want to randomize movies from theMovieDB API. First I send a request to access the ID of the latest entry:
const { loading: loadingLatest, error: errorLatest, data: latestData, refetch: refetchLatest } = useQuery(
LATEST_MOVIE_QUERY
);
Then I want to fetch data from a randomly selected ID between 1 and the number of the latest id. Using a variable dependant on the first query seems to break the app, so for now I'm just using the same movie every time upon mounting the component:
const [
movieState,
setMovieState
] = useState(120);
const { loading, error, data, refetch } = useQuery(ONE_MOVIE_BY_ID_QUERY, {
variables : { movieId: movieState },
skip : !latestData
});
I want to press a button to fetch a new random movie, but the problem is that many of the IDs in the API lead to deleted entries and then I get an error back. I want to keep refetching until I get a good response back but I have no idea to implement it. Right now my randomize function just looks like this:
const randomizeClick = () => {
let mostRecentID = latestData.latestMovie.id;
setMovieState(Math.floor(Math.random() * mostRecentID));
};
I'd be grateful if someone can help me how to implement this.
I think what you needs is the "useLazyQuery" functionality of Apollo. You can find more information about it here: https://www.apollographql.com/docs/react/data/queries/#executing-queries-manually
With useLazyQuery you can change your variables easily and this is meant to be fired after a certain event (click or something similar). The useQuery functionality will be loaded when the component is mounted.

How to Format React Form Submission Before API Gets Payload

I am trying to submit a form that takes form field inputs into a React hook state object. However, the payload object is accessed by other components in the application that expect the payload object to be a certain format.
const [formInput, setFormInput] = useState();
const onChange = event => {
event.target.name = event.target.value;
}
return (
<form onSubmit="props.onSubmit">
<label>First Name</label>
<input value="formInput.first-name" name="first-name" onChange={onChange}></input>
<input value="formInput.person-dept" name="person-dept" onChange={onChange}></input>
<button type="submit">Add a cadet</button>
</form>
)
So, the formInput object has two properties. But they need to be nested, like this:
//acceptable payload:
{cadet: [
first-name: '',
dept: ''
]
}
I have tried calling a function for wrapping them using a new state object, but it gives me an undefined error for the schema property:
const schema = () => {
cadet: [
first-name: '',
dept: ''
]
}
const [formattedInput, setFormattedInput] = useState(schema);
const updateInput = () => {
setFormattedInput(
cadet: {
{first-name: {formInput.first-name} || null},
{dept: {formInput.person-dept} || null}
}
)
}
updateInput();
api.post('~/person/cadet', updateInput);
In the above example, the properties from schema are undefined, cadet and first-name.
Also, in order to set the setFormattedInput object before calling the API I need to instantiate the function that has it, but because of React rules, calling updateInput(); runs when the component is rendered and is undefined (sort of like needing a componentDidUpdate() for a functional component).
This should be very common- we all need to reformat our form state objects before they reach the API unless you are building an application from scratch. Does anybody know how?
To give some context, the NPM package mapper does what is needed, but it simply doesn't work (https://www.npmjs.com/package/mapper).
I ended up using a custom wrapper and fixing minor syntax issues along the way. In the payload,
api.post('~/person/cadet', formInput);
I added
const formattedObj = formatted(formInput);
api.post('~/person/cadet', formattedObj);
console.log(formattedObj);
Then in the same file I defined formatted like this:
const formatted = (formInput) => {
const payload = {cadet: [
first-name: formInput.first-name,
dept: formInput.person-dept
]}
return payload;
}
Therefore, formatted() gets passed in the state object and payload is behaving sort of like a block variable and doesn't need to be called for it to be the data being returned by formatted().
Just a minor step of wrapping the object passed to the URL path and some syntax in formatted() and the basic workflow was correct.
I wanted to share because it took several powerful coworker minds to make this work and it should be taught with React hooks everywhere to lower the barriers to entry.
Thanks for reading and I hope this works for you, too!
P.S.- If the object isn't being saved, it's due to other syntax issues in what you are passing into the object from the form fields. Also, minor syntax tweaks will get this solution to work if something is a tiny bit off.

Resources