Skip to content

Latest commit

 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

AI-Optimized Playwright E2E Guidelines

Strict Playwright E2E testing guidelines optimized for reliable AI-assisted code review and generation. (Cursor, GitHub Copilot, Claude Dev, and custom agents)

These rules are intentionally strict and concise to provide AI models with deterministic signals and minimize token consumption.

Why these guidelines?

  • Eliminate Flakiness: Enforces custom API-first setups and background sync patterns instead of unstable waiters.
  • Living Documentation: A single source of truth for the team where the guidelines used for review are the same ones used for generation.
  • AI-First Design: Structured to be easily parsed by LLMs, leading to higher-quality code suggestions and fewer refactoring cycles.
  • Signal-to-Noise Focus: Focuses strictly on E2E-specific patterns, leaving generic JS/TS rules to standard linters.

Quick start

  1. Read: Review the core rules below to understand the architectural requirements.
  2. Fork & Adapt: (Optional) Fork this repo to customize the naming conventions or tagging policies for your specific project.
  3. Configure AI: Use this document as a System Prompt. Feed it to your AI coding assistant (e.g., .cursorrules, GitHub Copilot instructions.md, or Claude Projects).
  4. Automate: (Optional) Integrate these patterns into your CI/CD pipeline, custom ESLint rules, or AI-powered review agents like CodeRabbit or PR-agent.

Table of Contents

Playwright E2E Testing Guidelines

✅ Required Patterns

Dependency Injection & Fixtures

  • Use Fixtures: Never instantiate page objects or helpers manually in tests. Inject everything via Playwright fixtures:

    // ✅ Good: Dependencies are injected;
    test('Submit form', async ({ loginPage, dashboardPage }) => {...});
    
    // ❌ Bad: Manual instantiation;
    test('Submit form', async ({ page }) => {
      const loginPage = new LoginPage(page); 
    });

Test Structure

  • Test Organization: Every test must use a top-level test.describe block to group related tests:

    test.describe('Feature: Resource Management', () => {
      test('Create new resource', async ({ ... }) => {
        // Test code
      });
    });
  • User Context: Every test.describe block that needs authentication must set the authenticated user context:

    test.describe('Feature: Resource Management', () => {
      test.use({ authenticatedUser: users.admin });
      // ...
    });
  • Step Documentation: Every UI/API action sequence must be wrapped in test.step(). Maximum 4 actions per step. Label must describe user intent:

    await test.step('Fill and submit the registration form', async () => {
      await registrationPage.fillDetails(userData);
      await registrationPage.submit();
    });
  • Categorization: Every test must have tag e.g. @Smoke, @FeatureX. See our separate Tag Policy guide. Why: This allows for targeted execution in CI/CD.

    test('…', { tag: ['@Smoke', '@FeatureX'] }, async () => {  });
  • Test isolation: Tests must be completely independent and atomic. No test should depend on the state, data, or side effects created by another test. Ensure data-level isolation - create/verify/clean your own data via fixtures or API:

    // ✅ Good: self-contained
    test('Create & verify resource', async ({ apiHelpers, authenticatedUser }) => {
      const id = await apiHelpers.createResource(authenticatedUser, { name });
      // Test code
      await apiHelpers.deleteResource(id); // or rely on global cleanup
    });
    
    // ❌ Bad: cross-test dependency
    test('Edit resource from previous test', async () => {
      await page.getByText('Resource left by previous test').click();
    });

Test Data Management

  • Unique Test Data: Every created entity name/title must contain a unique suffix (${Date.now()} or uuid) identifier. Why: To avoid collisions between parallel or repeated test runs.

    const resourceName = `Test Resource ${Date.now()}`;
  • UI Data Creation: UI creation methods must use @registerForCleanup() decorator:

    @registerForCleanup()
    public async createResource(name: string): Promise<number> {
      // Creation code
    }
  • API Data Creation: API creation must push ID to global.testDataRegistry (global cleanup):

    const resourceId = await apiClient.createResource(data);
    global.testDataRegistry.push(resourceId);

