I am writing a test like so:
class Foo extends Component {
render() {
return (
<div data-test="foo-test">
<span data-test="foo-test">Bar</span>
</div>
)
}
}
describe('Login Page', () => {
test('logs in to the application', async () => {
const component = await componentBuilder(Foo, '[data-test="foo-test"]');
});
});
The componentBuilder looks like this:
const componentBuilder = async (RootComponent, selector) => {
try {
const component = mount(
<Wrapper store={store}>
<RootComponent />
</Wrapper>
);
console.log(component.debug());
if (selector) {
await waitForComponentSelector(selector, component);
}
return component;
} catch(err) {
throw err;
}
};
Wrapper looks like this:
class Wrapper extends Component {
...
render() {
return (
<Provider store={this.props.store}>
<LocaleProviderWrapper>
<ThemeProvider>{this.props.children}</ThemeProvider>
</LocaleProviderWrapper>
</Provider>
);
}
}
LocaleProviderWrapperlooks like this:
const LocaleProviderWrapper = ({ children, locale }) => {
...
return (
<IntlProvider locale={locale} messages={messages}>
{children}
</IntlProvider>
)
};
const propTypes = {
children: PropTypes.element.isRequired,
locale: PropTypes.string.isRequired,
};
LocaleProviderWrapper.propTypes = propTypes;
const mapStateToProps = state => ({
locale: getLoggedInUserLocale(state),
});
export default connect(mapStateToProps)(LocaleProviderWrapper);
The output from the console.log(component.debug()); in componentBuilder is:
<Wrapper store={{...}}>
<Provider store={{...}}>
<Connect(LocaleProviderWrapper)>
<LocaleProviderWrapper locale="en-GB" dispatch={[Function]}>
<IntlProvider locale="en-GB" messages={{...}} defaultLocale="en-GB" />
</LocaleProviderWrapper>
</Connect(LocaleProviderWrapper)>
</Provider>
</Wrapper>
As you can see, Foo does not appear as a child of <IntlProvider> which I would expect to see. Any reason why this is?
Related
I have a layout component that needs onAppBarInputChange prop. The onAppBarInputChange prop expected a function that take the input value from the layout component, and filter the todos based on that input value.
How do I pass the props from the todos page to the layout component?
todos.jsx
import {useState} from 'react'
import Layout from './layout'
const Todos = () => {
const [query, setQuery] = useState('')
const todos = [
{
id: 0,
text: 'make some projects'
},
{
id: 1,
text: 'fix some bugs'
},
{
id: 2,
text: 'cook food at home'
}
]
const searchedTodos = todos.filter(todo => todo.toLowerCase().includes(query.toLowerCase()))
return (
<ul>
{searchedTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
Todos.getLayout = function getLayout(page) {
return (
{/* how to set the query like this? */}
<Layout onAppBarInputChage={() => setQuery(e.targe.value)}>
{page}
</Layout>
)
}
export default Todos;
layout.jsx
const Layout = ({children, onAppBarInputChange}) => {
return (
<div>
<header>
<div>Todo page</div>
<input onChange={onAppBarInputChange} />
</header>
<main>{children}</main>
<footer>
some footer here
</footer>
</div>
)
}
export default Layout
Note: I had read the documentation from the next.js website, about how to add layout in next.js, however they don't show any examples on how to pass the props to the layout component
How about passing the input value through Context?
By adopting Context every component can observe the input value easily.
context/app.jsx
const AppContext = createContext(null);
const AppContextProvider = ({ children }) => {
const [query, setQuery] = useState("");
const value = {
query,
setQuery,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
const useAppContext = () => useContext(AppContext);
export { AppContext, AppContextProvider, useAppContext };
pages/_app.jsx
function App({ Component, pageProps }) {
return (
<AppContextProvider>
{Component.getLayout(<Component {...pageProps} />)}
</AppContextProvider>
);
}
component/layout.jsx
const Layout = ({ children }) => {
const { setQuery } = useAppContext();
const onAppBarInputChange = (e) => setQuery(e.target.value);
...(snip)...
todos.jsx
const Todos = () => {
const { query } = useAppContext();
...(snip)...
};
Todos.getLayout = function getLayout(page) {
return <Layout>{page}</Layout>;
};
In the bellow example, how can I pass imageAsUrl.imgUrl from the Child component (ImageUpload) to the parent component (UpdateCard).
CHILD Component
import React, { useState, useEffect } from 'react';
import { storage } from '../firebase';
const ImageUpload = () => {
const allInputs = { imgUrl: '' };
const [imageAsUrl, setImageAsUrl] = useState(allInputs);
const [image, setImage] = useState(null);
const handleChange = (e) => {
if (e.target.files[0]) {
setImage(e.target.files[0]);
}
};
useEffect(() => {
if (image) {
const uploadTask = storage.ref(`images/${image.name}`).put(image);
uploadTask.on(
'state_changed',
(snapshot) => {},
(error) => {
console.log(error);
},
() => {
storage
.ref('images')
.child(image.name)
.getDownloadURL()
.then((fireBaseUrl) => {
setImageAsUrl((prevObject) => ({
...prevObject,
imgUrl: fireBaseUrl,
}));
});
}
);
}
}, [image]);
return (
<>
<label className='custom-file-upload'>
<input type='file' onChange={handleChange} />
</label>
<img src={imageAsUrl.imgUrl} alt='sample' />
</>
);
};
export default ImageUpload;
PARENT Component
import React, { useState } from 'react';
import firebase from '../firebase';
import ImageUpload from './ImageUpload';
const UpdateCard = ({ card }) => {
const [originalText, setOriginalText] = useState(card.originalText);
const [translatedText, setTranslatedText] = useState(card.translatedText);
const onUpdate = () => {
const db = firebase.firestore();
db.collection('FlashCards')
.doc(card.id)
.set({ ...card, originalText, translatedText });
timeOutScroll();
};
return (
<>
<div>
{card.imageURL ? (
<img src={card.imageURL} alt='' className='img' />
) : (
<textarea
className='upload-textarea'
value={originalText}
onChange={(e) => {
setOriginalText(e.target.value);
}}
/>
)}
<ImageUpload />
</div>
<textarea
value={translatedText}
onChange={(e) => {
setTranslatedText(e.target.value);
}}
/>
<button onClick={onUpdate}>Update</button>
</>
);
};
export default UpdateCard;
Inside parent,You can define a callback function as prop ref to be called inside the child.
const ImageUpload = ({getURLtoParent}) =>{ <--------------------
const [imageAsUrl, setImageAsUrl] = useState(allInputs);
useEffect(() => {
uploadTask.on(
..............
...
);
if(imageAsUrl.imgUrl !== '')
getURLtoParent(imageAsUrl.imgUrl) <-----------------------
},[image])
}
const UpdateCart = () => {
const[imgURL,setimgURL] = useState(null)
return (
......
<ImageUpload getURLtoParent={ (url) => setimgURL(url) } /> <----------------
.........
)
}
I have a UserFeed component and EditForm component. As soon as I edit the form, I need to be redirected to the UserFeed and the updated data should be shown on UserFeed(title and description of the post).
So, the flow is like-UserFeed, which list the posts, when click on edit,redirected to EditForm, updates the field, redirected to UserFeed again, but now UserFeed should list the posts with the updated data, not the old one.
In this I'm just redirectin to / just to see if it works. But I need to be redirected to the feed with the updated data.
EditForm
import React, { Component } from "react";
import { connect } from "react-redux";
import { getPost } from "../actions/userActions"
class EditForm extends Component {
constructor() {
super();
this.state = {
title: '',
description: ''
};
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
})
};
componentDidMount() {
const id = this.props.match.params.id
this.props.dispatch(getPost(id))
}
componentDidUpdate(prevProps) {
if (prevProps.post !== this.props.post) {
this.setState({
title: this.props.post.post.title,
description: this.props.post.post.description
})
}
}
handleSubmit = () => {
const id = this.props.match.params.id
const data = this.state
this.props.dispatch(updatePost(id, data, () => {
this.props.history.push("/")
}))
}
render() {
const { title, description } = this.state
return (
<div>
<input
onChange={this.handleChange}
name="title"
value={title}
className="input"
placeholder="Title"
/>
<textarea
onChange={this.handleChange}
name="description"
value={description}
className="textarea"
></textarea>
<button>Submit</button>
</div>
);
}
}
const mapStateToProps = store => {
return store;
};
export default connect(mapStateToProps)(EditForm)
UserFeed
import React, { Component } from "react"
import { getUserPosts, getCurrentUser } from "../actions/userActions"
import { connect } from "react-redux"
import Cards from "./Cards"
class UserFeed extends Component {
componentDidMount() {
const authToken = localStorage.getItem("authToken")
if (authToken) {
this.props.dispatch(getCurrentUser(authToken))
if (this.props && this.props.userId) {
this.props.dispatch(getUserPosts(this.props.userId))
} else {
return null
}
}
}
render() {
const { isFetchingUserPosts, userPosts } = this.props
return isFetchingUserPosts ? (
<p>Fetching....</p>
) : (
<div>
{userPosts &&
userPosts.map(post => {
return <Cards key={post._id} post={post} />
})}
</div>
)
}
}
const mapStateToPros = state => {
return {
isFetchingUserPosts: state.userPosts.isFetchingUserPosts,
userPosts: state.userPosts.userPosts.userPosts,
userId: state.auth.user._id
}
}
export default connect(mapStateToPros)(UserFeed)
Cards
import React, { Component } from "react"
import { connect } from "react-redux"
import { compose } from "redux"
import { withRouter } from "react-router-dom"
class Cards extends Component {
handleEdit = _id => {
this.props.history.push(`/post/edit/${_id}`)
}
render() {
const { _id, title, description } = this.props.post
return (
<div className="card">
<div className="card-content">
<div className="media">
<div className="media-left">
<figure className="image is-48x48">
<img
src="https://bulma.io/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div className="media-content" style={{ border: "1px grey" }}>
<p className="title is-5">{title}</p>
<p className="content">{description}</p>
<button
onClick={() => {
this.handleEdit(_id)
}}
className="button is-success"
>
Edit
</button>
</div>
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return {
nothing: "nothing"
}
}
export default compose(withRouter, connect(mapStateToProps))(Cards)
updatePost action
export const updatePost = (id, data, redirect) => {
return async dispatch => {
dispatch( { type: "UPDATING_POST_START" })
try {
const res = await axios.put(`http://localhost:3000/api/v1/posts/${id}/edit`, data)
dispatch({
type: "UPDATING_POST_SUCCESS",
data: res.data
})
redirect()
} catch(error) {
dispatch({
type: "UPDATING_POST_FAILURE",
data: { error: "Something went wrong"}
})
}
}
}
I'm not sure if my action is correct or not.
Here's the updatePost controller.
updatePost: async (req, res, next) => {
try {
const data = {
title: req.body.title,
description: req.body.description
}
const post = await Post.findByIdAndUpdate(req.params.id, data, { new: true })
if (!post) {
return res.status(404).json({ message: "No post found "})
}
return res.status(200).json({ post })
} catch(error) {
return next(error)
}
}
One mistake is that to set the current fields you need to use $set in mongodb , also you want to build the object , for example if req.body does not have title it will generate an error
updatePost: async (req, res, next) => {
try {
const data = {};
if(title) data.title=req.body.title;
if(description) data.description=req.body.description
const post = await Post.findByIdAndUpdate(req.params.id, {$set:data}, { new: true })
if (!post) {
return res.status(404).json({ message: "No post found "})
}
return res.status(200).json({ post })
} catch(error) {
return next(error)
}
}
I'm attempting to follow this guide in Reat-Testing-Library documentation to wrap all the components I want to test. I'm doing this because I need access to the various context providers that are defined in _app.js within the components I'm testing.
This is my /pages/_app.js file:
export class MyApp extends App {
public componentDidMount() {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles && jssStyles.parentNode) {
jssStyles.parentNode.removeChild(jssStyles);
}
}
public render() {
const { Component, pageProps, apolloClient } = this.props;
return (
<Container>
<StateProvider>
<ThemeProvider theme={theme}>
<ApolloProvider client={apolloClient}>
<CssBaseline />
<Component {...pageProps} />
<SignUp />
<Snackbar />
</ApolloProvider>
</ThemeProvider>
</StateProvider>
</Container>
);
}
}
export default withApollo(MyApp);
This is my /utils/testProviders.js file:
class AllTheProvidersWrapped extends App {
public componentDidMount() {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles && jssStyles.parentNode) {
jssStyles.parentNode.removeChild(jssStyles);
}
}
public render() {
const { pageProps, apolloClient, children } = this.props;
return (
<Container>
<StateProvider>
<ThemeProvider theme={theme}>
<ApolloProvider client={apolloClient}>
<CssBaseline />
{React.cloneElement(children, { pageProps })}
<SignUp />
<Snackbar />
</ApolloProvider>
</ThemeProvider>
</StateProvider>
</Container>
);
}
}
const AllTheProviders = withApollo(AllTheProvidersWrapped);
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options });
export * from "react-testing-library";
export { customRender as render };
This is my /jest.config.js file:
module.exports = {
testPathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/"],
moduleDirectories: ["node_modules", "utils", __dirname]
};
And this is an example of a test I'm trying to run:
import React from "react";
import { render, cleanup } from "testProviders";
import OutlinedInput from "./OutlinedInput";
afterEach(cleanup);
const mockProps = {
id: "name",
label: "Name",
fieldStateString: "signUpForm.fields"
};
describe("<OutlinedInput />", (): void => {
it("renders as snapshot", (): void => {
const { asFragment } = render(<OutlinedInput {...mockProps} />, {});
expect(asFragment()).toMatchSnapshot();
});
});
The error message outputted from the test is:
TypeError: Cannot read property 'pathname' of undefined
52 |
53 | const customRender: CustomRender = (ui, options) =>
> 54 | render(ui, { wrapper: AllTheProviders, ...options });
| ^
55 |
56 | // re-export everything
57 | export * from "react-testing-library";
If I had to guess, I'd say that the <Component {...pageProps} /> component in /pages/_app.js is what provides the pathName as part of Next.js's routing.
The examples provided by Next.js don't cover how to do this so I'm hoping someone here might be able to help.
We are using #loadable-component and we are having trouble rendering them server side. The problem is that the layouts are not rendering
import { ChunkExtractor } from '#loadable/server';
app.get('/*', async (req, res) => {
const extractor = new ChunkExtractor({ statsFile });
const WrappedLayout = withRootQuery(withLayout(layouts));
const App = (
<ApolloProvider client={client}>
<StaticRouter context={context} location={req.url}>
<WrappedLayout />
</StaticRouter>
</ApolloProvider>
);
renderToStringWithData(extractor.collectChunks(App))
.then((content) => {
if (context.url) {
res.redirect(context.status ? context.status : 301, context.url);
return;
}
const initialState = client.extract();
const helmetData = Helmet.renderStatic();
console.log(content);
const html = (
<Html
content={content}
state={initialState}
helmetData={helmetData}
linkTags={extractor.getLinkElements()}
scriptTags={extractor.getScriptElements()}
styleTags={extractor.getStyleElements()}
/>
);
})
export const withLayout = layouts => (props) => {
// eslint-disable-next-line react/prop-types
const { data, type } = props;
let Layout;
if (type) {
Layout = getLayoutOfType(type, layouts);
} else {
Layout = getLayoutFromResponse(data, layouts);
}
console.log(Layout);
return <Layout {...props} />;
};
export const layouts = {
INDEX_PAGE: loadable(() => import('./components/atomic/templates/home')),
ARTICLE: loadable(() => import('./components/atomic/templates/article')),
BASIC_PAGE: loadable(() => import('./components/atomic/templates/page')),
loading: loadable(() => import('./components/atomic/templates/loading')),
notFound: loadable(() => import('./components/atomic/templates/not-found')),
error: loadable(() => import('./components/atomic/templates/error-page')),
};
The layout loaded with loadable render correctly on the client but not on the server