e2e-tester
npx skills add https://github.com/redpanda-data/console --skill e2e-tester
Agent 安装分布
Skill 文档
E2E Testing with Playwright & Testcontainers
Write end-to-end tests using Playwright against a full Redpanda Console stack running in Docker containers via testcontainers.
When to Use This Skill
- Testing 2+ step user journeys (login â action â verify)
- Multi-page workflows
- Browser automation with Playwright
NOT for: Component unit tests â use testing
Critical Rules
ALWAYS:
- Run
bun run buildbefore running E2E tests (frontend assets required) - Use
testcontainersAPI for container management (never manualdockercommands in tests) - Test 2+ step user journeys (multi-page, multi-step scenarios)
- Use
page.getByRole()andpage.getByLabel()selectors (avoid CSS selectors) - Add
data-testidattributes to components when semantic selectors aren’t available - Use Task tool with MCP Playwright agents to analyze failures and get test status
- Use Task tool with Explore agent to find missing testids in UI components
- Clean up test data using
afterEachto call cleanup API endpoints
NEVER:
- Test UI component rendering (that belongs in unit/integration tests)
- Use brittle CSS selectors like
.class-nameor#id - Use force:true when calling .click()
- Use waitForTimeout in e2e tests
- Hard-code wait times (use
waitForwith conditions) - Leave containers running after test failures
- Commit test screenshots to git (add to
.gitignore) - Add testids without understanding the component’s purpose and context
Test Architecture
Stack Components
OSS Mode (bun run e2e-test):
- Redpanda container (Kafka broker + Schema Registry + Admin API)
- Backend container (Go binary serving API + embedded frontend)
- OwlShop container (test data generator)
Enterprise Mode (bun run e2e-test-enterprise):
- Same as OSS + Enterprise features (RBAC, SSO, etc.)
- Requires
console-enterpriserepo checked out alongsideconsole
Network Setup:
- All containers on shared Docker network
- Internal addresses:
redpanda:9092,console-backend:3000 - External access:
localhost:19092,localhost:3000
Test Container Lifecycle
Setup (global-setup.mjs):
1. Build frontend (frontend/build/)
2. Copy frontend assets to backend/pkg/embed/frontend/
3. Build backend Docker image with testcontainers
4. Start Redpanda container with SASL auth
5. Start backend container serving frontend
6. Wait for services to be ready
Tests run...
Teardown (global-teardown.mjs):
1. Stop backend container
2. Stop Redpanda container
3. Remove Docker network
4. Clean up copied frontend assets
Workflow
1. Prerequisites
# Build frontend (REQUIRED before E2E tests)
bun run build
# Verify Docker is running
docker ps
2. Write Test
File location: tests/<feature>/*.spec.ts
Structure:
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test('user can complete workflow', async ({ page }) => {
// Navigate to page
await page.goto('/feature');
// Interact with elements
await page.getByRole('button', { name: 'Create' }).click();
await page.getByLabel('Name').fill('test-item');
// Submit and verify
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success')).toBeVisible();
// Verify navigation or state change
await expect(page).toHaveURL(/\/feature\/test-item/);
});
});
3. Selectors Best Practices
Prefer accessibility selectors:
// â
GOOD: Role-based (accessible)
page.getByRole('button', { name: 'Create Topic' })
page.getByLabel('Topic Name')
page.getByText('Success message')
// â
GOOD: Test IDs when role isn't clear
page.getByTestId('topic-list-item')
// â BAD: CSS selectors (brittle)
page.locator('.btn-primary')
page.locator('#topic-name-input')
Add test IDs to components:
// In React component
<Button data-testid="create-topic-button">
Create Topic
</Button>
// In test
await page.getByTestId('create-topic-button').click();
4. Async Operations
// â
GOOD: Wait for specific condition
await expect(page.getByRole('status')).toHaveText('Ready');
// â
GOOD: Wait for navigation
await page.waitForURL('**/topics/my-topic');
// â
GOOD: Wait for API response
await page.waitForResponse(resp =>
resp.url().includes('/api/topics') && resp.status() === 200
);
// â BAD: Fixed timeouts
await page.waitForTimeout(5000);
5. Authentication
OSS Mode: No authentication required
Enterprise Mode: Basic auth with e2euser:very-secret
test.use({
httpCredentials: {
username: 'e2euser',
password: 'very-secret',
},
});
6. Run Tests
# OSS tests
bun run build # Build frontend first!
bun run e2e-test # Run all OSS tests
# Enterprise tests (requires console-enterprise repo)
bun run build
bun run e2e-test-enterprise
# UI mode (debugging)
bun run e2e-test:ui
# Specific test file
bun run e2e-test tests/topics/create-topic.spec.ts
# Update snapshots
bun run e2e-test --update-snapshots
7. Debugging
Failed test debugging:
# Check container logs
docker ps -a | grep console-backend
docker logs <container-id>
# Check if services are accessible
curl http://localhost:3000
curl http://localhost:19092
# Run with debug output
DEBUG=pw:api bun run e2e-test
# Keep containers running on failure
TESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test
Playwright debugging tools:
// Add to test for debugging
await page.pause(); // Opens Playwright Inspector
// Screenshot on failure (automatic in config)
await page.screenshot({ path: 'debug.png' });
// Get page content for debugging
console.log(await page.content());
Common Patterns
Multi-Step Workflows
test('user creates, configures, and tests topic', async ({ page }) => {
// Step 1: Navigate and create
await page.goto('/topics');
await page.getByRole('button', { name: 'Create Topic' }).click();
// Step 2: Fill form
await page.getByLabel('Topic Name').fill('test-topic');
await page.getByLabel('Partitions').fill('3');
await page.getByRole('button', { name: 'Create' }).click();
// Step 3: Verify creation
await expect(page.getByText('Topic created successfully')).toBeVisible();
await expect(page).toHaveURL(/\/topics\/test-topic/);
// Step 4: Configure topic
await page.getByRole('button', { name: 'Configure' }).click();
await page.getByLabel('Retention Hours').fill('24');
await page.getByRole('button', { name: 'Save' }).click();
// Step 5: Verify configuration
await expect(page.getByText('Configuration saved')).toBeVisible();
});
Testing Forms
test('form validation works correctly', async ({ page }) => {
await page.goto('/create-topic');
// Submit empty form - should show errors
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('Name is required')).toBeVisible();
// Fill valid data - should succeed
await page.getByLabel('Topic Name').fill('valid-topic');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('Success')).toBeVisible();
});
Testing Data Tables
test('user can filter and sort topics', async ({ page }) => {
await page.goto('/topics');
// Filter
await page.getByPlaceholder('Search topics').fill('test-');
await expect(page.getByRole('row')).toHaveCount(3); // Header + 2 results
// Sort
await page.getByRole('columnheader', { name: 'Name' }).click();
const firstRow = page.getByRole('row').nth(1);
await expect(firstRow).toContainText('test-topic-a');
});
API Interactions
test('creating topic triggers backend API', async ({ page }) => {
// Listen for API call
const apiPromise = page.waitForResponse(
resp => resp.url().includes('/api/topics') && resp.status() === 201
);
// Trigger action
await page.goto('/topics');
await page.getByRole('button', { name: 'Create Topic' }).click();
await page.getByLabel('Name').fill('api-test-topic');
await page.getByRole('button', { name: 'Create' }).click();
// Verify API was called
const response = await apiPromise;
const body = await response.json();
expect(body.name).toBe('api-test-topic');
});
Testcontainers Setup
Frontend Asset Copy (Required)
The backend Docker image needs frontend assets embedded at build time:
// In global-setup.mjs
async function buildBackendImage(isEnterprise) {
// Copy frontend build to backend embed directory
const frontendBuildDir = resolve(__dirname, '../build');
const embedDir = join(backendDir, 'pkg/embed/frontend');
await execAsync(`cp -r "${frontendBuildDir}"/* "${embedDir}"/`);
// Build Docker image using testcontainers
// Docker doesn't allow referencing files outside build context,
// so we temporarily copy the Dockerfile into the build context
const tempDockerfile = join(backendDir, '.dockerfile.e2e.tmp');
await execAsync(`cp "${dockerfilePath}" "${tempDockerfile}"`);
try {
await GenericContainer
.fromDockerfile(backendDir, '.dockerfile.e2e.tmp')
.build(imageTag, { deleteOnExit: false });
} finally {
await execAsync(`rm -f "${tempDockerfile}"`).catch(() => {});
await execAsync(`find "${embedDir}" -mindepth 1 ! -name '.gitignore' -delete`).catch(() => {});
}
}
Container Configuration
Backend container:
const backend = await new GenericContainer(imageTag)
.withNetwork(network)
.withNetworkAliases('console-backend')
.withExposedPorts({ container: 3000, host: 3000 })
.withBindMounts([{
source: configPath,
target: '/etc/console/config.yaml'
}])
.withCommand(['--config.filepath=/etc/console/config.yaml'])
.start();
Redpanda container:
const redpanda = await new GenericContainer('redpandadata/redpanda:v25.2.1')
.withNetwork(network)
.withNetworkAliases('redpanda')
.withExposedPorts(
{ container: 19_092, host: 19_092 }, // Kafka
{ container: 18_081, host: 18_081 }, // Schema Registry
{ container: 9644, host: 19_644 } // Admin API
)
.withEnvironment({ RP_BOOTSTRAP_USER: 'e2euser:very-secret' })
.withHealthCheck({
test: ['CMD-SHELL', "rpk cluster health | grep -E 'Healthy:.+true' || exit 1"],
interval: 15_000,
retries: 5
})
.withWaitStrategy(Wait.forHealthCheck())
.start();
CI Integration
GitHub Actions Setup
e2e-test:
runs-on: ubuntu-latest-8
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build frontend
run: |
REACT_APP_CONSOLE_GIT_SHA=$(echo $GITHUB_SHA | cut -c 1-6)
bun run build
- name: Install Playwright browsers
run: bun run install:chromium
- name: Run E2E tests
run: bun run e2e-test
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: frontend/playwright-report/
Test ID Management
Finding Missing Test IDs
Use the Task tool with Explore agent to systematically find missing testids:
Use Task tool with:
subagent_type: Explore
prompt: Search through [feature] UI components and identify all interactive
elements (buttons, inputs, links, selects) missing data-testid attributes.
List with file:line, element type, purpose, and suggested testid name.
Example output:
schema-list.tsx:207 - button - "Edit compatibility" - schema-edit-compatibility-btn
schema-list.tsx:279 - button - "Create new schema" - schema-create-new-btn
schema-details.tsx:160 - button - "Edit compatibility" - schema-details-edit-compatibility-btn
Adding Test IDs
Naming Convention:
- Use kebab-case:
data-testid="feature-action-element" - Be specific: Include feature name + action + element type
- For dynamic items: Use template literals
data-testid={\item-delete-${id}`}`
Examples:
// â
GOOD: Specific button action
<Button data-testid="schema-create-new-btn" onClick={onCreate}>
Create new schema
</Button>
// â
GOOD: Form input with context
<Input
data-testid="schema-subject-name-input"
placeholder="Subject name"
/>
// â
GOOD: Table row with dynamic ID
<TableRow data-testid={`schema-row-${schema.name}`}>
{schema.name}
</TableRow>
// â
GOOD: Delete button in list
<IconButton
data-testid={`schema-delete-btn-${schema.name}`}
icon={<TrashIcon />}
onClick={() => deleteSchema(schema.name)}
/>
// â BAD: Too generic
<Button data-testid="button">Create</Button>
// â BAD: Using CSS classes as identifiers
<Button className="create-btn">Create</Button>
Where to Add:
- Primary actions: Create, Save, Delete, Edit, Submit, Cancel buttons
- Navigation: Links to detail pages, breadcrumbs
- Forms: All input fields, selects, checkboxes, radio buttons
- Lists/Tables: Row identifiers, action buttons within rows
- Dialogs/Modals: Open/close buttons, form elements inside
- Search/Filter: Search inputs, filter dropdowns, clear buttons
Process:
- Use Task/Explore to find missing testids in target feature
- Read the component file to understand context
- Add
data-testidfollowing naming convention - Update tests to use new testids
- Run tests to verify selectors work
Analyzing Test Failures
Using MCP Playwright Agents
Check Test Status:
// Use mcp__playwright-test__test_list to see all tests
// Use mcp__playwright-test__test_run to get detailed results
// Use mcp__playwright-test__test_debug to analyze specific failures
Reading Playwright Logs
Common failure patterns and fixes:
1. Element Not Found
Error: locator.click: Target closed
Error: Timeout 30000ms exceeded waiting for locator
Analysis steps:
- Check if element has correct testid/role
- Verify element is visible (not hidden/collapsed)
- Check for timing issues (element loads async)
- Look for dynamic content that changes selector
Fix:
// â BAD: Element might not be loaded
await page.getByRole('button', { name: 'Create' }).click();
// â
GOOD: Wait for element to be visible
await expect(page.getByRole('button', { name: 'Create' })).toBeVisible();
await page.getByRole('button', { name: 'Create' }).click();
// â
BETTER: Add testid for stability
await page.getByTestId('create-button').click();
2. Selector Ambiguity
Error: strict mode violation: locator('button') resolved to 3 elements
Analysis:
- Multiple elements match the selector
- Need more specific selector or testid
Fix:
// â BAD: Multiple "Edit" buttons on page
await page.getByRole('button', { name: 'Edit' }).click();
// â
GOOD: More specific with testid
await page.getByTestId('schema-edit-compatibility-btn').click();
// â
GOOD: Scope within container
await page.getByRole('region', { name: 'Schema Details' })
.getByRole('button', { name: 'Edit' }).click();
3. Timing/Race Conditions
Error: expect(locator).toHaveText()
Expected string: "Success"
Received string: "Loading..."
Analysis:
- Test assertion ran before UI updated
- Need to wait for specific state
Fix:
// â BAD: Doesn't wait for state change
await page.getByRole('button', { name: 'Save' }).click();
expect(page.getByText('Success')).toBeVisible();
// â
GOOD: Wait for the expected state
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Success')).toBeVisible({ timeout: 5000 });
4. Navigation Issues
Error: page.goto: net::ERR_CONNECTION_REFUSED
Analysis:
- Backend/frontend not running
- Wrong URL or port
Fix:
# Check containers are running
docker ps | grep console-backend
# Check container logs
docker logs <container-id>
# Verify port mapping
curl http://localhost:3000
# Check testcontainer state file
cat .testcontainers-state.json
Systematic Failure Analysis Workflow
When tests fail:
-
Get Test Results
Use mcp__playwright-test__test_run or check console output Identify which tests failed and error messages -
Analyze Error Patterns
- Selector not found â Missing/wrong testid or element not visible
- Strict mode violation â Need more specific selector
- Timeout â Element loads async, need waitFor
- Connection refused â Container/service not running
-
Find Missing Test IDs
Use Task tool with Explore agent to find missing testids in the components related to failed tests -
Add Test IDs
- Read component file
- Add
data-testidto problematic elements - Follow naming convention
- Format with biome
-
Update Tests
- Replace brittle selectors with stable testids
- Add proper wait conditions
- Verify with test run
-
Verify Fixes
Run specific test file to verify fix Run full suite to ensure no regressions
Troubleshooting
Container Fails to Start
# Check if frontend build exists
ls frontend/build/
# Check if Docker image built successfully
docker images | grep console-backend
# Check container logs
docker logs <container-id>
# Verify Docker network
docker network ls | grep testcontainers
Test Timeout Issues
// Increase timeout for slow operations
test('slow operation', async ({ page }) => {
test.setTimeout(60000); // 60 seconds
await page.goto('/slow-page');
await expect(page.getByText('Loaded')).toBeVisible({ timeout: 30000 });
});
Port Already in Use
# Find and kill process using port 3000
lsof -ti:3000 | xargs kill -9
# Or use different ports in test config
Quick Reference
Test types:
- E2E tests (
*.spec.ts): Complete user workflows, browser interactions - Integration tests (
*.test.tsx): Component + API, no browser - Unit tests (
*.test.ts): Pure logic, utilities
Commands:
bun run build # Build frontend (REQUIRED first!)
bun run e2e-test # Run OSS E2E tests
bun run e2e-test-enterprise # Run Enterprise E2E tests
bun run e2e-test:ui # Playwright UI mode (debugging)
Selector priority:
getByRole()– Best for accessibilitygetByLabel()– For form inputsgetByText()– For content verificationgetByTestId()– When semantic selectors aren’t clear- CSS selectors – Avoid if possible
Wait strategies:
waitForURL()– Navigation completewaitForResponse()– API call finishedwaitFor()withexpect()– Element state changed- Never use fixed
waitForTimeout()unless absolutely necessary
Output
After completing work:
- Confirm frontend build succeeded
- Verify all E2E tests pass
- Note any new test IDs added to components
- Mention cleanup of test containers
- Report test execution time and coverage