Aller au contenu principal

Skill: qa-e2e

Inline

End-to-end tests with Playwright or Cypress. Trigger when the user wants to create user journey tests, UI integration tests, or browser automation.

Configuration​

PropertyValue
Contextinline
Pre-approved toolsNone declared: the project's permission settings apply
Triggerkeywords: e2e, end-to-end, end-to-end test, playwright, cypress

Detailed description​

E2E Testing Skill

Triggers​

This skill activates when the user mentions:

  • "E2E", "end-to-end", "end-to-end test"
  • "Playwright", "Cypress", "Puppeteer"
  • "integration test", "user journey"
  • "browser automation", "UI test"
FrameworkAdvantagesUse case
PlaywrightMulti-browser, fast, auto-waitModern apps
CypressExcellent DX, easy debuggingPrototyping

Default recommendation: Playwright

Project structure​

e2e/
├── fixtures/ # Custom fixtures
├── pages/ # Page Objects
│ ├── login.page.ts
│ └── dashboard.page.ts
├── tests/
│ ├── auth/
│ │ └── login.spec.ts
│ └── checkout/
│ └── purchase.spec.ts
├── utils/ # Helpers
└── playwright.config.ts

Page Object Model​

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

export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;

constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Login' });
}

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();
}
}

Tests​

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

test.describe('Authentication', () => {
let loginPage: LoginPage;

test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});

test('should login with valid credentials', async ({ page }) => {
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});

test('should show error with invalid credentials', async ({ page }) => {
await loginPage.login('user@example.com', 'wrong');
await expect(page.getByRole('alert')).toContainText('Invalid');
});
});

Critical journeys​

JourneyTest points
SignupForm validation, email, success
LoginValid/invalid, remember me, forgot
NavigationMenu, breadcrumbs, deep links
SearchQuery, filters, pagination
CheckoutCart, payment, confirmation
PrioritySelectorExample
1RolegetByRole('button', { name: 'Submit' })
2LabelgetByLabel('Email')
3TextgetByText('Welcome')
4Test IDgetByTestId('submit-btn')
5CSS.btn-primary (avoid)

Useful commands​

# Run the tests
npx playwright test

# Interactive UI mode
npx playwright test --ui

# Headed mode (see the browser)
npx playwright test --headed

# Debug
npx playwright test --debug

# Generate code
npx playwright codegen http://localhost:3000

# Report
npx playwright show-report

Custom fixtures​

Playwright fixtures centralize the setup and inject the Page Objects into the tests:

// e2e/fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';

type Fixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};

export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});

export { expect } from '@playwright/test';
// e2e/tests/auth/login.spec.ts (with fixtures)
import { test, expect } from '../../fixtures';

test('should login with valid credentials', async ({ loginPage, page }) => {
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});

Reusable authentication fixture​

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

export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('/dashboard');
await use(page);
},
});

Playwright best practices​

PracticeDescription
Fresh contextEach test starts in an isolated browser context (no shared state)
Auto-waitingDon't add waitForTimeout - Playwright waits automatically
Web-first assertionsUse expect(locator) which retries automatically, not expect(await locator.textContent())
ParallelismfullyParallel: true in the config for parallel execution
Tracestrace: 'on-first-retry' to debug flaky tests

Anti-patterns​

Anti-patternAlternative
page.waitForTimeout(3000)await expect(locator).toBeVisible()
page.$('.my-class')page.getByRole('button', { name: '...' })
XPath selectorsrole/label/text selectors
Tests dependent on each otherEach test is independent
page.evaluate() for assertionsWeb-first assertions with expect
Page Objects with business logicPage Objects = actions + locators only

Rules​

IMPORTANT: E2E tests are slow - reserve them for critical journeys (10% of the pyramid).

IMPORTANT: Always use accessible selectors (role, label).

IMPORTANT: Use Playwright fixtures to inject the Page Objects - no new Page() in each test.

YOU MUST implement the Page Object Model for maintainability.

NEVER test implementation details - test user behavior.

NEVER use fragile CSS selectors (classes, dynamic IDs).

NEVER use waitForTimeout - use web-first assertions which retry automatically.

See also​

The Microsoft Playwright team publishes their own SKILL.md at microsoft/playwright-cli/skills/playwright-cli (pin a release, v0.1.22 or later: since that release its allowed-tools no longer grants bare npm/npx). Authoritative on Playwright API patterns, kept in sync with each Playwright release.

