[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6R0V1NGP3349HAJM2QHC5GC":3,"skill-same:01M6R0V1NGP3349HAJM2QHC5GC":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":6,"ownerHtmlUrl":56,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":57,"install":58,"resources":71},"01M6R0V1NGP3349HAJM2QHC5GC","playwright-e2e-builder",null,"Plan and build comprehensive Playwright E2E test suites with Page Object Model, authentication state persistence, custom fixtures, visual regression, and CI integration. Uses interview-driven planning to clarify critical user flows, auth strategy, test data approach, and parallelization before writing any tests.","Mishit18\u002Fclaude-code-templates","Mishit18","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F244127857?v=4","01M6QXBYKWM76HSKYDNWSKKE41","Mishit Sharma","01M6QXBYRJ0CAVM51S1RTEKTZZ",1,"MIT","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fdevelopment\u002Fplaywright-e2e-builder\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"playwright e2e","playwright","playwright e2e builder","e2e","builder","model","visual-regression","flows","critical","visual regression","e2e test","clarify","regression","testing","plan","ci","authentication","auth","typescript","writing","2026-11-10 10:12:30",false,"---\nname: playwright-e2e-builder\ndescription: Plan and build comprehensive Playwright E2E test suites with Page Object Model, authentication state persistence, custom fixtures, visual regression, and CI integration. Uses interview-driven planning to clarify critical user flows, auth strategy, test data approach, and parallelization before writing any tests.\ntags: [playwright, e2e, testing, automation, typescript, ci, visual-regression]\n---\n\n# Playwright E2E Test Suite Builder\n\n## When to use\n\nUse this skill when you need to:\n\n- Set up Playwright from scratch in an existing project\n- Build E2E tests for critical user flows (signup, checkout, dashboards)\n- Implement Page Object Model for maintainable test architecture\n- Configure authentication state persistence across tests\n- Set up visual regression testing with screenshots\n- Integrate Playwright into CI\u002FCD with sharding and retries\n\n## Phase 1: Explore (Plan Mode)\n\nEnter plan mode. Before writing any tests, explore the existing project:\n\n### Project structure\n- Find the tech stack: is this React, Next.js, Vue, SvelteKit, or another framework?\n- Check if Playwright is already installed (`playwright.config.ts`, `@playwright\u002Ftest` in package.json)\n- Look for existing test directories (`e2e\u002F`, `tests\u002F`, `__tests__\u002F`)\n- Check for existing E2E tests in Cypress, Selenium, or other frameworks (migration context)\n- Find the dev server command and port (`npm run dev`, `next dev`, etc.)\n\n### Application structure\n- Identify the main routes\u002Fpages (look at router config, pages directory, or route files)\n- Find authentication flow (login page URL, auth API endpoints, token storage)\n- Check for test IDs in components (`data-testid`, `data-test`, `data-cy` attributes)\n- Look for API routes that tests might need to seed data through\n- Check `.env` files for test-specific environment variables\n\n### CI\u002FCD\n- Check for existing CI config (`.github\u002Fworkflows\u002F`, `.gitlab-ci.yml`, `Jenkinsfile`)\n- Look for Docker or docker-compose setup (useful for consistent test environments)\n- Check if there's a staging\u002Fpreview environment URL pattern\n\n## Phase 2: Interview (AskUserQuestion)\n\nUse AskUserQuestion to clarify requirements. Ask in rounds.\n\n### Round 1: Scope and critical flows\n\n```\nQuestion: \"What are the critical user flows to test?\"\nHeader: \"Flows\"\nmultiSelect: true\nOptions:\n  - \"Authentication (signup, login, logout, password reset)\" — Core auth flows\n  - \"Core CRUD (create, read, update, delete main resources)\" — Primary data operations\n  - \"Checkout\u002Fpayments (cart, billing, confirmation)\" — E-commerce or payment flows\n  - \"Dashboard\u002Fadmin (data views, filters, exports)\" — Admin panel interactions\n```\n\n```\nQuestion: \"How many pages\u002Froutes does the application have approximately?\"\nHeader: \"App size\"\nOptions:\n  - \"Small (\u003C 10 routes)\" — Landing page, auth, a few feature pages\n  - \"Medium (10-30 routes)\" — Multiple feature areas, settings, profiles\n  - \"Large (30+ routes)\" — Complex app with many sections and user roles\n```\n\n### Round 2: Authentication strategy for tests\n\n```\nQuestion: \"How does your app handle authentication?\"\nHeader: \"Auth type\"\nOptions:\n  - \"Cookie\u002Fsession based (Recommended)\" — Server sets httpOnly cookies after login\n  - \"JWT in localStorage\" — Token stored in browser localStorage\n  - \"OAuth\u002FSSO (Google, GitHub, etc.)\" — Third-party auth provider redirect flow\n  - \"No auth (public app)\" — No login required\n\nQuestion: \"How should tests authenticate?\"\nHeader: \"Test auth\"\nOptions:\n  - \"Login via UI once, reuse state (Recommended)\" — storageState pattern: login in setup, share cookies across tests\n  - \"API login in beforeEach\" — Call auth API directly before each test, skip UI login\n  - \"Seed auth token in fixtures\" — Inject pre-generated tokens, no login flow needed\n  - \"Test login UI every time\" — Actually test the login form in each test suite\n```\n\n### Round 3: Test data and environment\n\n```\nQuestion: \"How should test data be managed?\"\nHeader: \"Test data\"\nOptions:\n  - \"API seeding in fixtures (Recommended)\" — Call API endpoints to create\u002Fclean test data before each test\n  - \"Database seeding (direct SQL)\" — Run SQL scripts or ORM commands to populate test database\n  - \"Shared test environment (pre-populated)\" — Tests run against a persistent staging environment with existing data\n  - \"Mock API responses\" — Intercept network requests and return mock data\n\nQuestion: \"What environment do E2E tests run against?\"\nHeader: \"Environment\"\nOptions:\n  - \"Local dev server (Recommended)\" — Start dev server before tests, run against localhost\n  - \"Preview\u002Fstaging URL\" — Run against a deployed preview or staging environment\n  - \"Docker Compose stack\" — Full stack in containers, tests run outside or inside\n```\n\n### Round 4: CI and parallelization\n\n```\nQuestion: \"How should tests run in CI?\"\nHeader: \"CI\"\nOptions:\n  - \"GitHub Actions (Recommended)\" — Native Playwright support with sharding\n  - \"GitLab CI\" — Docker-based runners with Playwright image\n  - \"Local only (no CI yet)\" — Just local test runs for now\n  - \"Other CI (Jenkins, CircleCI)\" — Custom CI configuration\n\nQuestion: \"Do you need visual regression testing?\"\nHeader: \"Visual\"\nOptions:\n  - \"No — functional tests only (Recommended)\" — Assert behavior, not pixels\n  - \"Yes — screenshot comparisons\" — Capture and compare page screenshots\n  - \"Yes — component screenshots\" — Capture specific components, not full pages\n```\n\n## Phase 3: Plan (ExitPlanMode)\n\nWrite a concrete implementation plan covering:\n\n1. **Directory structure** — test files, page objects, fixtures, config\n2. **Playwright config** — projects (browsers), base URL, retries, workers\n3. **Auth setup** — global setup for storageState or API-based auth\n4. **Page objects** — classes for each page with locators and actions\n5. **Test fixtures** — custom fixtures for data seeding, auth, API client\n6. **Test suites** — test files for each critical flow from the interview\n7. **CI config** — workflow file with sharding, artifact upload, reporting\n\nPresent via ExitPlanMode for user approval.\n\n## Phase 4: Execute\n\nAfter approval, implement following this order:\n\n### Step 1: Playwright config\n\n```typescript\nimport { defineConfig, devices } from '@playwright\u002Ftest';\n\nexport default defineConfig({\n  testDir: '.\u002Fe2e',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,\n  reporter: process.env.CI\n    ? [['html', { open: 'never' }], ['github']]\n    : [['html', { open: 'on-failure' }]],\n\n  use: {\n    baseURL: process.env.BASE_URL || 'http:\u002F\u002Flocalhost:3000',\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    video: 'on-first-retry',\n  },\n\n  projects: [\n    \u002F\u002F Auth setup — runs before all tests\n    {\n      name: 'setup',\n      testMatch: \u002F.*\\.setup\\.ts\u002F,\n    },\n    {\n      name: 'chromium',\n      use: {\n        ...devices['Desktop Chrome'],\n        storageState: 'e2e\u002F.auth\u002Fuser.json',\n      },\n      dependencies: ['setup'],\n    },\n    {\n      name: 'firefox',\n      use: {\n        ...devices['Desktop Firefox'],\n        storageState: 'e2e\u002F.auth\u002Fuser.json',\n      },\n      dependencies: ['setup'],\n    },\n    {\n      name: 'mobile',\n      use: {\n        ...devices['iPhone 14'],\n        storageState: 'e2e\u002F.auth\u002Fuser.json',\n      },\n      dependencies: ['setup'],\n    },\n  ],\n\n  webServer: {\n    command: 'npm run dev',\n    url: 'http:\u002F\u002Flocalhost:3000',\n    reuseExistingServer: !process.env.CI,\n    timeout: 120_000,\n  },\n});\n```\n\n### Step 2: Auth setup (global)\n\n```typescript\n\u002F\u002F e2e\u002Fauth.setup.ts\nimport { test as setup, expect } from '@playwright\u002Ftest';\n\nconst authFile = 'e2e\u002F.auth\u002Fuser.json';\n\nsetup('authenticate', async ({ page }) => {\n  \u002F\u002F Navigate to login page\n  await page.goto('\u002Flogin');\n\n  \u002F\u002F Fill login form\n  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL || 'test@example.com');\n  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD || 'testpassword');\n  await page.getByRole('button', { name: 'Sign in' }).click();\n\n  \u002F\u002F Wait for auth to complete — adjust selector to your app\n  await page.waitForURL('\u002Fdashboard');\n  await expect(page.getByRole('navigation')).toBeVisible();\n\n  \u002F\u002F Save signed-in state\n  await page.context().storageState({ path: authFile });\n});\n```\n\n### Step 3: Custom fixtures\n\n```typescript\n\u002F\u002F e2e\u002Ffixtures.ts\nimport { test as base, expect } from '@playwright\u002Ftest';\nimport { LoginPage } from '.\u002Fpages\u002Flogin-page';\nimport { DashboardPage } from '.\u002Fpages\u002Fdashboard-page';\n\n\u002F\u002F API client for test data seeding\nclass ApiClient {\n  constructor(private baseURL: string, private token?: string) {}\n\n  async createResource(data: Record\u003Cstring, unknown>) {\n    const response = await fetch(`${this.baseURL}\u002Fapi\u002Fresources`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application\u002Fjson',\n        ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),\n      },\n      body: JSON.stringify(data),\n    });\n    if (!response.ok) throw new Error(`Seed failed: ${response.status}`);\n    return response.json();\n  }\n\n  async deleteResource(id: string) {\n    await fetch(`${this.baseURL}\u002Fapi\u002Fresources\u002F${id}`, {\n      method: 'DELETE',\n      headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},\n    });\n  }\n}\n\ntype Fixtures = {\n  loginPage: LoginPage;\n  dashboardPage: DashboardPage;\n  api: ApiClient;\n};\n\nexport const test = base.extend\u003CFixtures>({\n  loginPage: async ({ page }, use) => {\n    await use(new LoginPage(page));\n  },\n\n  dashboardPage: async ({ page }, use) => {\n    await use(new DashboardPage(page));\n  },\n\n  api: async ({ baseURL }, use) => {\n    const client = new ApiClient(baseURL!);\n    await use(client);\n  },\n});\n\nexport { expect };\n```\n\n### Step 4: Page Object Model\n\n```typescript\n\u002F\u002F e2e\u002Fpages\u002Flogin-page.ts\nimport { type Page, type Locator, expect } from '@playwright\u002Ftest';\n\nexport class LoginPage {\n  readonly emailInput: Locator;\n  readonly passwordInput: Locator;\n  readonly submitButton: Locator;\n  readonly errorMessage: Locator;\n\n  constructor(private page: Page) {\n    this.emailInput = page.getByLabel('Email');\n    this.passwordInput = page.getByLabel('Password');\n    this.submitButton = page.getByRole('button', { name: 'Sign in' });\n    this.errorMessage = page.getByRole('alert');\n  }\n\n  async goto() {\n    await this.page.goto('\u002Flogin');\n  }\n\n  async login(email: string, password: string) {\n    await this.emailInput.fill(email);\n    await this.passwordInput.fill(password);\n    await this.submitButton.click();\n  }\n\n  async expectError(message: string) {\n    await expect(this.errorMessage).toContainText(message);\n  }\n}\n\n\u002F\u002F e2e\u002Fpages\u002Fdashboard-page.ts\nimport { type Page, type Locator, expect } from '@playwright\u002Ftest';\n\nexport class DashboardPage {\n  readonly heading: Locator;\n  readonly createButton: Locator;\n  readonly searchInput: Locator;\n  readonly resourceList: Locator;\n\n  constructor(private page: Page) {\n    this.heading = page.getByRole('heading', { level: 1 });\n    this.createButton = page.getByRole('button', { name: 'Create' });\n    this.searchInput = page.getByPlaceholder('Search');\n    this.resourceList = page.getByTestId('resource-list');\n  }\n\n  async goto() {\n    await this.page.goto('\u002Fdashboard');\n  }\n\n  async createResource(name: string) {\n    await this.createButton.click();\n    await this.page.getByLabel('Name').fill(name);\n    await this.page.getByRole('button', { name: 'Save' }).click();\n  }\n\n  async search(query: string) {\n    await this.searchInput.fill(query);\n    \u002F\u002F Wait for debounced search to trigger\n    await this.page.waitForResponse(resp =>\n      resp.url().includes('\u002Fapi\u002Fresources') && resp.status() === 200\n    );\n  }\n\n  async expectResourceVisible(name: string) {\n    await expect(this.resourceList.getByText(name)).toBeVisible();\n  }\n\n  async expectResourceCount(count: number) {\n    await expect(this.resourceList.getByRole('listitem')).toHaveCount(count);\n  }\n}\n```\n\n### Step 5: Test suites\n\n```typescript\n\u002F\u002F e2e\u002Fauth.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Authentication', () => {\n  \u002F\u002F These tests run WITHOUT storageState (unauthenticated)\n  test.use({ storageState: { cookies: [], origins: [] } });\n\n  test('successful login redirects to dashboard', async ({ loginPage, page }) => {\n    await loginPage.goto();\n    await loginPage.login('test@example.com', 'testpassword');\n    await expect(page).toHaveURL('\u002Fdashboard');\n  });\n\n  test('invalid credentials shows error', async ({ loginPage }) => {\n    await loginPage.goto();\n    await loginPage.login('test@example.com', 'wrongpassword');\n    await loginPage.expectError('Invalid credentials');\n  });\n\n  test('logout clears session', async ({ page }) => {\n    \u002F\u002F Login first\n    await page.goto('\u002Flogin');\n    \u002F\u002F ... login steps ...\n\n    \u002F\u002F Logout\n    await page.getByRole('button', { name: 'Logout' }).click();\n    await expect(page).toHaveURL('\u002Flogin');\n\n    \u002F\u002F Verify can't access protected route\n    await page.goto('\u002Fdashboard');\n    await expect(page).toHaveURL('\u002Flogin');\n  });\n});\n\n\u002F\u002F e2e\u002Fdashboard.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Dashboard', () => {\n  test('displays resource list', async ({ dashboardPage }) => {\n    await dashboardPage.goto();\n    await expect(dashboardPage.heading).toHaveText('Dashboard');\n    await expect(dashboardPage.resourceList).toBeVisible();\n  });\n\n  test('create new resource', async ({ dashboardPage, page }) => {\n    await dashboardPage.goto();\n    await dashboardPage.createResource('New E2E Resource');\n\n    \u002F\u002F Verify resource appears in list\n    await dashboardPage.expectResourceVisible('New E2E Resource');\n  });\n\n  test('search filters results', async ({ dashboardPage, api }) => {\n    \u002F\u002F Seed test data via API\n    await api.createResource({ name: 'Alpha Item' });\n    await api.createResource({ name: 'Beta Item' });\n\n    await dashboardPage.goto();\n    await dashboardPage.search('Alpha');\n    await dashboardPage.expectResourceVisible('Alpha Item');\n  });\n\n  test('empty state shown when no resources', async ({ dashboardPage, page }) => {\n    await dashboardPage.goto();\n    await dashboardPage.search('nonexistent-query-xyz');\n    await expect(page.getByText('No results found')).toBeVisible();\n  });\n});\n\n\u002F\u002F e2e\u002Fcrud.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Resource CRUD', () => {\n  let resourceId: string;\n\n  test.beforeEach(async ({ api }) => {\n    \u002F\u002F Seed a resource for tests that need one\n    const resource = await api.createResource({ name: 'Test Resource' });\n    resourceId = resource.id;\n  });\n\n  test.afterEach(async ({ api }) => {\n    \u002F\u002F Clean up seeded data\n    if (resourceId) {\n      await api.deleteResource(resourceId).catch(() => {});\n    }\n  });\n\n  test('edit resource name', async ({ page }) => {\n    await page.goto(`\u002Fresources\u002F${resourceId}`);\n    await page.getByRole('button', { name: 'Edit' }).click();\n    await page.getByLabel('Name').clear();\n    await page.getByLabel('Name').fill('Updated Resource');\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    await expect(page.getByRole('heading')).toHaveText('Updated Resource');\n  });\n\n  test('delete resource with confirmation', async ({ page }) => {\n    await page.goto(`\u002Fresources\u002F${resourceId}`);\n    await page.getByRole('button', { name: 'Delete' }).click();\n\n    \u002F\u002F Confirm deletion dialog\n    await expect(page.getByRole('dialog')).toBeVisible();\n    await page.getByRole('button', { name: 'Confirm' }).click();\n\n    \u002F\u002F Should redirect to list\n    await expect(page).toHaveURL('\u002Fdashboard');\n  });\n});\n```\n\n### Step 6: Visual regression (if selected)\n\n```typescript\n\u002F\u002F e2e\u002Fvisual.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Visual regression', () => {\n  test('dashboard matches snapshot', async ({ dashboardPage, page }) => {\n    await dashboardPage.goto();\n    \u002F\u002F Wait for dynamic content to stabilize\n    await page.waitForLoadState('networkidle');\n    await expect(page).toHaveScreenshot('dashboard.png', {\n      maxDiffPixelRatio: 0.01,\n    });\n  });\n\n  test('login page matches snapshot', async ({ loginPage, page }) => {\n    test.use({ storageState: { cookies: [], origins: [] } });\n    await loginPage.goto();\n    await expect(page).toHaveScreenshot('login.png', {\n      maxDiffPixelRatio: 0.01,\n    });\n  });\n\n  \u002F\u002F Component-level screenshots\n  test('navigation component matches snapshot', async ({ page }) => {\n    await page.goto('\u002Fdashboard');\n    const nav = page.getByRole('navigation');\n    await expect(nav).toHaveScreenshot('navigation.png');\n  });\n});\n```\n\n### Step 7: GitHub Actions CI\n\n```yaml\n# .github\u002Fworkflows\u002Fe2e.yml\nname: E2E Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  e2e:\n    timeout-minutes: 30\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: [1\u002F4, 2\u002F4, 3\u002F4, 4\u002F4]\n\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n\n      - run: npm ci\n\n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium\n\n      - name: Run E2E tests\n        run: npx playwright test --shard=${{ matrix.shard }}\n        env:\n          BASE_URL: http:\u002F\u002Flocalhost:3000\n          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}\n          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}\n\n      - name: Upload test report\n        uses: actions\u002Fupload-artifact@v4\n        if: ${{ !cancelled() }}\n        with:\n          name: playwright-report-${{ strategy.job-index }}\n          path: playwright-report\u002F\n          retention-days: 14\n\n      - name: Upload test results\n        uses: actions\u002Fupload-artifact@v4\n        if: ${{ !cancelled() }}\n        with:\n          name: test-results-${{ strategy.job-index }}\n          path: test-results\u002F\n          retention-days: 7\n```\n\n## Directory structure reference\n\n```\ne2e\u002F\n├── .auth\u002F\n│   └── user.json            # Saved auth state (gitignored)\n├── fixtures.ts              # Custom test fixtures and API client\n├── pages\u002F\n│   ├── login-page.ts        # Login page object\n│   ├── dashboard-page.ts    # Dashboard page object\n│   └── resource-page.ts     # Resource detail page object\n├── auth.setup.ts            # Global auth setup (runs once)\n├── auth.spec.ts             # Authentication tests\n├── dashboard.spec.ts        # Dashboard tests\n├── crud.spec.ts             # CRUD operation tests\n└── visual.spec.ts           # Visual regression tests (optional)\nplaywright.config.ts         # Playwright configuration\n```\n\n## Best practices\n\n### Use role-based locators first\nPrefer `getByRole()`, `getByLabel()`, `getByText()` over CSS selectors or test IDs. These locators mirror how users interact with the page and catch accessibility issues:\n\n```typescript\n\u002F\u002F Preferred — accessible and resilient\nawait page.getByRole('button', { name: 'Submit' }).click();\nawait page.getByLabel('Email').fill('user@test.com');\n\n\u002F\u002F Fallback — when role-based doesn't work\nawait page.getByTestId('custom-widget').click();\n\n\u002F\u002F Avoid — fragile, breaks on refactors\nawait page.locator('.btn-primary').click();\nawait page.locator('#email-input').fill('user@test.com');\n```\n\n### Wait for network, not timers\nNever use `page.waitForTimeout()`. Wait for specific conditions:\n\n```typescript\n\u002F\u002F Wait for API response\nawait page.waitForResponse(resp => resp.url().includes('\u002Fapi\u002Fdata'));\n\n\u002F\u002F Wait for element state\nawait expect(page.getByText('Saved')).toBeVisible();\n\n\u002F\u002F Wait for navigation\nawait expect(page).toHaveURL('\u002Fdashboard');\n\n\u002F\u002F Wait for loading to finish\nawait expect(page.getByTestId('spinner')).toBeHidden();\n```\n\n### Isolate test data\nEach test should create its own data and clean up after:\n\n```typescript\ntest('edit resource', async ({ api, page }) => {\n  \u002F\u002F Arrange — seed via API\n  const resource = await api.createResource({ name: 'Test' });\n\n  \u002F\u002F Act\n  await page.goto(`\u002Fresources\u002F${resource.id}`);\n  \u002F\u002F ... test logic ...\n\n  \u002F\u002F Cleanup (also runs on failure via afterEach)\n});\n```\n\n### Tag tests for selective runs\n\n```typescript\ntest('checkout flow @slow @checkout', async ({ page }) => {\n  \u002F\u002F Long test tagged for selective execution\n});\n\n\u002F\u002F Run only: npx playwright test --grep @checkout\n\u002F\u002F Skip slow: npx playwright test --grep-invert @slow\n```\n\n### .gitignore additions\n\n```\n# Playwright\ne2e\u002F.auth\u002F\ntest-results\u002F\nplaywright-report\u002F\nblob-report\u002F\n```\n\n## Checklist before finishing\n\n- [ ] `playwright.config.ts` has webServer configured to start the dev server\n- [ ] Auth setup saves storageState and all test projects depend on it\n- [ ] Page objects use role-based locators (`getByRole`, `getByLabel`, `getByText`)\n- [ ] No `waitForTimeout()` calls — only wait for elements, URLs, or responses\n- [ ] Tests create and clean up their own data (no shared mutable state)\n- [ ] CI config has sharding for parallel execution\n- [ ] Trace, screenshot, and video are captured on failure for debugging\n- [ ] `.auth\u002F` directory is in `.gitignore`\n- [ ] `npx playwright test` passes locally before pushing\n\n","# Playwright E2E Test Suite Builder\n\n## When to use\n\nUse this skill when you need to:\n\n- Set up Playwright from scratch in an existing project\n- Build E2E tests for critical user flows (signup, checkout, dashboards)\n- Implement Page Object Model for maintainable test architecture\n- Configure authentication state persistence across tests\n- Set up visual regression testing with screenshots\n- Integrate Playwright into CI\u002FCD with sharding and retries\n\n## Phase 1: Explore (Plan Mode)\n\nEnter plan mode. Before writing any tests, explore the existing project:\n\n### Project structure\n- Find the tech stack: is this React, Next.js, Vue, SvelteKit, or another framework?\n- Check if Playwright is already installed (`playwright.config.ts`, `@playwright\u002Ftest` in package.json)\n- Look for existing test directories (`e2e\u002F`, `tests\u002F`, `__tests__\u002F`)\n- Check for existing E2E tests in Cypress, Selenium, or other frameworks (migration context)\n- Find the dev server command and port (`npm run dev`, `next dev`, etc.)\n\n### Application structure\n- Identify the main routes\u002Fpages (look at router config, pages directory, or route files)\n- Find authentication flow (login page URL, auth API endpoints, token storage)\n- Check for test IDs in components (`data-testid`, `data-test`, `data-cy` attributes)\n- Look for API routes that tests might need to seed data through\n- Check `.env` files for test-specific environment variables\n\n### CI\u002FCD\n- Check for existing CI config (`.github\u002Fworkflows\u002F`, `.gitlab-ci.yml`, `Jenkinsfile`)\n- Look for Docker or docker-compose setup (useful for consistent test environments)\n- Check if there's a staging\u002Fpreview environment URL pattern\n\n## Phase 2: Interview (AskUserQuestion)\n\nUse AskUserQuestion to clarify requirements. Ask in rounds.\n\n### Round 1: Scope and critical flows\n\n```\nQuestion: \"What are the critical user flows to test?\"\nHeader: \"Flows\"\nmultiSelect: true\nOptions:\n  - \"Authentication (signup, login, logout, password reset)\" — Core auth flows\n  - \"Core CRUD (create, read, update, delete main resources)\" — Primary data operations\n  - \"Checkout\u002Fpayments (cart, billing, confirmation)\" — E-commerce or payment flows\n  - \"Dashboard\u002Fadmin (data views, filters, exports)\" — Admin panel interactions\n```\n\n```\nQuestion: \"How many pages\u002Froutes does the application have approximately?\"\nHeader: \"App size\"\nOptions:\n  - \"Small (\u003C 10 routes)\" — Landing page, auth, a few feature pages\n  - \"Medium (10-30 routes)\" — Multiple feature areas, settings, profiles\n  - \"Large (30+ routes)\" — Complex app with many sections and user roles\n```\n\n### Round 2: Authentication strategy for tests\n\n```\nQuestion: \"How does your app handle authentication?\"\nHeader: \"Auth type\"\nOptions:\n  - \"Cookie\u002Fsession based (Recommended)\" — Server sets httpOnly cookies after login\n  - \"JWT in localStorage\" — Token stored in browser localStorage\n  - \"OAuth\u002FSSO (Google, GitHub, etc.)\" — Third-party auth provider redirect flow\n  - \"No auth (public app)\" — No login required\n\nQuestion: \"How should tests authenticate?\"\nHeader: \"Test auth\"\nOptions:\n  - \"Login via UI once, reuse state (Recommended)\" — storageState pattern: login in setup, share cookies across tests\n  - \"API login in beforeEach\" — Call auth API directly before each test, skip UI login\n  - \"Seed auth token in fixtures\" — Inject pre-generated tokens, no login flow needed\n  - \"Test login UI every time\" — Actually test the login form in each test suite\n```\n\n### Round 3: Test data and environment\n\n```\nQuestion: \"How should test data be managed?\"\nHeader: \"Test data\"\nOptions:\n  - \"API seeding in fixtures (Recommended)\" — Call API endpoints to create\u002Fclean test data before each test\n  - \"Database seeding (direct SQL)\" — Run SQL scripts or ORM commands to populate test database\n  - \"Shared test environment (pre-populated)\" — Tests run against a persistent staging environment with existing data\n  - \"Mock API responses\" — Intercept network requests and return mock data\n\nQuestion: \"What environment do E2E tests run against?\"\nHeader: \"Environment\"\nOptions:\n  - \"Local dev server (Recommended)\" — Start dev server before tests, run against localhost\n  - \"Preview\u002Fstaging URL\" — Run against a deployed preview or staging environment\n  - \"Docker Compose stack\" — Full stack in containers, tests run outside or inside\n```\n\n### Round 4: CI and parallelization\n\n```\nQuestion: \"How should tests run in CI?\"\nHeader: \"CI\"\nOptions:\n  - \"GitHub Actions (Recommended)\" — Native Playwright support with sharding\n  - \"GitLab CI\" — Docker-based runners with Playwright image\n  - \"Local only (no CI yet)\" — Just local test runs for now\n  - \"Other CI (Jenkins, CircleCI)\" — Custom CI configuration\n\nQuestion: \"Do you need visual regression testing?\"\nHeader: \"Visual\"\nOptions:\n  - \"No — functional tests only (Recommended)\" — Assert behavior, not pixels\n  - \"Yes — screenshot comparisons\" — Capture and compare page screenshots\n  - \"Yes — component screenshots\" — Capture specific components, not full pages\n```\n\n## Phase 3: Plan (ExitPlanMode)\n\nWrite a concrete implementation plan covering:\n\n1. **Directory structure** — test files, page objects, fixtures, config\n2. **Playwright config** — projects (browsers), base URL, retries, workers\n3. **Auth setup** — global setup for storageState or API-based auth\n4. **Page objects** — classes for each page with locators and actions\n5. **Test fixtures** — custom fixtures for data seeding, auth, API client\n6. **Test suites** — test files for each critical flow from the interview\n7. **CI config** — workflow file with sharding, artifact upload, reporting\n\nPresent via ExitPlanMode for user approval.\n\n## Phase 4: Execute\n\nAfter approval, implement following this order:\n\n### Step 1: Playwright config\n\n```typescript\nimport { defineConfig, devices } from '@playwright\u002Ftest';\n\nexport default defineConfig({\n  testDir: '.\u002Fe2e',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,\n  reporter: process.env.CI\n    ? [['html', { open: 'never' }], ['github']]\n    : [['html', { open: 'on-failure' }]],\n\n  use: {\n    baseURL: process.env.BASE_URL || 'http:\u002F\u002Flocalhost:3000',\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    video: 'on-first-retry',\n  },\n\n  projects: [\n    \u002F\u002F Auth setup — runs before all tests\n    {\n      name: 'setup',\n      testMatch: \u002F.*\\.setup\\.ts\u002F,\n    },\n    {\n      name: 'chromium',\n      use: {\n        ...devices['Desktop Chrome'],\n        storageState: 'e2e\u002F.auth\u002Fuser.json',\n      },\n      dependencies: ['setup'],\n    },\n    {\n      name: 'firefox',\n      use: {\n        ...devices['Desktop Firefox'],\n        storageState: 'e2e\u002F.auth\u002Fuser.json',\n      },\n      dependencies: ['setup'],\n    },\n    {\n      name: 'mobile',\n      use: {\n        ...devices['iPhone 14'],\n        storageState: 'e2e\u002F.auth\u002Fuser.json',\n      },\n      dependencies: ['setup'],\n    },\n  ],\n\n  webServer: {\n    command: 'npm run dev',\n    url: 'http:\u002F\u002Flocalhost:3000',\n    reuseExistingServer: !process.env.CI,\n    timeout: 120_000,\n  },\n});\n```\n\n### Step 2: Auth setup (global)\n\n```typescript\n\u002F\u002F e2e\u002Fauth.setup.ts\nimport { test as setup, expect } from '@playwright\u002Ftest';\n\nconst authFile = 'e2e\u002F.auth\u002Fuser.json';\n\nsetup('authenticate', async ({ page }) => {\n  \u002F\u002F Navigate to login page\n  await page.goto('\u002Flogin');\n\n  \u002F\u002F Fill login form\n  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL || 'test@example.com');\n  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD || 'testpassword');\n  await page.getByRole('button', { name: 'Sign in' }).click();\n\n  \u002F\u002F Wait for auth to complete — adjust selector to your app\n  await page.waitForURL('\u002Fdashboard');\n  await expect(page.getByRole('navigation')).toBeVisible();\n\n  \u002F\u002F Save signed-in state\n  await page.context().storageState({ path: authFile });\n});\n```\n\n### Step 3: Custom fixtures\n\n```typescript\n\u002F\u002F e2e\u002Ffixtures.ts\nimport { test as base, expect } from '@playwright\u002Ftest';\nimport { LoginPage } from '.\u002Fpages\u002Flogin-page';\nimport { DashboardPage } from '.\u002Fpages\u002Fdashboard-page';\n\n\u002F\u002F API client for test data seeding\nclass ApiClient {\n  constructor(private baseURL: string, private token?: string) {}\n\n  async createResource(data: Record\u003Cstring, unknown>) {\n    const response = await fetch(`${this.baseURL}\u002Fapi\u002Fresources`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application\u002Fjson',\n        ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),\n      },\n      body: JSON.stringify(data),\n    });\n    if (!response.ok) throw new Error(`Seed failed: ${response.status}`);\n    return response.json();\n  }\n\n  async deleteResource(id: string) {\n    await fetch(`${this.baseURL}\u002Fapi\u002Fresources\u002F${id}`, {\n      method: 'DELETE',\n      headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},\n    });\n  }\n}\n\ntype Fixtures = {\n  loginPage: LoginPage;\n  dashboardPage: DashboardPage;\n  api: ApiClient;\n};\n\nexport const test = base.extend\u003CFixtures>({\n  loginPage: async ({ page }, use) => {\n    await use(new LoginPage(page));\n  },\n\n  dashboardPage: async ({ page }, use) => {\n    await use(new DashboardPage(page));\n  },\n\n  api: async ({ baseURL }, use) => {\n    const client = new ApiClient(baseURL!);\n    await use(client);\n  },\n});\n\nexport { expect };\n```\n\n### Step 4: Page Object Model\n\n```typescript\n\u002F\u002F e2e\u002Fpages\u002Flogin-page.ts\nimport { type Page, type Locator, expect } from '@playwright\u002Ftest';\n\nexport class LoginPage {\n  readonly emailInput: Locator;\n  readonly passwordInput: Locator;\n  readonly submitButton: Locator;\n  readonly errorMessage: Locator;\n\n  constructor(private page: Page) {\n    this.emailInput = page.getByLabel('Email');\n    this.passwordInput = page.getByLabel('Password');\n    this.submitButton = page.getByRole('button', { name: 'Sign in' });\n    this.errorMessage = page.getByRole('alert');\n  }\n\n  async goto() {\n    await this.page.goto('\u002Flogin');\n  }\n\n  async login(email: string, password: string) {\n    await this.emailInput.fill(email);\n    await this.passwordInput.fill(password);\n    await this.submitButton.click();\n  }\n\n  async expectError(message: string) {\n    await expect(this.errorMessage).toContainText(message);\n  }\n}\n\n\u002F\u002F e2e\u002Fpages\u002Fdashboard-page.ts\nimport { type Page, type Locator, expect } from '@playwright\u002Ftest';\n\nexport class DashboardPage {\n  readonly heading: Locator;\n  readonly createButton: Locator;\n  readonly searchInput: Locator;\n  readonly resourceList: Locator;\n\n  constructor(private page: Page) {\n    this.heading = page.getByRole('heading', { level: 1 });\n    this.createButton = page.getByRole('button', { name: 'Create' });\n    this.searchInput = page.getByPlaceholder('Search');\n    this.resourceList = page.getByTestId('resource-list');\n  }\n\n  async goto() {\n    await this.page.goto('\u002Fdashboard');\n  }\n\n  async createResource(name: string) {\n    await this.createButton.click();\n    await this.page.getByLabel('Name').fill(name);\n    await this.page.getByRole('button', { name: 'Save' }).click();\n  }\n\n  async search(query: string) {\n    await this.searchInput.fill(query);\n    \u002F\u002F Wait for debounced search to trigger\n    await this.page.waitForResponse(resp =>\n      resp.url().includes('\u002Fapi\u002Fresources') && resp.status() === 200\n    );\n  }\n\n  async expectResourceVisible(name: string) {\n    await expect(this.resourceList.getByText(name)).toBeVisible();\n  }\n\n  async expectResourceCount(count: number) {\n    await expect(this.resourceList.getByRole('listitem')).toHaveCount(count);\n  }\n}\n```\n\n### Step 5: Test suites\n\n```typescript\n\u002F\u002F e2e\u002Fauth.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Authentication', () => {\n  \u002F\u002F These tests run WITHOUT storageState (unauthenticated)\n  test.use({ storageState: { cookies: [], origins: [] } });\n\n  test('successful login redirects to dashboard', async ({ loginPage, page }) => {\n    await loginPage.goto();\n    await loginPage.login('test@example.com', 'testpassword');\n    await expect(page).toHaveURL('\u002Fdashboard');\n  });\n\n  test('invalid credentials shows error', async ({ loginPage }) => {\n    await loginPage.goto();\n    await loginPage.login('test@example.com', 'wrongpassword');\n    await loginPage.expectError('Invalid credentials');\n  });\n\n  test('logout clears session', async ({ page }) => {\n    \u002F\u002F Login first\n    await page.goto('\u002Flogin');\n    \u002F\u002F ... login steps ...\n\n    \u002F\u002F Logout\n    await page.getByRole('button', { name: 'Logout' }).click();\n    await expect(page).toHaveURL('\u002Flogin');\n\n    \u002F\u002F Verify can't access protected route\n    await page.goto('\u002Fdashboard');\n    await expect(page).toHaveURL('\u002Flogin');\n  });\n});\n\n\u002F\u002F e2e\u002Fdashboard.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Dashboard', () => {\n  test('displays resource list', async ({ dashboardPage }) => {\n    await dashboardPage.goto();\n    await expect(dashboardPage.heading).toHaveText('Dashboard');\n    await expect(dashboardPage.resourceList).toBeVisible();\n  });\n\n  test('create new resource', async ({ dashboardPage, page }) => {\n    await dashboardPage.goto();\n    await dashboardPage.createResource('New E2E Resource');\n\n    \u002F\u002F Verify resource appears in list\n    await dashboardPage.expectResourceVisible('New E2E Resource');\n  });\n\n  test('search filters results', async ({ dashboardPage, api }) => {\n    \u002F\u002F Seed test data via API\n    await api.createResource({ name: 'Alpha Item' });\n    await api.createResource({ name: 'Beta Item' });\n\n    await dashboardPage.goto();\n    await dashboardPage.search('Alpha');\n    await dashboardPage.expectResourceVisible('Alpha Item');\n  });\n\n  test('empty state shown when no resources', async ({ dashboardPage, page }) => {\n    await dashboardPage.goto();\n    await dashboardPage.search('nonexistent-query-xyz');\n    await expect(page.getByText('No results found')).toBeVisible();\n  });\n});\n\n\u002F\u002F e2e\u002Fcrud.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Resource CRUD', () => {\n  let resourceId: string;\n\n  test.beforeEach(async ({ api }) => {\n    \u002F\u002F Seed a resource for tests that need one\n    const resource = await api.createResource({ name: 'Test Resource' });\n    resourceId = resource.id;\n  });\n\n  test.afterEach(async ({ api }) => {\n    \u002F\u002F Clean up seeded data\n    if (resourceId) {\n      await api.deleteResource(resourceId).catch(() => {});\n    }\n  });\n\n  test('edit resource name', async ({ page }) => {\n    await page.goto(`\u002Fresources\u002F${resourceId}`);\n    await page.getByRole('button', { name: 'Edit' }).click();\n    await page.getByLabel('Name').clear();\n    await page.getByLabel('Name').fill('Updated Resource');\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    await expect(page.getByRole('heading')).toHaveText('Updated Resource');\n  });\n\n  test('delete resource with confirmation', async ({ page }) => {\n    await page.goto(`\u002Fresources\u002F${resourceId}`);\n    await page.getByRole('button', { name: 'Delete' }).click();\n\n    \u002F\u002F Confirm deletion dialog\n    await expect(page.getByRole('dialog')).toBeVisible();\n    await page.getByRole('button', { name: 'Confirm' }).click();\n\n    \u002F\u002F Should redirect to list\n    await expect(page).toHaveURL('\u002Fdashboard');\n  });\n});\n```\n\n### Step 6: Visual regression (if selected)\n\n```typescript\n\u002F\u002F e2e\u002Fvisual.spec.ts\nimport { test, expect } from '.\u002Ffixtures';\n\ntest.describe('Visual regression', () => {\n  test('dashboard matches snapshot', async ({ dashboardPage, page }) => {\n    await dashboardPage.goto();\n    \u002F\u002F Wait for dynamic content to stabilize\n    await page.waitForLoadState('networkidle');\n    await expect(page).toHaveScreenshot('dashboard.png', {\n      maxDiffPixelRatio: 0.01,\n    });\n  });\n\n  test('login page matches snapshot', async ({ loginPage, page }) => {\n    test.use({ storageState: { cookies: [], origins: [] } });\n    await loginPage.goto();\n    await expect(page).toHaveScreenshot('login.png', {\n      maxDiffPixelRatio: 0.01,\n    });\n  });\n\n  \u002F\u002F Component-level screenshots\n  test('navigation component matches snapshot', async ({ page }) => {\n    await page.goto('\u002Fdashboard');\n    const nav = page.getByRole('navigation');\n    await expect(nav).toHaveScreenshot('navigation.png');\n  });\n});\n```\n\n### Step 7: GitHub Actions CI\n\n```yaml\n# .github\u002Fworkflows\u002Fe2e.yml\nname: E2E Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  e2e:\n    timeout-minutes: 30\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: [1\u002F4, 2\u002F4, 3\u002F4, 4\u002F4]\n\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n\n      - run: npm ci\n\n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium\n\n      - name: Run E2E tests\n        run: npx playwright test --shard=${{ matrix.shard }}\n        env:\n          BASE_URL: http:\u002F\u002Flocalhost:3000\n          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}\n          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}\n\n      - name: Upload test report\n        uses: actions\u002Fupload-artifact@v4\n        if: ${{ !cancelled() }}\n        with:\n          name: playwright-report-${{ strategy.job-index }}\n          path: playwright-report\u002F\n          retention-days: 14\n\n      - name: Upload test results\n        uses: actions\u002Fupload-artifact@v4\n        if: ${{ !cancelled() }}\n        with:\n          name: test-results-${{ strategy.job-index }}\n          path: test-results\u002F\n          retention-days: 7\n```\n\n## Directory structure reference\n\n```\ne2e\u002F\n├── .auth\u002F\n│   └── user.json            # Saved auth state (gitignored)\n├── fixtures.ts              # Custom test fixtures and API client\n├── pages\u002F\n│   ├── login-page.ts        # Login page object\n│   ├── dashboard-page.ts    # Dashboard page object\n│   └── resource-page.ts     # Resource detail page object\n├── auth.setup.ts            # Global auth setup (runs once)\n├── auth.spec.ts             # Authentication tests\n├── dashboard.spec.ts        # Dashboard tests\n├── crud.spec.ts             # CRUD operation tests\n└── visual.spec.ts           # Visual regression tests (optional)\nplaywright.config.ts         # Playwright configuration\n```\n\n## Best practices\n\n### Use role-based locators first\nPrefer `getByRole()`, `getByLabel()`, `getByText()` over CSS selectors or test IDs. These locators mirror how users interact with the page and catch accessibility issues:\n\n```typescript\n\u002F\u002F Preferred — accessible and resilient\nawait page.getByRole('button', { name: 'Submit' }).click();\nawait page.getByLabel('Email').fill('user@test.com');\n\n\u002F\u002F Fallback — when role-based doesn't work\nawait page.getByTestId('custom-widget').click();\n\n\u002F\u002F Avoid — fragile, breaks on refactors\nawait page.locator('.btn-primary').click();\nawait page.locator('#email-input').fill('user@test.com');\n```\n\n### Wait for network, not timers\nNever use `page.waitForTimeout()`. Wait for specific conditions:\n\n```typescript\n\u002F\u002F Wait for API response\nawait page.waitForResponse(resp => resp.url().includes('\u002Fapi\u002Fdata'));\n\n\u002F\u002F Wait for element state\nawait expect(page.getByText('Saved')).toBeVisible();\n\n\u002F\u002F Wait for navigation\nawait expect(page).toHaveURL('\u002Fdashboard');\n\n\u002F\u002F Wait for loading to finish\nawait expect(page.getByTestId('spinner')).toBeHidden();\n```\n\n### Isolate test data\nEach test should create its own data and clean up after:\n\n```typescript\ntest('edit resource', async ({ api, page }) => {\n  \u002F\u002F Arrange — seed via API\n  const resource = await api.createResource({ name: 'Test' });\n\n  \u002F\u002F Act\n  await page.goto(`\u002Fresources\u002F${resource.id}`);\n  \u002F\u002F ... test logic ...\n\n  \u002F\u002F Cleanup (also runs on failure via afterEach)\n});\n```\n\n### Tag tests for selective runs\n\n```typescript\ntest('checkout flow @slow @checkout', async ({ page }) => {\n  \u002F\u002F Long test tagged for selective execution\n});\n\n\u002F\u002F Run only: npx playwright test --grep @checkout\n\u002F\u002F Skip slow: npx playwright test --grep-invert @slow\n```\n\n### .gitignore additions\n\n```\n# Playwright\ne2e\u002F.auth\u002F\ntest-results\u002F\nplaywright-report\u002F\nblob-report\u002F\n```\n\n## Checklist before finishing\n\n- [ ] `playwright.config.ts` has webServer configured to start the dev server\n- [ ] Auth setup saves storageState and all test projects depend on it\n- [ ] Page objects use role-based locators (`getByRole`, `getByLabel`, `getByText`)\n- [ ] No `waitForTimeout()` calls — only wait for elements, URLs, or responses\n- [ ] Tests create and clean up their own data (no shared mutable state)\n- [ ] CI config has sharding for parallel execution\n- [ ] Trace, screenshot, and video are captured on failure for debugging\n- [ ] `.auth\u002F` directory is in `.gitignore`\n- [ ] `npx playwright test` passes locally before pushing\n\n",{"name":5,"tags":49,"description":7},[25,27,37,50,42,39,30],"automation","main","015422534a7457348a313b6381f0255d3364eb8c","cli-tool\u002Fcomponents\u002Fskills\u002Fdevelopment\u002Fplaywright-e2e-builder",0,"https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates","https:\u002F\u002Fgithub.com\u002FMishit18","2026-11-12 18:26:48",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add Mishit18\u002Fclaude-code-templates --skill playwright-e2e-builder",{"tool":64,"command":65},"bunx","bunx skills add Mishit18\u002Fclaude-code-templates --skill playwright-e2e-builder",{"tool":67,"command":68},"pnpm","pnpm dlx skills add Mishit18\u002Fclaude-code-templates --skill playwright-e2e-builder","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fdevelopment\u002Fplaywright-e2e-builder\u002FSKILL.md",true,[],[73,104,126,153,180,205],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":77,"licenseFileUrl":78,"sourceHtmlUrl":79,"categories":80,"keywords":84,"publishedAt":44},"01M6R4ARN4W2VZRDERDTMN1KP0","web-artifacts-builder","Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn\u002Fui). Use for complex artifacts requiring state management, routing, or shadcn\u002Fui components - not for simple single-file HTML\u002FJSX artifacts.","Apache-2.0","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Futilities\u002Fweb-artifacts-builder\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Futilities\u002Fweb-artifacts-builder\u002FSKILL.md",[81],{"categoryNo":82,"categoryName":83,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",[85,86,87,88,28,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103],"web artifacts builder","web","artifacts","artifacts builder","frontend","css","components","react","routing","state management","state","file","Tailwind","use","single-file","ui components","multi","for","single",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":77,"licenseFileUrl":108,"sourceHtmlUrl":109,"categories":110,"keywords":113,"publishedAt":44},"01M6R45GXREM5DP8T08MAC788H","webapp-testing","Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fsecurity\u002Fwebapp-testing\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fsecurity\u002Fwebapp-testing\u002FSKILL.md",[111,112],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":82,"categoryName":83,"taxonomyType":21,"taxonomyTypeDesc":22},[37,114,115,89,116,25,86,117,118,119,120,121,122,123,124,125,102],"webapp testing","webapp","screenshots","debugging","toolkit","browser","ui","logs","applications","behavior","local","with",{"skillNo":127,"name":128,"nameZh":6,"description":129,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":130,"sourceHtmlUrl":131,"categories":132,"keywords":133,"publishedAt":44},"01M6R2GYJ470X8EB7R58V0B68K","think-tank","Run a Virtual Think Tank — a structured multi-persona debate — before planning or making architectural\u002Fdesign\u002Fstrategic decisions. Use this skill whenever the user is about to plan a system, make a technology choice, evaluate trade-offs, decide on an approach, or faces any decision where multiple perspectives would sharpen the outcome. Also trigger when the user says \"think tank\", \"debate this\", \"perspectives on\", \"trade-offs\", \"should I use X or Y\", \"help me decide\", \"before we plan\", or asks for pros\u002Fcons of competing approaches. This skill should run BEFORE any implementation planning begins — it produces a structured analysis that feeds into better plans.","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fthink-tank\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fthink-tank\u002FSKILL.md",[],[134,135,136,137,138,139,140,38,141,142,143,144,145,146,147,148,149,150,151,152],"think tank","think","design","run","feeds","analysis","persona","sharpen","virtual","decision","choice","decide","perspectives","outcome","me","debate","make","trade-offs","when",{"skillNo":154,"name":155,"nameZh":6,"description":156,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":77,"licenseFileUrl":157,"sourceHtmlUrl":158,"categories":159,"keywords":163,"publishedAt":44},"01M6R2FSR55AVRFA9YZ8SC2GZG","skill-creator","Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fskill-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fskill-creator\u002FSKILL.md",[160],{"categoryNo":161,"categoryName":162,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[164,165,166,167,168,169,170,171,172,173,174,175,176,177,125,102,178,152,179],"skill creator","creator","workflows","create","update","capabilities","knowledge","tool","integrations","specialized","guide","new skill","existing skill","users","Existing","new",{"skillNo":181,"name":182,"nameZh":6,"description":183,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":184,"categories":185,"keywords":186,"publishedAt":44},"01M6R2FK42ZZ4HJTJVVK9WJBBP","ship-learn-next","Transform learning content (like YouTube transcripts, articles, tutorials) into actionable implementation plans using the Ship-Learn-Next framework. Use when user wants to turn advice, lessons, or educational content into concrete action steps, reps, or a learning quest.","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fship-learn-next\u002FSKILL.md",[],[187,188,189,190,191,192,193,194,98,195,196,197,198,199,200,201,202,203,204,152],"learn","ship learn next","ship","next","framework","learning","read","write","advice","implementation","youtube","plans","quest","transform","actionable","action","lessons","youtube transcripts",{"skillNo":206,"name":207,"nameZh":6,"description":208,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":77,"licenseFileUrl":209,"sourceHtmlUrl":210,"categories":211,"keywords":218,"publishedAt":44},"01M6R2088G1RHDA5MDX16HASRB","slack-gif-creator","Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like \"make me a GIF of X doing Y for Slack.\"","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fenterprise-communication\u002Fslack-gif-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FMishit18\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fenterprise-communication\u002Fslack-gif-creator\u002FSKILL.md",[212,215],{"categoryNo":213,"categoryName":214,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000005","邮件与沟通",{"categoryNo":216,"categoryName":217,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[219,220,221,165,222,223,224,225,170,98,148,177,150,226,227,102,228,152],"slack gif creator","slack","gif","validation","Tools","constraints","Animation","Utilities","concepts","request"]