Testing React component that using hook - reactjs

enter image description here
Find the attached image
I am getting this following error, while running Unit test for my react file.
I couldn't able to set value for a state method.
TestFile.tsx
const [attr, setAttributes] = useState<any>(initialState);
const getDetail = async () => {
if (Id) {
const form: IForm = await getResponse(Id);
console.log("||" + form.attributes.sourceName)
setFormResponseAttributes(form.attributes);
console.log("***" + JSON.stringify(attr));
};
useEffect(() => {
getDetail();
}, []);
return(
{attr.sourceName === 'ECR' ?
<div className='fe_u_padding--right-medium'>
<Button
id='saveDraftButtonId'
text='Save as Draft'
onClick={() => saveForm(true)}
variant='secondary'
className='hide-while-printing'
/>
</div>
: null
);
}
export
TestFile.test.tsx
it('load form response', () => {
const getResponseSpy = jest.spyOn(ResponseApi, 'getResponse');
getResponseSpy.mockImplementation(() => Promise.resolve(testUtils.Response));
const setAppContextSpy = jest.fn();
let setResponseAttributes;
const wrapper = mount(
<BrowserRouter>
<RegisteredApplicationContext.Provider
value={{ appContext: testUtils.registeredApplication, setAppContext: setAppContextSpy }}
>
<FormResponse match={{ params: { formResponseId: 'formResponseId1' } }} />
</RegisteredApplicationContext.Provider>
</BrowserRouter>
);
wrapper.find(Button).find('#saveDraftButtonId').first().simulate('click', {});
wrapper.update();
expect(getFormResponseSpy).toBeCalled();
TestUtils
export const Response: IForm = {
id: uuidv4(),
attributes: {"contextId": "111","sourceName" : "ECR"}
}
I am getting error while reading the SaveDraftButtonId,
Method “simulate” is meant to be run on 1 node. 0 found instead.,
it could able to find that button coz it couldn't set the value for attr state value
Any inputs would be helpful
TIA

Related

React - Jest test failing - TestingLibraryElementError: Unable to find an element with the text

I've a jest test that is failing on addition of a new component to the page. The test is about showing of an error alert once error occurs. Code works in local environment but fails during commit.
Error Text:
TestingLibraryElementError: Unable to find an element with the text:
Student is unable to perform register/unregister activities.. This could be because
the text is broken up by multiple elements. In this case, you can
provide a function for your text matcher to make your matcher more
flexible.
Test:
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useMutation: jest.fn((_key, cb) => {
cb();
return { data: null };
})
}));
const useMutation = useMutationHook as ReturnType<typeof jest.fn>;
describe('StatusAlert', () => {
beforeEach(() => {
useMutation.mockReturnValue({});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should show error', () => {
useMutation.mockReturnValueOnce({
isError: true
});
const { getByText } = render(
<StudentRegister
students={[studentStub, studentStub]}
onSuccess={jest.fn()}
/>
);
expect(getByText(ErrorDict.ErrorRequest)).toBeInTheDocument();
});
StudentRegister:
Adding this component is causing the above mentioned error:
interface Props {
selectedStudents: Array<Student>;
onSuccessCallback: () => void;
}
export const StudentSelectionBar: FC<Props> = ({
selectedStudents,
onSuccessCallback
}) => {
const [isOpenDropCourseModal, setisOpenDropCourseModal] =
useState(false);
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) =>
student.id));
}, [selectedStudents]);
const onToggleOpenDropCourseModal = useCallback(() => {
setisOpenDropCourseModal(
(state) => !state
);
}, []);
const {
isError: isDropCourseError,
isSuccess: isDropCourseSuccess,
isLoading: isDropCourseLoading,
mutateAsync: DropCourseMutation,
error: DropCourseError
} = useMutation<void, ApiError>(
() => dropCourse(selectedStudents.map((student) =>
student.id)),
{
onSuccess() {
onToggleOpenDropCourseModal();
onSuccess();
}
}
);
return (
<>
<StatusAlert
isError={isDropCourseError}
isSuccess={isDropCourseSuccess}
errorMessage={
dropCourseError?.errorMessage ||
ErrorMessages.FailedPostRequest
}
successMessage="Students successfully dropped from
course"
/>
<StatusAlert
isError={registerMutation.isError}
isSuccess={registerMutation.isSuccess}
errorMessage={
registerMutation.error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully registered"
/>
<StatusAlert
isError={isError}
isSuccess={isSuccess}
errorMessage={
error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully unregistered"
/>
<Permissions scope={[DropCourseUsers]}>
<LoadingButton
color="error"
variant="contained"
onClick={onToggleDropCourseUserModal}
className={styles['action-button']}
loading={isDropCourseLoading}
loadingPosition="center"
disabled={registerMutation.isLoading || isLoading}
>
drop Course
</LoadingButton>
</Permissions>
<DropCourseModal
isOpen={isOpenDropCourseModal}
onCloseModal={onToggleOpenDropCourseModal}
onArchiveUsers={DropCourseMutation}
users={studentIds}
/>
</>
);
};
Update:
I've noticed that removing useEffect() hook from the component, makes it render correctly in the test. Its function is to update the state variable holding studentIds on every selection on the list.
Is there a way to mock following useEffect hook with dependency in the test?
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) => student.id));
}, [selectedStudents]);

