This file provides guidance to AI coding assistants when working with code in this repository.
Claude, you have access to the following skills. Use them when appropriate:
/migrate-test- Comprehensive workflow for migrating Cypress tests to Playwright (see.claude/commands/migrate-test.md)- See
.claude/skills/pr_review.mdfor PR review steps
When migrating Cypress tests to Playwright, use the /migrate-test skill. Key principles:
- Always branch from
develfor migrations - Migrate in batches of 3-4 tests using parallel Task agents
- Follow the Common Pitfalls guide to avoid strict mode violations, API errors, and TypeScript issues
- Run tests immediately after writing (
--max-failures=1 --retries=0) - Commit once all tests pass with concise commit messages
This is the Ansible UI monorepo built with React, TypeScript, and PatternFly. The project uses NPM workspaces and is structured as a unified UI that integrates multiple services:
- Platform - Unified gateway UI for AAP (main entry point)
- AWX - AWX UI
- EDA - Event-Driven Ansible UI
- Hub - Automation Hub UI
- Chatbot - Ansible Virtual Assistant UI
- Framework - Shared UI framework using PatternFly
- Common - Shared components and utilities
- Unit/Component Tests:
npm run vitest(uses Vitest) - Playwright Integration Tests: See Playwright Testing section below
- Cypress Tests:
npm run e2e:run(uses Cypress legacy tests) - Linting:
npm run eslint - Type Checking:
npm run tsc
/platform- Main Platform UI (unified entry point)/framework- Shared UI framework/frontend/awx- AWX Controller UI/frontend/eda- Event-Driven Ansible UI/frontend/hub- Automation Hub UI/frontend/chatbot- Chatbot UI/frontend/common- Shared components/cypress- E2E tests/playwright- Additional E2E tests
- React 18 with TypeScript
- PatternFly for UI components
- React Hook Form for form management
- React Router for navigation
- SWR for data fetching
- i18next for internationalization
- Vite for build tooling
- Vitest for unit testing
- Cypress for E2E testing
- Playwright for additional E2E testing
- NX for monorepo management
Each service has its own API helper wrapper - use these instead of raw URLs:
gatewayAPI/users/``- Platform:/api/gateway/awxAPI/projects/``- AWX:/api/controller/v2/edaAPI/events/``- EDA:/api/eda/v1/hubAPI/collections/``- Hub:/api/galaxy/
# Setup and dependencies
npm ci # Install dependencies
npm run clean # Clean build artifacts
# Testing and quality
npm test # Run all tests (TypeScript, ESLint, Prettier, Vitest)
npm run tsc # Type checking
npm run fix # Fix linting and formatting
npm run i18n # Generate translations
# Playwright tests (from /playwright directory)
cd playwright && npm run live # Run against live server
cd playwright && npm run mock # Run against mocked data
npx playwright test tests/path/to/test.spec.ts --project 'live chromium'
# Development
npm start # Start platform dev server (from /platform)
npm run build # Build all workspaces- Follow workspace-based architecture - each UI has its own workspace
- Use the shared framework for common UI patterns
- Place shared utilities in
/frontend/common - Use TypeScript interfaces for type safety
- Use PatternFly components and design system
- CSS modules or styled-components for custom styling
- Use React hooks for local state
- SWR for server state management
- Zustand for global state when needed
CRITICAL: Follow the component hierarchy to maximize code reuse and consistency.
Before writing any new UI code, follow this checklist:
-
Check for Existing Components (in priority order)
- First: Search
/frameworkfor shared framework components (PageForm, PageTable, PageHeader, PageLayout, etc.) - Second: Check PatternFly 6 documentation at patternfly.org/components for available components
- Third: Search workspace-specific components (
/frontend/awx/components/,/frontend/eda/components/, etc.) - Last Resort: Create a new component only if nothing exists
- First: Search
-
Component Location Strategy
- Framework-level reusable components →
/framework/(PageForm, PageTable, PageDetails, etc.) - Workspace-specific components →
/frontend/{workspace}/components/(AWX-only, EDA-only, etc.) - Cross-workspace shared utilities →
/frontend/common/ - Always use PatternFly 6 components from
@patternfly/react-coreas the foundation
- Framework-level reusable components →
-
Building New Components
- ALWAYS use PatternFly 6 components as the foundation - never recreate PF components
- Build accessible components following PatternFly patterns and design system
- Include comprehensive Vitest tests (see existing
.test.tsxfiles) - For workspace-specific needs, place in workspace's component directory
- For cross-workspace needs, consider adding to
/frameworkor/frontend/common
-
Custom Hooks
- Extract reusable logic into custom hooks
- Place hooks in workspace's hooks directory (e.g.,
/frontend/awx/hooks/) - For cross-workspace hooks, place in
/frontend/common/hooks/ - Follow naming convention:
useXxx - Include proper TypeScript types
Example Workflow:
User Request: "Add a data table for displaying users"
Step 1: Check /framework for PageTable ✓ (exists)
Step 2: Use PageTable from framework with column configuration
Result: Reuse existing PageTable instead of creating new table component
User Request: "Add a confirmation dialog"
Step 1: Check /framework for confirmation components ✓ (may have modal utilities)
Step 2: Check PatternFly 6 for Modal component ✓ (exists)
Step 3: Use PatternFly Modal or framework helper
Result: Use PF Modal component, not a custom dialog
CRITICAL: Actively identify and eliminate code duplication to improve maintainability.
| Pattern Detected | Action Required |
|---|---|
| Repeated JSX structure (2+ times) | → Create a Component in appropriate location |
| Repeated logic/state (2+ times) | → Create a Custom Hook |
| Repeated utility functions | → Create a shared utility in /frontend/common |
| Similar components with variants | → Extend existing component with props/variants |
Signs you need a component:
- Same JSX structure appears in multiple files across workspaces
- Copy-pasted markup with minor variations (button groups, empty states, status badges)
- Similar styling patterns repeated
// ❌ BAD: Repeated JSX pattern across multiple pages
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h4">No {resourceType} found</Title>
<EmptyStateBody>Create a {resourceType} to get started.</EmptyStateBody>
</EmptyState>
// ✅ GOOD: Extract to reusable component
<ResourceEmptyState resourceType={resourceType} onCreate={handleCreate} />Signs you need a hook:
- Same useState + useEffect pattern repeated across components
- Identical data fetching logic with SWR
- Common form validation patterns
- Repeated RBAC permission checks
// ❌ BAD: Repeated logic in multiple components
const [selected, setSelected] = useState<Resource[]>([]);
const handleSelect = (resource: Resource, isSelecting: boolean) =>
setSelected((prev) =>
isSelecting ? [...prev, resource] : prev.filter((r) => r.id !== resource.id)
);
// ✅ GOOD: Extract to hook
const { selected, handleSelect, clearSelection } = useTableSelection<Resource>();For detailed code review questions about component reuse and abstraction patterns, see .claude/skills/pr_review.md section 3.
Extract to /framework:
- Component used across 2+ workspaces (AWX + EDA, or AWX + Hub, etc.)
- Follows PatternFly patterns and is domain-agnostic
- Provides core UI framework functionality (tables, forms, layouts, navigation)
Extract to /frontend/common:
- Utility functions or hooks used across multiple workspaces
- Type definitions shared across workspaces
- API helpers or error handling utilities
Keep in workspace:
- Component specific to one service (AWX-only concepts, EDA-only workflows)
- Business logic tied to specific domain
- Write unit tests and component tests with Vitest
- Use Playwright for integration and e2e tests and live testing
- CRITICAL: Avoid unnecessary mocks in Vitest tests - only mock external APIs, browser APIs, or genuinely difficult dependencies. Do NOT mock your own utility functions, hooks, or components. Test real behavior whenever possible.
- Use msw to mock API endpoints in Vitest, rather than mocking requestGet or other fetching helper functions.
Structure every test with three clear phases:
import { render, screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { expect, test } from 'vitest'
test('should increment counter when button clicked', async () => {
// Arrange - Set up test data and render component
const user = userEvent.setup()
render(<Counter initialValue={0} />)
// Act - Perform the user action
await user.click(screen.getByRole('button', { name: 'Increment' }))
// Assert - Verify the expected outcome
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})Why AAA Pattern:
- Makes tests easier to read and understand
- Clearly separates setup, action, and verification
- Helps identify what the test is actually testing
- Standard pattern used across the industry
| Type | Focus On | Example |
|---|---|---|
| Component | User interactions, conditional rendering, accessibility | Button clicks, form submissions, ARIA |
| Hook | Return values, state transitions, callback invocations | useTableSelection, usePageDialog |
| Utility | Input → output transformations, edge cases | formatDate, parseJobStatus |
| Form | Validation logic, field interactions | Required fields, format validation |
- Implementation details - Internal state, private methods, how something works internally
- Third-party library behavior - PatternFly components, React Router, i18next
- Static content - Hardcoded text that never changes
- Framework behavior - React rendering, hook lifecycle
// ❌ BAD: Testing implementation details
test('should update internal state', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.internalState).toBe(0) // Don't test private state
})
// ✅ GOOD: Testing behavior
test('should display incremented count', async () => {
render(<Counter />)
await user.click(screen.getByRole('button', { name: 'Increment' }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})Aim for meaningful coverage of critical paths:
- Happy path - Test the most common user flow
- Error cases - Test error handling and validation
- Edge cases - Test boundary conditions (empty lists, max values, null handling)
- User interactions - Test all clickable elements, forms, navigation
Example - Minimum coverage for a component:
describe('ResourceListPage', () => {
test('should display resources in table', () => {}); // Happy path
test('should handle empty state when no resources', () => {}); // Edge case
test('should delete resource when delete clicked', async () => {}); // Interaction
test('should display error message on API failure', () => {}); // Error case
});CRITICAL: Never use user-facing translatable strings in conditional logic or comparisons.
User-facing strings that will be translated should only be used for display purposes. Using them in logic creates bugs when the application is localized to other languages.
- Use
useTranslationhook from react-i18next:const { t } = useTranslation() - Mark strings for translation:
t('String to translate') - Run
npm run i18nto extract translation keys
Never compare translated strings - they break in other languages.
// ❌ BAD: Comparing translated text
if (t('Active') === 'Active') { ... } // Breaks in Spanish/French/etc.
if (jobType === t('Playbook run')) { ... } // Breaks when localized
if (t(status) === 'Running') { ... } // Never matches in other languages// ✅ GOOD: Check the raw API value, translate only for display
if (resource.status === 'active') {
// 'active' is from API contract, not a display string
setVariant('success')
}
return <Label variant={variant}>{t(resource.status)}</Label>
// ✅ GOOD: Use API enum values for logic
if (job.type === 'playbook') {
// 'playbook' is the API value
return <PlaybookIcon />
}// ✅ GOOD: Define internal constants separate from display
const ExecutionStatus = {
PENDING: 'pending',
RUNNING: 'running',
SUCCESS: 'successful',
FAILED: 'failed',
} as const;
// Compare internal values
if (execution.status === ExecutionStatus.RUNNING) {
return 'info';
}
// Map to display strings separately
const statusLabels: Record<string, string> = {
pending: t('Pending'),
running: t('Running'),
successful: t('Success'),
failed: t('Failed'),
};// ✅ GOOD: Separate logic values from display labels
const statusConfig: Record<string, { variant: 'success' | 'danger' | 'warning' }> = {
successful: { variant: 'success' },
failed: { variant: 'danger' },
running: { variant: 'warning' },
}
const config = statusConfig[execution.status] // Use API value
return <Label variant={config.variant}>{t(execution.status)}</Label>These types of strings are safe to use in logic (they won't be translated):
- API response values:
status === 'successful',type === 'job_template',kind === 'playbook' - Route paths:
pathname === '/organizations',path.includes('/settings') - Internal constants:
mode === 'edit',view === 'list' - Technical identifiers:
file.endsWith('.yaml'),name.startsWith('demo_')
Before writing conditional logic with strings:
- ✅ Is this string from an API response? → Safe to use in logic
- ✅ Is this an internal constant/route/identifier? → Safe to use in logic
- ❌ Is this string shown to users in the UI via
t()? → Do NOT use in logic - ❌ Would this string be translated to other languages? → Do NOT use in logic
// ✅ GOOD: Complete example of proper i18n usage
interface StatusBadgeProps {
status: 'pending' | 'running' | 'successful' | 'failed' // API values
}
const STATUS_CONFIG = {
pending: { variant: 'warning' as const, label: 'Pending' },
running: { variant: 'info' as const, label: 'Running' },
successful: { variant: 'success' as const, label: 'Success' },
failed: { variant: 'danger' as const, label: 'Failed' },
}
function StatusBadge({ status }: StatusBadgeProps) {
const { t } = useTranslation()
const config = STATUS_CONFIG[status] // Use API value for lookup
return <Label variant={config.variant}>{t(config.label)}</Label> // Translate for display
}
// Usage with API response
<StatusBadge status={job.status} /> // job.status is 'successful' from APICreate /playwright/.env:
PLATFORM_UI=http://localhost:4100
PLATFORM_USERNAME=your_username
PLATFORM_PASSWORD=your_passwordAlways use at least one top-level describe block:
import { test, expect } from '@playwright/test';
import { setupBefore, setupAfter } from '../../commands/setup';
test.beforeEach(setupBefore({ path: '/your/path' }));
test.afterEach(setupAfter);
test.describe('Feature Name - Description', () => {
test('your test description', { tag: ['@not_mock'] }, async ({ page }) => {
// Your test code here
});
});Unit Test: Pure logic/functions, no DOM rendering, mock dependencies, fast execution (milliseconds)
Component Test: Multiple units working together, mocked APIs, Vitest-based, tests UI interactions and form behaviors, don't mock unless necessary
Integration Test: Live API, no mocking, tests API interaction (RBAC changes, job execution, database operations)
User Acceptance Test: Full system-level user flows spanning multiple resources (create template → run job → verify output)
// BEST - Use getByTestId helper
await page.getByTestId('content-type').click();
// AVOID - Don't use data-cy in Playwright tests
await page.locator('[data-cy="content-type"]').click();When migrating from Cypress: Add data-testid alongside existing data-cy attributes. Keep data-cy until Cypress tests are migrated.
// Use exact matching to avoid ambiguity
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('value');
// Scope selectors to containers to avoid strict mode violations
await expect(page.locator('dialog').getByText('Success')).toBeVisible();
// Prefer semantic selectors
await page.getByRole('button', { name: 'Submit' }).click();Key principles:
- Use
exact: truewhen similar text exists (e.g., "Name" and "Name @ timestamp") - Scope to containers (dialog, main, nav) when elements appear multiple times
- Check
/playwright/commands/for existing utilities before writing custom logic - Search existing tests for patterns:
grep -r "pattern" playwright/tests/
ALWAYS use utility functions when available - in the playwright/commands directory.
Tests may run in an environment where data is paginated off the screen. Utilities
like clickTableRow() or getTableRow perform necessary filtering to avoid
test flakiness.
import { getTableRow } from '../../../commands/getTableRow';
// CORRECT - filters for needed row automatically
await clickTableRow({ filterLabel: 'Name', text: credentialTypeName }, page);
// NEVER do this - fails due to pagination
const roleRow = page.getByRole('row').filter({ hasText: roleName });Capture exact API-generated values instead of guessing patterns:
// Set up interception before action
const copyResponsePromise = page.waitForResponse(
(response) => response.url().includes('/copy/') && response.status() === 201
);
await page.getByRole('menuitem', { name: 'Duplicate template' }).click();
const copyResponse = await copyResponsePromise;
const copiedResource = (await copyResponse.json()) as ResourceType;
const copiedName = copiedResource.name; // Use for assertions and cleanupALWAYS run tests after creating/updating them before considering work complete:
# Fail-fast mode for immediate feedback
cd playwright && npx playwright test tests/path/to/test.spec.ts --project 'live chromium' --max-failures=1 --retries=0
# Debug mode if tests fail
cd playwright && npx playwright test tests/path/to/test.spec.ts --project 'live chromium' --debugCRITICAL RULE: Never conclude test work until ALL tests pass AND all linting/TypeScript issues are resolved.
ALWAYS validate workflows manually before writing tests using browser automation tools (MCP server):
- Navigate through complete user workflow manually
- Identify exact selectors by examining snapshots
- Test each interaction (clicks, form fills, etc.)
- Verify expected page states after actions
- Identify dynamic values requiring API interception
- Write tests using verified selectors and workflows
- Run tests as final validation (should pass on first run)
This prevents: write test → run test → fix selector → repeat.
Browser setup for AAP:
- Navigate to
https://localhost:4100(use HTTPS) - Handle SSL warning: Type
thisisunsafeon the warning page to bypass (or click "Advanced" → "Proceed to localhost (unsafe)") - Login with credentials from
/playwright/.env(default: admin / Admin!Password!Gw)
Important for Playwright MCP: The Playwright MCP browser automation tool will encounter the SSL certificate warning page when navigating to https://localhost:4100. You must bypass this warning by typing thisisunsafe on the warning page before the MCP can interact with the AAP UI.
Located in /playwright/commands/:
setupBefore()/setupAfter()- Test setup and teardownnavigateTo()- Navigate to specific pagesgetTableRow()- Find table rows (handles pagination) [USE THIS FOR ALL TABLE INTERACTIONS]clickTableRow()- Interact with table rowsclickPageAction()- Click page action buttonslogin()- Handle authenticationcreateE2EName()- Generate unique test namesconfirmAndAssertDeletion()- Handle deletion confirmationsbulkDeleteResources()- Generic bulk deletion from list viewdeleteResourceFromDetailsPage()- Generic deletion from details pagedeleteResourceFromList()- Generic deletion from list view
Located in /playwright/utils/
Resource utilities follow the Resource.api/ui.action() pattern:
import { Organization } from '@ansible/playwright/utils';
// API-based operations (faster, for test setup/teardown)
const org = await Organization.api.create(page, { name: 'Test Org' });
await Organization.api.delete(page, org.id);
// UI-based operations (for testing user workflows)
await Organization.ui.create(page, { organizationName: 'Test Org' });@not_mock- Don't run against mocked data
# Playwright Inspector
npx playwright test --debug
# View trace files
npx playwright show-trace trace.zip
# View coverage
npm run coverage# Platform server URL
export PLATFORM_SERVER='https://localhost:443'
# For standalone services (if needed)
export AWX_SERVER='https://localhost:8043'
export EDA_SERVER='http://localhost:8000'
export HUB_SERVER='http://localhost:5001'Prerequisites: Node.js 20.x+, NPM 8.x+
- Identify appropriate workspace (platform, awx, eda, hub, etc.)
- Use shared framework components when possible
- Add tests for new functionality
- Update translations if needed
- Use React Hook Form with framework form components
- Follow PageForm patterns in the framework
- For validation logic tests, prefer hook-level tests with
renderHookover rendering full forms
- Use appropriate API helper wrappers for each service
- Follow existing patterns for error handling
- Use SWR for data fetching and caching
- Build errors: Run
npm run cleanthennpm ci - Type errors: Check TypeScript configuration in relevant workspace
- Playwright test failures:
- Verify UI server running on port 4100
- Check
/playwright/.envcredentials - Use
exact: truefor selector specificity - Run with
--debugflag - Use
getTableRow()utility for table interactions - Use MCP server to examine live UI structure
- Platform: Check platform server logs
- Development: Browser console and terminal output
- Tests: Cypress/Playwright reports and traces (
npx playwright show-trace trace.zip)
- Follow TypeScript strict mode
- Use ESLint and Prettier configurations
- Write descriptive test names
- Add comments only for complex logic
- Ensure proper error handling
- Test files:
*.spec.tsor*.test.ts - Component files: PascalCase (e.g.,
UserTable.tsx) - Utility files: camelCase (e.g.,
apiHelpers.ts) - Constants: UPPER_SNAKE_CASE
- Always prefer editing existing files over creating new ones
- Use existing Playwright commands where possible
- Create generic commands for reusable patterns
- Validate all changes with tests, linting, and TypeScript checking
- Remove obvious comments from test files
- Begin test names with "should"
- Follow established patterns in the codebase