Back to catalog

playwright-bdd-testing

Writing BDD-style Playwright tests aligned with feature specifications

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • GitHub stars: 1
  • Updated: 2025-11-21

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: playwright-bdd-testing
description: Writing BDD-style Playwright tests aligned with feature specifications
---

# Playwright BDD Testing

This guide covers writing Behavior-Driven Development (BDD) style tests with Playwright for the Clientt CRM application.

## BDD Philosophy

BDD tests follow the Given-When-Then pattern to make tests readable and aligned with business requirements:

- **Given**: Setup and preconditions
- **When**: The action being performed
- **Then**: Expected outcomes and assertions

## Test Structure

### Feature-Based Organization

Tests in `playwright_tests/tests/features/` are organized by application features:

```
tests/features/
├── authentication.spec.js      # User login, registration, logout
├── navigation.spec.js          # Page navigation and routing
├── contacts.spec.js            # Contact management
├── deals.spec.js               # Deal pipeline
└── reports.spec.js             # Reporting features
```

Each feature file contains related scenarios using `test.describe` blocks.

### BDD Test Template

```javascript
// @ts-check
const { test, expect } = require('@playwright/test');

/**
 * Feature: [Feature Name]
 *
 * [Brief description of what this feature does]
 */

test.describe('Feature: [Feature Name]', () => {

  test.describe('Scenario: [Scenario Name]', () => {

    test('should [expected behavior]', async ({ page }) => {
      // Given: [Initial state/preconditions]
      await page.goto('/initial-page');

      // When: [Action performed]
      await page.click('button');

      // Then: [Expected outcome]
      await expect(page.locator('.result')).toBeVisible();
    });
  });
});
```

## Mapping BDD Specs to Tests

### Example Specification

```
Feature: Contact Management
  As a CRM user
  I want to create and manage contacts
  So that I can track customer information

  Scenario: Creating a new contact
    Given I am logged in as a CRM user
    When I navigate to the contacts page
    And I click the "Add Contact" button
    And I fill in the contact form with valid information
    And I submit the form
    Then I should see the new contact in the contacts list
    And I should see a success notification
```

### Corresponding Playwright Test

```javascript
/**
 * Feature: Contact Management
 *
 * CRM users can create and manage contacts to track customer information.
 */

test.describe('Feature: Contact Management', () => {

  test.describe('Scenario: Creating a new contact', () => {

    test('should create a new contact with valid information', async ({ page }) => {
      // Given: I am logged in as a CRM user
      await loginAsTestUser(page);

      // When: I navigate to the contacts page
      await page.goto('/contacts');

      // And: I click the "Add Contact" button
      await page.click('[data-test="add-contact-button"]');

      // And: I fill in the contact form with valid information
      await page.fill('input[name="name"]', 'John Doe');
      await page.fill('input[name="email"]', 'john@example.com');
      await page.fill('input[name="phone"]', '555-1234');

      // And: I submit the form
      await page.click('button[type="submit"]');
      await page.waitForLoadState('networkidle');

      // Then: I should see the new contact in the contacts list
      await expect(page.locator('text=John Doe')).toBeVisible();

      // And: I should see a success notification
      await expect(page.locator('.notification.success')).toBeVisible();
    });
  });
});
```

## BDD Best Practices

### 1. Use Descriptive Names

Test names should clearly describe the scenario:

```javascript
// Good
test('should display validation error when email format is invalid', ...)

// Bad
test('email validation', ...)
```

### 2. Group Related Scenarios

Use nested `test.describe` blocks:

```javascript
test.describe('Feature: User Authentication', () => {

  test.describe('Scenario: User Registration', () => {
    test('should allow registration with valid credentials', ...)
    test('should show error for duplicate email', ...)
  });

  test.describe('Scenario: User Login', () => {
    test('should login with valid credentials', ...)
    test('should show error for invalid credentials', ...)
  });
});
```

### 3. Keep Steps Clear

Each Given-When-Then step should be obvious:

```javascript
test('scenario', async ({ page }) => {
  // Given: Clear setup
  const email = generateTestEmail();
  await page.goto('/register');

  // When: Single clear action
  await fillRegistrationForm(page, { email, password: 'Pass123!' });
  await page.click('button[type="submit"]');

  // Then: Clear assertions
  await expect(page).toHaveURL(/.*dashboard/);
});
```

### 4. Use Helper Functions

Extract common operations:

```javascript
const { loginAsTestUser } = require('../support/auth-helpers');
const { fillForm, waitForNetworkIdle } = require('../support/test-helpers');

test('authenticated scenario', async ({ page }) => {
  // Given: I am logged in
  await loginAsTestUser(page);

  // When: I perform action
  await page.goto('/contacts');

  // Then: I see result
  await expect(page.locator('h1')).toContainText('Contacts');
});
```

## Common BDD Scenarios

### Authentication Scenarios

