- 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>
369 lines
9.3 KiB
Markdown
369 lines
9.3 KiB
Markdown
# 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`:
|
|
|
|
```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:
|
|
|
|
```dart
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```dart
|
|
TextField(
|
|
decoration: InputDecoration(hintText: 'Search...'),
|
|
).withSemantics(
|
|
id: DataGridSemantics.search,
|
|
label: 'Search data grid',
|
|
)
|
|
```
|
|
|
|
### Dynamic IDs
|
|
|
|
For lists and grids, use the generator methods:
|
|
|
|
```dart
|
|
// 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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```dart
|
|
// 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:
|
|
|
|
```dart
|
|
// 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`:
|
|
|
|
```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:
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```dart
|
|
// 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](#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
|