When working on a Playwright project, install the vendor skill alongside this one. This skill captures the opinionated workflow patterns the foundation imposes (TDD, anti-fragility rules, don't-do lists); the vendor skill captures the canonical API patterns that evolve with each Playwright release. Both together is the recommended setup for Playwright users.

Vendor-neutrality disclosure: Microsoft owns Playwright. Per the foundation's vendor curation policy, Microsoft tools that predate the company's deepening OpenAI commercial relationship (e.g. VSCode, GitHub, Playwright created in 2020) are evaluated case-by-case rather than auto-rejected. Playwright remains Apache-2.0-licensed and the de-facto standard for E2E testing. We point to the vendor skill because no equivalent vendor-neutral source maintains the API canonically and the community alternative lackeyjb/playwright-skill was 5 months stale at audit time. Re-evaluate this pointer if Microsoft's commercial alignment with OpenAI changes the project's roadmap visibly (e.g. direct OpenAI product integration).

For Cypress users, Cypress publishes its own skills in cypress-io/ai-toolkit (MIT, release v1.4.0): cypress-author pre-approves only Read and Edit. Install the skill folders, not the whole plugin, which also registers a remote MCP server, and only in a Cypress project: its description asks to apply it "even when the user does not say Cypress". Same split as for Playwright: the vendor owns the API, this skill the suite's shape (page objects, ~10% E2E, anti-patterns).

Install command and full list of validated vendor skills: docs/recipes/recommended-vendor-skills.md. Audit pilot trace: specs/marketplace-audit/qa-skills-pilot-2026-05-06.md.

Automatic triggering​

This skill is automatically activated when:

  • The matching keywords are detected in the conversation
  • The task context matches the skill's domain

Triggering examples​

  • "I want to e2e..."
  • "I want to end-to-end..."
  • "I want to end-to-end test..."

Context inline​

Inline means the skill runs in the conversation (the default, no context key):

  • Sees the full history
  • Its work is visible as it happens
  • Right for almost every skill

Practical examples​

1. E2E Example: Login flow test​

E2E Example: Login flow test

User request​

"Create an E2E test for the login flow with Playwright"


Flow analysis​

User steps​

  1. Access the login page
  2. Fill in email and password
  3. Click "Sign in"
  4. Verify the redirect to the dashboard
  5. Verify that the user is logged in

Cases to test​

  • Successful login
  • Invalid email
  • Wrong password
  • Empty fields

Playwright implementation​

// tests/e2e/login.spec.ts

import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});

test('should login successfully with valid credentials', async ({ page }) => {
// Arrange
const validEmail = 'user@example.com';
const validPassword = 'SecurePass123';

// Act
await page.fill('[data-testid="email-input"]', validEmail);
await page.fill('[data-testid="password-input"]', validPassword);
await page.click('[data-testid="login-button"]');

// Assert
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});

test('should show error for invalid email format', async ({ page }) => {
// Arrange
const invalidEmail = 'not-an-email';

// Act
await page.fill('[data-testid="email-input"]', invalidEmail);
await page.fill('[data-testid="password-input"]', 'anypassword');
await page.click('[data-testid="login-button"]');

// Assert
await expect(page.locator('[data-testid="email-error"]')).toHaveText(
'Email invalide'
);
await expect(page).toHaveURL('/login');
});

test('should show error for wrong password', async ({ page }) => {
// Arrange
const validEmail = 'user@example.com';
const wrongPassword = 'WrongPassword';

// Act
await page.fill('[data-testid="email-input"]', validEmail);
await page.fill('[data-testid="password-input"]', wrongPassword);
await page.click('[data-testid="login-button"]');

// Assert
await expect(page.locator('[data-testid="auth-error"]')).toHaveText(
'Email ou mot de passe incorrect'
);
});

test('should disable button when fields are empty', async ({ page }) => {
// Assert
await expect(page.locator('[data-testid="login-button"]')).toBeDisabled();

// Act - Fill only email
await page.fill('[data-testid="email-input"]', 'user@example.com');

// Assert - Still disabled
await expect(page.locator('[data-testid="login-button"]')).toBeDisabled();

// Act - Fill password too
await page.fill('[data-testid="password-input"]', 'password');

// Assert - Now enabled
await expect(page.locator('[data-testid="login-button"]')).toBeEnabled();
});
});

Playwright configuration​

// playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2: 0,
workers: process.env.CI ? 1: undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'mobile',
use: { ...devices['iPhone 13'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

Page Object Pattern (optional)​

// tests/e2e/pages/LoginPage.ts

import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly emailError: Locator;
readonly authError: Locator;

constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('[data-testid="email-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.emailError = page.locator('[data-testid="email-error"]');
this.authError = page.locator('[data-testid="auth-error"]');
}

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.loginButton.click();
}

async expectError(message: string) {
await expect(this.authError).toHaveText(message);
}
}

Execution​

# Run all E2E tests
npx playwright test

# Interactive UI mode
npx playwright test --ui

# Generate the report
npx playwright show-report

# Specific tests
npx playwright test login.spec.ts

# Debug mode
npx playwright test --debug

Best practices​

  1. data-testid: Use test attributes rather than CSS selectors
  2. Page Objects: Encapsulate page logic for reusability
  3. Explicit assertions: Always verify the expected state
  4. Isolation: Each test must be independent
  5. CI/CD: Configure retries and screenshots in CI

See also​