Skip to main content

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

ShellTest Cases
StaffPIN login, biometric, session expiry
CustomerEmail signup, social login (Google, Apple)
AdminSSO (Google), MFA flow
CockpitSSO (Google), impersonation
KioskDevice activation, pairing

API Connectivity Tests

CategoryValidations
EndpointsCorrect /api/v1 prefix
Status Codes200, 201, 400, 401, 403, 404, 500
Response FormatJSON structure validation
AuthenticationToken refresh, expiry handling
CategoryTest Cases
Deep LinksDirect URL navigation
Route GuardsAuth-required redirects
Shell SwitchingMulti-shell app transitions
Back NavigationHistory stack handling

Offline Mode Tests

CategoryTest Cases
DetectionOnline/offline state detection
QueueOperation queueing offline
SyncAutomatic sync on reconnect
Graceful DegradationUI 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

TestCoverage
auth_test.dartPIN login, biometric, session management
clock_test.dartClock in/out, break tracking
orders_test.dartNew order, modifiers, send to kitchen
tables_test.dartTable assignment, status updates
payments_test.dartCard, cash, split payments
offline_test.dartOffline mode, queue, sync

Restaurant Customer Shell

TestCoverage
auth_test.dartSignup, login, social auth
menu_test.dartBrowse, search, filter
cart_test.dartAdd, modify, remove items
checkout_test.dartAddress, payment, submit
tracking_test.dartOrder status, ETA
favorites_test.dartSave, view favorites

Platform Admin Shell

TestCoverage
auth_test.dartSSO login, MFA
tenants_test.dartCRUD operations
users_test.dartUser management
policies_test.dartPolicy configuration
audit_test.dartAudit log viewing

Cockpit Shell

TestCoverage
auth_test.dartSSO login
dashboard_test.dartMetrics display
alerts_test.dartAlert management
tenants_test.dartTenant switching
ai_test.dartAI 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

MetricRequirement
Pass Rateabove 95%
Flaky RateUnder 5%
RuntimeUnder 10 min per shell
Coverage80% 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 TypeCredentialsShell
StaffPIN: 1234Staff
ManagerPIN: 5678Staff
Customertest@example.comCustomer
Adminadmin@nebusai.com (SSO)Admin
Operatorops@nebusai.com (SSO)Cockpit

Best Practices

  1. Use semantic keys - Key('login_button') not Key('button_1')
  2. Wait for settle - Always pumpAndSettle() after actions
  3. Isolate tests - Each test should be independent
  4. Mock external services - Never hit real APIs
  5. Test error states - Include failure scenarios
  6. Keep tests fast - Target under 30 seconds per test

Troubleshooting

Flaky Tests

Symptoms: Tests pass sometimes, fail others

Solutions:

  1. Add explicit waits for async operations
  2. Use pumpAndSettle with timeout
  3. Check for animation interference
  4. Isolate shared state between tests

Timeout Errors

Symptoms: Test times out waiting for widget

Solutions:

  1. Verify widget is actually rendered
  2. Check for route guard blocking navigation
  3. Increase timeout if legitimately slow
  4. Check for infinite loops/rebuilds