Control Next.js Server-Side Data During Cypress Tests

How to modify the data passed from the server through props to Next.js React application when running end-to-end tests.

Let's take a Next.js example application that passes data from the server to the client-side through props. You can find my example in the repo bahmutov/next-state-overwrite-example. Here is the home page pages/index.js:

pages/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import Head from 'next/head'
import styles from '../styles/Home.module.css'

export async function getServerSideProps() {
const props = {
experiments: {
greeting: 'Server-side says hello!',
},
}
console.log('server-side props: %o', props)
return {
props,
}
}

export default function Home({ experiments }) {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>

<main className={styles.main}>
<h1 className={styles.title}>Hello there</h1>

<p className={styles.description} data-cy="greeting">
{experiments.greeting || 'Silence'}
</p>
</main>
</div>
)
}

The getServerSideProps executes server-side, we can see the console log message in the terminal

1
2
3
wait  - compiling...
event - compiled successfully
server-side props: { experiments: { greeting: 'Server-side says hello!' } }

The data is then passed to the client-side through prop to the component where it is used to render the text

1
2
3
<p className={styles.description} data-cy="greeting">
{experiments.greeting || 'Silence'}
</p>

We can see the result in the browser

The application shows the greeting text

The default test

We can write a test to confirm the expected message appears on the page.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
/// <reference types="cypress" />

describe('Next.js app', () => {
it('shows the default server-side greeting', () => {
cy.visit('/')
cy.contains('[data-cy=greeting]', 'Server-side says hello!').should(
'be.visible',
)
})
})

The test is green

The default test

Using the NEXT_DATA

We can avoid hard-coding the expected text. Instead let's grab the server-side greeting from the page itself. If we look at the source code for the page, we can find the <script id="__NEXT_DATA__" type="application/json"> element with the server-side props.

The page source

The Next.js code takes that script and parses it into an object window.__NEXT_DATA__.

The automatically created variable

We can access this object from the test to get the expected text.

1
2
3
4
5
6
7
8
9
it('shows the text from the __NEXT_DATA__', () => {
cy.visit('/')
// visit yields the "window" object
// and we can get nested property in a single command
.its('__NEXT_DATA__.props.pageProps.experiments.greeting')
.then((greeting) => {
cy.contains('[data-cy=greeting]', greeting).should('be.visible')
})
})

Using the greeting from the page props object

Modifying the NEXT_DATA

Let's overwrite the NEXT_DATA before the application uses it. We can do it in two ways.

Change the NEXT_DATA object when set

We can intercept the moment when the framework parses the <script id="__NEXT_DATA__" type="application/json"> and sets the window.__NEXT_DATA__ property. I will use the Object.defineProperty(win, '__NEXT_DATA__' ...) with set and get handlers. The set handler will be called when the framework sets the object. The test can replace the property in the object and the application happily continues from this point.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
it('removes the text from __NEXT_DATA__', () => {
const greeting = 'Cypress say Yo!'
cy.visit('/', {
onBeforeLoad: (win) => {
let nextData

Object.defineProperty(win, '__NEXT_DATA__', {
set(o) {
console.log('setting __NEXT_DATA__', o)
// here is our change to modify the injected parsed data
o.props.pageProps.experiments.greeting = greeting
nextData = o
},
get() {
return nextData
},
})
},
})
cy.contains('[data-cy=greeting]', greeting).should('be.visible')
})

Overwrite the greeting property during the test

Well, not 100% happily. If we used the prop to set the component's state, it would be enough. But we are using the greeting server-side to render the initial HTML. If we replace just the property value, the server-side HTML and the client-side HTML versions won't match. Which is what the React complains about in the DevTools:

React shows HTML mismatch warning

In this case, we need to replace both the page prop and "fix" the server-side HTML the browser receives.

Replace HTML

Let's "fix up" the HTML sent by the server before we replace the greeting inside the NEXT_DATA. Here I put an arrow on the string we need to replace to avoid the warning:

The HTML element we need to update to match the test prop

We can use the cy.intercept command for this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
it('patches HTML and removes the text from __NEXT_DATA__', () => {
const greeting = 'Cypress say Yo!'
cy.intercept('/', (req) =>
req.continue((res) => {
res.body = res.body.replace(
'>Server-side says hello!</',
`>${greeting}</`,
)
}),
)

cy.visit('/', {
onBeforeLoad: (win) => {
let nextData

Object.defineProperty(win, '__NEXT_DATA__', {
set(o) {
console.log('setting __NEXT_DATA__', o)
// here is our change to modify the injected parsed data
o.props.pageProps.experiments.greeting = greeting
nextData = o
},
get() {
return nextData
},
})
},
})
cy.contains('[data-cy=greeting]', greeting).should('be.visible')
})

No more React warnings.

Happy web app with controlled server-side props

Navigation

It is not enough to modify the __NEXT_DATA__ on the very first page visit. The application might navigate to other pages. For example, let's add "About" page.

/pages/index.js
1
2
3
<Link href="/about">
<a>About me</a>
</Link>

The About pages will have its own server-side props

/pages/about.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css'

export async function getServerSideProps() {
const props = {
experiments: {
greeting: 'About info',
},
}
console.log('About server-side props: %o', props)
return {
props,
}
}

export default function About({ experiments }) {
return (
<div className={styles.container}>
<Head>
<title>About</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>

<main className={styles.main}>
<h1 className={styles.title}>About</h1>

<p className={styles.description} data-cy="greeting">
{experiments.greeting || 'Silence'}
</p>

<Link href="/">
<a>Home</a>
</Link>
</main>
</div>
)
}

The Next.js framework sends these server-side props through a JSON fetch request to http://localhost:3000/_next/data/development/about.json endpoint.

The application fetching props for the About page

We can spy on this request using cy.intercept command. Important: the request comes with the header If-None-Match which controls the caching. In most cases, the server-side JSON object is the same, thus the Next server responds with 304 without data. We need to intercept the actual object during the test, thus we will remove this header to force the server to send the full object. For more examples of cy.intercept command read Cypress cy.intercept Problems.

The request headers used to fetch the server-side props for the About page

Let's intercept and modify the server-side props for the About page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
it('modifies __NEXT_DATA__ on navigation', () => {
// let the default greeting show on the home page
const defaultGreeting = 'Server-side says hello!'
cy.visit('/')
cy.contains('[data-cy=greeting]', defaultGreeting).should('be.visible')

// the About page will make a fetch request to get
// the server-side props, so we need to be ready
const greeting = 'Testing hi'
cy.intercept('_next/data/development/about.json', (req) => {
// prevent the server from responding with 304
// without an actual object
delete req.headers['if-none-match']
return req.continue((res) => {
// let's use the same test greeting
res.body.pageProps.experiments.greeting = greeting
})
})
cy.contains('a', 'About').click()
cy.location('pathname').should('equal', '/about')
cy.contains('[data-cy=greeting]', greeting).should('be.visible')
})

The finished About page test with expected greeting

Note: if we go back to the Home page, the framework fetches its page props the same way using the GET development/index.json request.

Related posts