React Testing Library custom render function and async await - reactjs

I have the following custom render function for my component.
It has two modes Create and Edit.
Create is synchronous and Edit is asynchronous.
The function is as follows:
const renderComponent = async (
scheduleId = "",
dialogMode = DialogMode.CREATE,
cohorts = MOCK_COHORT_LIST,
jobs = JOB_LIST,
availableForSchedule = domain === Domain.COHORT ? jobs : cohorts,
) => {
render(
<AddEditScheduleDialog
cohorts={cohorts}
jobs={jobs}
availableForSchedule={availableForSchedule}
scheduleToEdit={scheduleId}
handleToggleDialog={mockToggleDialog}
isDialogVisible={true}
domain={domain}
/>,
{
wrapper: queryWrapper,
},
);
if (dialogMode === DialogMode.EDIT) {
expect(screen.getByRole("progressbar")).toBeInTheDocument();
}
await waitFor(() =>
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(),
);
return {
header: screen.getByRole("heading", {
level: 1,
name: `schedules:addEditDialog.${domain}.${dialogMode}.title`,
}),
name: {
field: screen.getByTestId(`${domain}-name-field`),
button: within(screen.getByTestId(`${domain}-name-field`)).getByRole(
"button",
),
},
frequency: {
field: screen.getByTestId("schedule-frequency-field"),
input: within(
screen.getByTestId("schedule-frequency-field"),
).getByRole("textbox"),
helperText: `schedules:addEditDialog.form.frequency.helperText`,
},
};
};
Sometimes I get intermittent problems finding elements on the screen. Is this because the function returns before the progressbar has been awaited?
Is there a way i can wait for everything to be rendered prior to returning the screen elements that I need?

If your Edit mode is causing the progress bar to be displayed asynchronously you should wait for it by making use of findByRole e.g.
if (dialogMode === DialogMode.EDIT) {
expect(await screen.findByRole("progressbar")).toBeInTheDocument());
}
This is because find* queries use waitFor "under the hood".

Related

AG Grid React does not render cell values when testing using Enzyme and RTL

so recently we updated ag-grid-react and ag-grid-community from 27.0.1 to 28.0.0 and previous working tests now fail.
Test tries to get a value from a row cell and compares to the given one.
Test (v. 27.3.0)
describe("Simple list rendering", () => {
const handleRowDoubleClick = () => { }
const handleRowSelect = () => { }
const formDataWithId = formData.map((item, index) => {
const newItem = checkNumberLength(item, items);
return { ...newItem, id: index };
});
const colDefs = [{
headerName: "test",
valueGetter: "7"
}]
const listRender = (
<AgGridReact
columnDefs={colDefs}
rowData={formDataWithId}
rowSelection="multiple"
suppressClickEdit={true}
onRowClicked={handleRowSelect}
onRowDoubleClicked={handleRowDoubleClick}
immutableData={true}
rowHeight={28}
/>
)
let component
let agGridReact
beforeEach((done) => {
component = mount(listRender);
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', () => {
expect(agGridReact.api).toBeTruthy();
});
it("agGrid shows password field as * instead of string", () => {
console.log(component.find(".ag-cell-value").first().html())
expect(component.render().find(".ag-cell-value").first()).toEqual("7");
});
it("List contains derivative field", () => {
render(listRender);
expect(screen.getAllByText("5")).toHaveLength(1);
})})
RowData Used
[
{ CPS: 2, TST: 2, DERIVATIVE: "" },
{ CPS: 5, TST: 2, DERIVATIVE: "" },
]
Console log in test
When running the App the Grid renders the cell values using custom valueGetters.
Test is running in v27.3.0 and renders the div using ag-cell-value class but not the value (in v28.0.0 does not even render the div when trying to find node using ag-cell-value class)
Is there something wrong we are doing? Any help is appreciated!

ReactJS: axios POST with recursive function