Synchronization & Stability

  • UI Operations: UI actions that trigger requests must use Promise.all([waitForResponse, action]). Why: Properly handle network calls and parallel operations.

    await Promise.all([
      page.waitForResponse(resp => resp.url().includes('/api/save')),
      formPage.clickSaveButton(),
    ]);
  • API Operations: Execute independent API operations in parallel for better performance:

    await Promise.all([
      apiClient.createResource(data1),
      apiClient.createResource(data2),
    ]);
  • Search Index Sync: After data creation (UI or API), always wait for search indexing:

    await searchHelper.waitForIndexing([resourceName]);
  • Background Job Verification: When operations trigger background jobs, always wait for job completion. Why: Required for eventual consistency in async flows.

    await listingPage.performBulkAction();
    await jobHelper.waitForJobCompletion(
      JobType.BULK_OPERATION,
      authenticatedUser
    );

❌ Prohibited Patterns

  • Relative Imports: Do not use relative imports (../). Use path aliases only (@utils/…).

    // ❌ Bad: import { Something } from '../../../utils/helpers';
    // ✅ Good: import { Something } from '@utils/helpers';
  • Wait Anti-patterns: Never use fixed timeouts or unreliable wait helpers.

       .waitForTimeout() // use locators/assertions instead
       .waitForLoadState('networkidle') // too unstable for modern SPAs
       .waitForSelector(selector) without {state: 'visible'} // use expect(locator).toBeVisible() instead
       forceWait() or sleep() // The ultimate "flaky test" culprits
  • Page Object & Locator Discipline: All locators must be stored as private class properties in page objects. Never use locators:

    • directly in test files
    • inline inside page object methods
    // ❌ Bad: direct in test
    await page.locator('[data-testid="save"]').click();
    // ❌ Bad: inline (even good locator type)
    async clickSave() {
      await this.page.getByRole('button', { name: 'Save' }).click();  // duplicated, hard to change centrally
    }
    
    // ✅ Good
    class LoginPage {
      private readonly saveButton = this.page.getByRole('button', { name: 'Save' });
      async clickSave() {
        await this.saveButton.click();
      }
    }

🧪 Test Verification Best Practices

  • API-First Approach: Create all prerequisite data via API, never via UI unless testing UI creation itself.

    // ✅ Good: Create prerequisite data via API, test workflow via UI
    const resourceId = await apiHelpers.createResource(authenticatedUser, testData);
    // Now test UI workflow with the created resource
  • Soft Assertions: Use expect.soft(…) when verifying ≥3 related conditions in one step. Fail the test only after collecting all issues.

    // ✅ Good: Using soft assertions for multiple related UI checks
    await test.step('Verify user profile details', async () => {
      const profile = await profilePage.getUserDetails();
    
    // If the first check fails, Playwright will still run the next two
    expect.soft(profile.name).toBe(userData.name);
    expect.soft(profile.email).toBe(userData.email);
    expect.soft(profile.role).toBe(userData.role);
    
    // The test will fail at the end of this step if any of the above failed
    });
    
    // ❌ Bad: Standard assertions stop execution immediately
    await test.step('Verify user profile details', async () => {
      // If the name is wrong, the test stops here. 
      // You won't know if the email and role were also wrong until you fix the name.
      expect(profile.name).toBe(userData.name); 
      expect(profile.email).toBe(userData.email);
      expect(profile.role).toBe(userData.role);
    });
  • Notification Verification: Never verify business logic outcome via transient UI notifications/toasts. Use API calls or stable DOM elements. Why: transient notification messages may be unreliable.

    // ✅ Good: Verify state changes via API or stable UI elements
    const status = await apiClient.getResourceStatus(resourceId);
    expect(status).toBe('completed');

📝 Naming Conventions

  • Test Names: Start test names with a Ticket ID (if applicable) or a clear feature prefix:

    // ✅ Good: test('TICKET-123 Verify resource creation with special characters', ...);
    // ❌ Bad: test('test creation', ...);

🎨 Code Quality

  • Always Await: Never forget await for async operations - missing await causes race conditions and flaky tests.

    // ❌ Bad: Missing await causes race conditions
    page.click(selector); // Returns Promise<void>, not void
    const result = apiClient.getData(); // Returns Promise<Data>, not Data
    
    // ✅ Good: Properly awaited
    await page.click(selector);
    const result = await apiClient.getData();

🔗 Resources

(Optional) Additional resources: Playwright Best Practices | Playwright Test Documentation | Authentication & Storage State Reuse | Fixtures & Dependency Injection | Browser Contexts & Isolation

Appreciate your feedback → Open an Issue or contact me

MIT License

About

Team Playwright conventions encoded as token-efficient AI instructions applied at code generation and review.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors