import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tatlock_ui/core/api/api_client.dart'; import 'package:tatlock_ui/core/auth/auth_provider.dart'; import 'package:tatlock_ui/core/auth/auth_state.dart'; import 'package:tatlock_ui/core/theme/theme_provider.dart'; import 'package:tatlock_ui/routing/room_registry.dart'; // Import room routers for registry initialization import 'package:tatlock_ui/features/front_hall/router.dart'; import 'package:tatlock_ui/features/control_room/router.dart'; import 'package:tatlock_ui/features/security/router.dart'; import 'package:tatlock_ui/features/parlor/router.dart'; import 'package:tatlock_ui/features/media_room/router.dart'; import 'fixtures.dart'; import 'mock_api_client.dart'; import 'mock_auth.dart'; export 'fixtures.dart'; export 'mock_api_client.dart'; export 'mock_auth.dart'; /// Test harness for widget and integration tests. /// /// Provides a consistent testing environment with: /// - Mock authentication state /// - Mock API client with fixtures /// - Room registry initialization /// - Provider overrides /// /// Usage: /// ```dart /// void main() { /// final harness = TestHarness(); /// /// setUp(() => harness.setUp()); /// tearDown(() => harness.tearDown()); /// /// testWidgets('example test', (tester) async { /// harness.givenAuthenticatedUser(); /// harness.givenContainers(Fixtures.containers); /// /// await tester.pumpWidget(harness.wrap(MyWidget())); /// // assertions... /// }); /// } /// ``` class TestHarness { TestHarness(); /// Mock API client for registering responses. final api = MockApiClient(); /// Current auth state for the test. AuthState _authState = MockAuth.guest; /// Current theme mode for the test. ThemeMode _themeMode = ThemeMode.light; /// Set up the test environment. /// /// Call this in setUp() for each test. Future setUp() async { // Initialize SharedPreferences with empty values SharedPreferences.setMockInitialValues({}); // Initialize room registry if needed _initializeRoomRegistry(); // Reset state _authState = MockAuth.guest; _themeMode = ThemeMode.light; api.reset(); } /// Tear down the test environment. /// /// Call this in tearDown() for each test. Future tearDown() async { api.reset(); } void _initializeRoomRegistry() { if (roomRegistry.all.isEmpty) { registerFrontHall(); registerControlRoom(); registerSecurity(); registerParlor(); registerMediaRoom(); } } // ============================================================ // Given - Set up preconditions // ============================================================ /// Set up an unauthenticated (guest) user. void givenGuestUser() { _authState = MockAuth.guest; } /// Set up an authenticated user. void givenAuthenticatedUser({ String name = 'Test User', String email = 'test@example.com', String defaultRoom = 'front-hall', }) { _authState = MockAuth.user( name: name, email: email, defaultRoom: defaultRoom, ); } /// Set up an admin user. void givenAdminUser({ String name = 'Admin User', String email = 'admin@example.com', }) { _authState = MockAuth.admin(name: name, email: email); } /// Set up a custom auth state. void givenAuthState(AuthState state) { _authState = state; } /// Set up mock containers response. void givenContainers([List>? containers]) { api.whenGet( '/infrastructure/containers', containers ?? Fixtures.containers, ); } /// Set up mock domains response (for ProxyHostsPage). void givenDomains([List>? domains]) { api.whenGet( '/infrastructure/domains', domains ?? Fixtures.domains, ); } /// Set up mock proxy hosts response (raw NPM format). void givenProxyHosts([List>? hosts]) { api.whenGet( '/infrastructure/npm/proxy-hosts', hosts ?? Fixtures.proxyHosts, ); } /// Set up mock users response. void givenUsers([List>? users]) { api.whenGet('/auth/users', users ?? Fixtures.users); } /// Set up mock groups response. void givenGroups([List>? groups]) { api.whenGet('/auth/groups', groups ?? Fixtures.groups); } /// Set up mock quick links response. void givenQuickLinks([List>? links]) { api.whenGet('/dashboard/quick-links', links ?? Fixtures.quickLinks); } /// Set up mock system stats response. void givenSystemStats([Map? stats]) { api.whenGet('/tools/system/stats', stats ?? Fixtures.systemStats); } /// Set up mock environment response. void givenEnvironment([Map? environment]) { api.whenGet('/tools/environment', environment ?? Fixtures.environment); } /// Set up mock environment response without air quality. void givenEnvironmentNoAirQuality() { api.whenGet('/tools/environment', Fixtures.environmentNoAirQuality); } /// Set up theme mode. void givenThemeMode(ThemeMode mode) { _themeMode = mode; } /// Set up an API error response. void givenApiError({ required String method, required String path, int statusCode = 500, String? message, }) { api.whenError( method: method, path: path, statusCode: statusCode, message: message, ); } // ============================================================ // Wrap - Create test widget with providers // ============================================================ /// Wrap a widget with the test harness providers. /// /// This sets up: /// - ProviderScope with auth, theme, and API overrides /// - MaterialApp with theme /// - Scaffold for proper widget context Widget wrap(Widget child, {bool useScaffold = true}) { final content = useScaffold ? Scaffold(body: child) : child; return ProviderScope( overrides: [ // Override auth provider to return our mock state authProvider.overrideWith(() => _MockAuthNotifier(_authState)), // Override theme provider themeProvider.overrideWith(() => _MockThemeNotifier(_themeMode)), // Override API client to use mock coreApiClientProvider.overrideWithValue(api.dio), ], child: MaterialApp( theme: ThemeData.light(useMaterial3: true), darkTheme: ThemeData.dark(useMaterial3: true), themeMode: _themeMode, home: content, ), ); } /// Wrap with a custom route for navigation testing. Widget wrapWithRouter(Widget child) { return ProviderScope( overrides: [ authProvider.overrideWith(() => _MockAuthNotifier(_authState)), themeProvider.overrideWith(() => _MockThemeNotifier(_themeMode)), coreApiClientProvider.overrideWithValue(api.dio), ], child: MaterialApp( theme: ThemeData.light(useMaterial3: true), darkTheme: ThemeData.dark(useMaterial3: true), themeMode: _themeMode, home: child, ), ); } // ============================================================ // Verify - Check assertions // ============================================================ /// Verify that a specific API endpoint was called. void verifyApiCalled(String method, String path) { final called = api.requests.any( (r) => r.method.toUpperCase() == method.toUpperCase() && r.path == path, ); expect(called, isTrue, reason: 'Expected $method $path to be called'); } /// Verify that no API calls were made. void verifyNoApiCalls() { expect(api.requests, isEmpty, reason: 'Expected no API calls'); } /// Get the request body of the last API call. dynamic getLastRequestBody() { return api.lastRequest?.data; } } /// Mock auth notifier that returns a fixed state. class _MockAuthNotifier extends AuthNotifier { _MockAuthNotifier(this._state); final AuthState _state; @override Future build() async => _state; } /// Mock theme notifier that returns a fixed theme. class _MockThemeNotifier extends ThemeNotifier { _MockThemeNotifier(this._mode); final ThemeMode _mode; @override ThemeSetting build() { return switch (_mode) { ThemeMode.light => ThemeSetting.light, ThemeMode.dark => ThemeSetting.dark, ThemeMode.system => ThemeSetting.system, }; } }