Playwright snippets
Stabilize font rendering
Section titled “Stabilize font rendering”* { font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}config
Section titled “config”import { defineConfig, devices } from "@playwright/test";
/** * See https://playwright.dev/docs/test-configuration. */export default defineConfig({ testDir: "./e2e", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: Boolean(process.env.CI), /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [process.env.CI ? ["dot"] : ["list"]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://localhost:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "retain-on-failure", },
/* Configure projects for major browsers */ projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"], }, }, { name: "Mobile Chrome", use: { ...devices["Pixel 7"] }, }, { name: "Mobile Safari", use: { ...devices["iPhone 15"] }, }, ],
/* Run your local dev server before starting the tests */ webServer: { command: "npm run start", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, },
snapshotPathTemplate: "{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}",});Screenshot helper
Section titled “Screenshot helper”export async function expectToHaveScreenshot(page: Page) { if (!process.env.SCREENSHOT) return; await page.mouse.move(0, 0); // to avoid hover effects in the screenshot await expect(page).toHaveScreenshot({ fullPage: true });}
export function maskOverlay(page: Page) { return page.evaluate(async () => { // @ts-expect-error no check document.querySelector('[aria-modal="true"] ~ div').style.background = "pink"; });}Related:
await expect(page).toHaveScreenshot({ fullPage: true, mask: [page.getByText("...")],});Polly.js
Section titled “Polly.js”mport { test as base } from '@playwright/test';export { expect } from '@playwright/test';
import { join, resolve } from 'node:path';import { MODE, Polly } from '@pollyjs/core';import FSPersister from '@pollyjs/persister-fs';import { PlaywrightAdapter } from 'polly-adapter-playwright';
class FSPersisterFilter extends FSPersister { override onSaveRecording(recordingId: string, har: Har): Promise<void> { const data = JSON.parse(this.stringify(har)); for (const entry of data.log.entries) { // Remove all headers, including secrets entry.request.headers = []; } // @ts-expect-error ignore return this.api.saveRecording(recordingId, data); }}
Polly.register(PlaywrightAdapter);Polly.register(FSPersisterFilter);
export const test = base.extend<{ assets: void; polly: void;}>({ polly: [ async ({ page }, use) => { const info = test.info();
let mode: MODE; let recordIfMissing = true; switch (info.config.updateSnapshots) { case 'all': case 'changed': mode = 'record'; break; case 'missing': mode = 'replay'; break; case 'none': default: mode = 'replay'; recordIfMissing = false; break; }
if (info.config.updateSnapshots === 'none' || process.env.CI) { await page.route('**/aws_session.json', (route) => route.fulfill({ status: 200, json: { access_key_id: '', secret_access_key: '', session_token: '', expiration: '', uuid: '', session_id: '', }, }), ); }
const polly = new Polly(info.title, { recordIfMissing, mode, adapters: ['playwright'], adapterOptions: { playwright: { context: page, routesToIntercept: (url: URL) => { if (url.href.includes('aws_session.json')) return false;
if (/\.json$/.test(url.href)) return true; if (url.href.startsWith('http://localhost')) return false; if (/\.[^/.:?]+$/.test(url.href)) return false; return true; }, }, }, flushRequestsOnStop: true, logLevel: 'SILENT', // 'INFO', recordFailedRequests: false, persister: 'fs', persisterOptions: { keepUnusedRequests: false, disableSortingHarEntries: false, fs: { recordingsDir: join( info.config.rootDir, '__recordings__', info.titlePath[0], ), }, }, matchRequestsBy: { method: true, headers: false, body: true, order: false, }, });
await use();
await polly.stop(); }, { scope: 'test', auto: true, }, ],});Stub assets
Section titled “Stub assets”Usefull for external resources, like images and fonts from CDN
mport { test as base } from '@playwright/test';export { expect } from '@playwright/test';
const headers = { 'Cache-Control': 'max-age=31536000, public, immutable',};
export const test = base.extend<{ assets: void;}>({ assets: [ async ({ page }, use) => { await page.route('http://cdn/**.png', (route) => route.fulfill({ status: 200, headers, contentType: 'image/svg+xml', body: `<svg width="1" height="1" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg"> <rect width="100%" height="100%" fill="pink"/> </svg>`, }), );
await use(); }, { scope: 'test', auto: true, }, ],});.vscode/mcp.json:
{ "servers": { "playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] } }}.github/generate_tests.prompt.md:
---tools: ["playwright/*"]agent: "agent"---
# Playwright Test Generator
You are a playwright test generator.
- You are given a scenario and you need to generate a playwright test for it.- DO NOT generate test code based on the scenario alone.- DO run steps one by one using the tools provided by the Playwright MCP.
## Instructions
When asked to explore a website:
1. Navigate to the specified URL2. Explore 1 key functionality of the site and when finished close the browser.3. Implement a Playwright TypeScript test that uses `@playwright/test` based on message history using Playwright's best practices.4. Save the generated test in the tests directory and execute it.5. Iterate until the test passes.
## Best Practices
- Use `getByTestId` locator whenever possible, otherwise, use role-based locators (`getByRole`, `getByText`)- Strive to write tests that represent real user interactions as closely as possible.- DO NOT cover trivial cases, like typing into input and checking value right after that.- Make sure that tests are actually testing something meaningful. For example, do not check for presence of `body` element and instead make sure that key elements of the page are visible and/or interactive.- Use auto-retrying assertions- Document test purpose and steps in comments.gitignore:
.playwright-mcpRelated: https://dev.to/debs_obrien/letting-playwright-mcp-explore-your-site-and-write-your-tests-mf1 ↗
Test request after click
Section titled “Test request after click”const [request] = await Promise.all([ page.waitForEvent( "request", (r) => r.url().includes("/something") && r.method() === "POST", ), page.getByRole("button", { name: "something" }).click(),]);
expect((await request.response()).status()).toEqual(200);expect(request.postDataJSON()).toEqual({});dotenv
Section titled “dotenv”import dotenv from "dotenv";dotenv.config({ path: resolve(import.meta.dirname, ".env") });Related: import-meta-env ↗
MSW + Zod + Zocker
Section titled “MSW + Zod + Zocker”import { faker } from "@faker-js/faker";import { zocker as zockerDefault } from "zocker";
export const zocker: typeof zockerDefault = process.env.SCREENSHOT ? (x) => zockerDefault(x).setSeed(0) as any : zockerDefault;if (process.env.SCREENSHOT) { // see https://github.com/LorisSigrist/zocker/issues/76 faker.setDefaultRefDate("2025-09-01T10:00:00");}
import { http, HttpResponse } from "msw";
export const mswHandlers = [ http.get(`/something`, async () => HttpResponse.json(zocker(someSchema).generate()), ),];
import { test as base } from "@playwright/test";export { expect } from "@playwright/test";import { http } from "msw";import { type MockServiceWorker, createWorkerFixture } from "playwright-msw";
export const test = base.extend<{ worker: MockServiceWorker; http: typeof http;}>({ worker: createWorkerFixture(mswHandlers), http,});Alternatives:
Github actions
Section titled “Github actions”screenshots: runs-on: macos-latest strategy: matrix: browser: ["chromium", "Mobile Chrome", "Mobile Safari"]
steps: - name: Checkout uses: actions/checkout@v4
- uses: actions/setup-node@v3 with: node-version-file: ".node-version" cache: "npm"
- name: Install dependencies run: npm ci
- run: npx playwright install - run: SCREENSHOT=1 npm run test:e2e -- --project "${{ matrix.browser }}"
- uses: actions/upload-artifact@v5 if: failure() with: name: test-results path: test-resultsDebugging CI test failure
Section titled “Debugging CI test failure”gh run listgh run downloadnpx playwright show-trace test-results/.../trace.zipPrevent navigation from the page
Section titled “Prevent navigation from the page”page.on("dialog", (dialog) => dialog.dismiss());void page.evaluate(() => { window.addEventListener("beforeunload", (event) => { event.preventDefault(); });});Helps with coverage.
Scroll elelemnt
Section titled “Scroll elelemnt”await page.getByTestId("something").hover();await page.mouse.wheel(0, 10_000);Source: https://playwright.dev/docs/input#scrolling ↗
Coockies and localStorage
Section titled “Coockies and localStorage”await context.addCookies([ { name: "something", secure: true, url: "...", value: "true" },]);await context.addInitScript(() => { window.localStorage.setItem("something", "true");});Alternative to:
await page.context().storageState({ path: authFile });Monocart
Section titled “Monocart”monocart-coverage-reports ↗ provides integration for Vitest, Playwright (and Cypress). Can merge reports from several sources.
npm i -D monocart-coverage-reports monocart-reporter vitest-monocart-coverageBase config mcr.config.js:
// https://github.com/cenfun/monocart-coverage-reportsexport default { name: "Vitest Coverage Report", reports: process.env.CI ? ["raw"] : ["raw", "console-details", "v8"], entryFilter: { "**/src/**/*.ts": true, "**/src/**/*.tsx": true, }, sourceFilter: { "**/node_modules/**": false, "**/mock/**": false, "**/*.test.tsx": false, "**/*.test.ts": false, "**/**": true, }, sourcePath: (filePath, info) => { if (info.distFile?.startsWith("src/")) return info.distFile; if (info.distFile?.startsWith("localhost:3000/")) return info.distFile.replace("localhost:3000/", ""); if (info.url?.startsWith("http://localhost:3000/")) return info.url.replace("http://localhost:3000/", ""); if (filePath.startsWith("localhost-3000/")) return filePath.replace("localhost-3000/", ""); return filePath; }, outputDir: "./coverage/vitest",};Playwright-specific config e2e/setup/mcr.config.ts:
import { CoverageReportOptions } from "monocart-coverage-reports";import coverageOptions from "../../mcr.config";
export default { ...coverageOptions, name: "Playwright Coverage Report", outputDir: "./coverage/playwright",} as CoverageReportOptions;Coverage fixture in e2e/setup/base.ts:
import MCR from "monocart-coverage-reports";import coverageOptions from "./mcr.config";
export const test = base.extend<{ coverage: void;}>({ coverage: [ async ({ page }, use) => { const isChromium = test.info().project.name === "chromium";
// coverage API is chromium only if (isChromium) { await page.coverage.startJSCoverage({ resetOnNavigation: false, }); }
await use();
if (isChromium) { const jsCoverage = await page.coverage.stopJSCoverage(); const mcr = MCR(coverageOptions); await mcr.add(jsCoverage); } }, { scope: "test", auto: true, }, ],});Global setup e2e/setup/global-setup.ts:
import MCR from "monocart-coverage-reports";import coverageOptions from "./mcr.config";
async function globalSetup() { const mcr = MCR(coverageOptions); await mcr.cleanCache();}
export default globalSetup;Global teardown e2e/setup/global-teardown.ts:
import MCR from "monocart-coverage-reports";import coverageOptions from "./mcr.config";
async function globalTeardown() { const mcr = MCR(coverageOptions); await mcr.generate();}
export default globalTeardown;Don’t forget to add to playwright.config.ts:
globalSetup: require.resolve("./e2e/setup/global-setup"),globalTeardown: require.resolve("./e2e/setup/global-teardown"),Vitest integration
Section titled “Vitest integration”In vite.config.ts:
test: { coverage: { provider: "custom", customProviderModule: "vitest-monocart-coverage", },},Merging coverage
Section titled “Merging coverage”Script to merge Vitest and Playwright coverage .github/merge.js:
import { CoverageReport } from "monocart-coverage-reports";import coverageOptionsBase from "../mcr.config.js";
const inputDir = ["./coverage/vitest/raw", "./coverage/playwright/raw"];
const coverageOptions = { ...coverageOptionsBase, name: "Merged Coverage Report", inputDir, outputDir: "./coverage/merged", reports: process.env.CI ? ["lcov", "v8"] : ["console-details", "v8"],};
await new CoverageReport(coverageOptions).generate();Add /coverage to .gitignore.