[React-testing-library][FireEvent] Screen doesn't update after firing click event - reactjs

I'm trying to simulate the 'see more' functionality to a blog.
It works as expected on the browser but when I simulate the behavior on react testing library it doesn't.
describe('when 12 blogs', () => {
describe('fetch more blogs', () => {
beforeEach(() => {
const twelveBlogs = generateBlogs(12);
const twoBlogs = generateBlogs(10);
Api.query.mockReturnValueOnce(twelveBlogs);
Api.query.mockReturnValueOnce(twoBlogs);
});
test('should fetch more blog posts when clicking on "See More" button', async () => {
render(
<MemoryRouter>
<Blog />
</MemoryRouter>
);
const seeMoreButton = await screen.findByRole('button', {
name: /See More/i,
});
fireEvent.click(seeMoreButton);
await waitFor(() => expect(Api.query).toHaveBeenCalledTimes(2));
await waitFor(
() =>
expect(screen.getAllByText(/NaN de undefined de NaN/)).toHaveLength(
15
)
);
});
});
});
And the implementation
import React from 'react';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import Api from '../../api/api';
import BlogPreview from '../../components/BlogPreview/BlogPreview';
import './Blog.css';
function Blog() {
const [blogPosts, setBlogPosts] = useState([]);
const pageSize = 12;
const category = ['document.type', 'blog'];
const orderings = '[my.blog.data desc]';
const [apiPage, setApiPage] = useState(1);
const [shouldFetchMoreBlogs, setShouldFetchMoreBlogs] = useState(true);
useEffect(() => {
async function fetchApi(options) {
return Api.query(category, options);
}
const options = { pageSize, page: apiPage, orderings };
fetchApi(options).then((response) => {
if (response?.length > 0) {
if (blogPosts.length !== 0) {
setBlogPosts([...blogPosts, response]);
} else {
setBlogPosts(response);
}
} else {
setShouldFetchMoreBlogs(false);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiPage]);
async function handleSeeMoreClick() {
setApiPage(apiPage + 1);
}
function renderBlogPosts() {
if (blogPosts.length > 0) {
return blogPosts.map((blog, index) => (
<Link to={{ pathname: `/blog/${blog.uid}`, ...blog }} key={index}>
<BlogPreview key={index} {...blog} />
</Link>
));
}
}
function renderSeeMoreButton() {
debugger;
if (blogPosts.length > 0) {
if (blogPosts?.length % 12 === 0 && shouldFetchMoreBlogs) {
return (
<div className="see-more-container">
<button className="see-more-button" onClick={handleSeeMoreClick}>
Veja Mais
</button>
</div>
);
}
}
}
return (
<section className="content blog">
<h1>BLOG</h1>
<div className="blog-posts">{renderBlogPosts()}</div>
{renderSeeMoreButton()}
</section>
);
}
export default Blog;
It fails 'cause it only finds the initial 12 blog posts, even though it shows that the api was called twice.
There's obviously some async issue here.
I've tried switching from fireEvent to userEvent, from waitFor to find*, but it still doesn't work.
Thanks

Related

How to use useSearchParams Hook with React Router v6

I am trying to implement a search parameter functionality to my React image search app. And, I have learned that I need to (can) use the useSearchParams Hook, but I am not sure how to make these changes.
So, basically I want the URL to be something like localhost:3000/input&page=1, meaning that whatever comes after the slash is going to be the input value and key/value pair for page numbers.
As you can see in the app.js, I have these 3 main Routes and the Home Route (renders Main.js) is the one I am mainly working on. Also, Main.js renders Header.js (renders form and others).
I am thinking that I should create a new Route in the app.js but I am not sure what to do.
import './App.css';
import Home from './components/pages/Home';
import Favorites from './components/pages/Favorites';
import Error from './components/pages/Error';
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { SkeletonTheme } from 'react-loading-skeleton';
import { useDarkMode } from './components/Navbar';
function App() {
const darkMode = useDarkMode(state => state.darkMode)
let style
if (darkMode === 'light') {
style = 'wrapper'
} else {
style = 'wrapper-dark'
}
return (
<div className={style}>
<SkeletonTheme baseColor="#808080" highlightColor="#b1b1b1">
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
<Route path='favorites' element={<Favorites />} />
<Route path='*' element={<Error />} />
</Routes>
</BrowserRouter>
</SkeletonTheme>
</div>
);
}
export default App;
import React from 'react'
import Header from './Header'
import Image from './Image'
import { useState, useEffect, useRef } from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTriangleExclamation } from '#fortawesome/free-solid-svg-icons'
// import InfiniteScroll from 'react-infinite-scroll-component'
function Main() {
const [input, setInput] = useState('')
const [allImages, setAllImages] = useState([])
// const [totalResults, setTotalResults] = useState(null)
const [isVisible, setIsVisible] = useState(false)
const [error, setError] = useState(null)
const [showError, setShowError] = useState(false)
const [fadeOut, setFadeOut] = useState(false)
const [page, setPage] = useState(1)
const paginationRef = useRef(false)
// get
useEffect(() => {
if (localStorage.getItem('input')) {
setInput(JSON.parse(localStorage.getItem('input')))
}
if (localStorage.getItem('allImages')) {
setAllImages(JSON.parse(localStorage.getItem('allImages')))
// setTotalResults(JSON.parse(localStorage.getItem('totalResults')))
setIsVisible(JSON.parse(localStorage.getItem('isVisible')))
setPage(JSON.parse(localStorage.getItem('page')))
paginationRef.current = true
}
}, [])
// set
//* dryer?
useEffect(() => {
localStorage.setItem('input', JSON.stringify(input))
}, [input])
useEffect(() => {
localStorage.setItem('allImages', JSON.stringify(allImages))
}, [allImages])
// useEffect(() => {
// localStorage.setItem('totalResults', JSON.stringify(totalResults))
// }, [totalResults])
useEffect(() => {
localStorage.setItem('isVisible', JSON.stringify(isVisible))
}, [isVisible])
function handleChange(event) {
setInput(event.target.value)
}
// display nothing by default
// display image-list when user press search button
// function handleSubmit(event) {
// event.preventDefault()
// // interpolate input state and .env variable to API
// fetch(`https://api.unsplash.com/search/photos?query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
// .then(res => res.json())
// .then(data => setAllImages(data.results))
// }
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total !== 0) {
setAllImages(data.results)
// setTotalResults(data.total)
setIsVisible(true)
}
} catch(error) {
setError(error)
}
}
const handleSubmit = async (event) => {
event.preventDefault();
fetchImages()
setPage(1)
paginationRef.current = true
}
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error])
// total results
// let results
// if (totalResults >= 10000) {
// results = 'Total Results: ' + totalResults + '+'
// } else if (totalResults > 0) {
// results = 'Total Results: ' + totalResults
// } else if (totalResults === 0) {
// results = 'Nothing Found'
// }
// pagination
useEffect(() => {
if (paginationRef.current) {
fetchImages()
}
localStorage.setItem('page', JSON.stringify(page))
}, [page])
function handlePrev() {
setPage(prevState => prevState - 1)
fetchImages()
}
function handleNext() {
setPage(prevState => prevState + 1)
fetchImages()
}
return (
<main>
<Header
input={input}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
{showError && <div className={`network-error ${fadeOut ? 'fade-out' : ''}`}>
<i><FontAwesomeIcon icon={faTriangleExclamation} /></i>
<div className='network-error--message'>
<h5>Network Error</h5>
<p>Please check your Internet connection and try again</p>
</div>
</div>}
{/* <p className='main--results'>{results}</p> */}
<div className='main--image-list mt-5 pb-5'>
{allImages.map(el => (
<Image
key={el.id}
// do need spread operator below for img's src to work in Image.js
{...el}
el={el}
/>
))}
</div>
{isVisible && <div className='main--pagination'>
<button disabled={page === 1} onClick={handlePrev}>
Prev
</button>
<h5 className='main--pagination--h5'>{page}</h5>
<button onClick={handleNext}>
Next
</button>
</div>}
</main>
)
}
export default Main
import React from 'react'
import Navbar from './Navbar'
function Header(props) {
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={props.handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={props.handleChange}
name='input'
value={props.input}
/>
</form>
</div>
</div>
)
}
export default Header
If you are just wanting to initialize the page state to the page queryParam the the following could work. If uses the useSearchParams to access the queryString and return a constructed URLSearchParams object which can then access individual query params. Pass the "page" query param as the initial page state value.
const [searchParams] = useSearchParams();
const [page, setPage] = useState(Number(searchParams.get("page")) || 1);
In all likelihood though you'll not want competing "sources of truth" for what the current page is. If you want the URL queryString to be the source of truth then remove the page state and just read/update the "page` query parameter directly.
Example:
function Main() {
const [searchParams, setSearchParams] = useSearchParams();
...
const page = Number(searchParams.get("page"));
// get
useEffect(() => {
...
if (localStorage.getItem('allImages')) {
...
setSearchParams(params => {
params.set("page", JSON.parse(localStorage.getItem('page')) || 1);
return params;
});
...
}
}, []);
...
const handleSubmit = async (event) => {
event.preventDefault();
...
setSearchParams(params => {
params.set("page", 1);
return params;
});
...
}
...
// pagination
useEffect(() => {
if (paginationRef.current) {
fetchImages();
}
localStorage.setItem('page', JSON.stringify(page));
}, [page])
function handlePrev() {
setSearchParams(params => {
params.set("page", Math.max(1, page - 1));
return params;
});
...
}
function handleNext() {
setSearchParams(params => {
params.set("page", page + 1);
return params;
});
...
}
return (
...
);
}

