How do you write Jest tests for getInitialProps? - javascript

static async getInitialProps({ query }) {
let content;
let alert;
try {
const first = query.first ? query.first : '';
content = await getContent(first);
} catch (error) {
alert = 'There was an error loading data, please try again.';
content = [];
}
return {
content,
alert,
};
}
I'm trying to write tests for this logic but because it is server side code I'm struggling to understand how I write a test for it as it isn't available to me in instance().
Google hasn't shown me the way on this one so I'm wondering how others have tackled writing tests for getInitial props.

First, take a look at what's a static method and what does the static keyword do.
Since getInitialProps is just a static function on a component, you can test it manually like any other function.
import MyComponent from "./path/to/MyComponent";
// Mock the getContent function which I don't know where it comes from.
jest.mock("../someModule.js", () => ({
getContent: () => Promise.reject()
}));
describe("MyComponent", () => {
it('populates the "alert" prop on getContent failure.', async () => {
// Inject anything you want to test
const props = await MyComponent.getInitialProps({
query: { first: "whatever" }
});
// And make sure it results in what you want.
expect(props).toEqual({
content: [],
alert: "There was an error loading data, please try again."
});
});
});
Most of the time, getInitialProps is called like that anyway.
export default class MyApp extends App {
static async getInitialProps ({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
The documentation describes getInitialProps goal and we can confirm that it's ok to call it directly and test its return value as an Object.
Notice that to load data when the page loads, we use getInitialProps
which is an async static method. It can asynchronously fetch
anything that resolves to a JavaScript plain Object, which populates
props.
Data returned from getInitialProps is serialized when server
rendering, similar to a JSON.stringify. Make sure the returned
object from getInitialProps is a plain Object and not using
Date, Map or Set.
For the initial page load, getInitialProps will execute on the
server only. getInitialProps will only be executed on the client
when navigating to a different route via the Link component or using
the routing APIs.

Related

Nextjs getInitialProps blocked the page rendering in client side?

Since I like to add SSR to my upcoming project to improve SEO, I would like to try out next. What I want is that only use SSR for the initial page, and the rest of navigations in the site will be client side rendering. I see the getInitialProps fit the most in this case, accordingly the documentations.
As my understanding, getInitialProps is run in server for the initial page rendering, and is run in the browser when navigating using next/link. The issue I found is that the getInitialProps seems to block the page rendering. (i.e. page changed/rendered after getInitialProps is completed)
import axios from 'axios'
function Posts(props) {
return (
<div>
<div>Posts:</div>
<div>
{JSON.stringify(props)}
</div>
</div>
)
}
Posts.getInitialProps = async (context) => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
// Wait longer to see the effect
// await (new Promise((resolve) => {
// setTimeout(resolve, 5000)
// }))
return {
props: {
posts: response.data
}
}
}
export default Posts;
How can I do it like in pure React, render the jsx first, then fill in the props? (the execution JSON.stringify(props) might be ignored at first)
Also, in next 9.3, the team introduced getServerSideProps, which is recommended over getInitialProps. How can they be comparable when they are not the same that getServerSideProps will on run in server?
Based on your comments, you want to do the fetch on the server, on the initial page load. However, if navigating between pages you don't want rendering to block while waiting for getInitialProps to return.
One solution is to check if you're on the server, and do the fetch in getInitialProps. If on the client, don't do the fetch in getInitialProps and instead fetch using useEffect in your render method.
import {useEffect} from 'react'
import axios from 'axios'
const isServer = () => typeof window === 'undefined'
const getPosts = () => {
return axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => response.data)
}
function Posts({posts}) {
const [renderPosts, setRenderPosts] = useState(posts)
useEffect(() => {
if(posts === null) {
getPosts()
.then(setRenderPosts)
}
}, [])
return (
<div>
<div>Posts:</div>
<div>
{JSON.stringify(renderPosts)}
</div>
</div>
)
}
Posts.getInitialProps = async (context) => {
if(isServer()) {
return {
posts: await getPosts(),
}
}
else {
return {
posts: null,
}
}
}
export default Posts
By the way, you may be tempted to use getServerSideProps here, since it is only called if rendering on the server. However, when a page using getServerSideProps is rendered, it will actually make a call to the server to get data from getServerSideProps, even if you're navigating using next/link. From the Next.js 9.3 blog post:
When navigating between pages using next/link instead of executing getServerSideProps in the browser Next.js will do a fetch to the server which will return the result of calling getServerSideProps.
This would still cause the blocking issue you're wanting to avoid.
One final note, this might not be an idiomatic solution. There may be a more "standard" solution. I just wasn't able to find one. You could likely also use a wrapper around your page component that could do all of this in a more consistent way. If you use this pattern a lot, I'd recommend that.

Next/React-Apollo: React props not hooked up to apollo cache when query comes from getInitialProps

I'm using nextjs and react-apollo (with hooks). I am trying to update the user object in the apollo cache after a mutation (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).
Note that my ssr setup for apollo is very similar to the official nextjs example: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
The issue is that you're calling the client's query method. This method simply makes a request to the server and returns a Promise that resolves to the response. So getInitialProps is called before the page is rendered, query is called, the Promise resolves and you pass the resulting user object down to your page component as a prop. An update to your cache will not trigger getInitialProps to be ran again (although I believe navigating away and navigating back should), so the user prop will never change after the initial render.
If you want to subscribe to changes in your cache, instead of using the query method and getInitialProps, you should use the useQuery hook. You could also use the Query component or the graphql HOC to the same effect, although both of these are now deprecated in favor of the new hooks API.
export default () => {
const { data: { user } = {} } = useQuery(GetUser)
const [toggleActive] = useMutation(ToggleActive, { ... })
...
})
The getDataFromTree method (combined with setting the initial cache state) used in the boilerplate code ensures that any queries fetched for your page with the useQuery hook are ran before the page render, added to your cache and used for the actual server-side rendering.
useQuery utilizes the client's watchQuery method to create an observable which updates on changes to the cache. As a result, after the component is initially rendered server-side, any changes to the cache on the client-side will trigger a rerender of the component.

