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>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
ea6914b5f4
commit
fffc3d5baf
@@ -0,0 +1,219 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/presentation/pages/front_hall_page.dart';
|
||||
import 'package:tatlock_ui/shared/widgets/air_quality_widget.dart';
|
||||
import 'package:tatlock_ui/shared/widgets/forecast_widget.dart';
|
||||
import 'package:tatlock_ui/shared/widgets/sun_position_widget.dart';
|
||||
import 'package:tatlock_ui/shared/widgets/weather_widget.dart';
|
||||
|
||||
import '../../harness/screen_sizes.dart';
|
||||
import '../../harness/test_harness.dart';
|
||||
|
||||
void main() {
|
||||
final harness = TestHarness();
|
||||
|
||||
setUp(() => harness.setUp());
|
||||
tearDown(() => harness.tearDown());
|
||||
|
||||
// Set larger window size and suppress overflow errors
|
||||
Future<void> setLargeWindowSize(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
// Suppress overflow errors - not what we're testing
|
||||
final originalOnError = FlutterError.onError;
|
||||
FlutterError.onError = (details) {
|
||||
if (details.exceptionAsString().contains('overflowed')) {
|
||||
return;
|
||||
}
|
||||
originalOnError?.call(details);
|
||||
};
|
||||
addTearDown(() => FlutterError.onError = originalOnError);
|
||||
}
|
||||
|
||||
group('Environment Widgets', () {
|
||||
testWidgets('displays Sun Position widget', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SunPositionWidget), findsOneWidget);
|
||||
expect(find.text('Sun Position'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays Weather widget', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(WeatherWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays Forecast widget', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ForecastWidget), findsOneWidget);
|
||||
expect(find.text('Forecast'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays Air Quality widget when data available',
|
||||
(tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment(); // Has air quality data
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AirQualityWidget), findsOneWidget);
|
||||
expect(find.text('Air Quality'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hides Air Quality widget when no data', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironmentNoAirQuality(); // No air quality data
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Air Quality widget should not be present
|
||||
expect(find.byType(AirQualityWidget), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('calls environment API on mount', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
harness.verifyApiCalled('GET', '/tools/environment');
|
||||
});
|
||||
|
||||
testWidgets('displays error state when environment fails', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenApiError(
|
||||
method: 'GET',
|
||||
path: '/tools/environment',
|
||||
statusCode: 500,
|
||||
message: 'Internal server error',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should show environment error message or error indicator
|
||||
expect(
|
||||
find.text('Failed to load environment data').evaluate().isNotEmpty ||
|
||||
find.byIcon(Icons.error_outline).evaluate().isNotEmpty,
|
||||
isTrue,
|
||||
reason: 'Should display environment error',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('displays sunrise and sunset times', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should display sunrise/sunset labels
|
||||
expect(find.text('Sunrise'), findsOneWidget);
|
||||
expect(find.text('Sunset'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays daylight duration', (tester) async {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should display daylight label
|
||||
expect(find.text('Daylight'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('Environment Widgets Responsive', () {
|
||||
testWidgets('renders environment widgets at desktop size', (tester) async {
|
||||
configureScreenSize(tester, ScreenSizes.desktopLarge);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SunPositionWidget), findsOneWidget);
|
||||
expect(find.byType(WeatherWidget), findsOneWidget);
|
||||
expect(find.byType(ForecastWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders environment widgets at tablet size', (tester) async {
|
||||
configureScreenSize(tester, ScreenSizes.tabletLandscape);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SunPositionWidget), findsOneWidget);
|
||||
expect(find.byType(WeatherWidget), findsOneWidget);
|
||||
expect(find.byType(ForecastWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders environment widgets at mobile size', (tester) async {
|
||||
configureScreenSize(tester, ScreenSizes.mobile);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Page should render without crashing at mobile size
|
||||
// Widgets may be below the fold due to scrolling
|
||||
expect(find.byType(FrontHallPage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -34,6 +34,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -47,6 +48,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -60,6 +62,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -75,6 +78,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -88,6 +92,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -101,6 +106,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -114,6 +120,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -127,6 +134,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pump();
|
||||
@@ -142,6 +150,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -155,6 +164,7 @@ void main() {
|
||||
await setLargeWindowSize(tester);
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenEnvironment();
|
||||
harness.givenApiError(
|
||||
method: 'GET',
|
||||
path: '/tools/system/stats',
|
||||
@@ -179,6 +189,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -191,6 +202,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -204,6 +216,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks([]); // Empty list triggers fallback
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -219,6 +232,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks(); // Fixtures have Infrastructure, Development, Home
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -235,6 +249,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -248,6 +263,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -261,6 +277,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -273,6 +290,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -286,6 +304,7 @@ void main() {
|
||||
harness.givenAuthenticatedUser();
|
||||
harness.givenQuickLinks();
|
||||
harness.givenSystemStats();
|
||||
harness.givenEnvironment();
|
||||
|
||||
await tester.pumpWidget(harness.wrap(const FrontHallPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in New Issue
Block a user