Agent skill

Playwright Testing

by alinaqi in alinaqi/maggy

E2E testing with Playwright - Page Objects, cross-browser, CI/CD

MITAuto-check: notesTesting & QA

Install Playwright Testing

skills CLI
$ npx skills add alinaqi/maggy --skill playwright-testing -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install alinaqi/maggy playwright-testing --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/alinaqi/maggy.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/playwright-testing .claude/skills/playwright-testing && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
playwright-testing
GitHub stars
707
Token cost
~5.8k tokens
SKILL.md length
217 words
Files
1
Skills in repo
71
Repo updated
First seen
Licence
MIT

At a glance

E2E testing with Playwright - Page Objects, cross-browser, CI/CD

  • Tasks that involve Browser testing
  • SKILL.md covers Setup, Project Structure, Locator Strategy (Priority… and Page Object Model, plus 7 more sections
  • Calls npx and npm; needs TEST_USER_PASSWORD
  • Tasks that involve End-to-end testing

What it does

Playwright Testing is an agent skill from alinaqi/maggy. E2E testing with Playwright - Page Objects, cross-browser, CI/CD

Its SKILL.md is about 5.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Testing & QA, covering Browser testing, End-to-end testing and CI/CD. It works with Playwright. The repository describes itself as: What started as an opinionated Claude Code setup kit is now an autonomous AI engineering command center. The licence is MIT.

When your agent uses it

  • Tasks that involve Browser testing
  • Tasks that involve End-to-end testing
  • Tasks that involve CI/CD

Example prompts

  • “/playwright-testing”

Requirements

  • Node.js

What it can do on your machine

Read from SKILL.md and the folder at commit 72a456e. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • npx
    • npm

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • playwright.dev
    • betterstack.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • TEST_USER_PASSWORD

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Playwright Testing loads about 5.8k tokens when it runs. Until then it costs about 21 tokens; SKILL.md has 217 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~21
When it runs · the whole SKILL.md, loaded when a task matches
~5.8k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NoteMentions a .env fileSKILL.md:674
    # .env.test

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from alinaqi/maggy at commit 72a456e, republished under its MIT licence (© alinaqi). 217 words, ~5,781 tokens.

Download SKILL.mdSave it as .claude/skills/playwright-testing/SKILL.md (or your agent's skills folder).
name
playwright-testing
description
E2E testing with Playwright - Page Objects, cross-browser, CI/CD
when-to-use
When writing or debugging E2E tests with Playwright
user-invocable
true
paths
**/e2e/**, **/*.spec.ts, **/playwright/**, playwright.config.*
effort
medium

Playwright E2E Testing Skill

For end-to-end testing of web applications with Playwright - cross-browser, fast, reliable.

Sources: Playwright Best Practices | Playwright Docs | Better Stack Guide


Setup

Installation
bash
# New project
npm init playwright@latest

# Existing project
npm install -D @playwright/test
npx playwright install
Configuration
typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['list'],
    process.env.CI ? ['github'] : ['line'],
  ],

  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    // Auth setup - runs once before all tests
    { name: 'setup', testMatch: /.*\.setup\.ts/ },

    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
      dependencies: ['setup'],
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
      dependencies: ['setup'],
    },
    // Mobile viewports
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
      dependencies: ['setup'],
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 12'] },
      dependencies: ['setup'],
    },
  ],

  // Start dev server before tests
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});

Project Structure

project/
├── e2e/
│   ├── fixtures/
│   │   ├── auth.fixture.ts      # Auth fixtures
│   │   └── test.fixture.ts      # Extended test with fixtures
│   ├── pages/
│   │   ├── base.page.ts         # Base page object
│   │   ├── login.page.ts        # Login page object
│   │   ├── dashboard.page.ts    # Dashboard page object
│   │   └── index.ts             # Export all pages
│   ├── tests/
│   │   ├── auth.spec.ts         # Auth tests
│   │   ├── dashboard.spec.ts    # Dashboard tests
│   │   └── checkout.spec.ts     # Checkout flow tests
│   ├── utils/
│   │   ├── helpers.ts           # Test helpers
│   │   └── test-data.ts         # Test data factories
│   └── auth.setup.ts            # Global auth setup
├── playwright.config.ts
└── .auth/                        # Stored auth state (gitignored)

Locator Strategy (Priority Order)

Use locators that mirror how users interact with the page:

typescript
// ✅ BEST: Role-based (accessible, resilient)
page.getByRole('button', { name: 'Submit' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('link', { name: 'Sign up' })
page.getByRole('heading', { name: 'Welcome' })

// ✅ GOOD: User-facing text
page.getByLabel('Email address')
page.getByPlaceholder('Enter your email')
page.getByText('Welcome back')
page.getByTitle('Profile settings')

// ✅ GOOD: Test IDs (stable, explicit)
page.getByTestId('submit-button')
page.getByTestId('user-avatar')

// ⚠️ AVOID: CSS selectors (brittle)
page.locator('.btn-primary')
page.locator('#submit')

// ❌ NEVER: XPath (extremely brittle)
page.locator('//div[@class="container"]/button[1]')
Chaining Locators
typescript
// Narrow down to specific section
const form = page.getByRole('form', { name: 'Login' });
await form.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await form.getByRole('button', { name: 'Submit' }).click();

// Filter within a list
const productCard = page.getByTestId('product-card')
  .filter({ hasText: 'Pro Plan' });
await productCard.getByRole('button', { name: 'Buy' }).click();

Page Object Model

Base Page
typescript
// e2e/pages/base.page.ts
import { Page, Locator } from '@playwright/test';

export abstract class BasePage {
  constructor(protected page: Page) {}

  async navigate(path: string = '/') {
    await this.page.goto(path);
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }

  // Common elements
  get header() {
    return this.page.getByRole('banner');
  }

  get footer() {
    return this.page.getByRole('contentinfo');
  }

  // Common actions
  async clickNavLink(name: string) {
    await this.header.getByRole('link', { name }).click();
  }
}
Page Implementation
typescript
// e2e/pages/login.page.ts
import { Page, expect } from '@playwright/test';
import { BasePage } from './base.page';

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
    this.errorMessage = page.getByRole('alert');
  }

  async goto() {
    await this.navigate('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }

  async expectLoggedIn() {
    await expect(this.page).toHaveURL(/.*dashboard/);
  }
}
typescript
// e2e/pages/dashboard.page.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './base.page';

export class DashboardPage extends BasePage {
  readonly welcomeHeading: Locator;
  readonly userMenu: Locator;
  readonly logoutButton: Locator;

  constructor(page: Page) {
    super(page);
    this.welcomeHeading = page.getByRole('heading', { name: /welcome/i });
    this.userMenu = page.getByTestId('user-menu');
    this.logoutButton = page.getByRole('button', { name: 'Logout' });
  }

  async goto() {
    await this.navigate('/dashboard');
  }

  async logout() {
    await this.userMenu.click();
    await this.logoutButton.click();
  }

  async expectWelcome(name: string) {
    await expect(this.welcomeHeading).toContainText(name);
  }
}
Export All Pages
typescript
// e2e/pages/index.ts
export { BasePage } from './base.page';
export { LoginPage } from './login.page';
export { DashboardPage } from './dashboard.page';

Authentication

Global Auth Setup
typescript
// e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';

const authFile = path.join(__dirname, '../.auth/user.json');

setup('authenticate', async ({ page }) => {
  // Go to login page
  await page.goto('/login');

  // Login with test credentials
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Wait for auth to complete
  await expect(page).toHaveURL(/.*dashboard/);

  // Save auth state for reuse
  await page.context().storageState({ path: authFile });
});
Using Auth in Tests
typescript
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: '.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});
Tests Without Auth
typescript
// e2e/tests/public.spec.ts
import { test } from '@playwright/test';

// Override to skip auth
test.use({ storageState: { cookies: [], origins: [] } });

test('homepage loads for anonymous users', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});

Writing Tests

Basic Test Structure
typescript
// e2e/tests/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages';

test.describe('Authentication', () => {
  test.beforeEach(async ({ page }) => {
    // Skip stored auth for login tests
    await page.context().clearCookies();
  });

  test('successful login redirects to dashboard', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.goto();
    await loginPage.login('user@example.com', 'password123');
    await loginPage.expectLoggedIn();
  });

  test('invalid credentials show error', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.goto();
    await loginPage.login('wrong@example.com', 'wrongpass');
    await loginPage.expectError('Invalid email or password');
  });

  test('empty form shows validation errors', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.goto();
    await loginPage.submitButton.click();

    await expect(page.getByText('Email is required')).toBeVisible();
    await expect(page.getByText('Password is required')).toBeVisible();
  });
});
User Flow Tests
typescript
// e2e/tests/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test('complete purchase flow', async ({ page }) => {
    // 1. Browse products
    await page.goto('/products');
    await page.getByTestId('product-card')
      .filter({ hasText: 'Pro Plan' })
      .getByRole('button', { name: 'Add to cart' })
      .click();

    // 2. View cart
    await page.getByRole('link', { name: 'Cart' }).click();
    await expect(page.getByText('Pro Plan')).toBeVisible();
    await expect(page.getByTestId('cart-total')).toContainText('$29.99');

    // 3. Checkout
    await page.getByRole('button', { name: 'Checkout' }).click();

    // 4. Fill payment (use Stripe test card)
    const stripeFrame = page.frameLocator('iframe[name*="stripe"]');
    await stripeFrame.getByPlaceholder('Card number').fill('4242424242424242');
    await stripeFrame.getByPlaceholder('MM / YY').fill('12/30');
    await stripeFrame.getByPlaceholder('CVC').fill('123');

    // 5. Complete purchase
    await page.getByRole('button', { name: 'Pay now' }).click();

    // 6. Verify success
    await expect(page).toHaveURL(/.*success/);
    await expect(page.getByRole('heading', { name: 'Thank you' })).toBeVisible();
  });
});

Assertions

Web-First Assertions (Auto-Wait)
typescript
// ✅ These wait and retry automatically
await expect(page.getByRole('button')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('button')).toHaveText('Submit');
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle(/Dashboard/);

// ❌ Avoid manual waits
await page.waitForTimeout(3000);  // NEVER do this
Soft Assertions
typescript
// Continue test even if assertion fails
await expect.soft(page.getByTestId('price')).toHaveText('$29.99');
await expect.soft(page.getByTestId('stock')).toHaveText('In Stock');

// Fail at end if any soft assertions failed
Common Assertions
typescript
// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached();

// Text content
await expect(locator).toHaveText('exact text');
await expect(locator).toContainText('partial');
await expect(locator).toHaveValue('input value');

// State
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeFocused();

// Count
await expect(locator).toHaveCount(5);

// Page
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle('Dashboard | App');
await expect(page).toHaveScreenshot('dashboard.png');

Mocking & Network

Mock API Responses
typescript
test('shows error when API fails', async ({ page }) => {
  // Mock API to return error
  await page.route('**/api/users', (route) => {
    route.fulfill({
      status: 500,
      body: JSON.stringify({ error: 'Server error' }),
    });
  });

  await page.goto('/users');
  await expect(page.getByText('Failed to load users')).toBeVisible();
});

