- Create comprehensive html-template skill with 3 reference files - E2E Testing Attributes (data-what, data-which patterns) - ARIA Accessibility Attributes (roles, properties, states, WCAG) - Combined Patterns (real-world examples with both) - Move E2E attribute guidance from command to skill - Add extensive ARIA accessibility documentation - Update angular-template skill with cross-references - Remove dev-add-e2e-attrs command (functionality now in skill) The new skill provides 3,322 lines of comprehensive documentation covering both testing and accessibility best practices for HTML templates, with practical examples for forms, navigation, tables, dialogs, and more. Benefits: - Clean separation: Angular syntax vs HTML attributes - Reusable: html-template works with any HTML context - Comprehensive: E2E + ARIA in one place - Integrated: Works seamlessly with angular-template skill
20 KiB
E2E Testing Attributes - Complete Reference
This reference provides comprehensive guidance for adding E2E (End-to-End) testing attributes to HTML templates for reliable automated testing.
Table of Contents
- Overview
- Core Attribute Types
- Why E2E Attributes?
- Naming Conventions
- Patterns by Element Type
- Patterns by Component Type
- Dynamic Attributes
- Best Practices
- Validation
- Testing Integration
Overview
E2E testing attributes provide stable, semantic selectors for automated testing. They enable QA automation without relying on brittle CSS classes, IDs, or XPath selectors that frequently break when styling changes.
Core Attribute Types
1. data-what (Required)
Purpose: Semantic description of the element's purpose or type
Format: kebab-case string
Examples:
data-what="submit-button"data-what="search-input"data-what="product-link"data-what="list-item"
Guidelines:
- Describes WHAT the element is or does
- Should be consistent across similar elements
- Use descriptive, semantic names
- Keep it concise but clear
2. data-which (Required)
Purpose: Unique identifier for the specific instance of this element type
Format: kebab-case string or dynamic binding
Examples:
data-which="primary"(static)data-which="customer-form"(static)[attr.data-which]="item.id"(dynamic)[attr.data-which]="'customer-' + customerId"(dynamic with context)
Guidelines:
- Identifies WHICH specific instance of this element type
- Must be unique within the same view/component
- Use dynamic binding for list items:
[attr.data-which]="item.id" - Can combine multiple identifiers:
data-which="customer-123-edit"
3. data-* (Contextual)
Purpose: Additional contextual information about state, status, or data
Format: Custom attributes with kebab-case names
Examples:
data-status="active"data-index="0"data-role="admin"[attr.data-count]="items.length"
Guidelines:
- Use for additional context that helps testing
- Avoid sensitive data (passwords, tokens, PII)
- Use Angular binding for dynamic values:
[attr.data-*] - Keep attribute names semantic and clear
Why E2E Attributes?
Problems with Traditional Selectors
CSS Classes (Bad):
<!-- Brittle - breaks when styling changes -->
<button class="btn btn-primary submit">Submit</button>
// Test breaks when class names change
await page.click('.btn-primary.submit');
XPath (Bad):
// Brittle - breaks when structure changes
await page.click('//div[@class="form"]/button[2]');
IDs (Better, but limited):
<!-- IDs must be unique across entire page -->
<button id="submit-btn">Submit</button>
Benefits of E2E Attributes
Stable, Semantic Selectors (Good):
<button
class="btn btn-primary"
data-what="submit-button"
data-which="registration-form">
Submit
</button>
// Stable - survives styling and structure changes
await page.click('[data-what="submit-button"][data-which="registration-form"]');
Advantages:
- ✅ Decoupled from styling (CSS classes can change freely)
- ✅ Semantic and self-documenting
- ✅ Consistent across the application
- ✅ Easy to read and maintain
- ✅ Survives refactoring and restructuring
- ✅ QA and developers speak the same language
Naming Conventions
Common data-what Patterns
| Pattern | Use Case | Examples |
|---|---|---|
*-button |
All button elements | submit-button, cancel-button, delete-button, save-button |
*-input |
Text inputs and textareas | email-input, search-input, quantity-input, password-input |
*-select |
Dropdown/select elements | status-select, category-select, country-select |
*-checkbox |
Checkbox inputs | terms-checkbox, subscribe-checkbox, remember-checkbox |
*-radio |
Radio button inputs | payment-radio, shipping-radio |
*-link |
Navigation links | product-link, order-link, customer-link, home-link |
*-item |
List/grid items | list-item, menu-item, card-item, row-item |
*-dialog |
Modals and dialogs | confirm-dialog, error-dialog, info-dialog |
*-dropdown |
Dropdown menus | actions-dropdown, filter-dropdown |
*-toggle |
Toggle switches | theme-toggle, notifications-toggle |
*-tab |
Tab navigation | profile-tab, settings-tab |
*-badge |
Status badges | status-badge, count-badge |
*-icon |
Interactive icons | close-icon, menu-icon, search-icon |
data-which Naming Guidelines
Static unique identifiers (single instance):
data-which="primary"- Primary action buttondata-which="secondary"- Secondary action buttondata-which="main-search"- Main search inputdata-which="customer-form"- Customer form context
Dynamic identifiers (multiple instances):
[attr.data-which]="item.id"- List item by ID[attr.data-which]="'product-' + product.id"- Product item[attr.data-which]="index"- By array index (use sparingly)
Contextual identifiers (combine context):
data-which="customer-{{ customerId }}-edit"- Edit button for specific customerdata-which="order-{{ orderId }}-cancel"- Cancel button for specific order
Patterns by Element Type
Buttons
<!-- Submit Button -->
<button
type="submit"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form">
Submit
</button>
<!-- Cancel Button -->
<button
type="button"
(click)="onCancel()"
data-what="cancel-button"
data-which="registration-form">
Cancel
</button>
<!-- Delete Button with Confirmation -->
<button
(click)="onDelete(item)"
data-what="delete-button"
[attr.data-which]="item.id"
[attr.data-status]="item.canDelete ? 'enabled' : 'disabled'">
Delete
</button>
<!-- Icon Button -->
<button
(click)="toggleMenu()"
data-what="menu-button"
data-which="main-nav"
aria-label="Toggle menu">
<i class="icon-menu"></i>
</button>
<!-- Custom Button Component -->
<ui-button
(click)="save()"
data-what="save-button"
data-which="order-form">
Save Order
</ui-button>
Inputs
<!-- Text Input -->
<input
type="text"
[(ngModel)]="email"
placeholder="Email address"
data-what="email-input"
data-which="registration-form" />
<!-- Textarea -->
<textarea
[(ngModel)]="comments"
data-what="comments-textarea"
data-which="feedback-form"
rows="4"></textarea>
<!-- Number Input with State -->
<input
type="number"
[(ngModel)]="quantity"
data-what="quantity-input"
data-which="order-form"
[attr.data-min]="minQuantity"
[attr.data-max]="maxQuantity" />
<!-- Search Input -->
<input
type="search"
[(ngModel)]="searchTerm"
(input)="onSearch()"
placeholder="Search products..."
data-what="search-input"
data-which="product-catalog" />
<!-- Password Input -->
<input
type="password"
[(ngModel)]="password"
data-what="password-input"
data-which="login-form" />
Select/Dropdown
<!-- Basic Select -->
<select
[(ngModel)]="selectedStatus"
data-what="status-select"
data-which="order-filter">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
<!-- Custom Dropdown Component -->
<ui-dropdown
[(value)]="selectedCategory"
data-what="category-dropdown"
data-which="product-filter">
</ui-dropdown>
Checkboxes and Radios
<!-- Checkbox -->
<label>
<input
type="checkbox"
[(ngModel)]="agreedToTerms"
data-what="terms-checkbox"
data-which="registration-form" />
I agree to the terms
</label>
<!-- Radio Group -->
<div data-what="payment-radio-group" data-which="checkout-form">
<label>
<input
type="radio"
name="payment"
value="credit"
[(ngModel)]="paymentMethod"
data-what="payment-radio"
data-which="credit-card" />
Credit Card
</label>
<label>
<input
type="radio"
name="payment"
value="paypal"
[(ngModel)]="paymentMethod"
data-what="payment-radio"
data-which="paypal" />
PayPal
</label>
</div>
Links
<!-- Static Link -->
<a
routerLink="/about"
data-what="nav-link"
data-which="about">
About Us
</a>
<!-- Dynamic Link with ID -->
<a
[routerLink]="['/products', product.id]"
data-what="product-link"
[attr.data-which]="product.id">
{{ product.name }}
</a>
<!-- External Link -->
<a
href="https://example.com"
target="_blank"
data-what="external-link"
data-which="documentation">
Documentation
</a>
<!-- Action Link (not navigation) -->
<a
(click)="downloadReport()"
data-what="download-link"
data-which="sales-report">
Download Report
</a>
Lists and Tables
<!-- Dynamic List with @for -->
<ul data-what="product-list" data-which="catalog">
@for (product of products; track product.id) {
<li
(click)="selectProduct(product)"
data-what="list-item"
[attr.data-which]="product.id"
[attr.data-status]="product.stock > 0 ? 'in-stock' : 'out-of-stock'">
{{ product.name }}
</li>
}
</ul>
<!-- Table Row -->
<table data-what="orders-table" data-which="customer-orders">
<tbody>
@for (order of orders; track order.id) {
<tr
data-what="table-row"
[attr.data-which]="order.id">
<td>{{ order.id }}</td>
<td>{{ order.date }}</td>
<td>
<button
data-what="view-button"
[attr.data-which]="order.id">
View
</button>
</td>
</tr>
}
</tbody>
</table>
Dialogs and Modals
<!-- Confirmation Dialog -->
<div
*ngIf="showDialog"
data-what="confirmation-dialog"
data-which="delete-item">
<h2>Confirm Deletion</h2>
<p>Are you sure you want to delete this item?</p>
<button
(click)="confirmDelete()"
data-what="confirm-button"
data-which="delete-dialog">
Delete
</button>
<button
(click)="cancelDelete()"
data-what="cancel-button"
data-which="delete-dialog">
Cancel
</button>
</div>
<!-- Info Dialog with Close -->
<div
data-what="info-dialog"
data-which="welcome-message">
<button
(click)="closeDialog()"
data-what="close-button"
data-which="dialog">
×
</button>
<div data-what="dialog-content" data-which="welcome">
<h2>Welcome!</h2>
<p>Thank you for joining us.</p>
</div>
</div>
Patterns by Component Type
Form Components
<form data-what="user-form" data-which="registration">
<!-- Field inputs -->
<input
data-what="username-input"
data-which="registration-form"
type="text" />
<input
data-what="email-input"
data-which="registration-form"
type="email" />
<!-- Action buttons -->
<button
data-what="submit-button"
data-which="registration-form"
type="submit">
Submit
</button>
<button
data-what="cancel-button"
data-which="registration-form"
type="button">
Cancel
</button>
</form>
List/Table Components
<!-- Each item needs unique data-which -->
@for (item of items; track item.id) {
<div
data-what="list-item"
[attr.data-which]="item.id">
<span data-what="item-name" [attr.data-which]="item.id">
{{ item.name }}
</span>
<button
data-what="edit-button"
[attr.data-which]="item.id">
Edit
</button>
<button
data-what="delete-button"
[attr.data-which]="item.id">
Delete
</button>
</div>
}
Navigation Components
<nav data-what="main-navigation" data-which="header">
<a
routerLink="/dashboard"
data-what="nav-link"
data-which="dashboard">
Dashboard
</a>
<a
routerLink="/orders"
data-what="nav-link"
data-which="orders">
Orders
</a>
<a
routerLink="/customers"
data-what="nav-link"
data-which="customers">
Customers
</a>
</nav>
<!-- Breadcrumbs -->
<nav data-what="breadcrumb" data-which="page-navigation">
@for (crumb of breadcrumbs; track $index) {
<a
[routerLink]="crumb.url"
data-what="breadcrumb-link"
[attr.data-which]="crumb.id">
{{ crumb.label }}
</a>
}
</nav>
Dialog/Modal Components
<!-- All dialog buttons need clear identifiers -->
<div data-what="modal" data-which="user-settings">
<button
data-what="close-button"
data-which="modal">
Close
</button>
<button
data-what="save-button"
data-which="modal">
Save Changes
</button>
<button
data-what="reset-button"
data-which="modal">
Reset to Defaults
</button>
</div>
Dynamic Attributes
Using Angular Binding
When values need to be dynamic, use Angular's attribute binding:
<!-- Static (simple values) -->
<button data-what="submit-button" data-which="form">
<!-- Dynamic (from component properties) -->
<button
data-what="submit-button"
[attr.data-which]="formId">
<!-- Dynamic (from loop variables) -->
@for (item of items; track item.id) {
<div
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-status]="item.status"
[attr.data-index]="$index">
</div>
}
<!-- Dynamic (computed values) -->
<button
data-what="action-button"
[attr.data-which]="'customer-' + customerId + '-' + action">
</button>
Loop Variables
Angular's @for provides special variables:
@for (item of items; track item.id; let idx = $index; let isFirst = $first) {
<div
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-index]="idx"
[attr.data-first]="isFirst">
{{ item.name }}
</div>
}
Best Practices
Do's ✅
-
Add to ALL interactive elements
- Buttons, inputs, links, clickable elements
- Custom components that handle user interaction
- Form controls and navigation items
-
Use consistent naming
- Follow the naming patterns (e.g.,
*-button,*-input) - Use kebab-case consistently
- Be descriptive but concise
- Follow the naming patterns (e.g.,
-
Ensure uniqueness
data-whichmust be unique within the view- Use item IDs for list items:
[attr.data-which]="item.id" - Combine context when needed:
data-which="form-primary-submit"
-
Use Angular binding for dynamic values
[attr.data-which]="item.id"✅data-which="{{ item.id }}"❌ (avoid interpolation)
-
Document complex patterns
- Add comments for non-obvious attribute choices
- Document the expected test selectors
-
Keep attributes updated
- Update when element purpose changes
- Remove when elements are removed
- Maintain consistency across refactoring
Don'ts ❌
-
Don't include sensitive data
- ❌
data-which="password-{{ userPassword }}" - ❌
data-token="{{ authToken }}" - ❌
data-ssn="{{ socialSecurity }}"
- ❌
-
Don't use generic values
- ❌
data-what="button"(too generic) - ✅
data-what="submit-button"(specific)
- ❌
-
Don't duplicate
data-whichin the same view- ❌ Two buttons with
data-which="primary" - ✅
data-which="form-primary"anddata-which="dialog-primary"
- ❌ Two buttons with
-
Don't rely only on index for lists
- ❌
[attr.data-which]="$index"(changes when list reorders) - ✅
[attr.data-which]="item.id"(stable identifier)
- ❌
-
Don't forget about custom components
- Custom components need attributes too
- Attributes should be on the component tag, not just internal elements
Validation
Coverage Check
Ensure all interactive elements have E2E attributes:
# Count interactive elements
grep -E '\(click\)|routerLink|button|input|select|textarea' component.html | wc -l
# Count elements with data-what
grep -c 'data-what=' component.html
# Find elements missing E2E attributes
grep -E '\(click\)|button' component.html | grep -v 'data-what='
Uniqueness Check
Verify no duplicate data-which values in the same template:
// In component tests
it('should have unique data-which attributes', () => {
const elements = fixture.nativeElement.querySelectorAll('[data-which]');
const dataWhichValues = Array.from(elements).map(
(el: any) => el.getAttribute('data-which')
);
const uniqueValues = new Set(dataWhichValues);
expect(dataWhichValues.length).toBe(uniqueValues.size);
});
Validation Checklist
- All buttons have
data-whatanddata-which - All inputs have
data-whatanddata-which - All links have
data-whatanddata-which - All clickable elements have attributes
- Dynamic lists use
[attr.data-which]="item.id" - No duplicate
data-whichvalues in the same view - No sensitive data in attributes
- Custom components have attributes
- Attributes use kebab-case
- Coverage: 100% of interactive elements
Testing Integration
Using E2E Attributes in Tests
Unit Tests (Angular Testing Utilities):
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('MyComponent', () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
});
it('should have submit button with E2E attributes', () => {
const button = fixture.nativeElement.querySelector(
'[data-what="submit-button"][data-which="registration-form"]'
);
expect(button).toBeTruthy();
expect(button.textContent).toContain('Submit');
});
it('should have unique data-which for list items', () => {
const items = fixture.nativeElement.querySelectorAll('[data-what="list-item"]');
const dataWhichValues = Array.from(items).map(
(item: any) => item.getAttribute('data-which')
);
// All should have unique IDs
const uniqueValues = new Set(dataWhichValues);
expect(dataWhichValues.length).toBe(uniqueValues.size);
});
});
E2E Tests (Playwright):
import { test, expect } from '@playwright/test';
test('user registration flow', async ({ page }) => {
await page.goto('/register');
// Fill form using E2E attributes
await page.fill(
'[data-what="username-input"][data-which="registration-form"]',
'johndoe'
);
await page.fill(
'[data-what="email-input"][data-which="registration-form"]',
'john@example.com'
);
// Click submit using E2E attributes
await page.click(
'[data-what="submit-button"][data-which="registration-form"]'
);
// Verify success
await expect(page.locator('[data-what="success-message"]')).toBeVisible();
});
E2E Tests (Cypress):
describe('Order Management', () => {
it('should edit an order', () => {
cy.visit('/orders');
// Find specific order by ID using data-which
cy.get('[data-what="list-item"][data-which="order-123"]')
.should('be.visible');
// Click edit button for that specific order
cy.get('[data-what="edit-button"][data-which="order-123"]')
.click();
// Update quantity
cy.get('[data-what="quantity-input"][data-which="order-form"]')
.clear()
.type('5');
// Save changes
cy.get('[data-what="save-button"][data-which="order-form"]')
.click();
});
});
Documentation in Templates
Add comment blocks to document E2E attributes:
<!--
E2E Test Attributes:
- data-what="submit-button" data-which="registration-form" - Main form submission
- data-what="cancel-button" data-which="registration-form" - Cancel registration
- data-what="username-input" data-which="registration-form" - Username field
- data-what="email-input" data-which="registration-form" - Email field
- data-what="password-input" data-which="registration-form" - Password field
-->
<form data-what="registration-form" data-which="user-signup">
<!-- Form content -->
</form>
Related Documentation
- ARIA Accessibility Attributes - Accessibility guidance
- Combined Patterns - Examples with E2E + ARIA together
- Testing Guidelines:
docs/guidelines/testing.md- Project testing standards - CLAUDE.md: Project code quality requirements
Summary
E2E testing attributes are essential for:
- ✅ Stable, maintainable automated tests
- ✅ Clear communication between developers and QA
- ✅ Tests that survive styling and structural changes
- ✅ Self-documenting code that expresses intent
- ✅ Reliable CI/CD pipelines
Always add data-what and data-which to every interactive element.