AgentSkillsCN

E2e Testing

端到端测试

SKILL.md

E2E Testing Skill

When writing E2E/client tests for this codebase, follow the established patterns using Playwright and the BasePage pattern.

Overview

  • Framework: Playwright E2E tests (tests/e2e/*.e2e.spec.ts)
  • Pattern: Enhanced Page Object Model with BasePage class
  • Test Context: TestContext - centralized resource management
  • Fixtures: Extended Playwright fixtures in tests/test-fixtures.ts

Directory Structure

code
tests/
├── e2e/              # End-to-end browser tests (.e2e.spec.ts)
├── pages/            # Page object models
│   └── BasePage.ts   # Base class with common UI patterns
├── helpers/
│   └── TestContext.ts # User factory and cleanup
└── test-fixtures.ts  # Playwright fixture extensions

BasePage Pattern

All page objects extend BasePage for common functionality:

typescript
import { BasePage } from '@tests/pages/BasePage';

export class LoginPage extends BasePage {
  // Locators
  get googleSignInButton(): Locator {
    return this.page.getByRole('button', { name: 'Continue with Google' });
  }

  get emailInput(): Locator {
    return this.page.getByPlaceholder(/email/i);
  }

  // Actions
  async openLoginModal(): Promise<void> {
    await this.signInButton.click();
    await this.waitForModal();
  }

  async fillLoginForm(email: string, password: string): Promise<void> {
    await this.fillField(/email/i, email);
    await this.fillField(/password/i, password);
  }

  async submitForm(): Promise<void> {
    await this.clickButton('Sign In');
  }

  // Assertions
  async assertModalVisible(): Promise<void> {
    await expect(this.modal).toBeVisible();
  }
}

BasePage Available Methods

Navigation

typescript
await page.goto('/'); // Navigate to path
await page.waitForURL('/dashboard'); // Wait for URL

Modal Handling

typescript
await this.waitForModal(); // Wait for modal to appear
await this.closeModal(); // Close active modal
await this.clickModalButton('Submit'); // Click button in modal

Toast/Notifications

typescript
await this.waitForToast('Success!'); // Wait for toast with text
await this.dismissToast(); // Dismiss active toast

Loading States

typescript
await this.waitForLoadingComplete(); // Wait for page load
await this.waitForAuthLoadingComplete(); // Wait for auth load

Network Handling

typescript
await this.waitForApiResponse('/api/user'); // Wait for API response
await this.waitForApiRequest('/api/upscale'); // Wait for API request

Form Helpers

typescript
await this.fillField(/email/i, 'test@example.com');
await this.selectOption(/country/i, 'United States');

Accessibility

typescript
await this.checkBasicAccessibility(); // Basic a11y checks
await this.checkAriaLabels(); // Verify ARIA labels

Writing E2E Tests

Basic Test Structure

typescript
import { test, expect } from '@tests/e2e/fixtures';
import { LoginPage } from '@tests/pages/LoginPage';

test.describe('Authentication', () => {
  test('should show login form when clicking sign in', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto('/');
    await loginPage.openLoginModal();
    await loginPage.assertModalVisible();
  });
});

TestContext Setup

typescript
import { TestContext } from '@tests/helpers/TestContext';

let ctx: TestContext;

test.beforeAll(async () => {
  ctx = new TestContext();
});

test.afterAll(async () => {
  await ctx.cleanup();
});

User Factory

typescript
// Simple user
const user = await ctx.createUser();

// Fluent builder API
const user = await ctx.createUser().withSubscription('active', 'pro').withCredits(500).build();

// Preset configurations
const proUser = await ctx.userFactory.proUser(500);
const businessUser = await ctx.userFactory.businessUser(1000);

Common E2E Test Patterns

Form Submission

typescript
test('should submit login form', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto('/');

  await loginPage.openLoginModal();
  await loginPage.fillLoginForm('user@example.com', 'password123');
  await loginPage.submitForm();

  await loginPage.waitForToast('Welcome back!');
});

Navigation

typescript
test('should navigate to dashboard', async ({ page }) => {
  const dashboardPage = new DashboardPage(page);
  await dashboardPage.goto('/');
  await dashboardPage.clickNavLink('Dashboard');
  await dashboardPage.waitForURL('/dashboard');
});

Loading States

typescript
test('should show loading state during upload', async ({ page }) => {
  const uploadPage = new UploadPage(page);
  await uploadPage.goto('/upload');

  await uploadPage.uploadImage(testImage);
  await uploadPage.waitForLoadingComplete();

  await expect(uploadPage.resultPreview).toBeVisible();
});

Network Requests

typescript
test('should handle API errors gracefully', async ({ page }) => {
  const uploadPage = new UploadPage(page);

  // Intercept and mock API response
  await page.route('**/api/upscale', route =>
    route.fulfill({ status: 500, body: '{"error":"Server error"}' })
  );

  await uploadPage.goto('/upload');
  await uploadPage.uploadImage(testImage);

  await uploadPage.waitForToast('Something went wrong');
});

Keyboard Navigation

