Files
ISA-Frontend/.claude/skills/html-template/references/e2e-attributes.md
Lorenz Hilpert bf30ec1213 feat(skills): create html-template skill for E2E and ARIA attributes
- 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
2025-10-29 13:21:29 +01:00

20 KiB
Raw Blame History

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

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 button
  • data-which="secondary" - Secondary action button
  • data-which="main-search" - Main search input
  • data-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 customer
  • data-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>
<!-- 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

  1. Add to ALL interactive elements

    • Buttons, inputs, links, clickable elements
    • Custom components that handle user interaction
    • Form controls and navigation items
  2. Use consistent naming

    • Follow the naming patterns (e.g., *-button, *-input)
    • Use kebab-case consistently
    • Be descriptive but concise
  3. Ensure uniqueness

    • data-which must 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"
  4. Use Angular binding for dynamic values

    • [attr.data-which]="item.id"
    • data-which="{{ item.id }}" (avoid interpolation)
  5. Document complex patterns

    • Add comments for non-obvious attribute choices
    • Document the expected test selectors
  6. Keep attributes updated

    • Update when element purpose changes
    • Remove when elements are removed
    • Maintain consistency across refactoring

Don'ts

  1. Don't include sensitive data

    • data-which="password-{{ userPassword }}"
    • data-token="{{ authToken }}"
    • data-ssn="{{ socialSecurity }}"
  2. Don't use generic values

    • data-what="button" (too generic)
    • data-what="submit-button" (specific)
  3. Don't duplicate data-which in the same view

    • Two buttons with data-which="primary"
    • data-which="form-primary" and data-which="dialog-primary"
  4. Don't rely only on index for lists

    • [attr.data-which]="$index" (changes when list reorders)
    • [attr.data-which]="item.id" (stable identifier)
  5. 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-what and data-which
  • All inputs have data-what and data-which
  • All links have data-what and data-which
  • All clickable elements have attributes
  • Dynamic lists use [attr.data-which]="item.id"
  • No duplicate data-which values 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>
  • 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.