| name | playwright-kms |
| description | Create Playwright E2E tests for the KMS Web UI following project conventions: data-testid, Ant Design Select helpers, regex assertions, FIPS skips. Use when adding E2E tests. |
Playwright KMS Test Generator
Create Playwright E2E tests for the KMS Web UI following the project's established conventions.
When to Use
- Adding E2E coverage for a new UI feature
- Updating tests after a UI component change
- Converting manual test scenarios into automated E2E tests
Project E2E Conventions
Test location: ui/tests/e2e/
Config: ui/playwright.config.ts
Runner: cd ui && CI=true PLAYWRIGHT_BASE_URL="http://127.0.0.1:5173" pnpm run test:e2e
Timeout budget: 60 seconds per test (CI runs 10 parallel workers against one KMS server)
Step 1 — Understand the Feature
Read the existing spec files most similar to the new test:
ls ui/tests/e2e/
Read the helpers file — it contains Ant Design-specific utilities you must reuse:
cat ui/tests/e2e/helpers.ts
Read the relevant UI action component:
ls ui/src/actions/
Step 2 — Test File Structure
Name the file ui/tests/e2e/<feature>.spec.ts. Follow this structure:
import { test, expect } from '@playwright/test'
import { selectAntOption, waitForKmsResponse } from './helpers'
const FIPS_MODE = process.env.FIPS_MODE === 'true'
test.describe('<Feature Name>', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/<route>')
await page.waitForSelector('[data-testid="<main-component>"]')
})
test('<should do something>', async ({ page }) => {
})
test.skip(FIPS_MODE, '<non-fips feature> is not available in FIPS mode')
})
Step 3 — Element Selection Rules
Always use data-testid attributes. Never select by CSS class, text content, or DOM structure.
await page.locator('[data-testid="create-key-button"]').click()
await page.locator('[data-testid="key-uid-input"]').fill('my-key-id')
await page.locator('.ant-btn-primary').click()
await page.locator('button:has-text("Create")').click()
If the UI component doesn't have data-testid, add it to the component first (in ui/src/actions/<Module>/).
Step 4 — Ant Design Select Portals
Ant Design <Select> dropdowns render in document.body, not in the component tree. Always use the helpers from helpers.ts:
import { selectAntOption } from './helpers'
await selectAntOption(page, '[data-testid="algorithm-select"]', 'AES-256-GCM')
await page.locator('[data-testid="algorithm-select"]').selectOption('AES-256-GCM')
Step 5 — Assertions
Use regex-based toHaveText() — Playwright's toHaveText does not support an exact option for partial matches.
await expect(page.locator('[data-testid="result-message"]')).toHaveText(/Key created successfully/)
await expect(page.locator('[data-testid="key-uid"]')).toHaveText(/[0-9a-f-]{36}/)
await expect(page.locator('[data-testid="error-message"]')).toHaveText('Permission denied')
const text = await page.locator('[data-testid="result"]').textContent()
expect(text).toContain('success')
Step 6 — FIPS / Non-FIPS Skipping
const FIPS_MODE = process.env.FIPS_MODE === 'true'
test('Covercrypt encrypt', async ({ page }) => {
test.skip(FIPS_MODE, 'Covercrypt is not available in FIPS mode')
})
test.describe('Non-FIPS algorithms', () => {
test.skip(FIPS_MODE, 'These tests require non-FIPS mode')
})
Step 7 — WASM Call Tests
When testing features that use WASM (e.g. key creation with specific TTLV structure):
await page.waitForFunction(() => typeof window.wasmModule !== 'undefined', { timeout: 10000 })
await page.locator('[data-testid="create-button"]').click()
Step 8 — Update E2E README
After creating the test file, update ui/tests/e2e/README.md:
- Add the new spec file to the coverage table
- Add a row to the FIPS-skip table if applicable
Example: Full Test Skeleton
import { test, expect, Page } from '@playwright/test'
import { selectAntOption } from './helpers'
const FIPS_MODE = process.env.FIPS_MODE === 'true'
test.describe('Symmetric Key Creation', () => {
async function navigateToCreateKey(page: Page) {
await page.goto('/keys')
await page.locator('[data-testid="create-symmetric-key-tab"]').click()
await page.waitForSelector('[data-testid="symmetric-key-form"]')
}
test('creates an AES-256-GCM key', async ({ page }) => {
await navigateToCreateKey(page)
await page.locator('[data-testid="key-id-input"]').fill('test-key-001')
await selectAntOption(page, '[data-testid="algorithm-select"]', 'AES')
await selectAntOption(page, '[data-testid="key-length-select"]', '256')
await page.locator('[data-testid="create-key-button"]').click()
await expect(page.locator('[data-testid="success-banner"]')).toHaveText(
/Key created successfully/
)
await expect(page.locator('[data-testid="created-key-uid"]')).toHaveText(
/test-key-001/
)
})
test('shows error for invalid key length', async ({ page }) => {
await navigateToCreateKey(page)
})
})