agentleFS
sabahattinksabahattink/antigravity-fullstack-hq/skills/webapp-testing/SKILL.md

webapp-testing

Skill30 starsChanged 53 days agoMITView on GitHub

Playwright E2E patterns, Testing Library component tests, test selectors. Use when writing browser tests, component tests, or setting up an E2E testing pipeline for a Next.js or React app.

  • Reads credentials
  • Installs packages

196 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use academy-guide, skill-share and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • AnthropicSkill

    academy-guide

    Stop and check this skill before finishing any reply to a question about how to use Claude or a Claude product — it recommends matching courses, tutorials, and use cases from Claude Academy (academy.claude.com), Anthropic's learning hub. Trigger on: "how do I", "how can I", "getting started with", "what can Claude do", "teach me", "learn to use"; questions about artifacts, projects, skills, plugins, connectors, MCP; requests about rolling Claude out to a team, class, or organization; and any ask for training materials, onboarding content, or learning resources. Use it when the user is learning how to use a feature or product — not when they are mid-task and just want the task done. This skill composes with other skills: after consulting product documentation to answer how a Claude feature works, also check here for a matching course or tutorial — a docs-grounded answer and an Academy recommendation belong together. Only recommend on a strong match; never invent Academy content.

    Also in 8 projects that use this

  • ArogyaReddySkill

    skill-share

    A skill that creates new Claude skills and automatically shares them on Slack using Rube for seamless team collaboration and skill discovery.

    Also in 23 projects that use this

  • SKB3002Skill

    intelligent-routing

    Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.

    Also in 5 projects that use this

  • ArogyaReddySkill

    raffle-winner-picker

    Picks random winners from lists, spreadsheets, or Google Sheets for giveaways, raffles, and contests. Ensures fair, unbiased selection with transparency.

    Also in 26 projects that use this

  • zephyrwang6Skill

    topic-collector

    AI热点采集工具。从Twitter/X、Product Hunt、Reddit、Hacker News、博客等采集AI相关热点内容。当用户说"开始今日选题"、"采集热点"、"看看今天有什么新闻"、"今日AI热点"时触发。聚焦领域:Vibe Coding、Claude Skill、AI知识管理、AI模型更新、AI新产品、海外热点。

    Also in 4 projects that use this

  • zephyrwang6Skill

    deepl

    Übersetzt Texte, Dokumente und XLIFF-Dateien via DeepL API. Verwende für Übersetzungen mit /deepl translate, /deepl file oder /deepl xliff.

    Also in 4 projects that use this

  • +4 more from zephyrwang6
  • +1 more from ArogyaReddy

The file

We split this skill into the rules it gives an agent

7 rules. 0 are common, 5 are distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 190 edited versions we read keep these.

  • 5

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never use getByTestId when a role/label selector would work

    line 346
  2. Distinctive:

    Never test implementation details (state, method calls) — test user-visible behavior

    line 347
  3. Distinctive:

    Never write Playwright tests that depend on hardcoded data IDs — use dynamic selectors

    line 348
  4. Very specific:

    Never add sleep / waitForTimeout — use proper waitFor / findBy

    line 349
  5. Distinctive:

    Never skip error state tests — test the unhappy path

    line 350
  6. Distinctive:

    Never share browser context between tests — each test should be isolated

    line 351
  7. Distinctive:

    Never test styling with RTL — that belongs in visual regression (Chromatic/Percy)

    line 352

“Common” is measured: kept by at least 6 in 10 of the 190 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Web App Testing

Testing Pyramid

          /\
         /E2E\        ← Few, slow, high confidence (Playwright)
        /------\
       / Integr \     ← Some, medium speed (RTL + MSW)
      /----------\
     /  Unit/Comp \   ← Many, fast, isolated (Vitest + RTL)
    /--------------\

Playwright Setup

npm i -D @playwright/test
npx playwright install chromium firefox webkit
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir:   './e2e',
  timeout:   30_000,
  retries:   process.env.CI ? 2 : 0,
  workers:   process.env.CI ? 1 : undefined,
  reporter:  [['html'], ['list']],

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

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'mobile',   use: { ...devices['iPhone 14'] } },
  ],

  webServer: {
    command: 'npm run build && npm run start',
    url:     'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

Page Object Model

// e2e/pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test'

export class LoginPage {
  readonly emailInput:    Locator
  readonly passwordInput: Locator
  readonly submitButton:  Locator
  readonly errorMessage:  Locator

  constructor(private page: 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.page.goto('/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)
  }
}

// e2e/pages/dashboard.page.ts
export class DashboardPage {
  constructor(private page: Page) {}

  async expectWelcome(name: string) {
    await expect(this.page.getByRole('heading', { level: 1 })).toContainText(`Welcome, ${name}`)
  }
}

Playwright Test Examples

// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from '../pages/login.page'
import { DashboardPage } from '../pages/dashboard.page'

test.describe('Login flow', () => {
  test('successful login redirects to dashboard', async ({ page }) => {
    const loginPage = new LoginPage(page)
    const dashboard = new DashboardPage(page)

    await loginPage.goto()
    await loginPage.login('admin@example.com', 'password123')

    await expect(page).toHaveURL('/dashboard')
    await dashboard.expectWelcome('Admin')
  })

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

    await loginPage.goto()
    await loginPage.login('bad@example.com', 'wrongpassword')

    await loginPage.expectError('Invalid email or password')
    await expect(page).toHaveURL('/login')
  })

  test('email field is required', async ({ page }) => {
    const loginPage = new LoginPage(page)
    await loginPage.goto()

    await loginPage.submitButton.click()

    // HTML5 validation
    await expect(loginPage.emailInput).toBeFocused()
  })
})

