skills/playwright/SKILL.md
Instructions for using Playwright to test the blog. Use this when you need to write, run or debug Playwright tests for the blog.
npx skillsauth add nikoheikkila/nikoheikkila.fi playwrightInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
src/__tests__/feature/playwright.config.ts with local/CI environmentspage.getByRole("button", { name: "Submit" }) to select buttons by their accessible namepage.getByTestId("submit-button") to select dynamic elements with data-testid attributespage.getByText("Click me") to select elements based on visible textpage.getByLabel("Email address") to select form fields based on their associated labels// Required imports and fixture structure
import { test, expect, type Page } from "@playwright/test";
test.describe.parallel("Given I want to test feature X", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/target-page");
});
test("when I perform action Y, then outcome Z should occur", async ({ page }) => {
await test.step("Descriptive step name", async () => {
// Test implementation
});
});
});
// For mobile test suites, configure viewport at the describe level
test.describe.parallel("Given I visit the page using a mobile browser", () => {
test.use({
hasTouch: true,
viewport: { width: 375, height: 667 },
});
// Tests here run with mobile viewport
});
Critical: These are mandatory practices to follow.
test.step() to organize test actions and improve reportingawait expect(locator).toBeVisible() for auto-retrying assertionstest.describe.parallel()test.use({ viewport: { width: 375, height: 667 }, hasTouch: true })Critical: Avoid these at all costs.
page.waitForTimeout() - Never use hard-coded timeoutspage.locator(".css-class") - Avoid CSS selectors when possibledata-testid / data-test-id - Never use test-id attributes; this project requires web-first locators onlyany types - Import proper types like Page from PlaywrighttoMatchAriaSnapshot for component structure validationUse @axe-core/playwright to run WCAG 2.2 AA scans in E2E tests. Existing page-archetype scans live in src/__tests__/feature/a11y.test.ts.
import { expect, test, type Page } from "@playwright/test";
import { AxeBuilder } from "@axe-core/playwright";
const WCAG_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"];
test("page passes WCAG 2.2 AA", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" });
const results = await new AxeBuilder({ page }).withTags(WCAG_TAGS).analyze();
expect(results.violations).toEqual([]);
});
Notes:
waitUntil: "networkidle" so React has hydrated and all styles are applied before the scan.region rule is tagged best-practice, not WCAG — it does not run with the WCAG tag filter above..disableRules(["rule-id"]).main.scss).// Test pagination with proper edge case handling
const fixtures = {
postsPerPage: 30,
pagination: { minimumPostsOnPage: 1 }
};
// Check for dynamic pagination states
const nextPageExists = (await nextPageLink.count()) > 0;
if (nextPageExists) {
await expect(nextPageLink).toBeVisible();
}
// Test burger menu with proper element waiting
const burgerMenuButton = page.locator(".bm-burger-button");
await burgerMenuButton.click();
// Wait for menu to become visible (not timeout)
const menuContainer = page.locator(".bm-menu");
await expect(menuContainer).toBeVisible();
Shared navigation helpers are in src/__tests__/feature/navigate.ts:
import type { Page, Locator } from "@playwright/test";
// Handle popup windows for external links
export const toExternalSiteByClicking = async (page: Page, locator: Locator): Promise<Page> => {
const popupPromise = page.waitForEvent("popup");
await locator.click();
const popup = await popupPromise;
await popup.waitForLoadState("load");
return popup;
};
// Navigate to an internal page by clicking a link
export const toInternalPageByClicking = async (page: Page, locator: Locator): Promise<void> => {
await Promise.all([page.waitForURL(/\//), locator.click()]);
};
// Verify dynamic content differences between pages
const firstPostPage2 = await posts.first().textContent();
await page.goto("/");
const firstPostPage1 = await posts.first().textContent();
expect(firstPostPage1).not.toBe(firstPostPage2);
task build after modifying React components before running E2E testsgetByRole, getByTestId) over CSS selectorsawait expect(element).toBeVisible() instead of timeouts.bm-menu container visibility, not just overlaydevops
Instructions for managing the Cloudflare infrastructure with Terraform. Use this when you need to work with Terraform or Cloudflare resources.
testing
Instructions for running project tasks. Use this when you need to run a project-specific command or workflow.
testing
Review blog posts and pages for grammar, style, frontmatter validity, technical accuracy, and SEO. Use this when asked to review, proofread, or validate Markdown content.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.