test('displays user data from API', async ({ page }) => {
  // Mock successful response
  await page.route('**/api/users', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Doe', email: 'jane@example.com' },
      ]),
    });
  });

  await page.goto('/users');
  await expect(page.getByText('John Doe')).toBeVisible();
  await expect(page.getByText('Jane Doe')).toBeVisible();
});
Wait for API Calls
typescript
test('submits form and shows success', async ({ page }) => {
  await page.goto('/contact');

  // Fill form
  await page.getByLabel('Name').fill('John');
  await page.getByLabel('Email').fill('john@example.com');
  await page.getByLabel('Message').fill('Hello!');

  // Wait for API call on submit
  const responsePromise = page.waitForResponse('**/api/contact');
  await page.getByRole('button', { name: 'Send' }).click();

  const response = await responsePromise;
  expect(response.status()).toBe(200);

  await expect(page.getByText('Message sent!')).toBeVisible();
});

Visual Testing

typescript
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png');

// Element screenshot
await expect(page.getByTestId('chart')).toHaveScreenshot('chart.png');

// With options
await expect(page).toHaveScreenshot('dashboard.png', {
  maxDiffPixels: 100,
  mask: [page.getByTestId('timestamp')], // Ignore dynamic content
});

CI/CD Integration

GitHub Actions
yaml
# .github/workflows/e2e.yml
name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run E2E tests
        run: npx playwright test --project=chromium
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7
Run Specific Tests
bash
# Run all tests
npx playwright test

