Jun 28, 2025 Loading...
First install Playwright with npm i @playwright/test
Then add some scripts to run playwright, open the test runner, and update the stored screenshots:
"playwright": "playwright test",
"playwright-open": "playwright test --ui"
"playwright-update": "playwright test --update-snapshots",
Set in playwright.config.ts
a new env variable:
webServer: {
command: 'npm run dev',
url: devServerUrl,
reuseExistingServer: !process.env.CI,
env: {
NEXT_PUBLIC_E2E_TESTING: 'true',
},
},
Then we can reference that env variable in next.config.ts
to disable dev indicator (as it animates causing issues in playwright):
...(process.env.NEXT_PUBLIC_E2E_TESTING === 'true' ? { devIndicators: false } : {}),
We then need to create a test that uses the toHaveScreenshot()
method. Below is my test file where I loop through all my pages and take a screenshot:
import { blogPosts } from '@/constants/blogPosts'
import test, { expect } from '@playwright/test'
import { goToPath } from './helpers'
const blogPostPaths = blogPosts.map(post => '/blog/' + post.slug)
const allPages = ['/', '/blog/', ...blogPostPaths]
test.describe('page screenshots', () => {
for (const path of allPages) {
test(`${path}`, async ({ page }) => {
await goToPath(page, path)
await expect(page).toHaveScreenshot({
fullPage: true,
mask: [
page.locator('.ss-hidden'),
page.locator('img[src*=".gif"]'),
page.locator('iframe'),
],
})
})
}
})
I also use the mask
property to ignore various elements that might not be presented consistently, like iframes
and gifs.
I also added a .ss-hidden
helper class I can wrap anything in I want hidden from the screenshot tool - like my blog post view count.
Now run npm run playwright
, note this will show an error when it is run for the first time, as these snapshots did not previously exist.
Then in future when you run playwright and it captures new snapshots and detects changes compared to the old snapshots the tests will fail and show you an image diff.
Then take a look at the diff and if you are happy with the changes run npm run playwright-update
to update the snapshots 🚀
Animations in playwright can be problematic, so to disable animations in playwright, you have two options depending on if your animations are CSS based or JS based.
NEXT_PUBLIC_E2E_TESTING
env variable.First set prefers-reduce-motion: reduced
in playwright by updating the playwright.config.ts
:
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
contextOptions: { reducedMotion: 'reduce' },
},
},
and use media queries to only apply animations if users have no preference:
@media screen and (prefers-reduced-motion: no-preference) {
animation: ...;
}
In TailwindCSS you can use motion-safe media query:
<div className="motion-safe:animate-[slide-up_0.3s_ease-in-out_forwards]" />
Now when playwright runs, it will not have the animations applied to these elements.
I like to have this little helper variable:
export const isPlaywright = process.env.NEXT_PUBLIC_E2E_TESTING === 'true'
And it can be used in various ways. In the below I’m only applying framer motion animations if the server is not run from the playwright test runner.
import { isPlaywright } from '@/helpers/constants'
import * as motion from 'motion/react-client'
export const PageAnimation = (props: { children: React.ReactNode }) => {
const { children } = props
return (
<motion.div
{...(!isPlaywright && {
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.28, type: 'spring', bounce: 0.1 },
})}
>
{children}
</motion.div>
)
}
In this component below I have a Marquee component that animates some logos on my home page. .marquee-item
has the animation applied to it (using pigmentCSS styles). I’m overwriting those styles using a TailwindCSS class.
export const isPlaywright = process.env.NEXT_PUBLIC_E2E_TESTING === 'true'
...
<div className="marquee-container">
<div className="marquee-wrapper">
<div className={clsx('marquee-item', isPlaywright && '!animate-none')}>{children}</div>
<div className={clsx('marquee-item', isPlaywright && '!animate-none')}>{children}</div>
</div>
</div>
If I was using CSS-in-JS I could reference isPlaywright directly in my CSS, but pigmentCSS is a build time static css generator and does not support dynanmic values in CSS.
If you like React, Next.js or front-end development in general, feel free to follow and say hi on Twitter @_AshConnolly! 👋 🙂