feat: add dynamic environment widgets Add live weather, sun position, and forecast widgets to Front Hall dashboard, powered by data from the Qdrant volatile collection via core-api. New widgets: - SunPositionWidget: Animated arc showing sun/moon position with gradient colors - ForecastWidget: Multi-day weather outlook - Updated WeatherWidget and AirQualityWidget to accept API data Infrastructure: - Environment datasource calling GET /tools/environment - Environment provider with 5-minute auto-refresh - Freezed models for environment data Tests: - 12 widget tests for environment section - Updated existing tests with givenEnvironment() harness method 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
299 lines
8.5 KiB
Dart
299 lines
8.5 KiB
Dart
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<void> 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<void> 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<Map<String, dynamic>>? containers]) {
|
|
api.whenGet(
|
|
'/infrastructure/containers',
|
|
containers ?? Fixtures.containers,
|
|
);
|
|
}
|
|
|
|
/// Set up mock domains response (for ProxyHostsPage).
|
|
void givenDomains([List<Map<String, dynamic>>? domains]) {
|
|
api.whenGet(
|
|
'/infrastructure/domains',
|
|
domains ?? Fixtures.domains,
|
|
);
|
|
}
|
|
|
|
/// Set up mock proxy hosts response (raw NPM format).
|
|
void givenProxyHosts([List<Map<String, dynamic>>? hosts]) {
|
|
api.whenGet(
|
|
'/infrastructure/npm/proxy-hosts',
|
|
hosts ?? Fixtures.proxyHosts,
|
|
);
|
|
}
|
|
|
|
/// Set up mock users response.
|
|
void givenUsers([List<Map<String, dynamic>>? users]) {
|
|
api.whenGet('/auth/users', users ?? Fixtures.users);
|
|
}
|
|
|
|
/// Set up mock groups response.
|
|
void givenGroups([List<Map<String, dynamic>>? groups]) {
|
|
api.whenGet('/auth/groups', groups ?? Fixtures.groups);
|
|
}
|
|
|
|
/// Set up mock quick links response.
|
|
void givenQuickLinks([List<Map<String, dynamic>>? links]) {
|
|
api.whenGet('/dashboard/quick-links', links ?? Fixtures.quickLinks);
|
|
}
|
|
|
|
/// Set up mock system stats response.
|
|
void givenSystemStats([Map<String, dynamic>? stats]) {
|
|
api.whenGet('/tools/system/stats', stats ?? Fixtures.systemStats);
|
|
}
|
|
|
|
/// Set up mock environment response.
|
|
void givenEnvironment([Map<String, dynamic>? 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<AuthState> 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,
|
|
};
|
|
}
|
|
}
|