How to avoid state reset while using Intersection observer in React.js?

I'm trying to implement intersection observer in react functional component.
import React, { useEffect, useRef, useState } from "react";
import { getData } from "./InfiniteClient";
export default function InfiniteScroll() {
const [data, setData] = useState([]);
const [pageCount, setPageCount] = useState(1);
const sentinal = useRef();
useEffect(() => {
const observer = new IntersectionObserver(intersectionCallback);
observer.observe(sentinal.current, { threshold: 1 });
getData(setData, data, pageCount, setPageCount);
}, []);
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => pageCount + 1);
getData(setData, data, pageCount, setPageCount);
}
};
return (
<section>
{data &&
data.map((photos, index) => {
return <img alt="" src={photos.url} key={index} />;
})}
<div className="sentinal" ref={sentinal}>
Hello
</div>
</section>
);
}
When I'm consoling prevCount above or prevData in the below function is coming as 1 and [] which is the default state.
function getData(setData, prevData, pageCount, setPageCount) {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${pageCount}&limit=10`
)
.then((val) => val.json())
.then((val) => {
console.log("prevD", prevData,val,pageCount);
if (!prevData.length) setData([...val]);
else {
console.log("Here", pageCount, prevData, "ddd", val);
setData([...prevData, ...val]);
}
}).catch((e)=>{
console.log("Error",e);
});
}
export { getData };
The code is not entering the catch block. I have also tried setPageCount(pageCount=> pageCount+ 1); and setPageCount(pageCount+ 1); gives same result. What am I doing wrong?
Code Sandbox
Edit: I converted the above code to class based component and it is working fine. I'm more curious on how hooks based approach is resets the states.
import React, { Component } from "react";
export default class InfiniteClass extends Component {
constructor() {
super();
this.state = {
pageCount: 1,
photos: []
};
}
getData = () => {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${this.state.pageCount}&limit=3`
)
.then((val) => val.json())
.then((val) => {
this.setState({
photos: [...this.state.photos, ...val],
pageCount: this.state.pageCount + 1
});
})
.catch((e) => {
console.log("Error", e);
});
};
componentDidMount() {
console.log(this.sentinal);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.getData();
}
});
observer.observe(this.sentinal, { threshold: 1 });
}
render() {
return (
<section>
{this.state &&
this.state.photos.length &&
this.state.photos.map((photo, index) => {
return <img alt="" src={photo.url} key={index} />;
})}
<div
className="sentinal"
ref={(sentinal) => (this.sentinal = sentinal)}
>
Hello
</div>
</section>
);
}
}
Edit 2 : I tried consoling pageCount at two places one above IntersectionCallback and one inside. The value inside is not changing meaning it is storing its own variables.
useState in react takes either argument or function. So, I did something hackish. It is working but I'm looking for a better aproach.
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => {
setData(d=>{
getData(setData, d, pageCount);
return d;
})
return pageCount + 1;
});
}
};

