BMAD-METHOD/expansion-packs/bmad-playwright-expansion/templates/test-file-generation-tmpl.md

11 KiB

Test File Generation Template

Story Context

Story: {{Story Number}} - {{Story Title}} Test Execution Status: {{Must be PASSED - verified through *test-story command}} Interactive Testing Fixes: {{Capture any fixes discovered during Playwright MCP testing}}

Test File Structure

API Tests Template

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

// Define TypeScript interfaces for API responses
interface {{ResourceName}}Response {
  id: number;
  {{additional_fields_based_on_story}};
}

interface AuthResponse {
  access: string;
  refresh: string;
  user: {
    id: number;
    email: string;
    role: string;
  };
}

interface ErrorResponse {
  error: string;
  details?: string[];
}

test.describe('{{Story Title}} - API Tests', () => {
  let authToken: string;
  let testUserId: number;

  test.beforeAll(async ({ request }) => {
    // Setup: Create test user and get authentication token
    const registerResponse = await request.post('/api/auth/register/', {
      data: {
        email: 'test-{{timestamp}}@example.com',
        password: 'SecurePass123!',
        first_name: 'Test',
        last_name: 'User',
        organization_name: 'Test Org'
      }
    });
    expect(registerResponse.status()).toBe(201);

    const loginResponse = await request.post('/api/auth/login/', {
      data: {
        email: 'test-{{timestamp}}@example.com',
        password: 'SecurePass123!'
      }
    });
    expect(loginResponse.status()).toBe(200);
    
    const authData: AuthResponse = await loginResponse.json();
    authToken = authData.access;
    testUserId = authData.user.id;
  });

  test.afterAll(async ({ request }) => {
    // Cleanup: Remove test data
    if (testUserId) {
      await request.delete(`/api/users/${testUserId}/`, {
        headers: { Authorization: `Bearer ${authToken}` }
      });
    }
  });

  {{API_TEST_SCENARIOS}}

  test('should handle authentication errors correctly', async ({ request }) => {
    const response = await request.get('/api/{{protected_endpoint}}/', {
      headers: { Authorization: 'Bearer invalid-token' }
    });
    
    expect(response.status()).toBe(401);
    const errorData: ErrorResponse = await response.json();
    expect(errorData).toHaveProperty('error');
  });

  test('should handle authorization errors correctly', async ({ request }) => {
    // Test with valid token but insufficient permissions
    const response = await request.get('/api/admin/{{admin_endpoint}}/', {
      headers: { Authorization: `Bearer ${authToken}` }
    });
    
    expect(response.status()).toBe(403);
  });
});

E2E Tests Template

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

test.describe('{{Story Title}} - E2E Tests', () => {
  test.beforeEach(async ({ page }) => {
    // Mock API responses for consistent testing
    await page.route('**/api/auth/login/', async route => {
      const request = route.request();
      const body = await request.postDataJSON();
      
      if (body.email === 'test@example.com' && body.password === 'SecurePass123!') {
        await route.fulfill({
          status: 200,
          contentType: 'application/json',
          body: JSON.stringify({
            access: 'mock-jwt-token',
            refresh: 'mock-refresh-token',
            user: { id: 1, email: 'test@example.com', role: 'vendor' }
          })
        });
      } else {
        await route.fulfill({
          status: 401,
          contentType: 'application/json',
          body: JSON.stringify({ error: 'Invalid credentials' })
        });
      }
    });

    {{ADDITIONAL_API_MOCKS}}

    await page.goto('/');
  });

  {{E2E_TEST_SCENARIOS}}

  test('should handle network errors gracefully', async ({ page }) => {
    // Simulate network failure
    await page.route('**/api/**', route => route.abort());
    
    await page.goto('/{{test_page}}');
    
    // Verify error handling
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
    await expect(page.locator('[data-testid="error-message"]')).toContainText('Network error');
  });

  test('should maintain accessibility standards', async ({ page }) => {
    await page.goto('/{{test_page}}');
    
    // Test keyboard navigation
    await page.keyboard.press('Tab');
    await expect(page.locator(':focus')).toBeVisible();
    
    // Test ARIA labels
    const form = page.locator('form');
    await expect(form).toHaveAttribute('aria-label');
  });
});

Integration Tests Template

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

test.describe('{{Story Title}} - Integration Tests', () => {
  test.beforeEach(async ({ page }) => {
    // Set up realistic API responses
    await page.route('**/api/{{resource}}/**', async route => {
      const method = route.request().method();
      const url = route.request().url();
      
      if (method === 'GET' && url.includes('/api/{{resource}}/')) {
        await route.fulfill({
          status: 200,
          contentType: 'application/json',
          body: JSON.stringify({{mock_data}})
        });
      } else if (method === 'POST') {
        const body = await route.request().postDataJSON();
        await route.fulfill({
          status: 201,
          contentType: 'application/json',
          body: JSON.stringify({ id: Date.now(), ...body })
        });
      }
    });

    await page.goto('/{{test_page}}');
  });

  {{INTEGRATION_TEST_SCENARIOS}}

  test('should handle concurrent operations correctly', async ({ page }) => {
    // Test multiple simultaneous actions
    const promises = [
      page.click('[data-testid="action-1"]'),
      page.click('[data-testid="action-2"]'),
      page.click('[data-testid="action-3"]')
    ];
    
    await Promise.all(promises);
    
    // Verify final state is consistent
    await expect(page.locator('[data-testid="status"]')).toContainText('All operations completed');
  });
});