Authenticated Tests

// e2e/fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test'

interface AuthFixtures {
  authenticatedPage: Page
  adminPage: Page
}

export const test = base.extend<AuthFixtures>({
  // Regular user
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'e2e/.auth/user.json',
    })
    const page = await context.newPage()
    await use(page)
    await context.close()
  },

  // Admin user
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'e2e/.auth/admin.json',
    })
    const page = await context.newPage()
    await use(page)
    await context.close()
  },
})

// e2e/auth/setup.ts — run once before all tests
import { chromium } from '@playwright/test'

async function globalSetup() {
  const browser = await chromium.launch()

  // Save user session
  const userCtx = await browser.newContext()
  const userPage = await userCtx.newPage()
  await userPage.goto('http://localhost:3000/login')
  await userPage.fill('[name=email]', 'user@example.com')
  await userPage.fill('[name=password]', 'password')
  await userPage.click('button[type=submit]')
  await userPage.waitForURL('**/dashboard')
  await userCtx.storageState({ path: 'e2e/.auth/user.json' })

  await browser.close()
}

export default globalSetup

React Testing Library

// components/UserCard/UserCard.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { UserCard } from './UserCard'

describe('UserCard', () => {
  const user = {
    id: 1,
    name: 'Jane Doe',
    email: 'jane@example.com',
    role: 'admin' as const,
  }

  it('renders user name and email', () => {
    render(<UserCard user={user} />)

    expect(screen.getByRole('heading', { name: 'Jane Doe' })).toBeInTheDocument()
    expect(screen.getByText('jane@example.com')).toBeInTheDocument()
    expect(screen.getByText('admin')).toBeInTheDocument()
  })

  it('calls onDelete when delete button is clicked', async () => {
    const onDelete = vi.fn()
    render(<UserCard user={user} onDelete={onDelete} />)

    const deleteButton = screen.getByRole('button', { name: /delete/i })
    await userEvent.click(deleteButton)

    expect(onDelete).toHaveBeenCalledWith(user.id)
  })

  it('shows confirmation dialog before deleting', async () => {
    const onDelete = vi.fn()
    render(<UserCard user={user} onDelete={onDelete} />)

    await userEvent.click(screen.getByRole('button', { name: /delete/i }))

    expect(screen.getByRole('dialog')).toBeInTheDocument()
    expect(screen.getByText(/are you sure/i)).toBeInTheDocument()
    expect(onDelete).not.toHaveBeenCalled()

    await userEvent.click(screen.getByRole('button', { name: /confirm/i }))

    expect(onDelete).toHaveBeenCalledWith(user.id)
  })
})

Mocking API Calls with MSW

// test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/v1/users', () => {
    return HttpResponse.json({
      success: true,
      data: [
        { id: 1, name: 'Jane Doe', email: 'jane@example.com' },
        { id: 2, name: 'John Smith', email: 'john@example.com' },
      ],
      meta: { total: 2, page: 1, limit: 20, pages: 1 },
    })
  }),

  http.post('/api/v1/users', async ({ request }) => {
    const body = await request.json() as CreateUserDto
    return HttpResponse.json({
      success: true,
      data: { id: 3, ...body },
    }, { status: 201 })
  }),

  http.get('/api/v1/users/:id', ({ params }) => {
    if (params.id === '999') {
      return HttpResponse.json(
        { success: false, error: { code: 'USER_NOT_FOUND', message: 'Not found' } },
        { status: 404 }
      )
    }
    return HttpResponse.json({ success: true, data: { id: Number(params.id), name: 'Test' } })
  }),
]

// test/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)

// test/setup.ts
import { server } from './mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Selector Priority

// Priority order (highest to lowest confidence):
// 1. getByRole        — reflects what users and screen readers see
// 2. getByLabelText   — form fields
// 3. getByPlaceholderText
// 4. getByText        — visible text
// 5. getByDisplayValue
// 6. getByAltText     — img alt
// 7. getByTitle
// 8. getByTestId      — last resort, use data-testid sparingly

// Good
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText('Email address')
screen.getByRole('heading', { name: 'Dashboard', level: 1 })

// Avoid (brittle, tied to implementation)
document.querySelector('.btn-primary')
container.firstChild
screen.getByTestId('submit-btn')  // only when roles don't work

Async Patterns

// Wait for element to appear
await screen.findByText('Loading complete')
await screen.findByRole('table')

// Wait for element to disappear
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'))

// Wait for DOM change
await waitFor(() => {
  expect(screen.getByText('3 items')).toBeInTheDocument()
}, { timeout: 3000 })

// Playwright: wait for network
await page.waitForResponse('**/api/v1/users')
await page.waitForLoadState('networkidle')

Forbidden Patterns

  • Never use getByTestId when a role/label selector would work
  • Never test implementation details (state, method calls) — test user-visible behavior
  • Never write Playwright tests that depend on hardcoded data IDs — use dynamic selectors
  • Never add sleep / waitForTimeout — use proper waitFor / findBy
  • Never skip error state tests — test the unhappy path
  • Never share browser context between tests — each test should be isolated
  • Never test styling with RTL — that belongs in visual regression (Chromatic/Percy)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.