Building Deterministic & Automation-Friendly Apps
1. Core Principle
Frontend code must be test-addressable, deterministic, and automation-friendly.
This means:
- Stable selectors (never depend on CSS or layout)
- Predictable rendering
- Clear UI state
- Test hooks built into components
- No flaky async behavior
- Accessibility-first structure
2. Test ID Convention (Most Important)
Rule
Every interactive or important UI element must have a stable data-testid
Naming convention
<domain>-<component>-<element>-<action>
Examples
auth-login-email-input
auth-login-password-input
auth-login-submit-button
dashboard-bot-create-button
dashboard-bot-table-row
dashboard-bot-delete-button
editor-canvas-node
editor-canvas-save-button
Example Component
<Input
data-testid="auth-login-email-input"
type="email"
placeholder="Email"
/>
<Button
data-testid="auth-login-submit-button"
>
Login
</Button>
3. Component-Level Test ID Standard
Each component must expose a testIdPrefix
Pattern
type Props = {
testIdPrefix?: string;
};
Example
export function LoginForm({ testIdPrefix = "auth-login" }: Props) {
return (
<div data-testid={`${testIdPrefix}-container`}>
<Input data-testid={`${testIdPrefix}-email-input`} />
<Input data-testid={`${testIdPrefix}-password-input`} />
<Button data-testid={`${testIdPrefix}-submit-button`}>Login</Button>
</div>
);
}
Benefit
Reusable
auth-login-email-input
admin-login-email-input
user-login-email-input
Same component.
Different context.
4. Page-Level Test ID Convention
Each page must define a page root test id
Pattern
<div data-testid="page-dashboard">
Example
export default function DashboardPage() {
return (
<div data-testid="page-dashboard">
<BotTable />
<CreateBotButton />
</div>
);
}
5. Layout Test ID Convention
Layouts must expose:
layout-main
layout-sidebar
layout-header
layout-content
Example
<main data-testid="layout-main">
<Sidebar data-testid="layout-sidebar" />
<Header data-testid="layout-header" />
<section data-testid="layout-content">{children}</section>
</main>
6. Button Convention
Buttons must define action name
Bad
submit-button
button-1
blue-button
Good
auth-login-submit-button
bot-create-submit-button
bot-delete-confirm-button
Rule
<domain>-<action>-button
7. Form Convention
Each form must include:
form container
inputs
submit
error
success
loading
Example
<form data-testid="auth-login-form">
<Input data-testid="auth-login-email-input" />
<Input data-testid="auth-login-password-input" />
<Button data-testid="auth-login-submit-button" />
<p data-testid="auth-login-error-message" />
<p data-testid="auth-login-success-message" />
</form>
8. Async State Convention
Every async UI must expose state.
Required
loading
empty
error
success
Example
{
loading && <Spinner data-testid="bot-table-loading" />;
}
{
error && <Error data-testid="bot-table-error" />;
}
{
bots.length === 0 && <Empty data-testid="bot-table-empty" />;
}
{
bots.length > 0 && <Table data-testid="bot-table-data" />;
}
9. Table Convention
Tables must include:
table
row
cell
action
Example
<Table data-testid="bot-table">
{bots.map((bot) => (
<tr key={bot.id} data-testid={`bot-row-${bot.id}`}>
<td data-testid={`bot-name-${bot.id}`}>{bot.name}</td>
<td>
<Button data-testid={`bot-delete-${bot.id}`}>Delete</Button>
</td>
</tr>
))}
</Table>
10. Modal Convention
Each modal must have:
modal
overlay
title
content
confirm
cancel
Example
<Dialog data-testid="bot-delete-modal">
<DialogTitle data-testid="bot-delete-title">Delete Bot</DialogTitle>
<Button data-testid="bot-delete-confirm-button">Confirm</Button>
<Button data-testid="bot-delete-cancel-button">Cancel</Button>
</Dialog>
11. Navigation Convention
Navigation must expose route test id.
Example
<Link href="/dashboard" data-testid="nav-dashboard-link">
Dashboard
</Link>