Test Scenario Conversion Rules

Natural Language → TypeScript Conversion

Playwright MCP: Use Playwright_navigate to navigate to {{URL}} TypeScript: await page.goto('{{URL}}');

Playwright MCP: Use Playwright_fill to fill {{field}} with selector "{{selector}}" and value "{{value}}" TypeScript: await page.locator('{{selector}}').fill('{{value}}');

Playwright MCP: Use Playwright_click to click {{element}} with selector "{{selector}}" TypeScript: await page.locator('{{selector}}').click();

Playwright MCP: Use Playwright_get to call {{API_URL}} with Authorization header TypeScript:

const response = await request.get('{{API_URL}}', {
  headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.status()).toBe(200);

Playwright MCP: Use Playwright_post to call {{API_URL}} with JSON body TypeScript:

const response = await request.post('{{API_URL}}', {
  data: {{JSON_BODY}},
  headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.status()).toBe(201);

Playwright MCP: Use Playwright_screenshot to take screenshot named "{{name}}" TypeScript: await page.screenshot({ path: 'screenshots/{{name}}.png' });

Playwright MCP: Use Playwright_console_logs to check for errors TypeScript:

page.on('console', msg => {
  if (msg.type() === 'error') {
    console.error('Console error:', msg.text());
  }
});

Playwright MCP: Use Playwright_get_visible_text to verify {{content}} TypeScript: await expect(page.locator('{{selector}}')).toContainText('{{content}}');

Playwright MCP: Verify response status code is {{status}} TypeScript: expect(response.status()).toBe({{status}});

Playwright MCP: Use Playwright_select to select {{option}} with selector "{{selector}}" TypeScript: await page.locator('{{selector}}').selectOption('{{option}}');

Playwright MCP: Use Playwright_hover to hover over {{element}} with selector "{{selector}}" TypeScript: await page.locator('{{selector}}').hover();

Playwright MCP: Use Playwright_press_key to press "{{key}}" TypeScript: await page.keyboard.press('{{key}}');

Playwright MCP: Use Playwright_expect_response to monitor API calls with id "{{id}}" TypeScript:

const responsePromise = page.waitForResponse(response =>
  response.url().includes('{{endpoint}}') && response.status() === 200
);

Playwright MCP: Use Playwright_assert_response to validate response with id "{{id}}" TypeScript:

const response = await responsePromise;
const data = await response.json();
expect(data).toHaveProperty('{{expected_property}}');

File Organization Structure

packages/e2e-tests/
├── tests/
│   ├── api/
│   │   └── story-{{story_number}}-{{story_name}}.spec.ts
│   ├── e2e/
│   │   └── story-{{story_number}}-{{story_name}}.spec.ts
│   └── integration/
│       └── story-{{story_number}}-{{story_name}}.spec.ts
├── fixtures/
│   └── story-{{story_number}}-test-data.ts
└── utils/
    └── story-{{story_number}}-helpers.ts

Test Data and Fixtures

// fixtures/story-{{story_number}}-test-data.ts
export const testData = {
  validUser: {
    email: 'test-user@example.com',
    password: 'SecurePass123!',
    firstName: 'Test',
    lastName: 'User',
    organization: 'Test Organization'
  },
  invalidUser: {
    email: 'invalid-email',
    password: '123',
    firstName: '',
    lastName: ''
  },
  apiEndpoints: {
    register: '/api/auth/register/',
    login: '/api/auth/login/',
    profile: '/api/users/profile/',
    {{additional_endpoints}}
  }
};

export const mockResponses = {
  successfulLogin: {
    access: 'mock-jwt-token',
    refresh: 'mock-refresh-token',
    user: { id: 1, email: 'test@example.com', role: 'vendor' }
  },
  registrationSuccess: {
    message: 'Registration successful',
    user: { id: 1, email: 'test@example.com' }
  },
  {{additional_mocks}}
};

Helper Functions

// utils/story-{{story_number}}-helpers.ts
import { Page, Request } from '@playwright/test';

export class TestHelpers {
  static async loginUser(page: Page, email: string, password: string) {
    await page.goto('/login');
    await page.locator('[data-testid="email"]').fill(email);
    await page.locator('[data-testid="password"]').fill(password);
    await page.locator('[data-testid="login-button"]').click();
  }

  static async createTestUser(request: Request) {
    const response = await request.post('/api/auth/register/', {
      data: {
        email: `test-${Date.now()}@example.com`,
        password: 'SecurePass123!',
        first_name: 'Test',
        last_name: 'User'
      }
    });
    return response.json();
  }

  static async cleanupTestData(request: Request, userId: number, authToken: string) {
    await request.delete(`/api/users/${userId}/`, {
      headers: { Authorization: `Bearer ${authToken}` }
    });
  }

  {{additional_helpers}}
}