Efficient E2E Testing for Next.js: A Playwright Tutorial
Tags: nextjs, playwright
Published: 2023-10-15 00:10:15

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:

```typescript
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:

```typescript
npm install --save-dev @playwright/test
```

Now, update your `package.json` scripts to incorporate Playwright:

```json
{
  "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:

```typescript
// 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:

```typescript
// 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 the `playwright.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:

```typescript
npm run build
npm run start
```

Then in another terminal, run your Playwright tests:

```typescript
npm run test:e2e
```

## Use `webServer` to Start the Development Server

With Playwright's [`webServer` feature](https://playwright.dev/docs/test-webserver), 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:

```typescript
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:

```bash
npm run test:e2e
```

## Playwright on Continuous Integration (CI)

When running on CI, Playwright defaults to headless mode. To have all dependencies in place, execute:

```typescript
npx playwright install-deps
```

## 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](https://github.com/vercel/next.js/blob/canary/packages/next/src/experimental/testmode/playwright/README.md). 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`:

```ts
// 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`:

```ts
// 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.

```ts
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`](https://www.npmjs.com/package/msw) to mock responses:

```bash
npm install -D msw
```

Then you need to change your import to `next/experimental/testmode/playwright/msw`:

```ts
import { test, expect } from 'next/experimental/testmode/playwright/msw'
```

Now you can mock the response:

```ts
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:

```ts
// app/api/hello/route.ts
export async function GET() { 
  return Response.json({ name: 'Foo' })
}
```

Fetch the data in your page:

```ts
// 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:

```ts
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](https://playwright.dev/docs/trace-viewer) next as it will help you debug your tests. Now, go ahead and craft tests that leave no stone unturned. Happy testing!