# Run specific file
npx playwright test e2e/tests/auth.spec.ts

# Run tests with tag
npx playwright test --grep @critical

# Run in headed mode (debug)
npx playwright test --headed

# Run specific browser
npx playwright test --project=chromium

# Debug mode
npx playwright test --debug

# Show HTML report
npx playwright show-report

Test Data

Factories
typescript
// e2e/utils/test-data.ts
import { faker } from '@faker-js/faker';

export const createUser = (overrides = {}) => ({
  email: faker.internet.email(),
  password: faker.internet.password({ length: 12 }),
  name: faker.person.fullName(),
  ...overrides,
});

export const createProduct = (overrides = {}) => ({
  name: faker.commerce.productName(),
  price: faker.commerce.price({ min: 10, max: 100 }),
  description: faker.commerce.productDescription(),
  ...overrides,
});
Environment Variables
bash
# .env.test
BASE_URL=http://localhost:3000
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=testpassword123

Debugging

Trace Viewer
typescript
// Enable in config for failures
use: {
  trace: 'on-first-retry',
}

// View traces
npx playwright show-trace trace.zip
Debug Mode
bash
# Step through test
npx playwright test --debug

# Pause at specific point
await page.pause();  // In test code
VS Code Extension

Install "Playwright Test for VS Code" for:

  • Run tests from editor
  • Debug with breakpoints
  • Pick locators visually
  • Watch mode