How to resolve this reference error : Audio is not defined

Problem
I'm trying to play some audio files in some specific situations.
e.g)
When users access to login page, the audio plays 'Please enter your phone number'
when an error message alert comes up, audio file is played such as 'your phone number has been already registered'
So far, the audio files are played successfully when you access some pages, but I got the reference error in the image after I added two lines of code below in the root component (app.tsx)
import {kioskAudio} from '../src/common/utils/kioskAudio';
const {playAudio, stopAudio} = kioskAudio();
What I've tried to resolve this issue
First try:
I imported 'kioskAudio' method into KioskAlertError component directly. But I got the same reference error.
Second try:
So I thought, 'Then should I import the 'kioskAudio' method to the root component(app.tsx) and deliver the props(playAudio, stopAudio) to the component like this :
<KioskAlertError playAudio={playAudio} stopAudio={stopAudio} />
But I still got the reference error. How can I resolve this issue?
Source Code
app.tsx
import KioskAlert, {IKioskAlertProps} from './component/KioskAlert';
import KioskAlertError from './component/KioskAlertError';
import {kioskAudio} from '../src/common/utils/kioskAudio';
export default function CustomApp({Component, pageProps}) {
const router = useRouter();
const [shouldStartRender, setShouldStartRender] = useState(false);
const [kioskAlertInfo, setKioskAlertInfo] = useState({isShow: false, onOK: null} as IKioskAlertProps);
const {playAudio, stopAudio} = kioskAudio();
useEffect(() => {
setShouldStartRender(true);
}, [router]);
return (
<>
<KioskAlertContext.Provider
value={{
kioskAlertState: kioskAlertInfo,
openKioskAlert: openKioskAlert,
closeKioskAlert: closeKioskAlert,
}}
>
<SWRConfig
value={{
refreshInterval: 0,
revalidateOnReconnect: true,
revalidateOnFocus: true,
onErrorRetry: (error, key, config, revalidate, {retryCount}) => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken');
return;
}
if (retryCount >= 5) return;
setTimeout(() => {
revalidate({retryCount});
}, 5000);
},
}}
>
{shouldStartRender ? (
<DomRouter>
<DomRoutes>
<DomRoute path="/home/home.html" element={<Home />} />
<DomRoute path="/home/clause.html" element={<Clause />} />
<DomRoute path="/home/loginPhone.html" element={<LoginPhone />} />
<DomRoute path="/home/loginPin.html" element={<LoginPin />} />
<DomRoute path="/home/signUp-phone.html" element={<SignUpPhone />} />
<DomRoute path="/home/signUp-authCode.html" element={<SignUpAuthCode />} />
<DomRoute path="/home/signUp-pin.html" element={<SignUpPin />} />
<DomRoute path="/home/CheckUserByPin.html" element={<CheckUserByPin />} />
</DomRoutes>
</DomRouter>
) : null}
<KioskAlertError playAudio={playAudio} stopAudio={stopAudio} />
<KioskAlert {...kioskAlertInfo} />
</SWRConfig>
</KioskAlertContext.Provider>
</>
);
}
KioskAudio.ts
export const kioskAudio = () => {
const audio = new Audio();
const playAudio = (folder: string, file: string) => {
stopAudio();
audio.setAttribute('src', `/sounds/${folder}/${file}.mp3`);
audio.play();
};
const stopAudio = () => {
audio.pause();
audio.currentTime = 0;
};
return {
playAudio,
stopAudio,
};
};
KioskAlertError.tsx
const KioskAlertError: React.FC<IKioskAlertErrorProps> = ({playAudio, stopAudio}) => {
const [isShow, setIsShow] = useState(false);
const [content, setContent] = useState('');
useEffect(() => {
document.addEventListener('error', (data: CustomEvent) => {
const message = JSON.parse(data.detail);
const errorMessage = message.message;
setContent(getErrorMessage(message.message));
setIsShow(true);
switch (errorMessage) {
case 'Already Registered':
console.log('Already joined');
playAudio('alert', '2');
break;
case 'Can't find your numbers':
console.log('userNotFound');
playAudio('alert', '1');
break;
}
});
return () => {
document.removeEventListener('error', null);
};
}, []);
const getErrorMessage = (messageCode) => {
return messageCode;
};
return isShow ? (
<Alert
content={content}
okText={'OK'}
onOK={() => setIsShow(false)}
wrapperStyle={defaultWrapperStyle}
alertStyle={defaultAlertStyle}
upperSectionStyle={defaultUpperSectionStyle}
lowerSectionStyle={defaultLowerSectionStyle}
titleStyle={defaultTitleStyle}
contentStyle={defaultContentStyle}
cancelStyle={defaultButtonStyle}
okStyle={defaultButtonStyle}
/>
) : null;
};
export default KioskAlertError;
As you have used the audio variable inside your audio functions, the reference to the variable in function closures get lost between component re-renders. So you need to convert the kisokAudio util into a custom hook which holds the ref between renders & then use useKioskAudio instead of the simple function.
useKioskAudio.ts
import { useRef } from "react";
export const useKioskAudio = () => {
const audio = useRef(new Audio());
const playAudio = (folder: string, file: string) => {
stopAudio();
audio.current.setAttribute('src', `/sounds/${folder}/${file}.mp3`);
audio.current.play();
};
const stopAudio = () => {
audio.current.pause();
audio.current.currentTime = 0;
};
return {
playAudio,
stopAudio,
};
};
and then use it like
const { playAudio, stopAudio } = useKioskAudio();
in your app.tsx component.