Still re-rendering using React.memo when the prop remain unchanged

I am creating a dropzone uploader for video-uploading (using react-dropzone-uploader package),
and also get its thumbnail with package ThumbnailExtractor.
I used React.Memo(base64) to ensure if the thumbnail's base64 remain unchanged, React will not re-render.
However, when I click submit button, the thumbnail keep re-rendering even the base64 remain unchanged.
Any clue?
import React, { useEffect, useState } from "react";
import axios from "axios";
import produce from "immer";
import Dropzone from "react-dropzone-uploader";
import ThumbnailContainer from "./ThumbnailContainer";
require("./dropzone.scss");
const DropZoneUploader = ({ api, setFiles }) => {
const [clonedFiles, setClonedFiles] = useState([]);
const UploadFileWithProgress = async (f, formData) => {
try {
const options = {
onUploadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
let percent = Math.floor((loaded * 100) / total);
console.log(`${loaded}kb of ${total}kb | ${percent}%`);
if (percent < 100) {
setClonedFiles(
produce((draft) => {
const newItem = draft.find(
(item) => item.meta.id === f.meta.id
);
newItem.percent = percent;
})
);
} else {
setClonedFiles(
produce((draft) => {
const newItem = draft.find(
(item) => item.meta.id === f.meta.id
);
newItem.percent = 99;
})
);
}
}
};
await axios
.post("https://reqres.in/api/users", formData, options)
.then(async (res) => {
console.log(res);
f.meta.percent = 100;
setClonedFiles(
produce((draft) => {
const newItem = draft.find((item) => item.meta.id === f.meta.id);
newItem.true_status = "uploaded";
})
);
setFiles(
produce((draft) => {
draft.unshift(res.data.data);
})
);
f.remove();
});
} catch (err) {
console.log(err);
return null;
}
};
// called every time a file's `status` changes
const handleChangeStatus = ({ meta, file }, status) => {
if (status === "done") {
setClonedFiles(
produce((draft) => {
draft.push({
file,
meta,
true_status: "ready"
});
})
);
}
if (status === "removed") {
setClonedFiles(
produce((draft) => {
const newItemIndex = draft.findIndex(
(item) => item.meta.id === meta.id && item.true_status === "ready"
);
if (newItemIndex !== -1) {
draft.splice(newItemIndex, 1);
}
})
);
}
};
// receives array of files that are done uploading when submit button is clicked
const handleSubmit = (files, allFiles) => {
files.map(async (f) => {
const formData = new FormData();
formData.append("title", `${f.meta.name}`);
formData.append("type", f.meta.type);
formData.append("file_purpose", "private");
formData.append("file", f.file);
const reee = await UploadFileWithProgress(f, formData);
if (reee) {
console.log("pushed");
}
});
};
const PreviewOutput = ({ files }) => {
const iconByFn = {
cancel: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
},
remove: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
},
restart: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
}
};
return (
<div className="dropzone__imgs-wrapper">
{clonedFiles?.map((f) => (
<div key={f.meta.id}>
<ThumbnailContainer f={f} files={files} />
</div>
))}
</div>
);
};
const MyCustomPreview = ({
input,
previews,
submitButton,
dropzoneProps,
files,
}) => (
<div className="dropzone">
<div {...dropzoneProps}>
<PreviewOutput files={files} />
{input}
</div>
{submitButton}
</div>
);
return (
<Dropzone
onChangeStatus={handleChangeStatus}
onSubmit={handleSubmit}
autoUpload={true}
accept="video/*"
LayoutComponent={(props) => <MyCustomPreview {...props} />}
/>
);
};
export default DropZoneUploader;
ThumbnailContainer.jsx
import React, { useEffect, useState } from "react";
import ThumbnailExtractor from "react-thumbnail-extractor";
import ThumbnailImageSrc from "./ThumbnailImageSrc";
const ThumbnailContainer = ({ f, files }) => {
const [vFile, setVFile] = useState();
const [vImage, setVImage] = useState();
const [isCaptured, setIsCaptured] = useState(false);
const fileFunction = files.find((a) => a.meta.id === f.meta.id);
const { name, previewUrl } = f.meta;
const { true_status, percent } = f;
useEffect(() => {
setVFile(f.file);
console.log("fFile render");
}, [f.file]);
return (
<div key={f.meta.id} className="dropzone__img">
{!previewUrl && (
<>
<ThumbnailExtractor
maxWidth={600}
videoFile={vFile}
count={1}
onCapture={(image) => {
if (!isCaptured) {
console.log("capture Render");
setVImage(image[0]);
setIsCaptured(true);
}
}}
/>
<span className="dzu-previewFileName">{name}</span>
</>
)}
<div className="dzu-previewStatusContainer">
{fileFunction &&
fileFunction.meta.percent !== 100 &&
f.true_status !== "uploaded" ? (
<progress max={100} value={percent} />
) : (
""
)}
{true_status === "ready" && (
<button
className="dropzone__img-delete"
type="button"
onClick={fileFunction?.remove}
>
<i className="las la-times"></i>
</button>
)}
{vImage && <ThumbnailImageSrc src={vImage} />}
</div>
</div>
);
};
export default ThumbnailContainer;
ThumbnailImageSrc.jsx (React.memo)
import React from "react";
export const ThumbnailImageSrc = React.memo(function ThumbnailImageSrc({
src
}) {
return (
<div
key={src}
className="dropzone_thumbnail-preview"
style={{ backgroundImage: `url(${src}` }}
></div>
);
});
export default ThumbnailImageSrc;
Here is the codesandbox sample.