Every project MUST include dead link detection tests. Run these on every deployment.

typescript
// e2e/tests/links.spec.ts
import { test, expect } from '@playwright/test';

const PAGES_TO_CHECK = ['/', '/about', '/pricing', '/blog', '/contact'];

test.describe('Dead Link Detection', () => {
  for (const pagePath of PAGES_TO_CHECK) {
    test(`no dead links on ${pagePath}`, async ({ page, request }) => {
      await page.goto(pagePath);

      // Get all links on the page
      const links = await page.locator('a[href]').all();
      const hrefs = await Promise.all(
        links.map(link => link.getAttribute('href'))
      );

      // Filter to internal and absolute external links
      const uniqueLinks = [...new Set(hrefs.filter(Boolean))] as string[];

      for (const href of uniqueLinks) {
        // Skip mailto, tel, and anchor links
        if (href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('#')) {
          continue;
        }

        // Build full URL
        const url = href.startsWith('http') ? href : new URL(href, page.url()).href;

        // Check link status
        const response = await request.get(url, {
          timeout: 10000,
          ignoreHTTPSErrors: true,
        });

        expect(
          response.ok(),
          `Dead link found on ${pagePath}: ${href} returned ${response.status()}`
        ).toBeTruthy();
      }
    });
  }
});
typescript
// e2e/tests/site-links.spec.ts
import { test, expect, Page, APIRequestContext } from '@playwright/test';

interface LinkResult {
  url: string;
  status: number;
  foundOn: string;
}

async function checkAllLinks(
  page: Page,
  request: APIRequestContext,
  startUrl: string
): Promise<LinkResult[]> {
  const visited = new Set<string>();
  const results: LinkResult[] = [];
  const toVisit = [startUrl];
  const baseUrl = new URL(startUrl).origin;

  while (toVisit.length > 0) {
    const currentUrl = toVisit.pop()!;
    if (visited.has(currentUrl)) continue;
    visited.add(currentUrl);

    try {
      await page.goto(currentUrl);
      const links = await page.locator('a[href]').all();

      for (const link of links) {
        const href = await link.getAttribute('href');
        if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) {
          continue;
        }

        const fullUrl = href.startsWith('http') ? href : new URL(href, currentUrl).href;

        // Check link
        const response = await request.get(fullUrl, {
          timeout: 10000,
          ignoreHTTPSErrors: true,
        });

        results.push({
          url: fullUrl,
          status: response.status(),
          foundOn: currentUrl,
        });

        // Add internal links to queue
        if (fullUrl.startsWith(baseUrl) && !visited.has(fullUrl)) {
          toVisit.push(fullUrl);
        }
      }
    } catch (error) {
      results.push({
        url: currentUrl,
        status: 0,
        foundOn: 'navigation',
      });
    }
  }

  return results;
}

test('no dead links on entire site', async ({ page, request, baseURL }) => {
  const results = await checkAllLinks(page, request, baseURL!);
  const deadLinks = results.filter(r => r.status >= 400 || r.status === 0);

  if (deadLinks.length > 0) {
    console.error('Dead links found:');
    deadLinks.forEach(link => {
      console.error(`  ${link.url} (${link.status}) - found on ${link.foundOn}`);
    });
  }

  expect(deadLinks, `Found ${deadLinks.length} dead links`).toHaveLength(0);
});
typescript
// e2e/tests/images.spec.ts
import { test, expect } from '@playwright/test';