Testing State variable using Jest

I am learning to test React app using jest and Enzyme . I have created a component and using redux to maintain and update the state . The component code is below .
Now i want to write the test to check initial value of prodOverviewAccordion which we are setting as true in context file.
I have tried writing , but getting error . Sharing the test code also . Please help
const ProdOverview = () => {
const {
productState,
setProdOverviewAccordion
} = React.useContext(ProductContext);
const {prodOverviewAccordion } = productState;
const [completeStatusprod, setCompleteStatusprod] = useState(false);
return (
<div onClick={toggleTriggerProd}>
<s-box>
<Collapsible
trigger={
<Accordion
name={ProductConfig.accordionTriggerLabels.prodOverviewLabel}
completeStatusIcon={completeStatusprod ? 'check-circle' : 'alert-triangle'}
completeStatus={completeStatusprod}
/>
}
easing='ease-out'
handleTriggerClick={() => {
if (!prodOverviewAccordion) {
setProdOverviewAccordion(true);
} else {
setProdOverviewAccordion(false);
}
}}
open={prodOverviewAccordion}
data-test='prodOverViewCollapsible'
>
<p>Test</p>
</Collapsible>
</s-box>
</div>
);
};
export default ProdOverview;
const prodsetup = (props = {}) => {
return shallow(<ProdOverview />);
};
describe('Product Overview panel Test', () => {
const mockSetCurrentGuess = jest.fn();
beforeEach(() => {
mockSetCurrentGuess.mockClear();
});
test('should render Collapsible panel', () => {
const wrapper = prodsetup();
const component = findByTestAttr(wrapper, 'prodOverViewCollapsible');
expect(component.length).toBe(1);
});
test('Product Overview Panel should be in open state', () => {
const wrapper = prodsetup();
expect(wrapper.state().prodOverviewAccordion.to.equal(true));
});
});

How to verify method is not invoked on mock passed as prop

