Skip to content

Playwright snippets

* {
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
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}",
});
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("...")],
});
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,
},
],
});

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 URL
2. 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-mcp

Related: https://dev.to/debs_obrien/letting-playwright-mcp-explore-your-site-and-write-your-tests-mf1

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({});
import dotenv from "dotenv";
dotenv.config({ path: resolve(import.meta.dirname, ".env") });

Related: import-meta-env

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:

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-results
Terminal window
gh run list
gh run download
npx playwright show-trace test-results/.../trace.zip
page.on("dialog", (dialog) => dialog.dismiss());
void page.evaluate(() => {
window.addEventListener("beforeunload", (event) => {
event.preventDefault();
});
});

Helps with coverage.

await page.getByTestId("something").hover();
await page.mouse.wheel(0, 10_000);

Source: https://playwright.dev/docs/input#scrolling

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-coverage-reports provides integration for Vitest, Playwright (and Cypress). Can merge reports from several sources.

Terminal window
npm i -D monocart-coverage-reports monocart-reporter vitest-monocart-coverage

Base config mcr.config.js:

// https://github.com/cenfun/monocart-coverage-reports
export 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"),

In vite.config.ts:

test: {
coverage: {
provider: "custom",
customProviderModule: "vitest-monocart-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.