test('no broken images on homepage', async ({ page, request }) => {
  await page.goto('/');

  const images = await page.locator('img[src]').all();

  for (const img of images) {
    const src = await img.getAttribute('src');
    if (!src) continue;

    const url = src.startsWith('http') ? src : new URL(src, page.url()).href;

    // Skip data URLs
    if (url.startsWith('data:')) continue;

    const response = await request.get(url);
    expect(
      response.ok(),
      `Broken image: ${src}`
    ).toBeTruthy();

    // Verify it's actually an image
    const contentType = response.headers()['content-type'];
    expect(
      contentType?.startsWith('image/'),
      `${src} is not an image (${contentType})`
    ).toBeTruthy();
  }
});
yaml
# .github/workflows/link-check.yml
name: Link Check

on:
  schedule:
    - cron: '0 6 * * 1'  # Weekly on Monday
  push:
    branches: [main]

jobs:
  link-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install chromium
      - run: npx playwright test e2e/tests/links.spec.ts --project=chromium
        env:
          BASE_URL: ${{ secrets.PRODUCTION_URL }}

Anti-Patterns

  • Hardcoded waits - Use auto-waiting assertions instead
  • CSS/XPath selectors - Use role/text/testid locators
  • Testing third-party sites - Mock external dependencies
  • Shared state between tests - Each test must be isolated
  • Missing awaits - Use ESLint rule no-floating-promises
  • Flaky time-based tests - Mock dates/times
  • Testing implementation details - Test user-visible behavior
  • Huge test files - Split by feature/page

Quick Reference

bash
# Install
npm init playwright@latest

# Run tests
npx playwright test
npx playwright test --headed
npx playwright test --project=chromium
npx playwright test --grep @smoke

# Debug
npx playwright test --debug
npx playwright show-report
npx playwright show-trace trace.zip

# Generate tests
npx playwright codegen localhost:3000
Package.json Scripts
json
{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:headed": "playwright test --headed",
    "test:e2e:debug": "playwright test --debug",
    "test:e2e:report": "playwright show-report",
    "test:e2e:codegen": "playwright codegen"
  }
}

© alinaqi, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/playwright-testing of alinaqi/maggy.

Open the folder on GitHubat commit 72a456e

Compare with similar skills

Playwright Testing next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Playwright Testing compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Playwright Testing this skillalinaqi/maggy707—~5.8kAutomated safety check: NotesMIT
Debug Playwrightquay/quay2.8k—~1.2kAutomated safety check: PassApache-2.0
E2E Testingaffaan-m/ECC275k3 repos~1.9kAutomated safety check: PassMIT
E2E Testingaffaan-m/ECC275k1 repos~2kAutomated safety check: PassMIT
Lambdatest Agent Skillssickn33/agentic-awesome-skills47k1 repos~3kAutomated safety check: PassMIT
Playwright E2Eforcedotcom/salesforcedx-vscode1k—~3kAutomated safety check: PassBSD-3-Clause

Similar skills

  • Debug Playwright E2E test failures from GitHub Actions CI runs.

    2.8k GitHub stars~1.2k tokensUpdated today
    Testing & QAAuto-check passed
  • E2E Testing

    affaan-m/ECC

    Playwright E2E 测试模式、页面对象模型、配置、CI/CD 集成、工件管理和不稳定测试策略. An agent skill from affaan-m/ECC.

    275k GitHub starsUsed in 3 repos~1.9k tokens
    Testing & QAAuto-check passed
  • E2E Testing

    affaan-m/ECC

    Playwright E2E test kalıpları, Page Object Model, yapılandırma, CI/CD entegrasyonu, artifact yönetimi ve kararsız test stratejileri.

    275k GitHub starsUsed in 1 repo~2k tokens
    Testing & QAAuto-check passed
  • Lambdatest Agent Skills

    sickn33/agentic-awesome-skills

    Production-grade test automation skills for 46 frameworks across E2E, unit, mobile, BDD, visual, and cloud testing in 15+ languages.

    47k GitHub starsUsed in 1 repo~3k tokens
    Testing & QAAuto-check passed
  • Playwright E2E

    forcedotcom/salesforcedx-vscode

    writing, running, and debugging Playwright tests; creating and recreating scratch orgs (Dreamhouse, minimal, non-tracking); working with their output from github actions

    1k GitHub stars~3k tokensUpdated today
    Testing & QAAuto-check passed
  • E2E Testing

    affaan-m/ECC

    Patrones de pruebas E2E con Playwright, Page Object Model, configuración, integración CI/CD, gestión de artefactos y estrategias para pruebas inestables.

    275k GitHub stars~2k tokensUpdated 3 days ago
    Testing & QAAuto-check passed

