How to test dates with jest? - reactjs

I'm writing a test to check if the selected date is greater than the current date.
This is the component I'm trying to test:
const [selectedDate, setSelectedDate] = useState<Moment>();
<TimeComponent>
<SetTimeButton
data-testid="scheduleTime"
onClick={() =>
scheduleTimeHandler(
selectedDate,
timeSelect,
setSelectedDate,
setTimeDialog,
setDisableConfirm
)
}
/>
<DisplayDate data-testid="selectedDate">{selectedDate}</DisplayDate>
</TimeComponent>
and this is the test that I wrote:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import TimeComponent from './TimeComponent';
test('set schedule time', () => {
render(
//#ts-ignore
<TimeComponent />
);
const mockDateObject = new Date();
const scheduleTimeBtn = screen.queryByTestId('scheduleTime');
const selectedDate = screen.queryByTestId('selectedDate');
fireEvent.click(scheduleTimeBtn);
expect(selectedDate).toBeGreaterThan(+mockDateObject);
});
and I'm getting this kind of error regarding the selectedDate in expect
expect(received).toBeGreaterThan(expected)
Matcher error: received value must be a number or bigint
Received has value: null
I understand this is because selectedDate is an HTML element instead of being date, but I'm not sure how can I make it a date. What am I doing wrong?

Related

How to change the value of an MUI DatePicker in Jest (x-date-pickers)

Regarding { DatePicker } from '#mui/x-date-pickers':
I can't figure out how to change the value using Jest.
Here's my DatePicker-wrapper DatePickerX:
import React, { useState } from 'react';
import { DatePicker } from '#mui/x-date-pickers';
import { LocalizationProvider } from '#mui/x-date-pickers/LocalizationProvider';
import AdapterDateFns from '#mui/lab/AdapterDateFns';
import { de } from 'date-fns/locale';
import { TextField } from '#mui/material';
export const DatePickerX: React.FC = () => {
const [date, setDate] = useState<Date>(new Date());
const changeDate = (newDate: Date | null) => {
if (newDate) {
setDate(newDate);
}
};
return (
<>
<LocalizationProvider locale={de} dateAdapter={AdapterDateFns}>
<DatePicker
label="datepicker_label"
value={date}
inputFormat="yyyy/MM/dd"
views={['year', 'month', 'day']}
mask="____/__/__"
onChange={changeDate}
renderInput={(params) => (
<TextField type="text" {...params} data-testid="textInput_testid" name="textInput_name"/>
)}
/>
</LocalizationProvider>
</>
);
}
This works perfectly fine on the UI.
Here are my attempts to change the date. All tests fail:
describe('change date picker value test 1', () => {
test('use datepicker label; set string', async () => {
render(<DatePickerX />);
const input = screen.getByLabelText('datepicker_label');
await act(async () => {
await fireEvent.change(input, { target: { value: '3000/01/01' } });
});
expect(screen.getByText('3000/01/01')).toBeVisible();
});
test('use text input; set string', async () => {
render(<DatePickerX />);
const input2 = screen.getByTestId('textInput_testid');
await act(async () => {
await fireEvent.change(input2, { target: { value: '3000/01/01' } });
});
expect(screen.getByText('3000/01/01')).toBeVisible();
});
test('use datepicker label; set date', async () => {
render(<DatePickerX />);
const input = screen.getByLabelText('datepicker_label');
await act(async () => {
await fireEvent.change(input, { target: { value: new Date('3000/01/01') } });
});
expect(screen.getByText('3000/01/01')).toBeVisible();
});
test('use text input; set date', async () => {
render(<DatePickerX />);
const input2 = screen.getByTestId('textInput_testid');
await act(async () => {
await fireEvent.change(input2, { target: { value: new Date('3000/01/01') } });
});
expect(screen.getByText('3000/01/01')).toBeVisible();
});
});
What am I doing wrong?
Before you render any component that has dependencies, is important to load those before.
So one of the issue that Test are failing could be that when you render the component en each case the test runner is looking for the provider, adapters and the others dependencies.
To solve this you can use the jest.mock function or just import them
This is one of the example the doc link include.
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
})
Hope this help
Firstly: I wanted to note that #mui/lab adapter should not be used together with #mui/x pickers. I'd suggest syncing those usages by changing your adapter import to import { AdapterDateFns } from '#mui/x-date-pickers/AdapterDateFns'; (based on setup documentation).
Secondly: Have you checked which component is rendered during your test cases? I see that you are importing DatePicker, this component renders either Desktop or Mobile version of the picker depending on desktopModeMediaQuery prop. This rendering logic has some caveats in test environments, I'd suggested reading testing caveats doc section for more information on how to reach your desired result.
Lastly: Are those test cases you provided in the question your real project examples or just for illustration purposes? If they are real cases, I'd suggest thinking if it's worth testing behaviours, that are already tested by MUI on their end. Ideally, you should write tests asserting your own code.
Edit:
I've had a bit deeper investigation and manual testing of your cases and have the following conclusions:
3rd and 4th cases are invalid, because you can only set value on an input element, but those queries return a TextField root - div element.
2nd case does not work, because setting value to new Date() will cause the toString method to be called, which will not be in the format the component expects.
And as far as I can tell, your main issue might have been the usage of getByText query in the assertions. This query does not look for text in input element's value. Replacing it with getByDisplayValue seems to resolve your issue.
Please check this example repository with working examples.

