feat: add URL deep-linking for DataGrids
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m5s

- 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>
This commit is contained in:
Jeroen Schweitzer
2026-01-05 13:59:08 +01:00
co-authored by Claude Opus 4.5
parent a6c51f757a
commit c494ace5d8
19 changed files with 952 additions and 28 deletions
+26
View File
@@ -20,6 +20,7 @@ lib/
│ ├── auth/ # Authentik OIDC integration
│ ├── config/ # Environment configuration
│ ├── error/ # Error types and handling
│ ├── semantics/ # Semantic IDs for automation
│ └── theme/ # Material 3 theming
├── routing/ # go_router configuration
├── shared/ # Reusable components
@@ -544,3 +545,28 @@ import 'package:tatlock_ui/features/containers/domain/entities/container.dart';
// Bad - importing model in presentation
import '../data/models/container_model.dart'; // Don't do this
```
## Semantic Identifiers for Automation
All interactive widgets should have semantic identifiers for UI automation. This enables reliable testing with Puppeteer, Appium, and other automation tools.
### Quick Reference
```dart
import 'package:tatlock_ui/core/semantics/semantic_ids.dart';
// Wrap interactive widgets with Semantics
Semantics(
identifier: DataGridSemantics.row(item.id),
label: 'Select ${item.name}',
child: MyRowWidget(item: item),
)
```
### Key Points
1. **Central ID Registry** - All IDs defined in `lib/core/semantics/semantic_ids.dart`
2. **Naming Convention** - `{area}_{component}_{identifier}` (e.g., `dataGrid_row_abc123`)
3. **Web Enabled** - Semantics tree exposed via `SemanticsBinding.instance.ensureSemantics()` in `main.dart`
For complete documentation on semantic patterns, automation queries, and best practices, see **[TESTING.md](./TESTING.md)**.
+368
View File
@@ -0,0 +1,368 @@
# 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