More from alinaqi/maggy

All 71 skills in this repo
  • AI Models

    alinaqi/maggy

    Latest AI models reference - Claude, OpenAI, Gemini, Eleven Labs, Replicate

    707 GitHub starsUsed in 1 repo~4.1k tokens
    Auto-check passed
  • Azure Cosmosdb

    alinaqi/maggy

    Azure Cosmos DB partition keys, consistency levels, change feed, SDK patterns

    707 GitHub starsUsed in 1 repo~4.5k tokens
    Auto-check passed
  • LLM Patterns

    alinaqi/maggy

    AI-first application patterns, LLM testing, prompt management

    707 GitHub starsUsed in 1 repo~2.1k tokens
    Auto-check passed
  • Woocommerce

    alinaqi/maggy

    WooCommerce REST API - products, orders, customers, webhooks

    707 GitHub starsUsed in 1 repo~4.4k tokens
    Auto-check: notes
  • Aeo Optimization

    alinaqi/maggy

    AI Engine Optimization - semantic triples, page templates, content clusters for AI citations

    707 GitHub stars~3.7k tokensUpdated 14 days ago
    Auto-check passed
  • Agent Teams

    alinaqi/maggy

    Claude Code Agent Teams - default team-based development with strict TDD pipeline enforcement

    707 GitHub stars~5k tokensUpdated 14 days ago
    Auto-check: notes

Works with

Categories

Questions about Playwright Testing

What does Playwright Testing do?

E2E testing with Playwright - Page Objects, cross-browser, CI/CD. Playwright Testing is an agent skill from alinaqi/maggy.

When should I use Playwright Testing?

Playwright Testing fits situations like: tasks that involve Browser testing; tasks that involve End-to-end testing; tasks that involve CI/CD.

How do I install Playwright Testing in Claude Code?

Run `npx skills add alinaqi/maggy --skill playwright-testing -a claude-code`. Or copy the skill folder (skills/playwright-testing in alinaqi/maggy) into .claude/skills/playwright-testing in your project. Claude Code loads it when a task matches its description.

How do I install Playwright Testing in Codex?

Run `npx skills add alinaqi/maggy --skill playwright-testing -a codex`. Or copy the skill folder (skills/playwright-testing in alinaqi/maggy) into .agents/skills/playwright-testing in your project. Codex loads it when a task matches its description.

Can I use Playwright Testing in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add alinaqi/maggy --skill playwright-testing -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/playwright-testing, .gemini/skills/playwright-testing, .github/skills/playwright-testing and .opencode/skills/playwright-testing in your project.

What does Playwright Testing need to run?

Going by SKILL.md and its folder, Playwright Testing needs the command-line tools its instructions call (npx and npm) and credentials named TEST_USER_PASSWORD. Our summary lists: Node.js.

Does Playwright Testing access the network?

SKILL.md names 2 domains. As links in the text: playwright.dev and betterstack.com. This is read from the text; nothing was executed.

Is Playwright Testing safe to install?

Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Playwright Testing use?

Playwright Testing is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Playwright Testing use?

About 5.8k tokens (SKILL.md is roughly 23k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Playwright Testing?

Skills that share tags, products or a category with Playwright Testing: Debug Playwright (quay/quay, 2.8k stars), E2E Testing (affaan-m/ECC, 275k stars), E2E Testing (affaan-m/ECC, 275k stars) and Lambdatest Agent Skills (sickn33/agentic-awesome-skills, 47k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Playwright Testing?

alinaqi (a GitHub user) maintains it in alinaqi/maggy, which has 707 GitHub stars. The repository holds 71 skills in this directory. The repository was last updated on September 24, 2026.

Source: alinaqi/maggy on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.