I am developing a React application with jest and TypeMoq.
I can't test the negative path of a decision tree when the mocked call is a method on the object which needs to be undefined. Is there a method on TypeMoq that can help me verify that the provided method is not called?
type TopicComponentProps = {
topic: Topic
history?: History<any>
}
export const TopicComponent = ({topic, history} : TopicComponentProps) => {
const { Id, Name } = topic;
const filterTopic = () => {
if (history) { // <-- this is my problem
history.push(`/topic/overview/${Id}`);
}
}
return(
<Fragment>
<span
style={topicStyle}
onClick={() => filterTopic()}
className="topic">
{Name}
</span>
</Fragment>
)
}
The positive test case looks like this:
it('should trigger the navigation when clicked', () => {
const mockHistory = Mock.ofType<History<any>>();
const wrapper = mount(
<TopicComponent topic={testTopic} history={mockHistory.object} />
);
wrapper.simulate('click');
mockHistory.verify(x => x.push(It.isAnyString()), Times.once());
});
How do I setup the mock object, so i can test that no navigation happens when no history is provided?
it('should not trigger the navigation when history is undefined', () => {
let mockHistory = Mock.ofType<History<any>>();
???
const wrapper = mount(
<TopicComponent topic={testTopic} history={???} />
);
wrapper.simulate('click');
mockHistory.verify(x => x.push(It.isAnyString()), Times.never());
});

How can I update component whenever typing in input tag in React?

What the below code does is to get data from API, and then render it on the page. searchChange function takes a value from the input tag, and setValue for query state. My api endpoint takes argument to filter the API such as http://127.0.0.1:8000/api/deals/?q=${query}.
I'm very confused how I can update the DealList component with the API updated with query state whenever typing something in the input tag. I'm thinking of that I need to something in searchChange function, but not sure what to do there.
index.js
const useFetch = (url, query, defaultResponse) => {
const [result, setResult] = useState(defaultResponse);
const getDataFromAPI = async url => {
try {
const data = await axios.get(url);
setResult({
isLoading: false,
data
});
} catch (e) {}
};
useEffect(() => {
if (query.length > 0) {
getDataFromAPI(`${url}?q=${query}`);
} else {
getDataFromAPI(url);
}
}, []);
return result;
};
const Index = ({ data }) => {
const query = useInput("");
const apiEndpoint = "http://127.0.0.1:8000/api/deals/";
const dealFetchResponse = useFetch(apiEndpoint, query, {
isLoading: true,
data: null
});
const searchChange = e => {
query.onChange(e);
query.setValue(e.target.value);
};
return (
<Layout>
<Head title="Home" />
<Navigation />
<Container>
<Headline>
<h1>The best lease deal finder</h1>
<h4>See all the lease deals here</h4>
</Headline>
<InputContainer>
<input value={query.value} onChange={searchChange} />
</InputContainer>
{!dealFetchResponse.data || dealFetchResponse.isLoading ? (
<Spinner />
) : (
<DealList dealList={dealFetchResponse.data.data.results} />
)}
</Container>
</Layout>
);
};
export default Index;
The biggest challenge in something like this is detecting when a user has stopped typing.. If someone is searching for 'Milk' - when do you actually fire off the API request? How do you know they aren't searching for 'Milk Duds'? (This is hypothetical, and to demonstrate the 'hard' part in search bars/APIs due to their async nature)..
This is typically solved by debouncing, which has been proven to work, but is not very solid.
In this example, you can search Github repos...but even in this example, there are unnecessary requests being sent - this is simply to be used as a demonstration. This example will need some fine tuning..
const GithubSearcher = () => {
const [repos, setRepos] = React.useState();
const getGithubRepo = q => {
fetch("https://api.github.com/search/repositories?q=" + q)
.then(res => {
return res.json();
})
.then(json => {
let formattedJson = json.items.map(itm => {
return itm.name;
})
setRepos(formattedJson);
});
}
const handleOnChange = event => {
let qry = event.target.value;
if(qry) {
setTimeout(() => {
getGithubRepo(qry);
}, 500);
} else {
setRepos("");
}
};
return (
<div>
<p>Search Github</p>
<input onChange={event => handleOnChange(event)} type="text" />
<pre>
{repos ? "Repo Names:" + JSON.stringify(repos, null, 2) : ""}
</pre>
</div>
);
};
ReactDOM.render(<GithubSearcher />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

Resources