```javascript
test.describe('Feature: User Authentication', () => {

  test.describe('Scenario: Successful Login', () => {
    test('should login with valid credentials', async ({ page }) => {
      // Given: I have valid credentials
      const email = process.env.TEST_USER_EMAIL;
      const password = process.env.TEST_USER_PASSWORD;

      // When: I submit the login form
      await page.goto('/sign-in');
      await page.fill('input[name="user[email]"]', email);
      await page.fill('input[name="user[password]"]', password);
      await page.click('button[type="submit"]');

      // Then: I should be logged in
      await expect(page).not.toHaveURL(/.*sign-in/);
    });
  });

  test.describe('Scenario: Failed Login', () => {
    test('should show error for invalid credentials', async ({ page }) => {
      // Given: I have invalid credentials
      // When: I submit the login form with invalid credentials
      await page.goto('/sign-in');
      await page.fill('input[name="user[email]"]', 'invalid@example.com');
      await page.fill('input[name="user[password]"]', 'WrongPassword');
      await page.click('button[type="submit"]');

      // Then: I should see an error message
      await expect(page.locator('text=/invalid|error/i')).toBeVisible();
    });
  });
});
```

### CRUD Scenarios

```javascript
test.describe('Feature: Contact Management', () => {

  test.beforeEach(async ({ page }) => {
    // Given: I am logged in
    await loginAsTestUser(page);
  });

  test.describe('Scenario: Create Contact', () => {
    test('should create contact with valid data', async ({ page }) => {
      // When: I create a new contact
      await page.goto('/contacts');
      await page.click('[data-test="add-contact"]');
      await page.fill('input[name="name"]', 'Jane Smith');
      await page.fill('input[name="email"]', 'jane@example.com');
      await page.click('button[type="submit"]');

      // Then: The contact should appear in the list
      await expect(page.locator('text=Jane Smith')).toBeVisible();
    });
  });

  test.describe('Scenario: Update Contact', () => {
    test('should update contact information', async ({ page }) => {
      // Given: A contact exists
      await page.goto('/contacts');
      await page.click('text=Jane Smith');

      // When: I update the contact
      await page.click('[data-test="edit-contact"]');
      await page.fill('input[name="phone"]', '555-9999');
      await page.click('button[type="submit"]');

      // Then: The changes should be saved
      await expect(page.locator('text=555-9999')).toBeVisible();
    });
  });

  test.describe('Scenario: Delete Contact', () => {
    test('should delete contact with confirmation', async ({ page }) => {
      // Given: A contact exists
      await page.goto('/contacts');

      // When: I delete the contact
      await page.click('[data-test="contact-jane-smith"]');
      await page.click('[data-test="delete-contact"]');
      await page.click('[data-test="confirm-delete"]');

      // Then: The contact should be removed
      await expect(page.locator('text=Jane Smith')).not.toBeVisible();
    });
  });
});
```

### Navigation Scenarios

```javascript
test.describe('Feature: Application Navigation', () => {

  test.describe('Scenario: Protected Routes', () => {
    test('should redirect to login when accessing protected route', async ({ page }) => {
      // Given: I am not logged in
      // When: I try to access a protected route
      await page.goto('/dashboard');

      // Then: I should be redirected to the sign-in page
      await expect(page).toHaveURL(/.*sign-in/);
    });
  });
});
```

## Data-Driven Testing

For scenarios with multiple data sets:

```javascript
const testCases = [
  { email: 'invalid', expected: 'Invalid email format' },
  { email: '', expected: 'Email is required' },
  { email: 'a@b', expected: 'Invalid email format' },
];

testCases.forEach(({ email, expected }) => {
  test(`should show "${expected}" for email: "${email}"`, async ({ page }) => {
    // Given: I am on the registration page
    await page.goto('/register');

    // When: I enter an invalid email
    await page.fill('input[name="user[email]"]', email);
    await page.click('button[type="submit"]');

    // Then: I should see the validation error
    await expect(page.locator(`text=${expected}`)).toBeVisible();
  });
});
```

## Using Custom Fixtures

For scenarios requiring authentication:

```javascript
// Use authenticated fixture
const { test } = require('../fixtures/authenticated');
const { expect } = require('@playwright/test');

test.describe('Feature: Dashboard (Authenticated)', () => {

  test('should display user dashboard', async ({ page }) => {
    // Given: I am logged in (handled by fixture)

    // When: I navigate to the dashboard
    await page.goto('/dashboard');

    // Then: I should see my dashboard
    await expect(page.locator('h1')).toContainText('Dashboard');
  });
});
```

## Tagging Tests

Use test annotations for filtering:

```javascript
test('slow operation', async ({ page }) => {
  test.slow(); // Mark as slow (3x timeout)
  // ... test code
});

test('skip in CI', async ({ page }) => {
  test.skip(!!process.env.CI, 'Skipping in CI');
  // ... test code
});

test.only('debug this test', async ({ page }) => {
  // Only this test will run
});
```

## Reporting

Tests generate reports showing BDD scenarios:

```bash
npm run test:report
```

The HTML report groups tests by Feature > Scenario > Test.

## Tips

1. **One scenario per test**: Keep tests focused
2. **Use meaningful data**: Test data should make sense in context
3. **Avoid test interdependence**: Each test should run independently
4. **Clean up test data**: Remove created data after tests
5. **Document complex scenarios**: Add comments explaining business logic
6. **Align with specs**: Keep tests synchronized with BDD specifications

## Resources

- Project BDD specifications (if available in `/specs` or `/docs`)
- Playwright test examples: `playwright_tests/tests/features/`
- Helper functions: `playwright_tests/tests/support/`