Cypress component test with NextJS useRouter function - next.js

My Navbar component relies on the useRouter function provided by nextjs/router in order to style the active links.
I'm trying to test this behavior using Cypress, but I'm unsure of how I'm supposed to organize it. Cypress doesn't seem to like getRoutePathname() and undefined is returned while within my testing environment.
Here's the component I'm trying to test:
import Link from 'next/link'
import { useRouter } from 'next/router'
function getRoutePathname() {
const router = useRouter()
return router.pathname
}
const Navbar = props => {
const pathname = getRoutePathname()
return (
<nav>
<div className="mr-auto">
<h1>Cody Bontecou</h1>
</div>
{props.links.map(link => (
<Link key={link.to} href={link.to}>
<a
className={`border-transparent border-b-2 hover:border-blue-ninja
${pathname === link.to ? 'border-blue-ninja' : ''}`}
>
{link.text}
</a>
</Link>
))}
</nav>
)
}
export default Navbar
I have the skeleton setup for the Cypress component test runner and have been able to get the component to load when I hardcode pathname, but once I rely on useRouter, the test runner is no longer happy.
import { mount } from '#cypress/react'
import Navbar from '../../component/Navbar'
const LINKS = [
{ text: 'Home', to: '/' },
{ text: 'About', to: '/about' },
]
describe('<Navbar />', () => {
it('displays links', () => {
mount(<Navbar links={LINKS} />)
})
})

Ideally, there'd be a provider for Next.js's useRouter to set the router object and wrap the component in the provider in mount. Without going through the code or Next.js supplying the documentation, here's a workaround to mock useRouter's pathname and push:
import * as NextRouter from 'next/router'
// ...inside your test:
const pathname = 'some-path'
const push = cy.stub()
cy.stub(NextRouter, 'useRouter').returns({ pathname, push })
I've added push because that's the most common use case, which you may also need.

Related

Why isn't my t() texts refreshing in localhost/en but refreshing in localhost/fr on i18n.changeLanguage()?

Hi
I just made a website with a darkmode and multilanguage support to test around but I ran into an issue.
the code
I got rid of all things that aren't an issue
portfolio/src/pages/index.tsx
import { useTranslation } from 'react-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
export default () => {
const { t,i18n } = useTranslation('common')
return <div onClick={()=>i18n.changeLanguage(i18n.language=='fr'?'en':'fr')}>
<div>{i18n.language}</div>
<span>{t('debug')}</span>
</div>
}
export async function getStaticProps({ locale }:any) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
};
}
portfolio/src/public/locales/en/common.js
{"debug":"english"}
portfolio/src/public/locales/fr/common.js
{"debug":"français"}
portfolio/next-i18next.config.js
const path = require("path");
module.exports = {
debug: false,
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
localePath: path.resolve('./src/public/locales'),
};
portfolio/src/pages/_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import {appWithTranslation} from 'next-i18next'
export default appWithTranslation(({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />
})
The issue
When I do npm run dev and go to http://localhost:3000/fr, the page defaults to french and works good I can swap between languages without problems but when i go to http://localhost:3000/en the t('debug') doesn't translate when the i18n.language changes as intended.
Found what I wanted
So basicaly I need to use a next Link that will change the local and the link
Code application
index.js
//...
export default () => {
const { t,i18n } = useTranslation('common')
return (
<div>
<Link
href={i18n.language=='fr'?'/en':'/fr'}
locale={i18n.language=='fr'?'en':'fr'}
>{i18n.language}</Link>
<div>{t('debug')}</div>
</div>
)
}
//...
result
Now the text changes as intended both in the /fr and /en because it switches between the 2 however the result is far from smooth. It reloads the page and i'd like to avoid that because I use some animations on it.
Found what i wanted part 2
Browsing through the next-i18next documentation I found what I wanted.
solution
I needed to load the props using getStaticProps and in the serverSideTranslation function i needed to pass as argument the array off ALL the language necessary to load the page ['en','fr'] because i switched between the 2

router-link won't load page

Can't find what I am doing wrong. If I type the url on the browser the page does load, but the links from the navbar do nothing. I am trying three different ways to use router-link, but makes no differences. It just won't render the page via link. No errors on the console. On the vue devtools the routing displays the correct path.
App.vue:
<template>
<nav class="navbar">
<router-link :to="{ path: '/' }">Home</router-link>
<router-link :to="{name:'TheDashboard'}"> Dashboard</router-link>
<router-link to="/games">Games</router-link>
</nav>
<router-view></router-view>
</template>
router/index.js:
import { createRouter, createWebHistory } from 'vue-router'
import TheHomePage from '#/pages/TheHomePage'
import TheDashboard from '#/pages/TheDashboard'
const routes = [
{
path: '/',
name: 'TheHomePage',
component: TheHomePage
},
{
path: '/dashboard',
name: 'TheDashboard',
component: TheDashboard
},
{
path: '/games',
name: 'TheGames',
component: () => import(/*webpackChunkName: "games" */ '#/pages/TheGames.vue'),
props: true
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
main.js:
import { createApp } from 'vue'
import App from '#/App.vue'
import router from '#/router'
import store from '#/store'
const prototype = createApp(App)
prototype.use(router)
prototype.use(store)
prototype.mount('#app')
In the end this was a bug caused by Vue devtools. I removed previous versions and updated to latest, and it is now working. Nothing wrong with the code. I thought I might as well leave the code here, since it might be helpful for other people.
your code seems optimal to me. However, you can use the beforeEach and afterEach hooks to help you to debug your App.
you can read about them here
do something like:
//router/index.js
//...
//const router = ...
router.afterEach((to, from, failure) => {
console.log('to: ', to);
console.log('from: ', from);
if (isNavigationFailure(failure)) {
console.log('failed navigation', failure)
}
});
export router;

Next.js and Styled Components go out of sync between the server and the client on refresh

I have a Next.js app using styled components. On first load of any page, there are no complaints, and everything looks properly styled. When I refresh a page however, everything still looks proper, but I get a console error reading:
Warning: Prop `className` did not match. Server: "sc-TXQaF bfnBGK" Client: "sc-bdnylx kKokSB"
I've tried simplifying the styles on the specific component, and the error persists. I've tried removing the component entirely from the DOM, and that results in the same error on the next element in the DOM. So it seems to be a global issue.
I've followed the guide for using Next.js and Styled Components found here: https://github.com/vercel/next.js/tree/master/examples/with-styled-components
I have the .babelrc file in the root:
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}
I have the _document.js file in my pages directory:
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
}
Here is an example of one of my styled components:
import styled from 'styled-components';
export const Block = styled.div`
margin: ${props => props.small ? '2rem 0' : '4rem 0'};
margin-top: ${props => props.clearTop ? '0' : null};
`;
... although I've tried to dumb it down to something as simple as this with no change in the console error:
import styled from 'styled-components';
export const Block = styled.div`
position: relative;
`;
Finally, here's a dumbed down page that still produces the error:
import { useContext, useEffect } from 'react';
import { useRouter } from 'next/router';
import Layout from '../components/layout';
import { Block } from '../components/styled/Block';
import { userContext } from '../context/userContext';;
function Profile() {
const router = useRouter();
const { loggedIn } = useContext(userContext);
useEffect(() => {
if (!loggedIn) router.push('/login');
}, [loggedIn]);
return (
<Layout>
<Block>
<h1>Test</h1>
</Block>
</Layout>
)
}
export default Profile;
Kind of at my wits end here.
I believe I figured out an answer. I didn't have the dev dependency for babel styled components.
npm install babel-plugin-styled-components --save-dev
Your package.json file should have this:
"devDependencies": {
"babel-plugin-styled-components": "^1.11.1"
}
After this was installed, along with the _document.js and .babelrc files correctly placed in your app, you shouldn't have any problems.
I had this issue for the last 1 month and finally got a solution that worked for me!
So the solution here is to get the styling exclusively from the server.
from the docs:
Basically you need to add a custom pages/_document.js (if you don't
have one). Then copy the logic for styled-components to inject the
server side rendered styles into the <head>
To solve this issue is you need something like this in your Document component:
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
}
The final step (if the error persists) is to delete the cache: delete the .next folder and restart the server
The full example code from Next documentation is Here
with the Next Complier on the latest version of next, you should only update your next.config file and _document file, and you will be all set. Babel will cause conflict with the NextJS compiler.
Here you can check the files. If you don't use TS, just replace
ctx: DocumentContext
with only
ctx
Example app with styled-components

Update redux state with new route params when route changes

I am currently trying to implement a universal app and am using route params throughout my whole application. As such I want to put the route params into state.
I am able to do this ok for the SSR using the below...
router.get('/posts/:id', (req, res) => {
res.locals.id = req.params.id
const store = createStore(reducers, getDefaultStateFromProps(res.locals), applyMiddleware(thunk));
const router = <Provider store={store}><StaticRouter location={req.url} context={}><App {...locals} /></StaticRouter></Provider>;
const html = renderToString(router);
const helmet = Helmet.renderStatic();
res.render('index', {
content: html,
context: JSON.stringify(store.getState()),
meta: helmet.meta,
title: helmet.title,
link: helmet.link
});
});
And from here the id is put into state using the getDefaultStateFromProps function... export function getDefaultStateFromProps({ id = ''} = {}) => ({ id })
This all works perfectly and puts the correct id into the redux state, which I can then use when hitting this route.
The problem I have is that when I change route on the client side, I'm not sure how to update the redux state for the id from the url.
In terms of my handling of routes I am using the following:
import React, {Component} from 'react';
import { Switch } from 'react-router-dom';
import Header from './header/';
import Footer from './footer';
import { renderRoutes } from 'react-router-config';
export default class App extends Component {
render() {
return (
<div>
<Header />
<Switch>
{renderRoutes(routes)}
</Switch>
<Footer />
</div>
);
}
}
export const routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/posts/:id',
component: Post,
}
{
path: '*',
component: PageNotFound
}
];
And then use the following to hydrate...
const store = createStore(reducers, preloadedState, applyMiddleware(thunk));
const renderRouter = Component => {
ReactDOM.hydrate((
<Provider store={store}>
<Router>
<Component />
</Router>
</Provider>
), document.querySelectorAll('[data-ui-role="content"]')[0]);
};
So what I'm wondering is how when I make a route change... how can I update the redux state for the new :id from the route param?
I'm a little lost in how to approach this... any help is appreciated.
You'll need to import routes from your route definition file.
import { matchPath } from 'react-router';
import { LOCATION_CHANGE } from 'react-router-redux';
// LOCATION_CHANGE === '##router/LOCATION_CHANGE';
someReducerFunction(state, action){
switch(action.type){
case LOCATION_CHANGE:
const match = matchPath(action.payload.location.pathname, routes[1]);
const {id} = match.params;
// ...
default:
return state;
}
}
Fully working example:
https://codesandbox.io/s/elegant-chaum-7cm3m?file=/src/index.js

How to pass FlowRouter context in testing React components

I am testing a react component that have 5 links on it. Each link becomes active based on the current route. I am using Meteor with Mantra and enzyme for testing these components.
Footer component:
import React from 'react';
class Footer extends React.Component{
render(){
let route = FlowRouter.current().route.name;
return(
<a className={route == 'hub page' ? 'some-class active' : 'some-class'}> . . . (x5)
)
}
}
Testing
describe {shallow} from 'enzyme';
import Footer from '../core/components/footer';
describe('footer',() => {
it('should have 5 links', () => {
const fooWrapper = shallow(<Footer/>);
expect(fooWrapper.find('a')).to.have.length(5);
})
})
But when I run npm test, it says that FlowRouter is not defined. How do I pass the FlowRouter context to a react component in testing? Thanks in advance
First of all, to comply with Mantra specifications, you should rewrite your Footer component like this:
import React from 'react';
const Footer = ({ route }) => (
<a className={
route == 'hub page' ? 'some-class active' : 'some-class'
}> ... (x5)
);
export default footer;
Now to test your Footer, you don't now need FlowRouter at all:
import { shallow } from 'enzyme';
import Footer from '../core/components/footer';
describe('footer', () => {
it('should have 5 links', () => {
const fooWrapper = shallow(<Footer route={'foo'}/>);
expect(fooWrapper.find('a')).to.have.length(5);
})
})
To make the footer reactively re-render as FlowRouter.current() changes, you need to create a Mantra container to wrap it in. To test the container, you can mock FlowRouter like this:
it('should do something', () => {
const FlowRouter = { current: () => ({ route: { name: 'foo' } }) };
const container = footerContainer({ FlowRouter }, otherArguments);
...
})
Since Mantra uses the mocha package directly from NPM instead of the practicalmeteor:mocha or similar Meteor package to run tests, you cannot (to my knowledge) load Meteor packages such as kadira:flow-router in your tests.

Resources