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>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
a6c51f757a
commit
c494ace5d8
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user