Supercharge your Next.js app testing with Playwright – a tool for automating Chromium, Firefox, and WebKit browsers. Whether you're gearing up for End-to-End (E2E) or Integration tests, Playwright offers a seamless experience across all platforms. In this guide, I will walk you through setting up and running your first Playwright E2E test for a Next.js application.
Quickstart
If you're eager to dive right in, the quickest way to kick things off is by leveraging the create-next-app
with the with-playwright
example:
npx create-next-app@latest --example with-playwright with-playwright-app
Then run npm run test:e2e
to run the tests. Continue reading to learn how to setup Playwright in an existing Next.js project.
Manual Setup
For those of you with an existing NPM project and would prefer a manual setup, no worries, you're covered! Begin by installing the @playwright/test
package:
npm install --save-dev @playwright/test
Now, update your package.json
scripts to incorporate Playwright:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test:e2e": "playwright test"
}
}
Crafting Your First Playwright E2E Test
Imagine you have two Next.js pages set up as shown:
// pages/index.js
import Link from 'next/link'
export default function Home() {
return (
<nav>
<Link href="/about">About</Link>
</nav>
)
}
// pages/about.js
export default function About() {
return (
<div>
<h1>About Page</h1>
</div>
)
}
It's time to ensure your navigation operates flawlessly. Here's how to craft a test for it:
// e2e/example.spec.ts
import { test, expect } from '@playwright/test'
test('navigates to the about page', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: /About/ }).click();
await expect(page).toHaveURL('/about')
await expect(page.getByRole('heading', { level: 1 })).toContainText('About Page');
})
Note: Use
page.goto('/')
and have"baseURL": "http://ray.run"
set in theplaywright.config.ts
file for concise code.
Running Playwright Tests
Remember, Playwright tests the actual Next.js app, so the server should be up and running. It is also a good idea to test against production build to mimic real-world behavior.
Start your Next.js server:
npm run build
npm run start
Then in another terminal, run your Playwright tests:
npm run test:e2e
Use webServer
to Start the Development Server
With Playwright's webServer
feature, you can let it handle starting the development server and ensuring it's ready for action.
Just add this configration to your playwright.config.ts
file:
import { type PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
};
export default config;
Now you can run your tests without having to start the development server manually:
npm run test:e2e
Playwright on Continuous Integration (CI)
Playwright official documentation has a section dedicated to CI.
If you are using GitHub Actions, the basic setup looks as follows:
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Using the Experimental test mode for Playwright
There is not a lot of documentation on this yet, but Vercel team is working on an experimental test mode for Playwright. This mode will allow you to intercept requests and mock responses. This is useful for testing API routes and other server-side code.
Suppose you have an API route at /api/hello
:
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
type ResponseData = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.status(200).json({ name: 'Foo' });
}
This route is called using getServerSideProps
:
// pages/index.tsx
import { GetServerSideProps } from 'next'
export default function Home({ name }) {
return <div data-testid="message">Hello {name}</div>
}
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch('http://localhost:3000/api/hello')
const data = await res.json();
return {
props: {
name: data.name,
},
}
}
Now you want to write a test that verifies that the page renders the name returned by the API.
import { test, expect } from '@playwright/test';
test('prints name retrieved from the API', async ({ page, msw }) => {
await page.goto('/');
await expect(page.getByTestId('message')).toHaveText('Hello Foo');
});
The latter asserts that the page renders the name returned by the API. However, using the experimental test proxy, you can intercept the request to /api/hello
and mock the response.
Mocking API responses
First, you need to install msw
to mock responses:
npm install -D msw
Then you need to change your import to next/experimental/testmode/playwright/msw
:
import { test, expect } from 'next/experimental/testmode/playwright/msw'
Now you can mock the response:
import { test, expect, rest } from 'next/experimental/testmode/playwright/msw';
test('prints name retrieved from the API', async ({ page, msw }) => {
msw.use(
rest.get('http://localhost:3000/api/hello', (req, res, ctx) => {
return res.once(
ctx.status(200),
ctx.json({
name: 'Bar',
})
)
})
);
await page.goto('/');
await expect(page.getByTestId('message')).toHaveText('Hello Bar');
});
App Router
You can also use the experimental test mode to test the Next.js app/ router.
Define a route:
// app/api/hello/route.ts
export async function GET() {
return Response.json({ name: 'Foo' })
}
Fetch the data in your page:
// app/page.tsx
const getData = async () => {
const res = await fetch('http://localhost:3000/api/hello')
const data = await res.json();
return {
name: data.name,
}
};
export default async () => {
const { name } = await getData();
return <div data-testid="message">Hello {name}</div>
}
Now you can test the page:
import { test, expect, rest } from 'next/experimental/testmode/playwright/msw';
test('prints name retrieved from the API', async ({ page, msw }) => {
msw.use(
rest.get('http://localhost:3000/api/hello', (req, res, ctx) => {
return res.once(
ctx.status(200),
ctx.json({
name: 'Bar',
})
)
})
);
await page.goto('/');
await expect(page.getByTestId('message')).toHaveText('Hello Bar');
});
Error: No test info
It appears that when you are running tests in the experimental test mode, you may get the following error:
Error: No test info
This happens when you try to access the web server directly, outside of the test. As far as I can tell, there is no way to access the Next.js web server directly when running in the experimental test mode. You can only access it by running a Playwright test.
Conclusion
In closing, integrating Playwright into your Next.js workflow ensures a robust, tested, and reliable application. If this is your first time using Playwright, you should familiarize with Trace viewer next as it will help you debug your tests. Now, go ahead and craft tests that leave no stone unturned. Happy testing!