How can I test an input with Jest

I've been trying to figure out how to test different input methods but since I am new to this test methodology, I cannot get even close to the answer. Here is what I have:
const App = (props) => {
const newGame = props.newGame;
const [typeracertext, setTyperacertext] = useState(props.typeracertext);
const [wholeText, setWholeText] = useState("");
const onChange = (e) => {
//here I have code that read the input and is comparing it with variable - typeracertext and if so, it sets the property wholeText to that value
};
return (
<input ref={(node) => this.textInput = node} placeholder="Message..." onChange={onChange}></input>
);
}
so what I am trying to figure out is a test that should set the typeracertext to a certain value (for example "This is a test), and set the input value to "This" so if it passes the onChange() check it should set wholeText to "This". I hope that makes sense.
This is the best I could get and I don't have an idea what should I write on "expect".
test('Test the input value', () => {
const node = this.textInput;
node.value = 'This';
ReactTestUtils.Simulate.change(node);
expect()
});
Since this is a react app, I'll advice you take advantage of react testing library to make this easy
import React from 'react';
import { fireEvent, render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// In describe block
test('Test input component', () => {
const onChange = jest.fn();
render(<InputComponent onChange={onChange} data-test-id="input" />);
const input = screen.getByTestId('input');
fireEvent.change(input, { target: { value: 'a value' } });
// You can also do this with userEvent
userEvent.type(input, 'test')
// Check if change event was fired
expect((input as HTMLInputElement).onchange).toHaveBeenCalled();
});
See documentation here

Test for a component: How to access to a property of a tag with getByTestId

I'm doing a component test in react (My first) and I want to verify a number, when I pass it the value, it returns undefined and I remove the value to see what it returned and it was fine, find the element
import React, { useState } from 'react';
import { render, cleanup, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import { NumberUpDown } from '../../components/number-
updown/NumberUpDown';
import '#testing-library/jest-dom/extend-expect'
const UpDownNumber = () => {
const [quantity, setQuantity] = useState<number>(1);
const packageType = 'box'
return (
<NumberUpDown
value={quantity}
valueToShow={
packageType === 'box' || 'pack' || 'piece' || 'bag' || 'sbox'
? quantity : quantity * 12
}
min={1}
max={5000}
step={1}
onChange={value => setQuantity(value)}
/>
);
};
describe('Plus or minus in the product modal', () => {
afterEach(cleanup);
beforeEach(() => render(<UpDownNumber />));
it('Validate is if exists', () => {
expect(screen.getByTestId('product-minus')).toBeInTheDocument();
expect(screen.getByTestId('product-input')).toBeInTheDocument();
expect(screen.getByTestId('product-plus')).toBeInTheDocument();
});
it('Validate function onclick', () => {
const minusButton = screen.getByTestId('product-minus');
const plusButton = screen.getByTestId('product-plus');
const input = screen.getByTestId('product-input');
userEvent.click(plusButton);
userEvent.click(plusButton);
expect(getByRole('textbox', { name: /email/i })).toHaveValue('test#email.com);
expect((input as HTMLInputElement).value).toBe(3);
userEvent.click(minusButton);
expect((input as HTMLInputElement)).toBe(2);
});
});
Expected: 3
Received: <ion-input class="value-cell" data-testid="product-input" type="number"
value="3" />
expect((input as HTMLInputElement).value).toBe(3);
Expected: 3
Received: undefined
I need that when I access the tag, when it finds it, get the value...
You already use #testing-library, so I suggest taking it one step further and add https://www.npmjs.com/package/#testing-library/jest-dom as a devDependency. If using a Create React App based app, you can add an import like to your setupTests.js file e.g.
import '#testing-library/jest-dom/extend-expect';
You can then write tests to check the value of a field using something like:
expect(getByRole('textbox', { name: /email/i })).toHaveValue('test#email.com);
Using the jest-dom lets you write tests that read far nicer, but that is just my opinion.

React Custom Hook produce warning: Maximum update depth exceeded

I'm new to React Hooks and are taking the first steps... Any help appreciated! I want to re-use logic for sorting and transforming data sets before rendering in charts. So I split it into a Custom hook but get a warning and it seems to be in a re-render loop (slowly counting up)
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I should have a dependency array and the dependencies only change on button click.. So I don't understand why it goes into a re-render loop...?
CigarettesDetailsContainer receives "raw" data in props and passes transformed data to a child component rendering the chart. It also handles changing dates from the child so I keep that state in here.
The useSingleValueChartData hook transforms the raw data and should re-run when changes to date and time period.
CigarettesDetailsContainer
import React, { FC, useState } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { CigarettesDetails } from './layout'
import { useSingleValueChartData } from 'hooks/useSingleValueChartData'
import { TimePeriod } from 'models/TimePeriod'
interface Props {
registrations: ApiRegistration[]
}
const initialStart = moment()
.year(2018)
.week(5)
.startOf('isoWeek')
const initialEnd = initialStart.clone().add(1, 'week')
const initialPeriod = TimePeriod.Week
const CigarettesDetailsContainer: FC<Props> = ({ registrations }) => {
const [startDate, setStartDate] = useState(initialStart)
const [endDate, setEndDate] = useState(initialEnd)
const [timePeriod, setTimePeriod] = useState(initialPeriod)
const data = useSingleValueChartData(
registrations,
startDate.toDate(),
endDate.toDate(),
timePeriod
)
const handleTimeChange = (change: number) => {
let newStartDate = startDate.clone()
let newEndDate = endDate.clone()
switch (timePeriod) {
default:
newStartDate.add(change, 'week')
newEndDate.add(change, 'week')
break
}
setStartDate(newStartDate)
setEndDate(newEndDate)
}
return <CigarettesDetails onTimeChange={handleTimeChange} data={data} />
}
export default CigarettesDetailsContainer
useSingleValueChartData
import React, { useEffect, useState } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { TimePeriod } from 'models/TimePeriod'
import { GroupedChartData, SingleValueChartData } from 'models/ChartData'
import { createWeekdaysList } from 'components/Core/Utils/dateUtils'
export function useSingleValueChartData(
registrations: ApiRegistration[],
startDate: Date,
endDate: Date,
timePeriod: TimePeriod = TimePeriod.Week
) {
const [data, setData] = useState<SingleValueChartData[]>([])
// used for filling chart data set with days without registrations
let missingWeekDays: string[] = []
useEffect(() => {
// which days are missing data
// eslint-disable-next-line react-hooks/exhaustive-deps
missingWeekDays = createWeekdaysList(startDate)
const filteredByDates: ApiRegistration[] = registrations.filter(reg =>
moment(reg.date).isBetween(startDate, endDate)
)
const filteredByDirtyValues = filteredByDates.filter(reg => reg.value && reg.value > -1)
const grouped: SingleValueChartData[] = Object.values(
filteredByDirtyValues.reduce(groupByWeekDay, {} as GroupedChartData<
SingleValueChartData
>)
)
const filled: SingleValueChartData[] = grouped.concat(fillInMissingDays())
const sorted: SingleValueChartData[] = filled.sort(
(a: SingleValueChartData, b: SingleValueChartData) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
)
setData(sorted)
}, [startDate, timePeriod])
function groupByWeekDay(
acc: GroupedChartData<SingleValueChartData>,
{ date: dateStr, value }: { date: string; value?: number }
): GroupedChartData<SingleValueChartData> {
const date: string = moment(dateStr).format('YYYY-MM-DD')
acc[date] = acc[date] || {
value: 0,
}
acc[date] = {
date,
value: value ? acc[date].value + value : acc[date].value,
}
// remove day from list of missing week days
const rest = missingWeekDays.filter(d => d !== date)
missingWeekDays = rest
return acc
}
function fillInMissingDays(): SingleValueChartData[] {
return missingWeekDays.map(date => {
return {
value: 0,
date,
}
})
}
return data
}
In the custom hook, though you want to run the effect only on change of startDate or timePeriod, at present the effect is run everytime.
This is because how startDate and endDate params are being passed to custom hook.
const data = useSingleValueChartData(
registrations,
startDate.toDate(),
endDate.toDate(),
timePeriod
)
.toDate returns new date object.
So every time new date object is being passed to custom hook.
To correct this, pass the startDate and endDate directly (i.e. without toDate) to custom hook and manage the moment to date conversion in the custom hook.

Cannot pass value to parent component in React.Js

I'm strugelling with that one.
I'm trying to build a calendar using react-calendar package. In one place of my app I need to have an access to the calendar month's value. I'm calling the components property onActiveDateChange which I found in docs of the component (react-calendar). It is indeed a callback so I'm trying to use it as my chance to extract month value and send it up to the parent component. But that does not work, not only the value is not changed as expected, but the calendar stops working. Do you know what is causing that? I've tried also with setting a state in the callback but the same results, value is not correct.
Here's my chunk of code:
import React, { Component } from 'react';
import Calendar from 'react-calendar';
import ChooseHour from './ChooseHour';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Calendario extends Component {
state = { showHours: false,}
onChange = date => this.setState({
date }, () => {
const { chosenRoom } = this.props;
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const fullDate = `${year}/${month}/${day}`;
const roomAndDayObj = {fullDate, chosenRoom};
this.props.sendRoomAndDay(roomAndDayObj);
}
)
onClickDay(e) {
this.setState({ showHours: true });
}
passActiveDate(activeDate) {
const monthAfterPress = activeDate.getMonth() + 1;
console.log(monthAfterPress);
// this is weird in my opinion, I can log it and it works as expected,
// when I'm firing the next line the values are incorrect
// this.props.activeMonthToPass(monthAfterPress); //this line makes a problem
}
render() {
const { booked } = this.props;
return (
<div>
<div className="calendarsCont">
<Calendar
onChange={this.onChange}
onClickDay={(e) => this.onClickDay(e)}
onActiveDateChange={({ activeStartDate }) => this.passActiveDate(activeStartDate)}
value={this.state.date}
locale="pl-PL"
tileDisabled={({date, view }) =>
date.getDate()===15 && date.getMonth()===6 && date.getFullYear()===2018}
/>
</div>
<div>
{this.state.showHours ?
<ChooseHour chosenDay={this.state.date} chosenRoom={this.props.chosenRoom}/> :
null}
</div>
</div>
)
}
}
export default connect (null, actions)(Calendario);

Resources