How can I use getInitialProps only during the NextJS site build?

When using NextJS to build a static site, I would like the getInitialProps method to fire only during the build step and not on the client.
In the build step, NextJS runs the getInitialProps method before each component's rendered HTML is used to generate the page's static HTML. On the client, NextJS also runs this method before the page component is rendered in order to return the necessary props for the component. Thus, large requests can delay the client's first paint as this is a blocking request.
// example usage of API call in getInitialProps
import fetch from 'isomorphic-unfetch'
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async ({ req }) => {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
export default Page
I'm unwilling to move my slow API request to componentDidMount in order to avoid the blocking request because I want to use the data returned during the build step to populate the static HTML, and this particular request doesn't need to be dynamic or updated after the build.
Is there a way I can make getInitialProps run only when next export builds and not as the client loads the page?
Is this good practice?
I found the workaround with NextJs 9.0.3 (other versions may also work, I didn't test that)
// XXXPage is your page
XXXPage.getInitialProps = async (req) => {
if (process.browser) {
return __NEXT_DATA__.props.pageProps;
}
// original logic
}
For version 9.3 or newer, it's recommended that you use getStaticProps for providing static build props.
export async function getStaticProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
Old answer
There are two ways is one way that I've found to prevent code in getInitialProps from running on a page component load.
1. Use a regular anchor tag without next/link to that page.
getInitialProps only runs when the page is linked from a next/link component. If a regular JSX anchor click me is used instead, the component's getInitialProps will not be invoked. Direct page loads to NextJS static site pages will not invoke getInitialProps.
Note that using a standard anchor instead of the next/link component will cause a full page refresh.
Because this is a poor solution, I've submitted a feature request.
2. Use req in the context argument to conditionally make the API call in getInitialProps.
I believe what #evgenifotia wanted to convey is that req is undefined in a site that's been exported.
// example usage of API call in getInitialProps
import fetch from 'isomorphic-unfetch'
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async (ctx) => {
const { req } = ctx // context object: { req, res, pathname, query, asPath }
if (req) { // will only run during the build (next export)
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
return {}
}
export default Page
For more information about getInitialProps, see the documentation. One example there confirms that req is expected to only be defined on the server (or during the exporting build):
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent`
This second option may work for some scenarios, but not mine where returning an empty result from getInitialProps will affect the component's this.props.
Note:
Shallow routing is not the answer. According to the documentation (see under "Notes" section):
Shallow routing works only for same page URL changes.
A more detailed and updated version as of the accepted answer:
const isInBroswer = typeof window !== 'undefined';
if (isInBroswer) {
const appCustomPropsString =
document.getElementById('__NEXT_DATA__')?.innerHTML;
if (!appCustomPropsString) {
throw new Error(`__NEXT_DATA__ script was not found`);
}
const appCustomProps = JSON.parse(appCustomPropsString).props;
return appCustomProps;
}

Aurelia inject mock dependency

I have this aurelia component for displaying a feed to the user which depends on a custom API service class called Api for fetching the feed. The Api class has a get() function which in turn uses HttpClient to fetch the data.
Trying to test the component I want to mock the service class, specifically the get function, to return suitable test data and have this mock injected into the component via aurelia's DI container. The DI part I am having trouble with.
Here is the relevant part of component's js file
import {bindable, inject} from 'aurelia-framework';
import {Api} from 'services/api';
#inject(Api)
export class Feed {
events = null;
constructor(api) {
console.info('feed.js constructor, api:', api)
this.api = api;
}
And the relevant code from my test
beforeEach(done => {
...
let mockApi = new Api();
spyOn(mockApi, 'get').and.returnValue(mockGetResponse);
const customConfig = (aurelia) => {
let conf = aurelia.use.standardConfiguration().instance("Api", mockApi);
console.info('Registering Api:', conf.container.get("Api"));
return conf;
}
const ct = new ComponentTester();
ct.configure = customConfig;
sut = ct.withResources('activityfeed/feed');
sut.inView('<feed username.bind="username"></feed>')
.boundTo({username: TEST_USER});
sut.create(bootstrap).then(() => {
done();
});
});
This code is actually working the way I intended as far as I can tell. On creation of the component my customConfig function is called and the mockApi instance is logged to the console.
However later in the bootstrapping process the component constructor still receives an instance of the actual Api service class instead of my mock instance which was registered to the container.
Spent the last couple of hours trying to dig up any documentation or examples for doing things like this without success so if anyone can assist I would greatly appreciate it.
Or if there is / are alternative ways to accomplish this that would work just as well.
When testing a standard component that consists of both a view and a view model, using the aurelia-testing package, I find that a cleaner approach might be to let Aurelia create both the view and view-model, and use mocked classes for all view model dependencies.
export class MockApi {
response = undefined;
get() { return Promise.resolve(this.response) }
}
describe("the feed component", () => {
let component;
let api = new MockApi();
beforeEach(() => {
api.response = null;
component = StageComponent
.withResources("feed/feed")
.inView("<feed></feed>");
component.bootstrap(aurelia => {
aurelia.use
.standardConfiguration();
aurelia.container.registerInstance(Api, api);
});
});
it("should work", done => {
api.response = "My response";
component.create(bootstrap).then(() => {
const element = document.querySelector("#selector");
expect(element.innerHTML).toBe("My response, or something");
done();
});
});
});
This approach lets you verify the rendered HTML using the normal view model class, mocking the dependencies to control the test data.
Just to answer my own question, at least partially, if it can be useful to someone.
By using the actual Api class constructor as the key instead of the string "Api" the mock seems to be correctly injected.
I.e.
import {Api} from 'services/api';
...
let conf = aurelia.use.standardConfiguration().instance(Api, mockApi);

Testing fetch() method inside React component

I have an App component that is responsible for rendering child input components, it is also responsible for handling fetch requests to the Twitch API via a method called channelSearch. I have tried to adhere to suggested best practices outlined here for working with ajax/fetch with React.
The method is passed down through props and called via a callback.
Note the fetch method is actually isomorphic-fetch.
channelSearch (searchReq, baseUrl="https://api.twitch.tv/kraken/channels/") {
fetch(baseUrl + searchReq)
.then(response => {
return response.json();
})
.then(json => {
this.setState({newChannel:json});
})
.then( () => {
if (!("error" in this.state.newChannel) && this.channelChecker(this.state.newChannel._id, this.state.channelList) ) {
this.setState(
{channelList: this.state.channelList.concat([this.state.newChannel])}
);
}
})
.catch(error => {
return error;
});
}
I am currently trying to write a test for the channelSearch method. I am currently using enzyme and jsdom to mount the entire <App> component in a DOM. Find the child node with the callback, simulate a click (which should fire the callback) and check to see if the state of the component has been changed. However, this does not seem to work.
I have also tried calling the method directly, however, I run into problems with this.state being undefined.
test('channel search method should change newChannel state', t => {
const wrapper = mount(React.createElement(App));
wrapper.find('input').get(0).value = "test";
console.log(wrapper.find('input').get(0).value);
wrapper.find('input').simulate("change");
wrapper.find('button').simulate("click");
console.log(wrapper.state(["newChannel"]));
});
I am really lost, I am not sure if the method itself is poorly written or I am not using the correct tools for the job. Any guidance will be greatly appreciated.
Update #1:
I included nock as recommended in comments, test now looks like this:
test('channel search method should change newChannel state', t => {
// Test object setup
var twitch = nock('https://api.twitch.tv')
.log(console.log)
.get('/kraken/channels/test')
.reply(200, {
_id: '001',
name: 'test',
game: 'testGame'
});
function checker() {
if(twitch.isDone()) {
console.log("Done!");
console.log(wrapper.state(["newChannel"]));
}
else {
checker();
}
}
const wrapper = mount(React.createElement(App));
wrapper.find('input').get(0).value = "test";
wrapper.find('input').simulate("change");
wrapper.find('button').simulate("click");
checker();
});
This still does not seem to change the state of the component.
fetch is asynchronous but you're testing synchronously, you need to either mock fetch with a synchronous mock or make the test asynchronous.
nock may work for you here.
I suggest you create a sample of your test using plnkr.
I agree with Tom that you're testing synchronously. It would of course be helpful to show off your actual component code (all of the relevant portions, like what calls channelSearch, or at the least describe it by saying e.g. "channelSearch is called by componentDidMount()". You said:
I run into problems with this.state being undefined.
This is because this.setState() is asynchronous. This is for performance reasons, so that React can batch changes.
I suspect you'll need to change your code that is currently:
.then(json => {
this.setState({newChannel:json});
})
to:
.then(json => {
return new Promise(function(resolve, reject) {
this.setState({newChannel:json}, resolve);
})
})
Note that your checker() method won't work. It's looping, but twitch.isDone() will never be true because it never has a chance to run. Javascript is single threaded, so your checker code will run continuously, not allowing anything else in between.
If you set up the plnkr, I'll take a look.
Refactor out the fetch code from the component then pass it it to the component as a callback function in the properties.
export class Channel extends React.Component {
componentDidMount() {
this.props.searchFunction().then(data => this.setState(data));
}
render() {
return <div>{this.state}</div>;
}
}
Uage:
function channelSearch(name) {
return fetch(`https://api.twitch.tv/kraken/search/channels?query=${name}`);
}
<Channel searchFunction={channelSearch} />
Now you can test the API functionality independently of the component.

Categories

Resources