
playwright-testing
by jonit-dev
SKILL.md
name: playwright-testing description: Enhanced testing infrastructure with TestContext and Page Objects for Playwright. Covers E2E testing patterns, accessibility testing, API testing, and best practices for maintainable test organization.
Playwright Testing
MCP Tools Available
mcp__microsoft-playwright-mcp__browser_*- Browser automation and interactionmcp__microsoft-playwright-mcp__browser_navigate- Navigate to URLsmcp__microsoft-playwright-mcp__browser_click- Click elements on pagemcp__microsoft-playwright-mcp__browser_type- Type text into fieldsmcp__microsoft-playwright-mcp__browser_snapshot- Capture accessibility snapshotsmcp__microsoft-playwright-mcp__browser_take_screenshot- Take screenshotsmcp__microsoft-playwright-mcp__browser_evaluate- Execute JavaScriptmcp__microsoft-playwright-mcp__browser_console_messages- Get console messages
Testing Infrastructure Overview
This project uses an enhanced Playwright testing infrastructure with:
Core Abstractions
- TestContext - Centralized test resource management with automatic cleanup
- ApiClient - Fluent API client with typed responses and assertion chaining
- BasePage - Rich base class with common UI patterns and accessibility support
- Page Objects - Specialized page classes extending BasePage
Test Organization
tests/
├── e2e/ # End-to-end browser tests
├── api/ # API endpoint tests
├── integration/ # Service integration tests
├── unit/ # Unit tests with Vitest
├── pages/ # Page Object Models
├── helpers/ # Test utilities and infrastructure
└── fixtures/ # Test data and mocks
TestContext Pattern
TestContext provides centralized resource management for tests, handling user creation, cleanup, and database operations.
import { TestContext } from '../helpers';
let ctx: TestContext;
test.beforeAll(async () => {
ctx = new TestContext({ autoCleanup: true });
});
test.afterAll(async () => {
await ctx.cleanup();
});
test('creates user with subscription', async () => {
const user = await ctx.createUser({
subscription: 'active',
tier: 'pro',
credits: 500,
});
// Test with user...
// Cleanup handled automatically
});
test('creates multiple users for collaboration scenario', async () => {
const [owner, collaborator] = await ctx.createUsers(2, {
subscription: 'active',
tier: 'pro',
});
// Test multi-user scenario...
});
TestContext Features
- User Management: Create users with different subscription tiers
- Automatic Cleanup: Automatic resource cleanup after tests
- Database Access: Direct Supabase admin client access
- Resource Tracking: Track and clean up all created resources
- Mock User Support: Fallback to mock users in test environment
ApiClient with Fluent Assertions
ApiClient provides a fluent interface for API testing with built-in response validation.
import { ApiClient } from '../helpers';
test('API endpoint with authentication', async ({ request }) => {
const api = new ApiClient(request).withAuth(user.token);
const response = await api.post('/api/checkout', { priceId: 'price_123' });
// Fluent assertions
response.expectStatus(200).expectSuccess();
await response.expectData({ url: expect.any(String) });
});
test('API error handling', async ({ request }) => {
const api = new ApiClient(request);
const response = await api.post('/api/protected', {});
response.expectStatus(401).expectError();
await response.expectErrorCode('UNAUTHORIZED');
});
ApiClient Features
- Typed Responses: Generic type support for response data
- Fluent Assertions: Chainable assertion methods
- Authentication: Built-in bearer token support
- Error Handling: Structured error response validation
- Headers/Params: Easy header and parameter management
Enhanced Page Objects
Page Objects extend BasePage to get common UI patterns and accessibility support.
import { LoginPage } from '../pages/LoginPage';
test('login flow with enhanced page object', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto('/');
await loginPage.openLoginModal();
await loginPage.assertModalVisible();
// Enhanced base methods available
await loginPage.checkBasicAccessibility();
await loginPage.waitForLoadingComplete();
await loginPage.fillFieldByLabel('Email', 'test@example.com');
await loginPage.fillFieldByLabel('Password', 'password123');
await loginPage.clickButton('Sign In');
});
BasePage Features
Navigation & Page Management
const page = new BasePage(pageInstance);
await page.goto('/dashboard'); // Navigate with network idle wait
await page.waitForURL('/dashboard'); // Wait for URL pattern
await page.reload(); // Reload and wait for stability
await page.waitForPageLoad(); // Wait for full page load
Modal Handling
await page.waitForModal(); // Wait for modal to appear
await page.clickModalButton('Submit'); // Click button in modal
await page.closeModal(); // Close modal (escape/backdrop)
await page.isModalVisible(); // Check modal visibility
Toast/Notification Management
await page.waitForToast('Success'); // Wait for specific toast
await page.dismissToast(); // Dismiss any toast
await page.isToastVisible('Error'); // Check toast visibility
Form Interactions
await page.fillField('Email', 'user@example.com');
await page.fillFieldByLabel('Password', 'secret');
await page.clickButton('Submit');
await page.selectOption('Country', 'United States');
await page.checkCheckbox('Remember me');
Loading States
await page.waitForLoadingComplete(); // Wait for spinners to disappear
await page.waitForAuthLoadingComplete(); // Wait for auth loading
await page.waitForNetworkIdle(); // Wait for network idle
Network Request Handling
const response = await page.waitForApiResponse('/api/user');
await page.waitForApiRequest('/api/log');
const responses = await page.waitForMultipleApiResponses(['/api/user', '/api/credits']);
Accessibility Testing
await page.checkBasicAccessibility(); // Basic a11y checks
await page.checkAriaLabels(); // ARIA label validation
E2E Test Patterns
Critical User Journey Testing
test('complete upscaler workflow', async ({ page }) => {
const ctx = new TestContext();
const user = await ctx.createUser({ subscription: 'active', credits: 100 });
const upscalerPage = new UpscalerPage(page);
await upscalerPage.goto('/');
await upscalerPage.signIn(user.token);
await upscalerPage.uploadImage('./test-assets/image.jpg');
await upscalerPage.selectModel('realistic');
await upscalerPage.startUpscaling();
await upscalerPage.waitForCompletion();
await upscalerPage.downloadResult();
await ctx.cleanup();
});
Mobile/Responsive Testing
test('mobile upscaller experience', async ({ page }) => {
// Configure mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
const upscalerPage = new UpscalerPage(page);
await upscalerPage.goto('/');
// Test mobile-specific interactions
await upscalerPage.openMobileMenu();
await upscalerPage.uploadImageMobile('./test-assets/image.jpg');
await upscalerPage.verifyMobileUI();
});
Accessibility Testing Integration
test('upscaler accessibility compliance', async ({ page }) => {
const upscalerPage = new UpscalerPage(page);
await upscalerPage.goto('/');
await upscalerPage.checkBasicAccessibility();
// Navigate through flow checking a11y at each step
await upscalerPage.uploadImage('./test-assets/image.jpg');
await upscalerPage.checkAriaLabels();
await upscalerPage.selectModel('realistic');
await upscalerPage.checkKeyboardNavigation();
});
API Testing Patterns
Authentication Testing
test('protected endpoints require authentication', async ({ request }) => {
const api = new ApiClient(request);
// Test without auth
await api
.get('/api/user')
.then(response => response.expectStatus(401).expectErrorCode('UNAUTHORIZED'));
// Test with valid auth
const authenticatedApi = api.withAuth(user.token);
await authenticatedApi
.get('/api/user')
.then(response => response.expectStatus(200).expectSuccess());
});
Webhook Testing
import { WebhookClient } from '../helpers';
test('stripe webhook processing', async ({ request }) => {
const webhookClient = new WebhookClient(request);
const event = webhookClient.createStripeEvent('invoice.payment_succeeded', {
customer: 'cus_123',
amount_paid: 2000,
});
const response = await webhookClient.send('/api/webhooks/stripe', event);
response.expectStatus(200);
await response.expectWebhookResponse({ received: true });
});
Error Scenario Testing
test('API error responses are properly formatted', async ({ request }) => {
const api = new ApiClient(request).withAuth(user.token);
// Test validation errors
await api.post('/api/checkout', { invalidData: true }).then(response =>
response
.expectStatus(400)
.expectError()
.expectErrorCode('VALIDATION_ERROR')
.expectErrorMessage(/required/)
);
// Test rate limiting
await api
.get('/api/rate-limited')
.then(response =>
response
.expectStatus(429)
.expectErrorCode('RATE_LIMITED')
.expectHeaders({ 'retry-after': '60' })
);
});
Integration Testing Patterns
Database Integration
test('user credit deduction workflow', async () => {
const ctx = new TestContext();
const user = await ctx.createUser({ credits: 100 });
// Perform action that should deduct credits
const result = await deductCredits(user.id, 50);
// Verify database state
const updatedUser = await ctx.supabaseAdmin
.from('user_credits')
.select('credits')
.eq('user_id', user.id)
.single();
expect(updatedUser.data.credits).toBe(50);
await ctx.cleanup();
});
Service Integration
test('stripe subscription integration', async () => {
const ctx = new TestContext();
const user = await ctx.createUser({ subscription: 'active' });
// Test subscription change
const result = await changeSubscription(user.id, 'pro');
// Verify both local database and Stripe
const localSubscription = await ctx.data.getUserSubscription(user.id);
expect(localSubscription.tier).toBe('pro');
const stripeCustomer = await getStripeCustomer(user.id);
expect(stripeCustomer.subscription?.tier).toBe('pro');
await ctx.cleanup();
});
Best Practices
Test Organization
- Use TestContext for all user lifecycle management
- Extend BasePage for all page objects to get common patterns
- Group related tests in describe blocks with clear descriptions
- Use descriptive test names that explain the behavior being tested
Test Data Management
// Good: Use TestContext for user management
let ctx: TestContext;
beforeAll(async () => {
ctx = new TestContext();
});
afterAll(async () => {
await ctx.cleanup();
});
// Good: Use UserFactory for complex setups
const user = await UserFactory.create()
.withSubscription('active', 'pro')
.withCredits(1000)
.withProfile({ name: 'Test User' })
.build();
// Avoid: Hardcoded test data
const userId = '123e4567-e89b-12d3-a456-426614174000';
Assertion Patterns
// Good: Fluent assertions with ApiClient
await api
.post('/api/endpoint', data)
.expectStatus(200)
.expectSuccess()
.expectData({ id: expect.any(String) });
// Good: Page object assertions
await upscalerPage.assertUploadSuccess();
await upscalerPage.assertCreditsDeducted(50);
// Avoid: Manual JSON parsing and assertions
const response = await api.post('/api/endpoint', data);
const data = await response.json();
expect(response.status()).toBe(200);
expect(data.success).toBe(true);
expect(data.data.id).toBeDefined();
Error Handling
// Good: Test both success and error scenarios
test('successful request', async () => {
const response = await api.post('/api/valid', validData);
response.expectStatus(200).expectSuccess();
});
test('invalid request handling', async () => {
const response = await api.post('/api/valid', invalidData);
response.expectStatus(400).expectErrorCode('VALIDATION_ERROR');
});
// Good: Test rate limiting behavior
test('rate limiting', async () => {
// Make rapid requests
const responses = await Promise.all([
api.get('/api/rate-limited'),
api.get('/api/rate-limited'),
api.get('/api/rate-limited'),
]);
// At least one should be rate limited
expect(responses.some(r => r.status === 429)).toBe(true);
});
Accessibility Testing
// Good: Include accessibility checks in E2E tests
test('user journey with accessibility', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto('/');
await loginPage.checkBasicAccessibility();
await loginPage.signIn(user);
await loginPage.checkAriaLabels();
// Continue with flow...
});
// Good: Test keyboard navigation
test('keyboard navigation support', async ({ page }) => {
await page.keyboard.press('Tab'); // Navigate to next element
await page.keyboard.press('Enter'); // Activate focused element
await page.keyboard.press('Escape'); // Close modal/dropdown
});
Running Tests
Individual Test Types
# E2E tests (Chrome desktop)
yarn test:e2e
# Mobile/tablet tests
yarn test:mobile
# API tests
yarn test:api
# Integration tests
yarn test:integration
# Unit tests
yarn test:unit
Specific Test Files
# Run specific test file
yarn playwright tests/e2e/upscaler.e2e.spec.ts
# Run with specific reporter
yarn playwright --reporter=html tests/e2e/upscaler.e2e.spec.ts
# Run in debug mode
yarn playwright --debug tests/e2e/upscaler.e2e.spec.ts
Test Configuration
The project uses a sophisticated Playwright configuration with:
- Dynamic Ports: Random test ports to avoid conflicts
- Device Simulation: Mobile, tablet, and desktop testing
- Parallel Execution: Optimized worker counts for CI/local
- Automatic Server: Built-in test server management
- Trace Retention: Debug traces on failure only
Environment Variables
# Test environment (loaded from .env.test)
TEST_PORT=3101 # Dynamic test server port
TEST_WRANGLER_PORT=8801 # Workers preview port
ENV=test # Test environment flag
# Supabase test configuration
SUPABASE_URL_TEST= # Test Supabase instance
SUPABASE_SERVICE_KEY_TEST= # Test service key
Debugging Tests
Screenshots and Traces
// Manual screenshot
await page.screenshot('test-scenario', { fullPage: true });
// Automatic screenshot on failure (configured in playwright.config.ts)
// Screenshots saved to test-results/screenshots/
// Trace files for debugging
// Access via HTML report: test-results/index.html
Console Messages
// Check for JavaScript errors
const errors = await page.evaluate(() => {
return Array.from(document.querySelectorAll('div.error')).map(el => el.textContent);
});
// Browser console logs
const consoleMessages = await page.evaluate(() => {
return window.consoleMessages || [];
});
Network Debugging
// Wait for specific API call
const response = await page.waitForApiResponse('/api/upscale');
console.log('API Response:', await response.json());
// Monitor all network requests
page.on('request', request => {
console.log('Request:', request.url());
});
page.on('response', response => {
console.log('Response:', response.status(), response.url());
});
Advanced Patterns
Custom Matchers
// Extend expect with custom matchers
expect.extend({
async toHaveAccessibleLabel(page, expectedLabel) {
const element = page.locator(`[aria-label="${expectedLabel}"]`);
const isVisible = await element.isVisible();
return {
pass: isVisible,
message: () => `Expected element with label "${expectedLabel}" to be visible`,
};
},
});
// Use custom matcher
await expect(page).toHaveAccessibleLabel('Upload image');
Test Utilities
// Create reusable test utilities
class TestUtils {
static async createAuthenticatedUser(ctx: TestContext) {
const user = await ctx.createUser({ subscription: 'active' });
return { user, apiClient: new ApiClient(request).withAuth(user.token) };
}
static async simulateFileUpload(page, fileName: string) {
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(fileName);
}
}
Performance Testing
test('page load performance', async ({ page }) => {
const startTime = Date.now();
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000); // 3 second threshold
});
This enhanced testing infrastructure provides a solid foundation for maintaining high-quality, reliable tests while minimizing test maintenance overhead through reusable patterns and abstractions.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon