E2E Testing with Patrol
Comprehensive end-to-end testing framework for all Flutter shells using Patrol.
Overview
The Patrol E2E Testing Suite provides automated testing across all 12 Flutter shells, catching API endpoint issues, integration bugs, and UI regressions before deployment. Tests run on every PR to ensure quality.
Test Architecture
frontend/integration_test/
├── common/
│ ├── test_fixtures.dart # Shared mock data
│ ├── test_utils.dart # Shared utilities
│ └── api_mocks.dart # API mock helpers
├── staff/
│ ├── auth_test.dart # PIN, biometric login
│ ├── orders_test.dart # Order workflow
│ └── offline_test.dart # Offline mode
├── customer/
│ ├── auth_test.dart # Email/social login
│ ├── ordering_test.dart # Full order flow
│ └── tracking_test.dart # Order tracking
├── admin/
│ ├── auth_test.dart # SSO login
│ ├── tenants_test.dart # Tenant CRUD
│ └── users_test.dart # User management
├── cockpit/
│ ├── auth_test.dart # SSO login
│ ├── metrics_test.dart # Real-time metrics
│ └── alerts_test.dart # Alert management
└── shared/
├── navigation_test.dart # Deep links, routes
└── error_handling_test.dart # Error states
Test Categories
Authentication Tests
| Shell | Test Cases |
|---|---|
| Staff | PIN login, biometric, session expiry |
| Customer | Email signup, social login (Google, Apple) |
| Admin | SSO (Google), MFA flow |
| Cockpit | SSO (Google), impersonation |
| Kiosk | Device activation, pairing |
API Connectivity Tests
| Category | Validations |
|---|---|
| Endpoints | Correct /api/v1 prefix |
| Status Codes | 200, 201, 400, 401, 403, 404, 500 |
| Response Format | JSON structure validation |
| Authentication | Token refresh, expiry handling |
Navigation Tests
| Category | Test Cases |
|---|---|
| Deep Links | Direct URL navigation |
| Route Guards | Auth-required redirects |
| Shell Switching | Multi-shell app transitions |
| Back Navigation | History stack handling |
Offline Mode Tests
| Category | Test Cases |
|---|---|
| Detection | Online/offline state detection |
| Queue | Operation queueing offline |
| Sync | Automatic sync on reconnect |
| Graceful Degradation | UI feedback for offline state |
Writing Tests
Basic Test Structure
import 'package:patrol/patrol.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
patrolTest('Staff login flow works', (tester) async {
// Arrange
await tester.pumpWidgetAndSettle(const StaffApp());
// Act - Enter PIN
await tester.enterText(
find.byKey(const Key('pin_input')),
'1234',
);
// Act - Tap login
await tester(#login_button).tap();
// Assert - Verify navigation to dashboard
await tester.pumpAndSettle();
expect(find.byType(StaffDashboard), findsOneWidget);
});
}
API Endpoint Test
patrolTest('Order API uses correct endpoint', (tester) async {
await tester.pumpWidgetAndSettle(const StaffApp());
// Login
await loginAsStaff(tester);
// Navigate to new order
await tester(#new_order_button).tap();
await tester.pumpAndSettle();
// Add item and submit
await tester(#menu_item_burger).tap();
await tester(#submit_order).tap();
// Verify API call (uses mock server)
final request = mockServer.getLastRequest();
expect(request.path, equals('/api/v1/orders'));
expect(request.method, equals('POST'));
});
Offline Mode Test
patrolTest('Orders queue when offline', (tester) async {
await tester.pumpWidgetAndSettle(const StaffApp());
await loginAsStaff(tester);
// Simulate offline
await tester.setNetworkConnectivity(false);
// Create order while offline
await createOrder(tester);
// Verify queued indicator
expect(find.text('Queued'), findsOneWidget);
// Restore connectivity
await tester.setNetworkConnectivity(true);
await tester.pumpAndSettle(Duration(seconds: 5));
// Verify synced
expect(find.text('Synced'), findsOneWidget);
});
Shell-Specific Tests
Restaurant Staff Shell
| Test | Coverage |
|---|---|
auth_test.dart | PIN login, biometric, session management |
clock_test.dart | Clock in/out, break tracking |
orders_test.dart | New order, modifiers, send to kitchen |
tables_test.dart | Table assignment, status updates |
payments_test.dart | Card, cash, split payments |
offline_test.dart | Offline mode, queue, sync |
Restaurant Customer Shell
| Test | Coverage |
|---|---|
auth_test.dart | Signup, login, social auth |
menu_test.dart | Browse, search, filter |
cart_test.dart | Add, modify, remove items |
checkout_test.dart | Address, payment, submit |
tracking_test.dart | Order status, ETA |
favorites_test.dart | Save, view favorites |
Platform Admin Shell
| Test | Coverage |
|---|---|
auth_test.dart | SSO login, MFA |
tenants_test.dart | CRUD operations |
users_test.dart | User management |
policies_test.dart | Policy configuration |
audit_test.dart | Audit log viewing |
Cockpit Shell
| Test | Coverage |
|---|---|
auth_test.dart | SSO login |
dashboard_test.dart | Metrics display |
alerts_test.dart | Alert management |
tenants_test.dart | Tenant switching |
ai_test.dart | AI agent monitoring |
CI/CD Integration
GitHub Actions Workflow
name: Patrol E2E Tests
on:
pull_request:
paths:
- 'frontend/**'
push:
branches: [main, develop]
jobs:
patrol-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shell: [staff, customer, admin, cockpit, kiosk]
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
channel: 'stable'
- name: Install patrol_cli
run: dart pub global activate patrol_cli
- name: Run Patrol tests for ${{ matrix.shell }}
run: |
cd frontend
patrol test integration_test/${{ matrix.shell }}/
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-artifacts-${{ matrix.shell }}
path: |
frontend/build/test-results/
frontend/screenshots/
Test Reporting
| Metric | Requirement |
|---|---|
| Pass Rate | above 95% |
| Flaky Rate | Under 5% |
| Runtime | Under 10 min per shell |
| Coverage | 80% of critical flows |
Test Fixtures
Mock Server
class MockApiServer {
late HttpServer _server;
final List<RecordedRequest> _requests = [];
Future<void> start() async {
_server = await HttpServer.bind('localhost', 8080);
_server.listen(_handleRequest);
}
RecordedRequest getLastRequest() => _requests.last;
void stubResponse(String path, int status, Map<String, dynamic> body) {
_stubs[path] = StubResponse(status, body);
}
}
Test Users
| User Type | Credentials | Shell |
|---|---|---|
| Staff | PIN: 1234 | Staff |
| Manager | PIN: 5678 | Staff |
| Customer | test@example.com | Customer |
| Admin | admin@nebusai.com (SSO) | Admin |
| Operator | ops@nebusai.com (SSO) | Cockpit |
Best Practices
- Use semantic keys -
Key('login_button')notKey('button_1') - Wait for settle - Always
pumpAndSettle()after actions - Isolate tests - Each test should be independent
- Mock external services - Never hit real APIs
- Test error states - Include failure scenarios
- Keep tests fast - Target under 30 seconds per test
Troubleshooting
Flaky Tests
Symptoms: Tests pass sometimes, fail others
Solutions:
- Add explicit waits for async operations
- Use
pumpAndSettlewith timeout - Check for animation interference
- Isolate shared state between tests
Timeout Errors
Symptoms: Test times out waiting for widget
Solutions:
- Verify widget is actually rendered
- Check for route guard blocking navigation
- Increase timeout if legitimately slow
- Check for infinite loops/rebuilds
Related Documentation
- Architecture Overview - System architecture