I'm trying to test a connected TSX component. I have tested connected components before and I exactly know how to implement it, but seems like there is some issue in the way that jest and typescript interact.
What I have tried ?
I have exported an unconnected component for testing purposes
I have created a mock store and wrapper the component around a provider in the test file
I have modified jest.config.js as suggest by the error
I keep getting the same error!
Cannot find module 'react' from 'Provider.js'
However, Jest was able to find:
'components/Provider.js'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'].
See https://jestjs.io/docs/en/configuration#modulefileextensions-array-string
However, Jest was able to find:
'./App.test.tsx'
'./App.tsx'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'].
See https://jestjs.io/docs/en/configuration#modulefileextensions-array-string
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:259:17)
at Object.<anonymous> (../node_modules/react-redux/lib/components/Provider.js:10:38)
My component is as below (App.tsx):
import React from "react";
import { connect } from "react-redux";
import { Album, Photo, fetchAlbums, fetchPhotos } from "../actions";
import { StoreState } from "../reducers";
// *Notice: in this file we have used React.UseEffect and React.UseState instead of importing
// hooks directly from React. That's for the reasons of testing and how Enzyme has not yet adopted
// very well with hooks.
// the type of your action creators has been intentionally set to "any", as typescript does not play well with redux-thunk
interface AppProps {
albums?: Album[];
photos?: Photo[];
fetchAlbums?(): any;
fetchPhotos?(id: number): any;
}
export const _App = ({
albums,
photos,
fetchAlbums,
fetchPhotos
}: AppProps) => {
// setting the initial state of the loader and thmbnail
const [fetching, setFetching] = React.useState(false);
const [image, setImage] = React.useState();
// setting the state back to false once our data updates
React.useEffect(() => {
setFetching(false);
}, [albums, photos]);
// click evnet handler
const ClickHandler = (): void => {
fetchAlbums();
setFetching(true);
};
// album entry event handler
const AlbumClickHandler = (id: number): void => {
fetchPhotos(id);
};
const display = (id: number): JSX.Element[] => {
const relevantThumbs = photos.filter(photo => photo.albumId === id);
return relevantThumbs.map((thumb, idx) => {
return (
<img
onClick={() => setImage(thumb.id)}
key={idx}
alt={thumb.title}
src={image === thumb.id ? thumb.url : thumb.thumbnailUrl}
></img>
);
});
};
// helper function to render jsx elements
const renderList = (): JSX.Element[] =>
albums.map(album => (
<div className="albums" key={album.id}>
<h2 onClick={() => AlbumClickHandler(album.id)}>{album.title}</h2>
{display(album.id).length !== 0 ? (
<div className="albums__thumbnails">{display(album.id)}</div>
) : null}
</div>
));
return (
<section className="container">
<button className="container__button" onClick={() => ClickHandler()}>
Fetch Albums
</button>
{/* conditionally rendering the loader */}
{fetching ? "loading" : null}
{renderList()}
</section>
);
};
const mapStateToProps = ({
albums,
photos
}: StoreState): { albums: Album[]; photos: Photo[] } => {
return { albums, photos };
};
export default connect(mapStateToProps, { fetchAlbums, fetchPhotos })(_App);
and here is my test file (App.test.tsx):
import React from "react";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { findByClass } from "../test/Utils";
import App from "./App";
Enzyme.configure({ adapter: new Adapter() });
// setting our initial mount, we use mount here bcause of the hooks
const setup = () => mount(<App />);
describe("app", () => {
it("renders succesfully", () => {
// Arrange
const wrapper = setup();
const component = findByClass(wrapper, "container");
// Assert & Act
expect(component.length).toBe(1);
});
});
What am I missing ?
Related
I am trying to create a unit test for my Profile component that calls the userService, and displays the user's information in a profile.
To do this I am using React-Testing-Library but am experiencing an issue when I am trying to mock the useParam() hook call executed within the Profile component.
This is necessary because the Profile() Component ordinarily passes in the user id of the user to generate a profile for through React Router dynamic link path variable.
Test:
import React from 'react'
// import API mocking utilities from Mock Service Worker
import {rest} from 'msw'
import {setupServer} from 'msw/node'
// import react-testing methods
import {render, fireEvent, waitFor, screen} from '#testing-library/react'
import routeData from 'react-router';
// add custom jest matchers from jest-dom
import '#testing-library/jest-dom';
import config from "../configuration/Configuration.json";
import {Profile} from "../pages/Profile";
const userId = 1;
const server = setupServer(
rest.get(config.URL + '/users/' + userId, (req, res, ctx) => {
return res(ctx.json({userId: 1, email: 'testEmail#outlook.com', password: null, firstName: 'Test', lastName: 'User'}))
}),
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
beforeEach(() => jest.spyOn(routeData, 'useParams').mockReturnValue(1))
test('loads and displays profile', async () => {
render(<Profile />)
await waitFor(() => screen.getByRole('Profile'))
expect(screen.getByText('testEmail#outlook.com')).toBeVisible();
})
Profile Component:
export const Profile = () => {
//array of compatible users fetched for a user.
const [userProfileInformation, setUserProfileInformation] = useState([]);
const [isLoading, setLoading] = useState(true);
const { userId } = useParams();
useEffect(() => {
getUserProfileInformation().then(() => {
setLoading(false);
});
}, []);
const getUserProfileInformation = async () => {
const response = await UserService.getUserProfileInformation(userId)
.then(response => response.json())
.then(data => {
setUserProfileInformation(data);
});
}
if (isLoading) {
return (
<div id="loading">
<h2>Loading...</h2>
</div>
)
}
return (
<div>
<div className="profileCard">
<h1 name='fullName'>{getFullName(userProfileInformation.firstName, userProfileInformation.lastName)}</h1>
<h2>{userProfileInformation.email}</h2>
</div>
</div>
)
}
Any help would be appreciated as as far as I can see online this should work, thanks.
Edit - Error:
Error: Cannot spyOn on a primitive value; undefined given
at ModuleMocker.spyOn
at Object.<anonymous>
at Promise.then.completed
at new Promise (<anonymous>)
at callAsyncCircusFn
at _callCircusHook
at _runTest frontend\node_modules\jest-circus\build\run.js:148:5)
at _runTestsForDescribeBlock
at run
at runAndTransformResultsToJestFormat
I am importing react-quill dynamically on the client side only using ssr: false. My functional component which is working fine, I want to add the quill-blot-formatter package to the modules part of my quill component.
My first roadblock is, I can't register this quill-blot-formatter with Quill as it shows:
ServerError
ReferenceError: document is not defined
This page is client rendered, therefore I don't understand where this error is coming from!
This is my code:
import dynamic from "next/dynamic";
import BlotFormatter from "quill-blot-formatter";
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
ssr: false,
loading: () => <p>Loading...</p>,
})
Quill.register("modules/blotFormatter", BlotFormatter);
Here, I don't understand how to bring Quill out of react-quill now that it's being imported dynamically. Therefore, I think that Quill.register isn't working. Now, how do I register quill-blot-formatter with react-quill? Following the Next.js example with react-quill, I am not even importing react-quill as ReactQuill as is the default export in the package.
Then I declared the blotFormatter module like this.
const modules = {
blotFormatter: {}, // here
toolbar: [
[{header: '1'}, {header: '2'}, {font: []}],
[{size: []}],
...
],
}
const formats = ['header','font','size','bold','italic','underline',...]
And used in the render() method like this:
export default function NewContent() {
...
render(
<QuillNoSSRWrapper
className={styles.quillTextArea}
id="quilleditor"
modules={modules}
formats={formats}
theme="snow"
onChange={handleTextChange}
readOnly={false}
/>
);
}
So far, this QuillNoSSRWrapper child component is doing it's job fine, but, how do I use the quill-blot-formatter in it's formats?
UPDATE
You don't need another useEffect to register module you can do it when you importing ReactQuill.
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
const { default: BlotFormatter } = await import("quill-blot-formatter");
RQ.Quill.register("modules/blotFormatter", BlotFormatter);
return function forwardRef({ forwardedRef, ...props }) {
return <RQ ref={forwardedRef} {...props} />;
};
},
{
ssr: false,
}
);
With this code you importing ReactQuill, register you module and pass ref that you can use later see details below. So with this code you now don't need any state. Additionally you can add custom loading function to dynamic details here.
After one day of searching i found the solution. First of all you import dynamic ReactQuill.
import dynamic from 'react/dynamic'
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
Or if you want to pass ref like this
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
// eslint-disable-next-line react/display-name
return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
},
{
ssr: false,
}
);
And then you can use ref like this <ReactQuill ... forwardedRef={quillRef}/>
So then after you imported ReactQuill on client side you need to register module i found this solution here it's looks strange and i had no time to improve it but it's work. Here the code.
const loadQuill = async () => {
return new Promise(async (resolve, reject) => {
const Quill = await require("react-quill").Quill;
const BlotFormatter = (await import("quill-blot-formatter")).default;
resolve({ Quill, BlotFormatter });
})
.then(({ Quill, BlotFormatter }) => {
Quill.register("modules/blotFormatter", BlotFormatter);
return;
})
.then((value) => {
setEnableEditor(true);
});
};
useEffect(() => {
loadQuill();
}, []);
This code will execute on client side end register module. Also as you can see you need to declarate state enableEditor. And render ReactQuill only when enableEditor is true
{enableEditor && <ReactQuill ... />}
Looks bit wierd so maybe I will update it later
I'm trying to access 2 different stores in a single component, but worry that perhaps the architecture of my app may need to change as easy-peasy may not have this functionality.
I have a GlobalStore
import { createStore } from 'easy-peasy';
const globalModel = {
menuOpen: false,
toggleMenu: action((state, payload) => {
state.menuOpen = payload;
}),
};
const GlobalStore = createStore(globalModel);
export default GlobalStore;
Just for this example, I'll use a single state and action used in the store to define whether the navigation menu is open or not.
The GlobalStore appears at the top level of my app in my App.js file.
import React from 'react';
import { StoreProvider } from 'easy-peasy';
import GlobalStore from './store/GlobalStore';
const App = () => {
return (
<StoreProvider store={GlobalStore}>
</StoreProvider>
);
};
export default App;
Now, further down the tree, I have another store SearchStore that dictates which view is active in the component.
import { createStore } from 'easy-peasy';
import { action } from 'easy-peasy';
const searchModel = {
view: 'filter',
setView: action((state, payload) => {
state.view = payload;
}),
};
const SearchStore = createStore(searchModel);
export default SearchStore;
The issue I have now is that in a component that I need to be able to access both stores to update the view with the setView action in the SearchStore and get the value of menuOpen from the GlobalStore but cannot access both concurrently.
The example I have in a component is that I have a styled component that when clicked calls the action setView but its position is also defined by whether the menuOpen is true or not. but obviously, if I try and get the state of menuOpen it will be undefined as it does not exist in SearchStore
const Close = styled.span`
$(({ menuOpen }) => menuOpen ? `
// styles go here
` : `` }
`;
const setView = useStoreActions((action) => action.setView);
const menuOpen = useStoreState((state) => state.menuOpen);
<Close menuOpen={menuOpen} onClick={() => setView('list')}>
Is this possible? Any help would be much appreciated.
Alternative 1: extending the global store
To access both store (via the useStoreState/Actions from the StoreProvider), you could nest both "sub" stores into the GlobalStore:
// SearchModel.js
import { action } from 'easy-peasy';
const searchModel = {
view: 'filter',
setView: action((state, payload) => {
state.view = payload;
}),
};
export default searchModel;
// MenuModel.js
import { action } from 'easy-peasy';
const menuModel = {
isOpen: false,
toggle: action((state, payload) => {
state.isOpen = !state.isOpen;
}),
};
export default menuModel;
// GlobalStore.js
import { createStore } from 'easy-peasy';
import menu from './MenuhModel';
import search from './SearchModel';
const globalModel = {
menu,
search,
};
const GlobalStore = createStore(globalModel);
export default GlobalStore;
This way, you can access both stores at your convenience, using the hooks:
const searchState = useStoreState((state) => state.search);
const menuState = useStoreState((state) => state.menu);
const searchActions = useStoreActions((action) => action.search);
const menuActions = useStoreActions((action) => action.menu);
Alternative 2: useLocalStore()
If you do not want to extend the global store, you could create a local store, by using the useLocalStore():
function Menu() {
const [state, actions] = useLocalStore(() => ({
isOpen: false,
toggle: action((state, payload) => {
state.isOpen = !state.isOpen;
}),
}));
return (
<div>
{state.isOpen && <MenuItems />}
<button onClick={() => actions.toggle()}>Open menu</button>
</div>
);
}
However, the drawback of this approach, is that the state is not global and only available at the component-level.
You could however get around this, by creating your own provider - but then again, alternative 1 would probably be the path of least resistance.
I'm quite new to React Hooks/Context so I'd appreciate some help. Please don' t jump on me with your sharp teeth. I Checked other solutions and some ways i've done this before but can't seem to get it here with the 'pick from the list' way.
SUMMARY
I need to get the municipios list of names inside of my const 'allMunicipios'(array of objects) inside of my Search.js and then display a card with some data from the chosen municipio.
TASK
Get the data from eltiempo-net REST API.
Use Combobox async element from Elastic UI to choose from list of municipios.
Display Card (from elastic UI too) with some info of chosen municipio.
It has to be done with function components / hooks. No classes.
I'd please appreciate any help.
WHAT I'VE DONE
I've created my reducer, context and types files in a context folder to fecth all data with those and then access data from the component.
I've created my Search.js file. Then imported Search.js in App.js.
I've accesed the REST API and now have it in my Search.js
PROBLEM
Somehow I'm not beeing able to iterate through the data i got.
Basically i need to push the municipios.NOMBRE from api to the array const allMunicipios in my search.js component. But when i console log it it gives me undefined. Can;t figure out why.
I'll share down here the relevant code/components. Thanks a lot for whoever takes the time.
municipiosReducer.js
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
export default (state, action) => {
switch (action.type) {
case SEARCH_MUNICIPIOS:
return {
...state,
municipios: action.payload,
};
case GET_MUNICIPIO:
return {
...state,
municipio: action.payload,
};
case CLEAR_MUNICIPIOS:
return {
...state,
municipios: [],
};
case GET_WEATHER: {
return {
...state,
weather: action.payload,
};
}
default:
return state;
}
};
municipiosContext.js
import { createContext } from "react";
const municipiosContext = createContext();
export default municipiosContext;
MunicipiosState.js
import React, { createContext, useReducer, Component } from "react";
import axios from "axios";
import MunicipiosContext from "./municipiosContext";
import MunicipiosReducer from "./municipiosReducer";
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
const MunicipiosState = (props) => {
const initialState = {
municipios: [],
municipio: {},
};
const [state, dispatch] = useReducer(MunicipiosReducer, initialState);
//Search municipios
//In arrow functions 'async' goes before the parameter.
const searchMunicipios = async () => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios`
// 08 means barcelona province. This should give me the list of all its municipios
);
dispatch({
type: SEARCH_MUNICIPIOS,
payload: res.data.municipios,
});
};
//Get Municipio
const getMunicipio = async (municipio) => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios/${municipio.CODIGOINE}`
//CODIGOINE is in this REST API kind of the ID for each municipio.
//I intent to use this later to get the weather conditions from each municipio.
);
dispatch({ type: GET_MUNICIPIO, payload: res.municipio });
};
const dataMunicipiosArray = [searchMunicipios];
//Clear Municipios
const clearMunicipios = () => {
dispatch({ type: CLEAR_MUNICIPIOS });
};
return (
<MunicipiosContext.Provider
value={{
municipios: state.municipios,
municipio: state.municipio,
searchMunicipios,
getMunicipio,
clearMunicipios,
dataMunicipiosArray,
}}
>
{props.children}
</MunicipiosContext.Provider>
);
};
export default MunicipiosState;
Search.js
import "#elastic/eui/dist/eui_theme_light.css";
import "#babel/polyfill";
import MunicipiosContext from "../contexts/municipiosContext";
import MunicipiosState from "../contexts/MunicipiosState";
import { EuiComboBox, EuiText } from "#elastic/eui";
import React, { useState, useEffect, useCallback, useContext } from "react";
const Search = () => {
const municipiosContext = useContext(MunicipiosContext);
const { searchMunicipios, municipios } = MunicipiosState;
useEffect(() => {
return municipiosContext.searchMunicipios();
}, []);
const municipiosFromContext = municipiosContext.municipios;
const bringOneMunicipio = municipiosContext.municipios[0];
let municipiosNames = municipiosFromContext.map((municipio) => {
return { label: `${municipio.NOMBRE}` };
});
console.log(`municipiosFromContext`, municipiosFromContext);
console.log(`const bringOneMunicipio:`, bringOneMunicipio);
console.log(`municipiosNames:`, municipiosNames);
const allMunicipios = [
{ label: "santcugat" },
{ label: "BARCELONETA" },
{ label: "BARCE" },
];
const [selectedOptions, setSelected] = useState([]);
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState([]);
let searchTimeout;
const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};
// combo-box
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, []);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
return (
<div>
<EuiComboBox
placeholder="Search asynchronously"
async
options={options}
selectedOptions={selectedOptions}
isLoading={isLoading}
onChange={onChange}
onSearchChange={onSearchChange}
/>
<button>Lista de municipios</button>
</div>
);
};
export default Search;
also the
Home.js
import React, { useState } from "react";
import { EuiComboBox, EuiText } from "#elastic/eui";
// import { DisplayToggles } from "../form_controls/display_toggles";
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import Search from "./Search";
import MunicipioCard from "./MunicipioCard";
const Home = () => {
return (
<div>
<EuiText grow={false}>
<h1>Clima en la provincia de Barcelona</h1>
<h2>Por favor seleccione un municipio</h2>
</EuiText>
<Search />
<MunicipioCard />
</div>
);
};
export default Home;
App.js
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import { EuiText } from "#elastic/eui";
import React from "react";
import Home from "./components/Home";
import MunicipiosState from "./contexts/MunicipiosState";
import "./App.css";
function App() {
return (
<MunicipiosState>
<div className="App">
<EuiText>
<h1>App Component h1</h1>
</EuiText>
<Home />
</div>
</MunicipiosState>
);
}
export default App;
You are using forEach and assigning the returned value to a variable, however forEach doesn't return anything. You should instead use map
let municipiosNames = municipiosFromContext.map((municipio) => {
return `label: ${municipio.NOMBRE}`;
});
As per your comment:
you data is loaded asynchronously, so it won't be available on first render and since functional components depend on closures, you onSearchChange function takes the value from the closure at the time of creation and even if you have a setTimeout within it the updated value won't reflect
The solution here is to add municipiosFromContext as a dependency to useEffect
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, [municipiosFromContext]);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
I try to fetch some data from the tvmaze api using react typescript and useContext, i can display the data but the useContext don't update with the return so when i use the map function nothing display any advice?
import React , { Fragment, useEffect, useContext, useState } from 'react'
import axios from 'axios'
import Store from '../Store/Store'
import "core-js/stable";
import "regenerator-runtime/runtime";
export default function App() {
const {state, dispatch} = useContext(Store)
useEffect(() => {
state.episodes.length === 0 && fetchDataAction()
})
const fetchDataAction = async () => {
const URL = 'http://api.tvmaze.com/singlesearch/shows?q=rick-&-morty&embed=episodes'
const data = await fetch(URL);
const dataJSON = await data.json();
console.log(dataJSON._embedded.episodes);
return dispatch({
type: "FETCH-DATA",
payload: dataJSON._embedded.episodes,
})
}
return (
<Fragment>
{console.log(state)}
<h1>Rick and Morty</h1>
<p>Pick your favorite episode!!!</p>
<section>
{state.episodes.map((episode: any) => {
return (
<section key={episode.id}>
<img src={episode.image.medium} alt={`Rick and Morty ${episode.name}`} />
<section>
Season: {episode.season} Number: {episode.number}
</section>
</section>
)
})}
</section>
</Fragment>
)
}
i'm not using redux but i'm using babel + webpack, i'm really new to all this so i'm a bit lost here my Store.tsx file,
import React from 'react'
interface IState {
episodes: [],
favorites: []
}
interface IAction {
type: string,
payload: any
}
const initialState:IState = {
episodes: [],
favorites: []
};
const Store = React.createContext<IState | any>(initialState)
function reducer(state: IState, action: IAction): IState {
switch (action.type) {
case 'FETCH_DATA':
return { ...state, episodes: action.payload}
default:
return state
}
}
export function StoreProvider(props: any): JSX.Element {
const [state, dispatch] = React.useReducer(reducer, initialState)
return <Store.Provider value={{state, dispatch}}>{props.children}</Store.Provider>
};
export default Store;
you have a typo. you are dispatching type: "FETCH-DATA", and your swich statement is checking for case 'FETCH_DATA'.
To avoid this problem in the future, common practice is to create a separate file where you would declare actions, in your case "FETCH_CASE". you can do literally const FETCH_ACTIONS = "FETCH_ACTIONS"
And then in your files where your reducer and dispatcher are, you would import the action to make sure they are referring to the same thing.
redux and webpack/babel are not used for the same purpose - redux is for store management, similar to useContext. webpack and babel compile your code, more or less.
there is no need to import axios if you are not using it, you are using fetch API for the same purpose you could use axios.
you have to declare fetchDataAction before you call it in the useEffect. in other words, useEffect should be below the function.