Files
tatlock-ui/docs/TESTING.md
T
Jeroen SchweitzerandClaude Opus 4.5 c494ace5d8
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m5s
feat: add URL deep-linking for DataGrids
- Add PageUrlState utility for URL ↔ state serialization
- Add column `id` field for unique column identification in URLs
- Update idSelector to return String for URL compatibility
- All DataGrid pages now support URL params: search, sort, order, id
- Browser URL updates via replaceState (no GoRouter rebuilds)
- Add FilterPanelSemantics for filter panel semantic IDs
- Add TESTING.md documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 13:59:08 +01:00

9.3 KiB

Testing & Automation Guide

This document covers automated testing patterns for Tatlock UI, focusing on semantic identifiers that enable reliable UI automation.

Overview

Tatlock UI uses Flutter's Semantics tree to expose stable identifiers for automated testing. These identifiers are accessible to:

  • Puppeteer (via Chrome DevTools accessibility API)
  • Appium (via accessibility labels)
  • WebDriver (via ARIA attributes)
  • Flutter integration tests

The semantics system is enabled on web in main.dart:

if (kIsWeb) {
  SemanticsBinding.instance.ensureSemantics();
}

Semantic Identifiers

All semantic IDs are centralized in lib/core/semantics/semantic_ids.dart. This provides:

  1. Stable selectors - IDs don't change with UI refactoring
  2. Type safety - Compile-time verification of ID usage
  3. Discoverability - Single source of truth for automation targets

Available ID Classes

Class Purpose Example IDs
ProfileSemantics User profile dropdown profile_button, profile_menu_settings
RoomTabSemantics Main navigation tabs roomTab_frontHall, roomTab_controlRoom
NavSemantics Side navigation panel nav_panel, nav_item_{id}
DataGridSemantics Data tables dataGrid_row_{id}, dataGrid_search
DialogSemantics Modal dialogs dialog_confirm, dialog_cancel
SettingsSemantics Settings page settings_theme, settings_defaultRoom
StateSemantics Loading/error states state_auth_loading, snackbar_{type}

ID Naming Convention

{area}_{component}_{identifier}
  • area: Feature or section (e.g., profile, nav, dataGrid)
  • component: Widget type (e.g., menu, button, row)
  • identifier: Specific item (e.g., light, settings, selectAll)

Examples:

  • profile_menu_theme_dark - Dark theme option in profile menu
  • dataGrid_row_abc123 - Row with ID "abc123" in data grid
  • nav_item_containers - Containers nav item

Adding Semantics to Widgets

Method 1: Direct Semantics Widget

Use Flutter's Semantics widget with the identifier property:

import 'package:tatlock_ui/core/semantics/semantic_ids.dart';

Semantics(
  identifier: ProfileSemantics.button,
  label: 'Open profile menu',
  button: true,
  child: IconButton(
    icon: Icon(Icons.person),
    onPressed: () => ...,
  ),
)

Method 2: SemanticWidget Wrapper

Use the convenience wrapper from lib/core/semantics/semantic_widget.dart:

import 'package:tatlock_ui/core/semantics/semantic_ids.dart';
import 'package:tatlock_ui/core/semantics/semantic_widget.dart';

SemanticWidget(
  id: DataGridSemantics.search,
  label: 'Search data grid',
  textField: true,
  child: TextField(
    decoration: InputDecoration(hintText: 'Search...'),
  ),
)

Method 3: Extension Method

Use the withSemantics extension for inline wrapping:

TextField(
  decoration: InputDecoration(hintText: 'Search...'),
).withSemantics(
  id: DataGridSemantics.search,
  label: 'Search data grid',
)

Dynamic IDs

For lists and grids, use the generator methods:

// Row in a data grid
Semantics(
  identifier: DataGridSemantics.row(item.id),  // "dataGrid_row_abc123"
  child: DataGridRow(item: item),
)

// Navigation item
Semantics(
  identifier: NavSemantics.item(route.id),  // "nav_item_containers"
  child: NavItem(route: route),
)

// Bulk action button
Semantics(
  identifier: DataGridSemantics.bulkAction('delete'),  // "dataGrid_bulk_delete"
  child: IconButton(icon: Icon(Icons.delete), ...),
)

Querying from Puppeteer

Puppeteer can query semantic identifiers via Chrome's accessibility tree:

// Connect to Chrome with DevTools protocol
const browser = await puppeteer.connect({
  browserURL: 'http://localhost:9222'
});
const page = await browser.newPage();

// Get accessibility snapshot
const snapshot = await page.accessibility.snapshot({ interestingOnly: false });

// Find element by semantic identifier
function findBySemanticId(node, id) {
  if (node.name === id || node.description === id) {
    return node;
  }
  for (const child of node.children || []) {
    const found = findBySemanticId(child, id);
    if (found) return found;
  }
  return null;
}

// Example: Find profile button
const profileButton = findBySemanticId(snapshot, 'profile_button');

