mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
- 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
28 KiB
28 KiB
Combined E2E & ARIA Patterns - Real-World Examples
This reference demonstrates how to use E2E testing attributes and ARIA accessibility attributes together in production-ready components.
Table of Contents
- Overview
- Complete Form Example
- Product Listing
- Shopping Cart
- Modal Dialogs
- Navigation Patterns
- Data Tables
- Search and Filters
- Notifications and Alerts
- Multi-Step Forms
- Best Practices Summary
Overview
Combining E2E and ARIA attributes creates components that are:
- Testable: Reliable selectors for automated testing
- Accessible: Usable with assistive technologies
- Maintainable: Attributes survive refactoring
- Compliant: Meet WCAG and legal requirements
Pattern: Every interactive element should have both types of attributes.
Complete Form Example
Registration Form
<form
(ngSubmit)="onSubmit()"
data-what="registration-form"
data-which="user-signup"
role="form"
aria-labelledby="form-title">
<h2 id="form-title">Create Account</h2>
<!-- Username Field -->
<div class="form-field">
<label id="username-label" for="username-input">
Username <span aria-label="required">*</span>
</label>
<input
id="username-input"
type="text"
[(ngModel)]="username"
name="username"
data-what="username-input"
data-which="registration-form"
aria-labelledby="username-label"
aria-required="true"
[attr.aria-invalid]="usernameError ? 'true' : null"
aria-describedby="username-hint username-error"
(blur)="validateUsername()" />
<span id="username-hint" class="hint">
3-20 characters, letters and numbers only
</span>
@if (usernameError) {
<span
id="username-error"
class="error"
data-what="error-message"
data-which="username-field"
role="alert"
aria-live="polite">
{{ usernameError }}
</span>
}
</div>
<!-- Email Field -->
<div class="form-field">
<label id="email-label" for="email-input">
Email Address <span aria-label="required">*</span>
</label>
<input
id="email-input"
type="email"
[(ngModel)]="email"
name="email"
data-what="email-input"
data-which="registration-form"
aria-labelledby="email-label"
aria-required="true"
[attr.aria-invalid]="emailError ? 'true' : null"
aria-describedby="email-hint email-error"
(blur)="validateEmail()" />
<span id="email-hint" class="hint">
We'll never share your email
</span>
@if (emailError) {
<span
id="email-error"
class="error"
data-what="error-message"
data-which="email-field"
role="alert"
aria-live="polite">
{{ emailError }}
</span>
}
</div>
<!-- Password Field -->
<div class="form-field">
<label id="password-label" for="password-input">
Password <span aria-label="required">*</span>
</label>
<input
id="password-input"
type="password"
[(ngModel)]="password"
name="password"
data-what="password-input"
data-which="registration-form"
aria-labelledby="password-label"
aria-required="true"
[attr.aria-invalid]="passwordError ? 'true' : null"
aria-describedby="password-hint password-error"
(input)="checkPasswordStrength()" />
<span id="password-hint" class="hint">
At least 8 characters with uppercase, lowercase, and numbers
</span>
<!-- Password Strength Indicator -->
<div
class="password-strength"
data-what="password-strength"
data-which="registration-form"
[attr.data-strength]="passwordStrength"
role="status"
aria-live="polite"
aria-label="Password strength indicator">
Strength: {{ passwordStrength }}
</div>
@if (passwordError) {
<span
id="password-error"
class="error"
data-what="error-message"
data-which="password-field"
role="alert"
aria-live="polite">
{{ passwordError }}
</span>
}
</div>
<!-- Terms Checkbox -->
<div class="form-field">
<label>
<input
id="terms-checkbox"
type="checkbox"
[(ngModel)]="agreedToTerms"
name="terms"
data-what="terms-checkbox"
data-which="registration-form"
aria-required="true"
aria-describedby="terms-label" />
<span id="terms-label">
I agree to the <a href="/terms" target="_blank" aria-label="Terms of service (opens in new tab)">Terms of Service</a>
</span>
</label>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button
type="submit"
[disabled]="!isFormValid"
data-what="submit-button"
data-which="registration-form"
[attr.aria-disabled]="!isFormValid"
aria-label="Submit registration form">
Create Account
</button>
<button
type="button"
(click)="onCancel()"
data-what="cancel-button"
data-which="registration-form"
aria-label="Cancel registration">
Cancel
</button>
</div>
<!-- Loading State -->
@if (isSubmitting) {
<div
class="loading"
data-what="loading-indicator"
data-which="registration-form"
role="status"
aria-live="polite"
aria-label="Submitting registration">
<span aria-hidden="true">⏳</span>
Submitting...
</div>
}
</form>
Product Listing
Product Grid with Filters
<div class="product-catalog">
<!-- Search and Filter Section -->
<div
class="filters"
data-what="filter-section"
data-which="product-catalog"
role="search"
aria-label="Product search and filters">
<!-- Search Input -->
<input
type="search"
[(ngModel)]="searchQuery"
(input)="onSearch()"
placeholder="Search products..."
data-what="search-input"
data-which="product-catalog"
aria-label="Search products"
aria-describedby="search-hint" />
<span id="search-hint" class="sr-only">
Search by product name or SKU
</span>
<!-- Category Filter -->
<select
[(ngModel)]="selectedCategory"
(change)="applyFilters()"
data-what="category-select"
data-which="product-filter"
aria-label="Filter by category">
<option value="">All Categories</option>
@for (category of categories; track category.id) {
<option [value]="category.id">{{ category.name }}</option>
}
</select>
<!-- Price Range Filter -->
<div class="price-range">
<label id="price-range-label">Price Range</label>
<input
type="range"
[(ngModel)]="maxPrice"
(change)="applyFilters()"
min="0"
max="1000"
data-what="price-slider"
data-which="product-filter"
aria-labelledby="price-range-label"
aria-valuemin="0"
aria-valuemax="1000"
[attr.aria-valuenow]="maxPrice"
[attr.aria-valuetext]="'Maximum price: $' + maxPrice" />
<output>${{ maxPrice }}</output>
</div>
<!-- Results Count -->
<div
class="results-count"
data-what="results-count"
data-which="product-catalog"
role="status"
aria-live="polite"
aria-atomic="true">
Showing {{ filteredProducts.length }} of {{ totalProducts }} products
</div>
</div>
<!-- Product Grid -->
<div
class="product-grid"
data-what="product-grid"
data-which="catalog"
role="list"
aria-label="Product list">
@for (product of filteredProducts; track product.id) {
<article
class="product-card"
data-what="product-card"
[attr.data-which]="product.id"
[attr.data-status]="product.stock > 0 ? 'in-stock' : 'out-of-stock'"
role="listitem"
[attr.aria-label]="product.name + ', $' + product.price">
<!-- Product Image -->
<a
[routerLink]="['/products', product.id]"
data-what="product-link"
[attr.data-which]="product.id"
[attr.aria-label]="'View details for ' + product.name">
<img
[src]="product.image"
[alt]="product.name"
loading="lazy" />
</a>
<!-- Product Info -->
<div class="product-info">
<h3>
<a
[routerLink]="['/products', product.id]"
data-what="product-link"
[attr.data-which]="product.id">
{{ product.name }}
</a>
</h3>
<p class="price" aria-label="Price">
{{ product.price | currency }}
</p>
<!-- Stock Status -->
<div
class="stock-status"
data-what="stock-badge"
[attr.data-which]="product.id"
[attr.data-status]="product.stock > 0 ? 'in-stock' : 'out-of-stock'"
role="status"
[attr.aria-label]="product.stock > 0 ? 'In stock' : 'Out of stock'">
@if (product.stock > 0) {
<span class="in-stock">In Stock</span>
} @else {
<span class="out-of-stock">Out of Stock</span>
}
</div>
<!-- Add to Cart Button -->
<button
(click)="addToCart(product)"
[disabled]="product.stock === 0"
data-what="add-to-cart-button"
[attr.data-which]="product.id"
[attr.aria-disabled]="product.stock === 0"
[attr.aria-label]="'Add ' + product.name + ' to cart'">
@if (product.stock > 0) {
<span aria-hidden="true">🛒</span> Add to Cart
} @else {
<span>Unavailable</span>
}
</button>
</div>
</article>
}
</div>
<!-- Empty State -->
@if (filteredProducts.length === 0) {
<div
class="empty-state"
data-what="empty-state"
data-which="product-catalog"
role="status"
aria-live="polite">
<p>No products found matching your criteria.</p>
<button
(click)="clearFilters()"
data-what="clear-filters-button"
data-which="product-catalog"
aria-label="Clear all filters">
Clear Filters
</button>
</div>
}
</div>
Shopping Cart
<div
class="shopping-cart"
data-what="shopping-cart"
data-which="checkout"
role="region"
aria-labelledby="cart-title">
<h2 id="cart-title">Shopping Cart</h2>
<!-- Cart Items List -->
<ul
class="cart-items"
data-what="cart-items-list"
data-which="shopping-cart"
role="list"
aria-label="Items in your cart">
@for (item of cartItems; track item.id) {
<li
class="cart-item"
data-what="cart-item"
[attr.data-which]="item.id"
role="listitem">
<!-- Item Image -->
<img
[src]="item.image"
[alt]="item.name"
class="item-image" />
<!-- Item Details -->
<div class="item-details">
<h3>{{ item.name }}</h3>
<p class="item-price" aria-label="Price per item">
{{ item.price | currency }}
</p>
<!-- Quantity Controls -->
<div
class="quantity-controls"
role="group"
aria-labelledby="quantity-label-{{ item.id }}">
<span id="quantity-label-{{ item.id }}" class="sr-only">
Quantity for {{ item.name }}
</span>
<button
(click)="decreaseQuantity(item)"
[disabled]="item.quantity <= 1"
data-what="decrease-button"
[attr.data-which]="item.id"
[attr.aria-disabled]="item.quantity <= 1"
[attr.aria-label]="'Decrease quantity of ' + item.name">
<span aria-hidden="true">−</span>
</button>
<input
type="number"
[(ngModel)]="item.quantity"
(change)="updateQuantity(item)"
min="1"
[max]="item.maxQuantity"
data-what="quantity-input"
[attr.data-which]="item.id"
[attr.aria-label]="'Quantity of ' + item.name"
aria-valuemin="1"
[attr.aria-valuemax]="item.maxQuantity"
[attr.aria-valuenow]="item.quantity" />
<button
(click)="increaseQuantity(item)"
[disabled]="item.quantity >= item.maxQuantity"
data-what="increase-button"
[attr.data-which]="item.id"
[attr.aria-disabled]="item.quantity >= item.maxQuantity"
[attr.aria-label]="'Increase quantity of ' + item.name">
<span aria-hidden="true">+</span>
</button>
</div>
<!-- Remove Button -->
<button
(click)="removeItem(item)"
class="remove-button"
data-what="remove-button"
[attr.data-which]="item.id"
[attr.aria-label]="'Remove ' + item.name + ' from cart'">
<span aria-hidden="true">🗑️</span> Remove
</button>
<!-- Item Subtotal -->
<p
class="item-subtotal"
data-what="item-subtotal"
[attr.data-which]="item.id"
[attr.aria-label]="'Subtotal for ' + item.name">
Subtotal: {{ item.price * item.quantity | currency }}
</p>
</div>
</li>
}
</ul>
<!-- Cart Summary -->
<div
class="cart-summary"
data-what="cart-summary"
data-which="shopping-cart"
role="region"
aria-labelledby="summary-title">
<h3 id="summary-title">Order Summary</h3>
<dl>
<dt>Subtotal:</dt>
<dd data-what="subtotal" data-which="cart-summary">
{{ subtotal | currency }}
</dd>
<dt>Tax:</dt>
<dd data-what="tax" data-which="cart-summary">
{{ tax | currency }}
</dd>
<dt>Shipping:</dt>
<dd data-what="shipping" data-which="cart-summary">
{{ shipping | currency }}
</dd>
<dt><strong>Total:</strong></dt>
<dd data-what="total" data-which="cart-summary">
<strong>{{ total | currency }}</strong>
</dd>
</dl>
<!-- Checkout Button -->
<button
(click)="proceedToCheckout()"
[disabled]="cartItems.length === 0"
class="checkout-button"
data-what="checkout-button"
data-which="shopping-cart"
[attr.aria-disabled]="cartItems.length === 0"
aria-label="Proceed to checkout">
Proceed to Checkout
</button>
</div>
<!-- Empty Cart Message -->
@if (cartItems.length === 0) {
<div
class="empty-cart"
data-what="empty-state"
data-which="shopping-cart"
role="status"
aria-live="polite">
<p>Your cart is empty</p>
<a
routerLink="/products"
data-what="continue-shopping-link"
data-which="shopping-cart"
aria-label="Continue shopping">
Continue Shopping
</a>
</div>
}
</div>
Modal Dialogs
Confirmation Dialog
@if (showDeleteDialog) {
<!-- Overlay -->
<div
class="modal-overlay"
(click)="cancelDelete()"
data-what="modal-overlay"
data-which="delete-confirmation">
</div>
<!-- Dialog -->
<div
class="modal"
data-what="confirmation-dialog"
data-which="delete-item"
role="alertdialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
#dialogElement
tabindex="-1">
<h2 id="dialog-title">Confirm Deletion</h2>
<p id="dialog-description">
Are you sure you want to delete "{{ itemToDelete?.name }}"?
This action cannot be undone.
</p>
<!-- Dialog Actions -->
<div class="dialog-actions">
<button
(click)="confirmDelete()"
class="danger-button"
data-what="confirm-button"
data-which="delete-dialog"
aria-label="Confirm deletion">
Delete
</button>
<button
(click)="cancelDelete()"
data-what="cancel-button"
data-which="delete-dialog"
aria-label="Cancel deletion"
autofocus>
Cancel
</button>
</div>
<!-- Close Button -->
<button
(click)="cancelDelete()"
class="close-button"
data-what="close-button"
data-which="dialog"
aria-label="Close dialog">
<span aria-hidden="true">×</span>
</button>
</div>
}
Navigation Patterns
Main Navigation with Dropdown
<nav
class="main-nav"
data-what="main-navigation"
data-which="header"
role="navigation"
aria-label="Main navigation">
<ul role="menubar">
@for (item of menuItems; track item.id) {
<li role="none">
@if (item.children) {
<!-- Dropdown Menu Item -->
<button
(click)="toggleSubmenu(item.id)"
(keydown.arrowDown)="openSubmenu(item.id)"
[attr.aria-expanded]="item.expanded"
aria-haspopup="menu"
[attr.aria-controls]="'submenu-' + item.id"
data-what="nav-menu-button"
[attr.data-which]="item.id"
role="menuitem">
{{ item.label }}
<span aria-hidden="true">▼</span>
</button>
<!-- Submenu -->
<ul
[id]="'submenu-' + item.id"
[attr.aria-hidden]="!item.expanded"
role="menu"
[attr.aria-label]="item.label + ' submenu'"
data-what="submenu"
[attr.data-which]="item.id">
@for (child of item.children; track child.id) {
<li role="none">
<a
[routerLink]="child.url"
role="menuitem"
data-what="nav-link"
[attr.data-which]="child.id"
[attr.aria-current]="isCurrentPage(child.url) ? 'page' : null">
{{ child.label }}
</a>
</li>
}
</ul>
} @else {
<!-- Simple Link -->
<a
[routerLink]="item.url"
role="menuitem"
data-what="nav-link"
[attr.data-which]="item.id"
[attr.aria-current]="isCurrentPage(item.url) ? 'page' : null">
{{ item.label }}
</a>
}
</li>
}
</ul>
</nav>
Data Tables
Sortable Data Table
<table
class="data-table"
data-what="orders-table"
data-which="customer-dashboard"
role="table"
aria-labelledby="table-title"
aria-describedby="table-description">
<caption id="table-title">Recent Orders</caption>
<p id="table-description" class="sr-only">
A table showing your recent orders with sortable columns
</p>
<thead>
<tr role="row">
<!-- Sortable Column Headers -->
<th scope="col" role="columnheader">
<button
(click)="sortBy('orderId')"
data-what="sort-button"
data-which="order-id-column"
[attr.aria-sort]="getSortDirection('orderId')"
aria-label="Sort by order ID">
Order ID
<span aria-hidden="true">{{ getSortIcon('orderId') }}</span>
</button>
</th>
<th scope="col" role="columnheader">
<button
(click)="sortBy('date')"
data-what="sort-button"
data-which="date-column"
[attr.aria-sort]="getSortDirection('date')"
aria-label="Sort by date">
Date
<span aria-hidden="true">{{ getSortIcon('date') }}</span>
</button>
</th>
<th scope="col" role="columnheader">
<button
(click)="sortBy('total')"
data-what="sort-button"
data-which="total-column"
[attr.aria-sort]="getSortDirection('total')"
aria-label="Sort by total">
Total
<span aria-hidden="true">{{ getSortIcon('total') }}</span>
</button>
</th>
<th scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader">Actions</th>
</tr>
</thead>
<tbody>
@for (order of sortedOrders; track order.id) {
<tr
role="row"
data-what="table-row"
[attr.data-which]="order.id"
[attr.data-status]="order.status">
<td role="cell">{{ order.id }}</td>
<td role="cell">{{ order.date | date }}</td>
<td role="cell">{{ order.total | currency }}</td>
<td role="cell">
<span
class="status-badge"
data-what="status-badge"
[attr.data-which]="order.id"
[attr.data-status]="order.status">
{{ order.status }}
</span>
</td>
<td role="cell">
<button
(click)="viewOrder(order)"
data-what="view-button"
[attr.data-which]="order.id"
[attr.aria-label]="'View order ' + order.id">
View
</button>
<button
(click)="downloadInvoice(order)"
data-what="download-button"
[attr.data-which]="order.id"
[attr.aria-label]="'Download invoice for order ' + order.id">
Invoice
</button>
</td>
</tr>
}
</tbody>
</table>
Search and Filters
Advanced Search with Live Results
<div
class="search-container"
data-what="search-container"
data-which="site-search"
role="search"
aria-label="Site search">
<!-- Search Input with Autocomplete -->
<div class="search-input-wrapper">
<input
type="search"
[(ngModel)]="searchQuery"
(input)="onSearchInput()"
(focus)="showSuggestions = true"
(blur)="hideSuggestions()"
placeholder="Search..."
data-what="search-input"
data-which="site-search"
aria-label="Search site"
aria-autocomplete="list"
[attr.aria-expanded]="showSuggestions && suggestions.length > 0"
[attr.aria-controls]="'search-suggestions'"
[attr.aria-activedescendant]="activeSuggestionId"
role="combobox" />
<!-- Search Button -->
<button
(click)="performSearch()"
data-what="search-button"
data-which="site-search"
aria-label="Perform search">
<span aria-hidden="true">🔍</span>
</button>
</div>
<!-- Search Suggestions -->
@if (showSuggestions && suggestions.length > 0) {
<ul
id="search-suggestions"
class="suggestions"
data-what="suggestions-list"
data-which="site-search"
role="listbox"
aria-label="Search suggestions">
@for (suggestion of suggestions; track suggestion.id; let idx = $index) {
<li
[id]="'suggestion-' + idx"
role="option"
[attr.aria-selected]="idx === activeSuggestionIndex"
(click)="selectSuggestion(suggestion)"
data-what="suggestion-item"
[attr.data-which]="suggestion.id"
[attr.data-index]="idx">
{{ suggestion.text }}
</li>
}
</ul>
}
<!-- Search Results Count -->
@if (searchPerformed) {
<div
class="results-summary"
data-what="results-summary"
data-which="site-search"
role="status"
aria-live="polite"
aria-atomic="true">
Found {{ searchResults.length }} results for "{{ searchQuery }}"
</div>
}
</div>
Notifications and Alerts
Toast Notifications
<!-- Success Toast -->
@if (showSuccessToast) {
<div
class="toast success"
data-what="toast-notification"
data-which="success"
[attr.data-type]="'success'"
role="status"
aria-live="polite"
aria-atomic="true">
<span aria-hidden="true">✓</span>
<span>{{ successMessage }}</span>
<button
(click)="dismissToast('success')"
data-what="dismiss-button"
data-which="success-toast"
aria-label="Dismiss success notification">
<span aria-hidden="true">×</span>
</button>
</div>
}
<!-- Error Alert -->
@if (showErrorAlert) {
<div
class="alert error"
data-what="alert-notification"
data-which="error"
[attr.data-type]="'error'"
role="alert"
aria-live="assertive"
aria-atomic="true">
<span aria-hidden="true">⚠</span>
<span>{{ errorMessage }}</span>
<button
(click)="dismissAlert('error')"
data-what="dismiss-button"
data-which="error-alert"
aria-label="Dismiss error alert">
<span aria-hidden="true">×</span>
</button>
</div>
}
Multi-Step Forms
Wizard with Progress Indicator
<div
class="form-wizard"
data-what="multi-step-form"
data-which="checkout-wizard"
role="region"
aria-labelledby="wizard-title">
<h2 id="wizard-title">Checkout Process</h2>
<!-- Progress Indicator -->
<ol
class="progress-steps"
data-what="progress-indicator"
data-which="checkout-wizard"
role="list"
aria-label="Checkout progress">
@for (step of steps; track step.id; let idx = $index) {
<li
role="listitem"
data-what="progress-step"
[attr.data-which]="step.id"
[attr.data-status]="getStepStatus(idx)"
[attr.aria-current]="currentStep === idx ? 'step' : null">
<span
class="step-number"
[attr.aria-label]="'Step ' + (idx + 1)">
{{ idx + 1 }}
</span>
<span class="step-label">{{ step.label }}</span>
</li>
}
</ol>
<!-- Step Content -->
<div
class="step-content"
data-what="step-content"
[attr.data-which]="'step-' + currentStep"
role="tabpanel"
[attr.aria-labelledby]="'step-label-' + currentStep">
<!-- Content varies by step -->
{{ currentStepContent }}
</div>
<!-- Navigation Buttons -->
<div class="wizard-navigation">
<button
(click)="previousStep()"
[disabled]="currentStep === 0"
data-what="previous-button"
data-which="checkout-wizard"
[attr.aria-disabled]="currentStep === 0"
aria-label="Go to previous step">
← Previous
</button>
@if (currentStep < steps.length - 1) {
<button
(click)="nextStep()"
[disabled]="!isCurrentStepValid"
data-what="next-button"
data-which="checkout-wizard"
[attr.aria-disabled]="!isCurrentStepValid"
aria-label="Go to next step">
Next →
</button>
} @else {
<button
(click)="submitForm()"
[disabled]="!isFormComplete"
data-what="submit-button"
data-which="checkout-wizard"
[attr.aria-disabled]="!isFormComplete"
aria-label="Complete checkout">
Complete Order
</button>
}
</div>
</div>
Best Practices Summary
Every Interactive Element Needs Both
<!-- ✅ GOOD: Both E2E and ARIA attributes -->
<button
data-what="submit-button"
data-which="order-form"
aria-label="Submit order">
Submit
</button>
<!-- ❌ BAD: Missing ARIA -->
<button
data-what="submit-button"
data-which="order-form">
Submit
</button>
<!-- ❌ BAD: Missing E2E attributes -->
<button aria-label="Submit order">
Submit
</button>
Attributes Work Together
- E2E
data-what: Describes element type for testing - ARIA role: Describes element purpose for accessibility
- E2E
data-which: Identifies specific instance for testing - ARIA label: Provides accessible name for screen readers
- E2E
data-*: Additional test context - ARIA states: Dynamic state information
Maintain Both During Updates
When you update HTML:
- Add/update E2E attributes for testing
- Add/update ARIA attributes for accessibility
- Keep both in sync with element's purpose
- Update tests to use new selectors
- Test with screen readers
Related Documentation
- E2E Testing Attributes - Complete E2E attribute reference
- ARIA Accessibility Attributes - Complete ARIA reference
- Main Skill - HTML template skill overview
- Testing Guidelines:
docs/guidelines/testing.md- Project testing standards
Summary
Combining E2E and ARIA attributes creates:
- ✅ Testable components with reliable selectors
- ✅ Accessible interfaces for all users
- ✅ Maintainable code that survives refactoring
- ✅ Compliant applications meeting legal requirements
- ✅ Professional quality expected in production
Always use both attribute types together on every interactive element.