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

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.

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

How to fix invalid hook call error in my airtable extension?

I'm getting this error in my Airtable blocks extension:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm unsure as to why. My program uses airtable blocks api to create a dropdown that sets a useState value, before useEffect updates a database live. Both hooks are being used inside the function body, and the function should be a react component, so I don't understand where the error lies. I've seen it can be due to react version conflicts and such as well, but I'm not sure how to confirm whether or not that is the underlying issue.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import {
Select,
initializeBlock,
SelectSynced,
useBase,
useRecords,
BaseProvider,
useGlobalConfig,
expandRecord,
TablePickerSynced,
ViewPickerSynced,
FieldPickerSynced,
FormField,
Input,
Button,
Box,
Icon,
} from '#airtable/blocks/ui';
import { FieldType } from '#airtable/blocks/models';
const base = useBase();
const table = base.getTable("National Works In Progress");
export default function FilterApp() {
// YOUR CODE GOES HERE
let records = useRecords(table);
var aunumbers_array = [];
const [value, setValue] = useState("");
const queryResult = table.selectRecords({ fields: ["AU Number"] });
records.forEach(function (x) {
if (aunumbers_array.indexOf(x.getCellValueAsString("AU Number"), -1)) {
aunumbers_array.push({ value: x.getCellValueAsString("AU Number"), label: x.getCellValueAsString("AU Number") })
}
});
queryResult.unloadData();
let updates = [];
useEffect(() => {
const fetchData = async () => {
records.forEach(function (x) {
if (x.getCellValueAsString('AU Number') == value) {
updates.push({ id: x.id, fields: { 'Matches Filter': true } });
console.log(value);
}
else if (x.getCellValueAsString('AU Number') !== value && x.getCellValueAsString('Matches Filter') == 'checked') {
updates.push({ id: x.id, fields: { 'Matches Filter': false } });
}
});
while (updates.length) {
await table.updateRecordsAsync(updates.splice(0, 50));
}
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [value])
return (
<div>
<FormField label="Text field">
<Select
options={aunumbers_array}
value={value}
onChange={newValue => setValue(newValue.toString())}
width="320px"
/>
</FormField>
</div>
);
}
The error is because you are calling useBase, a custom hook in an invalid way.
const base = useBase();
This is wrong way of calling a hook as hooks can only be called inside of the body of a function component.
Move the hook call inside FilterApp() functional component.

Describe method can only pass with 1 test unless re-rendering each component again and again

I'm trying to figure out why my test - which passes when ran alone - is failing whenever the describe block contains more than 1 test. Take this example, which I've taken from my real code and simplified:
describe('Create Account Form', () => {
const {container} = render(<CreateAccountForm />);
const email = container.querySelector('input[name="email"]');
const password1 = container.querySelector('input[name="password1"]');
it('Should render all fields', () => {
allInputs.forEach((input) => {
expect(input).toBeInTheDocument();
});
});
it('Another test', () => {
expect(email).toBeInTheDocument(); // fails
});
});
The 2nd test fails, but passes only when commenting out the first test, or re-rendering the container again in the test like this:
it('Another test', () => {
const {container} = render(<CreateAccountForm />);
const email = container.querySelector('input[name="email"]');
expect(email).toBeInTheDocument(); // passes
});
Why does this have to happen? I would much rather not have to re-render the container and declare new variables inside each test block.
Thank you
RTL will unmount React trees that were mounted with render in afterEach hook. See cleanup.
Please note that this is done automatically if the testing framework you're using supports the afterEach global and it is injected to your testing environment (like mocha, Jest, and Jasmine).
Move the render code into beforeEach or individual test case. So that we can create react trees before each test case. Isolate test cases from each other, using their own test data without affecting the rest.
E.g.
index.tsx:
import React from 'react';
export function Example() {
return (
<div>
<input name="email" />
<input name="password1" />
</div>
);
}
index.test.tsx:
import { render } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
import { Example } from './';
describe('70753645', () => {
let email, password1, allInputs;
beforeEach(() => {
const { container } = render(<Example />);
email = container.querySelector('input[name="email"]');
password1 = container.querySelector('input[name="password1"]');
allInputs = container.querySelectorAll('input');
});
it('Should render all fields', () => {
allInputs.forEach((input) => {
expect(input).toBeInTheDocument();
});
});
it('Another test', () => {
expect(email).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/70753645/index.test.tsx (9.222 s)
70753645
✓ Should render all fields (24 ms)
✓ Another test (3 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 9.717 s
package versions:
"#testing-library/react": "^11.2.2",
"jest": "^26.6.3",

How do I write this test for complete coverage?

I am new to React development and am studying testing with Jest and React Testing Library (RTL).
But I'm having difficulty doing the complete coverage of the component below:
import {
CustomCardActions,
CustomCardHeader,
} from '#Custom/react';
import React from 'react';
import {
PortalAccessButton,
PortalAccessContext,
PortalAccessInternalCard,
PortalAccessTitle,
} from './styles';
interface PortalAccessCard {
children: React.ReactNode
buttonText: string;
hrefLink: string;
}
export const redirectToUrl = (hrefLink: string) => {
window.open(hrefLink, '_self');
};
const PortalAccessCard = (props: PortalAccessCard) => {
const { children, buttonText, hrefLink } = props;
return (
<PortalAccessContext inverse>
<PortalAccessInternalCard>
<CustomCardHeader>
<PortalAccessTitle variant="heading-4">
{children}
</PortalAccessTitle>
</CustomCardHeader>
<CustomCardActions>
<PortalAccessButton onCustomClick={() => redirectToUrl(hrefLink)}>
{buttonText}
</PortalAccessButton>
</CustomCardActions>
</PortalAccessInternalCard>
</PortalAccessContext>
);
};
export default React.memo(PortalAccessCard);
There are two details here:
1- I exported the "redirectToUrl" method to be able to test it. I can't say if there's a better way out, but maybe the second question solves this one.
2- When I check the coverage report it says that this part () => redirectToUrl(hrefLink) has not been tested, but it is basically the pointer to the method I exported above.
My test looks like this:
import { render, RenderResult } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import PortalAccessCard from '.';
import * as PortalAccessCardComponent from '.';
describe('PortalAccessCard', () => {
let renderResult: RenderResult;
const hrefLink = '#';
beforeEach(() => {
renderResult = render(
<PortalAccessCard
buttonText="Texto do botão"
hrefLink={hrefLink}
>
Texto interno PortalAccessCard.
</PortalAccessCard>,
);
});
it('should call onCustomClick and redirectToUrl', async () => {
window.open = jest.fn();
jest.spyOn(PortalAccessCardComponent, 'redirectToUrl');
const onCustomClick = jest.fn(() => PortalAccessCardComponent.redirectToUrl(hrefLink));
const CustomButtonElement = renderResult.container.getElementsByTagName('Custom-button')[0];
CustomButtonElement.onclick = onCustomClick;
await userEvent.click(CustomButtonElement);
expect(onCustomClick).toBeCalledTimes(1);
expect(PortalAccessCardComponent.redirectToUrl).toBeCalledTimes(1);
});
});
What can I do to make the test call of the onCustomClick event call the redirectToUrl method so that Jest understands that this snippet has been tested?
Not sure which exactly line is not covered... Though, toBeCalledTimes is a sign of bad test expectation, so try to append to the very bottom line:
expect(PortalAccessCardComponent.redirectToUrl).toBeCalledWith(hrefLink);
It's better to test for the side effect you want (opening a window). redirectToUrl is an implementation detail. I think you're making this much harder than it needs to be.
Spy on window.open, click the item, check the spy. I think that's all you need.
jest.spyOn(window, 'open')
const CustomButtonElement = renderResult.container.getElementsByTagName('Custom-button')[0];
await userEvent.click(CustomButtonElement);
// or maybe: getByRole('something...').click()
expect(window.open).toHaveBeenCallWith('#', '_self')

Resources