// Example: Find a specific data grid row
const row = findBySemanticId(snapshot, 'dataGrid_row_abc123');

Using Chrome DevTools MCP

With the Chrome DevTools MCP server, you can query semantics directly:

// Take a snapshot (returns accessibility tree)
const snapshot = await mcp__chrome_devtools__take_snapshot();

// Click by semantic ID (uid in snapshot)
await mcp__chrome_devtools__click({ uid: 'profile_button' });

// Fill input by semantic ID
await mcp__chrome_devtools__fill({
  uid: 'dataGrid_search',
  value: 'my search query'
});

Best Practices

1. Add Semantics to Interactive Elements

Every clickable, tappable, or input element should have a semantic identifier:

// Buttons
Semantics(
  identifier: 'myFeature_submit',
  button: true,
  label: 'Submit form',
  child: ElevatedButton(...),
)

// Text fields
Semantics(
  identifier: 'myFeature_email',
  textField: true,
  label: 'Email address',
  child: TextField(...),
)

// Checkboxes
Semantics(
  identifier: 'myFeature_rememberMe',
  checked: isChecked,
  label: 'Remember me',
  child: Checkbox(...),
)

2. Use Meaningful Labels

Labels help both accessibility tools and test debugging:

// Good - descriptive label
Semantics(
  identifier: DataGridSemantics.rowAction(item.id, 'delete'),
  label: 'Delete ${item.name}',
  button: true,
  child: ...,
)

// Bad - no context
Semantics(
  identifier: 'btn1',
  child: ...,
)

3. Register New IDs Centrally

Always add new semantic IDs to semantic_ids.dart:

/// My new feature IDs.
abstract class MyFeatureSemantics {
  static const submitButton = 'myFeature_submit';
  static const cancelButton = 'myFeature_cancel';
  static const nameField = 'myFeature_name';

  /// Generate ID for a list item.
  static String item(String id) => 'myFeature_item_$id';
}

4. Test ID Stability

Semantic IDs should remain stable across releases. When refactoring:

  • Keep existing IDs unchanged
  • Add deprecation comments if IDs must change
  • Update automation tests when IDs change

5. Exclude Decorative Elements

Don't add semantic IDs to purely decorative elements:

// Decorative icon - no semantics needed
Icon(Icons.star, color: Colors.yellow)

// Interactive icon - needs semantics
Semantics(
  identifier: 'rating_star_3',
  button: true,
  label: 'Rate 3 stars',
  child: IconButton(
    icon: Icon(Icons.star),
    onPressed: () => rate(3),
  ),
)

DataGrid Semantic Patterns

The DataGrid component has comprehensive semantic coverage:

dataGrid                     - The grid container
dataGrid_search              - Search input field
dataGrid_search_clear        - Clear search button
dataGrid_selectAll           - Select all checkbox
dataGrid_header_{columnId}   - Column header (sortable)
dataGrid_row_{itemId}        - Row container
dataGrid_row_{itemId}_checkbox    - Row selection checkbox
dataGrid_row_{itemId}_actions     - Row actions menu trigger
dataGrid_row_{itemId}_action_{actionId}  - Specific row action
dataGrid_bulk_{actionId}     - Bulk action button
dataGrid_bulk_clear          - Clear selection button
dataGrid_loading             - Loading indicator
dataGrid_empty               - Empty state message
dataGrid_error               - Error state message
dataGrid_refresh             - Refresh button

Example: Automating DataGrid Selection

// Select all rows
await click('dataGrid_selectAll');

// Select specific row
await click('dataGrid_row_abc123_checkbox');

// Perform bulk delete
await click('dataGrid_bulk_delete');

// Confirm in dialog
await click('dialog_confirm');

Debugging Semantics

Flutter DevTools

  1. Open Flutter DevTools
  2. Go to "Inspector" tab
  3. Enable "Semantics" overlay
  4. Click widgets to see their semantic properties

Chrome DevTools

  1. Open DevTools (F12)
  2. Go to "Accessibility" tab
  3. Inspect the accessibility tree
  4. Search for semantic identifiers

Programmatic Inspection

// In a test, dump the semantics tree
debugDumpSemanticsTree();

// Check if semantics are enabled
print('Semantics enabled: ${SemanticsBinding.instance.semanticsEnabled}');

Integration with URL Routing

For deep-linkable test scenarios, semantic IDs work with URL query parameters:

/control-room/containers?selected=abc123

Automation can:

  1. Navigate to URL with query params
  2. Verify selection state via dataGrid_row_abc123_checkbox (checked: true)
  3. Interact with selected rows via semantic IDs

See URL Routing section for query parameter patterns.

Checklist for New Features

When adding a new feature, ensure semantic coverage:

  • Add semantic ID class to semantic_ids.dart
  • Wrap all buttons with Semantics + identifier
  • Wrap all inputs with Semantics + identifier
  • Wrap list/grid items with dynamic IDs
  • Add labels for accessibility
  • Test that IDs appear in accessibility snapshot
  • Document IDs in this file if they establish new patterns