Unable to update react state with an array

I can make a successful call to getApiSuggestions with data returned. However I'm unable to assign this to my state.
As you can see my console output shows that the value for response has an array. However, when attempting to assign it to wikiResults:response the array remains empty.
note that this is a modification of react-search-autocomplete
Am I attempting to pass the variables incorrectly?
NarrativeSearch.js
import React, {useContext, useState, useEffect} from "react";
import './search.css'
import { ReactSearchAutocomplete } from 'react-search-autocomplete'
import { getApiSuggestions } from '../../requests/requests';
import {TextSearchContext} from "../../contexts/TextSearchContext"
import {SearchContext} from "../../contexts/SearchContext"
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const [wikiTitleResults, setWikiTitleResults] = useState({wikiResults:[]});
var cnJson = wikiTitleResults;
const items = wikiTitleResults.wikiResults;
const handleOnSearch = (string, results) => {
console.log("STRING: ", string)
getApiSuggestions(string).then(response => {
console.log("RESPONSE: ", response);
setWikiTitleResults({wikiResults:response}); //<---- This doesn't update the state
console.log("WikiTitle: ", wikiTitleResults.wikiResults);
console.log("Items: ", items);
})
}
const handleOnHover = (result) => {
// the item hovered
console.log(result)
}
const handleOnSelect = (item) => {
// the item selected
setTextFilterState({textFilter:item.name});
console.log(item)
}
const handleOnFocus = () => {
console.log('Focused')
}
const handleOnClear = () => {
setTextFilterState({textFilter:""});
}
const formatResult = (item) => {
return (
<>
<span style={{ display: 'block', textAlign: 'left' }}>id: {item.title}</span>
</>
)
}
return (
<div >
<div className="searchbar">
<ReactSearchAutocomplete
items={items}
onSearch={handleOnSearch}
onHover={handleOnHover}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onClear={handleOnClear}
styling={{ zIndex: 4 }} // To display it on top of the search box below
autoFocus
/>
</div>
</div>
)
}
export default Search
getApiSuggesetions
const getApiSuggestions = (title) => {
//console.log("URL Being called"+ urlSingleResult);
//console.log(title);
let result = urlMultiResult
.get(`${title}`)
.then((response) => {
console.log(Object.values(response.data.query.pages))
return Object.values(response.data.query.pages);
})
.catch((error) => {
return error;
console.log(error);
});
console.log(result);
return result;
};
I fixed this by including a useEffect and a context from the parent component.
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const {wikiTitleResults, setWikiTitleResults} = useContext(SearchContext);
var items = wikiTitleResults.wikiTitles;
useEffect(() => {
const fetchData = async () => {
const data = await getApiSuggestions(textFilterState.textFilter)
setWikiTitleResults({wikiTitles:data})
}
fetchData();
},
[textFilterState])
const handleOnSearch = (string, results) => {
setTextFilterState({textFilter:string});
}