typescript
test('should handle keyboard navigation properly', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto('/');
  await loginPage.openLoginModal();

  await page.keyboard.press('Tab');
  await expect(loginPage.emailInput).toBeFocused();

  await page.keyboard.press('Tab');
  await expect(loginPage.passwordInput).toBeFocused();
});

Accessibility Testing

Always include accessibility checks in E2E tests:

typescript
test('should have proper accessibility attributes', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto('/');
  await loginPage.openLoginModal();

  // Check basic accessibility
  await loginPage.checkBasicAccessibility();

  // Check ARIA labels
  await loginPage.checkAriaLabels();
});

Manual Accessibility Checks

typescript
test('should be keyboard accessible', async ({ page }) => {
  const page = new BasePage(page);
  await page.goto('/');

  // Test Tab navigation
  await page.keyboard.press('Tab');
  // Verify focus indicator visible
});

test('should have proper ARIA labels', async ({ page }) => {
  await page.goto('/');

  const button = page.getByRole('button', { name: /submit/i });
  await expect(button).toHaveAttribute('aria-label', 'Submit form');
});

Mobile Testing

The Playwright config includes mobile device projects. Tests automatically run on:

  • Desktop (chromium)
  • Mobile iPhone (iPhone 12)
  • Mobile Android (Pixel 5)
  • Tablet (iPad)

Write mobile-specific tests:

typescript
test.describe('Mobile Navigation', () => {
  test('should show hamburger menu on mobile', async ({ page }) => {
    // This test runs on mobile devices
    const nav = new Navigation(page);
    await nav.goto('/');

    await expect(nav.hamburgerMenu).toBeVisible();
    await nav.openMobileMenu();
    await expect(nav.mobileMenuItems).toBeVisible();
  });
});

Common Pitfalls to Avoid

1. Fragile Selectors

Bad: Text-based selectors break easily

typescript
await page.click('Submit');

Good: Role-based selectors

typescript
await page.getByRole('button', { name: 'Submit' }).click();

Good: Test IDs (most stable)

typescript
await page.getByTestId('submit-button').click();

2. Hardcoded Waits

Bad: Fixed timeouts are flaky

typescript
await page.waitForTimeout(2000);

Good: Wait for specific condition

typescript
await page.waitForURL('/dashboard');
await expect(element).toBeVisible();
await this.waitForLoadingComplete();

3. Not Cleaning Up

Always clean up test data:

typescript
let ctx: TestContext;

test.beforeAll(async () => {
  ctx = new TestContext();
});

test.afterAll(async () => {
  await ctx.cleanup(); // Don't forget this!
});

4. Skipping Accessibility

Don't skip accessibility checks:

typescript
test('should work', async ({ page }) => {
  // Missing accessibility checks!
  await page.goto('/');
});

test('should work with accessibility', async ({ page }) => {
  await page.goto('/');
  await new BasePage(page).checkBasicAccessibility();
});

5. Ignoring Mobile

Don't only test desktop - tests run on all devices:

typescript
// This runs on desktop AND mobile/tablet
test('should work on all devices', async ({ page }) => {
  await page.goto('/');
  // Test works on all screen sizes
});

Best Practices

Test Naming

  • Use descriptive names: "should show login form when clicking sign in"
  • Group by feature: test.describe('Authentication')
  • Test user flows: "should complete checkout flow"

Page Object Structure

typescript
export class FeaturePage extends BasePage {
  // Locators (getters for lazy evaluation)
  get element(): Locator {
    return this.page.locator('...');
  }

  // Actions (async methods)
  async doSomething(): Promise<void> {}

  // Assertions (async methods with 'assert' prefix)
  async assertSomething(): Promise<void> {
    await expect(this.element).toBeVisible();
  }
}

Selector Priority

  1. Test IDs - Most stable: getByTestId()
  2. Role + Name - Semantic: getByRole('button', { name })
  3. Labels/Placeholders - Form inputs: getByPlaceholder(), getByLabel()
  4. Text - Only when necessary: getByText()
  5. CSS - Last resort: locator()

Testing Commands

bash
# Run all E2E tests
yarn test:e2e

# Run specific test file
yarn test:e2e tests/e2e/auth.e2e.spec.ts

# Playwright UI mode (interactive)
yarn test:e2e:ui

# Debug mode with inspector
yarn test:e2e:debug

# Run on specific device
yarn test:e2e --project=mobile-iphone

# Full verification suite
yarn verify

Before Finishing

  1. Write tests for your changes following the patterns above
  2. Run tests: yarn test:e2e
  3. Check accessibility: Include checkBasicAccessibility() in relevant tests
  4. Run verify: yarn verify (required before completing any task)

Key Files to Reference

  • tests/pages/BasePage.ts - Base class with common UI patterns
  • tests/helpers/TestContext.ts - User factory and cleanup
  • tests/test-fixtures.ts - Playwright fixture extensions
  • tests/e2e/fixtures.ts - Reusable test fixtures
  • playwright.config.ts - Playwright configuration (devices, projects)