CreateFolder.js
Hi!! I'm trying loop an js tree object to create a folder for the childs elements with de Egnyte API.
I think I have posed the problem wrong.
My recursive funcion is cheking if the parent have childs, if this true, loop his childs, if the child isn't a parent, i want to create the folder with this child id.
The problem is when I see the console, first there are all prints of the '-----ADD-----', after that, there are all prints of the '-----ADDED' so I don't understant why they aren't simultaneously
The API works fine, this create my 6 folders, the problem of this code is when I loop my original TREE that have 400 childs, and I need to create this 400 folders but the callbacks doesn't work, this creates about 60 folders, and there are much 403 errors. I think there are much calls in a short time.
I tried to call the function addFolder with a time out or doing the function async but doesn't work too.
I explained my problem well? Can someone help me?
Thank you so much!!
import { ConnectingAirportsOutlined } from "#mui/icons-material"
import axios from "axios"
import { useEffect, useState } from "react"
import { useSelector } from "react-redux"
export default function CreateFolders() {
console.log('======CreateFolders=====')
// const state = useSelector(state => state)
// const tree = state.assets.tree
const tree = [
{
id: '1000',
name: 'GRUPO GENERADORES',
child: [
{
id: '1100',
name: 'MOTORES',
child: [
{
id: '1100.1',
name: 'MOTOR 1'
},
{
id: '1100.2',
name: 'MOTOR 2'
},
{
id: '1100.3',
name: 'MOTOR 3'
},
]
},
{
id: '1200',
name: 'ALTERNADORES',
child: [
{
id: '1200.1',
name: 'ALTERNADOR 1'
},
{
id: '1200.2',
name: 'ALTERNADOR 2'
},
{
id: '1200.3',
name: 'ALTERNADOR 3'
}
]
}
]
}
]
useEffect(() => {
getTreeItems(tree)
}, [tree])
const api = 'https://test.egnyte.com/pubapi/v1/fs/Shared/test/'
const headers = {
"headers": {
"Authorization": "Bearer XXXXXXXXXX",
"Content_type": "application/json"
}
}
const body = {
"action": "add_folder"
}
const addFolder = async (id) => {
const endpoint = api + id
await axios.post(endpoint, body, headers).then(res => {
console.log('OK')
return
}).catch((error) => {
console.log('ERROR')
})
}
const getTreeItems = treeItems => {
return treeItems.map(item => {
if (item.child && item.child.length > 0) {
getTreeItems(item.child)
}
if (item.id.includes('.')) {
console.log('-------ADD-------')
console.log(item.id)
addFolder(item.id)
console.log('-------ADDED')
}
})
}
return (
<>
<h2>CreateFolders</h2>
</>
)
}
The description you provided seems like you reached the rate limit (per second/daily).
Try to explore the response headers in failed requests to get more information about:
What kind of quotas have you exceeded (per second/daily)
When you can retry the request (e.g Retry-After header)
There are 2 solutions you can try:
Use bulk operation (create all the entities by single request) if the API supports it. This is a preferable solution
Retry the request after the timeout you get from the failed request header (e.g Retry-After header)
I solved my problem with the setTimeout with an index, because my function was executing the whole thing after 3 seconds, and with this index, add more time to every item, and executes each item 3 seconds after the previous one
My function after:
folders.map((id) => {
setTimeout(() => {
addFolder(id)
}, 3000)
})
My function before:
folders.map((id, index) => {
setTimeout(() => {
addFolder(id)
}, 3000 * index)
})
It's silly but I didn't know how the setTimeout worked

React doesn't rerender on state change while testing

I am testing a react component which has a simple Material-ui switch that updates a boolean field. The component works without any issues when I run the app locally.
In my test, I am mocking the graphql calls via MockedProvider. The mocked provider works as expected, and I can see that the initial response and the update response arrive and they update the state. However, when I find the switch and check it on the screen, it stays unchecked. My test fails with:
Received element is checked: <input checked="" class="PrivateSwitchBase-input-40 MuiSwitch-input" name="callbackEnabled" type="checkbox" value="" />
My first guess is that React doesn't rerender this state change. Do I need to somehow force rerender? Or what is the correct way of testing this kind of behaviour?
The test:
it('should update boolean field', async () => {
const mocks = [
{
request: {
query: myQuery,
variables: {
clientId: 'cl_0',
},
},
result: {
data: {
myQuery: {
callbackEnabled: true
},
},
},
},
{
request: {
query: myMutation,
variables: {
clientId: 'cl_0',
callbackEnabled: false,
},
},
result: {
data: {
myMutation: {
callbackEnabled: false,
},
},
},
},
];
let base = null;
await act(async () => {
const { baseElement, } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<MyComponent clientId="cl_0" error={undefined} />
</MockedProvider>
);
base = baseElement;
});
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 0));
// check for info: https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
// testing initial state: these pass
expect(base).toBeTruthy();
expect(screen.getByRole('checkbox', { name: /callbacks/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /callbacks/i })).toBeChecked();
// simulate a switch click
await act(async () => {
userEvent.click(screen.getByRole('checkbox', { name: 'Callbacks' }));
});
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 0)); // wait for response
// fails here
expect(screen.getByRole('checkbox', { name: /callbacks/i })).not.toBeChecked();
});
First add a test-id to your checkbox component like this :
<input
type="checkbox"
checked={checkboxState}
data-testid="checkboxID" />
Then in ur test to test if checkbox status was changed :
const input = getByTestId("checkboxID");
// Assuming we set initial status for checkbox component to be false
expect(input.checked).toEqual(false);
fireEvent.click(input);
// check handling click in checkbox
expect(input.checked).toEqual(true);

How to convert react asynchronous tests synchronously?

