Files
tatlock-ui/test/harness/test_harness.dart
T
Jeroen SchweitzerandClaude Opus 4.5 fffc3d5baf
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m12s
chore: release v1.5.0
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>
2026-01-06 23:05:24 +01:00

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,
};
}
}