Testing Apollo Query with React Hook Client

I am trying to write test for this component using jest
import { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Query } from 'react-apollo';
import { updateYourDetails } from 'universal/domain/health/yourDetails/yourDetailsActions';
import Input from 'universal/components/input/input';
import InputNumber from 'universal/components/input/inputNumber/inputNumber';
import AsyncButton from 'universal/components/asyncButton/asyncButton';
import ErrorMessage from 'universal/components/errorMessage/errorMessage';
import Link from 'universal/components/link/link';
import analytics from 'universal/utils/analytics/analytics';
import { isChatAvailable } from 'universal/logic/chatLogic';
import { validators } from 'universal/utils/validation';
import { localTimezone, getWeekdays } from 'universal/utils/date';
import {
CALL_ME_BACK_LOADING_MSG,
CALL_ME_BACK_LABELS_SCHEDULE_TIME,
CALL_ME_BACK_LABELS_SELECTED_DATE,
CALL_ME_BACK_ERROR_MSG,
CALL_ME_BACK_TEST_PARENT_WEEKDAY,
CALL_ME_BACK_TEST_CHILD_WEEKDAY,
} from 'universal/constants/callMeBack';
import CallCenterAvailibility from './CallCenterAvailibility';
import SelectWrapper from './SelectWrapper';
import SelectOption from './SelectOption';
import styles from './callMeBackLightBox.css';
import { CALL_ME_BACK_QUERY } from './callMeBackQuery';
import postData from './postData';
export const CallMeForm = props => {
const initSelectedDate = getWeekdays()
.splice(0, 1)
.reduce(acc => ({ ...acc }));
const { onSubmissionComplete, className, variant } = props;
const [hasSuccessfullySubmitted, setHasSuccessfullySubmitted] = useState(false);
const [apiStatus, setApiStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [cellNumber, setCallNumber] = useState(props.cellNumber || '');
const [customerFirstName, setCustomerFirstName] = useState(props.customerFirstName || '');
const [number, setNumber] = useState(props.Number || '');
const [selectedDate, setSelectedDate] = useState(initSelectedDate || '');
const [scheduledTime, setScheduledTime] = useState('');
const weekdays = getWeekdays() || [];
const timezone = localTimezone || '';
const requestReceived = apiStatus === 'CALLBACK_ALREADY_EXIST';
const cellNumberInput = useRef(null);
const customerFirstNameInput = useRef(null);
const getQuery = () => (
<Query query={CALL_ME_BACK_QUERY} variables={{ weekday: selectedDate.weekday }}>
{({ data, error, loading }) => {
if (loading)
return (
<SelectWrapper disabled labelTitle={CALL_ME_BACK_LABELS_SCHEDULE_TIME} name="scheduledTime">
<SelectOption label={CALL_ME_BACK_LOADING_MSG} />
</SelectWrapper>
);
if (error) return <ErrorMessage hasError errorMessage={<p>{CALL_ME_BACK_ERROR_MSG}</p>} />;
return (
<CallCenterAvailibility
selectedDate={selectedDate}
callCenterBusinessHour={data.callCenterBusinessHour}
onChange={val => setScheduledTime(val)}
/>
);
}}
</Query>
);
const getPostSubmitMessage = (firstName: string, type: string) => {
const messages = {
callCentreClosed: `a`,
requestReceived: `b`,
default: `c`,
};
return `Thanks ${firstName}, ${messages[type] || messages.default}`;
};
const validate = () => {
const inputs = [customerFirstNameInput, cellNumberInput];
const firstInvalidIndex = inputs.map(input => input.current.validate()).indexOf(false);
const isValid = firstInvalidIndex === -1;
return isValid;
};
const onSubmitForm = event => {
event.preventDefault();
onSubmit();
};
const onSubmit = async () => {
if (variant === '0' && !validate()) {
return;
}
analytics.track(analytics.events.callMeBack.callMeBackSubmit, {
trackingSource: 'Call Me Form',
});
setIsLoading(true);
const srDescription = '';
const response = await postData({
cellNumber,
customerFirstName,
number,
scheduledTime,
timezone,
srDescription,
});
const { status } = response;
const updatedSubmissionFlag = status === 'CALLBACK_ALREADY_EXIST' || status === 'CALLBACK_ADDED_SUCCESSFULLY';
// NOTE: add a slight delay for better UX
setTimeout(() => {
setApiStatus(apiStatus);
setIsLoading(false);
setHasSuccessfullySubmitted(updatedSubmissionFlag);
}, 400);
// Update Redux store
updateYourDetails({
mobile: cellNumber,
firstName: customerFirstName,
});
if (onSubmissionComplete) {
onSubmissionComplete();
}
};
if (hasSuccessfullySubmitted) {
return (
<p aria-live="polite" role="status">
{getPostSubmitMessage(
customerFirstName,
(!requestReceived && !isChatAvailable() && 'callCentreClosed') || (requestReceived && 'requestReceived')
)}
</p>
);
}
return (
<form onSubmit={onSubmitForm} className={className}>
{variant !== '1' && (
<>
<label htmlFor="customerFirstName" className={styles.inputLabel}>
First name
</label>
<Input
className={styles.input}
initialValue={customerFirstName}
isMandatory
maxLength={20}
name="customerFirstName"
onChange={val => setCustomerFirstName(val)}
ref={customerFirstNameInput}
value={customerFirstName}
{...validators.plainCharacters}
/>
</>
)}
{variant !== '1' && (
<>
<label htmlFor="cellNumber" className={styles.inputLabel}>
Mobile number
</label>
<Input
className={styles.input}
initialValue={cellNumber}
isMandatory
maxLength={10}
name="cellNumber"
onChange={val => setCallNumber(val)}
ref={cellNumberInput}
type="tel"
value={cellNumber}
{...validators.tel}
/>
</>
)}
{variant !== '1' && (
<>
{' '}
<label htmlFor="number" className={styles.inputLabel}>
Qantas Frequent Flyer number (optional)
</label>
<InputNumber
className={styles.input}
disabled={Boolean(props.number)}
initialValue={number}
name="number"
onChange={val => setNumber(val)}
value={number}
/>
</>
)}
{weekdays && (
<>
<SelectWrapper
testId={`${CALL_ME_BACK_TEST_PARENT_WEEKDAY}`}
labelTitle={CALL_ME_BACK_LABELS_SELECTED_DATE}
name="selectedDate"
onChange={val =>
setSelectedDate({
...weekdays.filter(({ value }) => value === val).reduce(acc => ({ ...acc })),
})
}
tabIndex={0}
>
{weekdays.map(({ value, label }, i) => (
<SelectOption
testId={`${CALL_ME_BACK_TEST_CHILD_WEEKDAY}-${i}`}
key={value}
label={label}
value={value}
/>
))}
</SelectWrapper>
{getQuery()}
</>
)}
<AsyncButton className={styles.submitButton} onClick={onSubmit} isLoading={isLoading}>
Call me
</AsyncButton>
<ErrorMessage
hasError={(apiStatus >= 400 && apiStatus < 600) || apiStatus === 'Failed to fetch'}
errorMessage={
<p>
There was an error submitting your request to call you back. Please try again or call us at{' '}
<Link href="tel:134960">13 49 60</Link>.
</p>
}
/>
</form>
);
};
CallMeForm.propTypes = {
cellNumber: PropTypes.string,
customerFirstName: PropTypes.string,
number: PropTypes.string,
onSubmissionComplete: PropTypes.func,
className: PropTypes.string,
variant: PropTypes.string,
};
const mapStateToProps = state => {
const { frequentFlyer, yourDetails } = state;
return {
cellNumber: yourDetails.mobile,
customerFirstName: yourDetails.firstName,
number: frequentFlyer.memberNumber,
};
};
export default connect(mapStateToProps)(CallMeForm);
My test file is as below
import { render, cleanup } from '#testing-library/react';
import { MockedProvider } from 'react-apollo/test-utils';
import { shallow } from 'enzyme';
import MockDate from 'mockdate';
import { isChatAvailable } from 'universal/logic/chatLogic';
import { CALL_ME_BACK_QUERY } from './callMeBackQuery';
import { CallMeForm } from './CallMeForm';
import postData from './postData';
jest.mock('universal/components/input/input', () => 'Input');
jest.mock('universal/components/asyncButton/asyncButton', () => 'AsyncButton');
jest.mock('universal/components/errorMessage/errorMessage', () => 'ErrorMessage');
jest.mock('universal/logic/chatLogic');
jest.mock('./postData');
describe('CallMeForm', () => {
let output;
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
const mockQueryData = [
{
client:{},
request: {
query: CALL_ME_BACK_QUERY,
variables: { weekday: '' },
},
result: {
data: {
callCenterBusinessHour: {
timeStartHour: 9,
timeStartMinute: 0,
timeEndHour: 5,
timeEndMinute: 0,
closed: false,
},
},
},
},
];
const { container } = render(<MockedProvider mocks={mockQueryData} addTypename={false}><CallMeForm /></MockedProvider>);
output = container;
});
afterEach(cleanup);
it('renders correctly', () => {
expect(output).toMatchSnapshot();
});
});
I keep getting error: TypeError: this.state.client.stop is not a function
I also removed <MockedProvider> wrapper and I got another error Invariant Violation: Could not find "client" in the context or passed in as a prop. Wrap the root component in an , or pass an ApolloClient instance in
via props.
Does anyone know why I get this error and how to fix this?
I have not the solution, but I've got some information.
First of all, I'm having the same error here, rendering with #testing-library/react.
I then tried to render with ReactDOM, like that:
// inside the it() call with async function
const container = document.createElement("div");
ReactDOM.render(
< MockedProvider {...props}>
<MyComponent />
</MockedProvider>,
container
);
await wait(0);
expect(container).toMatchSnapshot();
And also tried to render with Enzyme, like that:
// inside the it() call, with async function too
const wrapper = mount(
<MockedProvider {...props}>
<MyComponent />
</MemoryRouter>
);
await wait(0);
expect(wrapper.html()).toMatchSnapshot();
Both ReactDOM and Enzyme approaches worked fine.
About the error we're getting, I think maybe it's something related with #testing-library/react =/
I didn't tried to render with react-test-renderer, maybe it works too.
Well, that's what I get... maybe it helps you somehow.
Ps.: About waait: https://www.apollographql.com/docs/react/development-testing/testing/#testing-final-state
EDIT 5 Feb 2020:
Based on https://github.com/apollographql/react-apollo/pull/2165#issuecomment-478865830, I found that solution (it looks ugly but works ¯\_(ツ)_/¯):
<MockedProvider {...props}>
<ApolloConsumer>
{client => {
client.stop = jest.fn();
return <MyComponent />;
}}
</ApolloConsumer>
</MockedProvider>
I had the same problem and was able to solve it. I had a missing peer dependency.
Your package.json is not shown so I am not sure if your problem is the same as mine but I was able to resolve the problem by installing "apollo-client".
I am using AWS Appsync for my client and hence did not have apollo-client installed.

Resources