Skip to main content

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>