Below is the test for ag-grid. Documentaion can be found at https://www.ag-grid.com/javascript-grid-testing-react/
Few of my tests are failing in CI when tests are asynchronous as test1. Is there any solution to make it consistent? I tried test2 approach make it synchronous but that is also failing. Is there any better way to run tests with consistency?
describe('ag grid test 1', () => {
let agGridReact;
let component;
const defaultProps = {
//.....
}
beforeEach((done) => {
component = mount(<CustomAGGridComp {...defaultProps} />);
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', async () => {
expect(agGridReact.api).toBeTruthy();
//..use the Grid API...
var event1 = {
type: 'cellClicked', rowIndex: 0, column: { field: "isgfg", colId: "isgfg", headerName: "Property 2" },
event: {
ctrlKey: false,
shiftKey: false
}
}
await agGridReact.api.dispatchEvent(event1)
//some expect statements
})
});
describe('ag grid test 2', () => {
let agGridReact;
let component;
const defaultProps = {
//.....
}
beforeEach((done) => {
component = mount(<CustomAGGridComp {...defaultProps} />);
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', () => {
expect(agGridReact.api).toBeTruthy();
//..use the Grid API...
var event1 = {
type: 'cellClicked', rowIndex: 0, column: { field: "isgfg", colId: "isgfg", headerName: "Property 2" },
event: {
ctrlKey: false,
shiftKey: false
}
}
agGridReact.api.dispatchEvent(event1);
setTimeout(() => {
//some expect statements
}, 500);
})
});

React Highcharts firing setExtremes event multiple times

I am using Highcharts React wrapper in an app using Hooks, when my chart is either loaded or zoomed it fires both setExtremes and setAfterExtremes multiple times each. I've looked through for similar questions but they are related to different issues.
I've reduced the code to the minimum setup, the page is not refreshing, the data is only parsed once and added to the chart once yet, animation is disabled and it's still consistently firing both events 7 times on:
* initial population
* on zoom
Versions: react 16.9, highcharts 7.2, highcharts-react-official 2.2.2
Chart
<HighchartsReact
ref={chart1}
allowChartUpdate
highcharts={Highcharts}
options={OPTIONS1}
/>
Chart Options:
const [series1, setSeries1] = useState([]);
const OPTIONS1 = {
chart: {
type: 'spline',
zoomType: 'x',
animation: false
},
title: {
text: ''
},
xAxis: {
events: {
setExtremes: () => {
console.log('EVENT setExtremes');
},
afterSetExtremes: () => {
console.log('EVENT sfterSetExtremes');
}
},
},
plotOptions: {
series: {
animation: false
}
},
series: series1
};
Data Population:
useEffect(() => {
if (data1) {
const a = [];
_.each(data1.labels, (sLabel) => {
a.push({
name: sLabel,
data: [],
})
});
... POPULATES DATA ARRAYS...
setSeries1(a);
}
}, [data1]);
Rather the question is old I also faced the same situation. The solution is to move the chart options to a state variable. Then the event will not fire multiple times.
It is mentioned on the library docs. https://github.com/highcharts/highcharts-react -- see the "optimal way to update"
import { render } from 'react-dom';
import HighchartsReact from 'highcharts-react-official';
import Highcharts from 'highcharts';
const LineChart = () => {
const [hoverData, setHoverData] = useState(null);
// options in the state
const [chartOptions, setChartOptions] = useState({
xAxis: {
categories: ['A', 'B', 'C'],
events: {
afterSetExtremes: afterSetExtremes,
},
},
series: [
{ data: [1, 2, 3] }
],
});
function afterSetExtremes(e: Highcharts.AxisSetExtremesEventObject) {
handleDateRangeChange(e);
}
return (
<div>
<HighchartsReact
highcharts={Highcharts}
options={chartOptions}
/>
</div>
)
}
render(<LineChart />, document.getElementById('root'));```
Your useEffect is getting fired multiple times probably because you are checking for data1 and data1 is changing. have you tried putting an empty array in your useEffect and see if it is firing multiple times?
if it only fires up once then the problem is that your useEffect is checking for a value that is constantly changing
if it still fires multiple times then there is something that is triggering your useEffect
I struggled the same problem after I SetState in useEffect().
My problem was I did a (lodash) deepcopy of the Whole options.
This also create a new Event every time.
// Create options with afterSetExtremes() event
const optionsStart: Highcharts.Options = {
...
xAxis: {
events: {
afterSetExtremes: afterSetExtremesFunc,
}
},
....
// Save in state
const [chartOptions, setChartOptions] = useState(optionsStart);
// On Prop Change I update Series
// This update deepcopy whole Options. This adds one Event Every time
React.useEffect(() => {
var optionsDeepCopy = _.cloneDeep(chartOptions);
optionsDeepCopy.series?.push({
// ... Add series data
});
setChartOptions(optionsDeepCopy);
}, [xxx]);
The fix is to Only update the Series. Not whole Options.
React.useEffect(() => {
var optionsDeepCopy = _.cloneDeep(chartOptions);
optionsDeepCopy.series?.push({
// ... Add series data
});
const optionsSeries: Highcharts.Options = { series: []};
optionsSeries.series = optionsDeepCopy.series;
setChartOptions(optionsSeries);
}, [xxx]);

Resources