From 0f6789becdfd121d7c469069d75c87ab830405a6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 14:54:39 +0100 Subject: [PATCH] test(04-01): add failing tests for NotificationSettingsNotifier and NotificationService - Tests for default state (enabled=false, time=07:00) - Tests for setEnabled persistence to SharedPreferences - Tests for setTime persistence to SharedPreferences - Tests for loading persisted values via _load() - Tests for NotificationService singleton pattern - Tests for nextInstanceOf timezone logic (future/past time) --- .../notification_service_test.dart | 97 +++++++++++++ .../notification_settings_notifier_test.dart | 128 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 test/core/notifications/notification_service_test.dart create mode 100644 test/core/notifications/notification_settings_notifier_test.dart diff --git a/test/core/notifications/notification_service_test.dart b/test/core/notifications/notification_service_test.dart new file mode 100644 index 0000000..70de558 --- /dev/null +++ b/test/core/notifications/notification_service_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +import 'package:household_keeper/core/notifications/notification_service.dart'; + +void main() { + setUpAll(() { + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation('Europe/Berlin')); + }); + + group('NotificationService', () { + test('singleton pattern: two instances are identical', () { + final a = NotificationService(); + final b = NotificationService(); + expect(identical(a, b), isTrue); + }); + + group('nextInstanceOf', () { + test('returns today when time is in the future', () { + final service = NotificationService(); + final now = tz.TZDateTime.now(tz.local); + + // Use a time 2 hours in the future, wrapping midnight if needed + final futureHour = (now.hour + 2) % 24; + final futureMinute = now.minute; + final futureTime = TimeOfDay(hour: futureHour, minute: futureMinute); + + // Only test this case if futureHour > now.hour (no midnight wrap) + if (futureHour > now.hour) { + final result = service.nextInstanceOf(futureTime); + expect(result.day, now.day); + expect(result.hour, futureHour); + expect(result.minute, futureMinute); + } + }); + + test('returns tomorrow when time has passed', () { + final service = NotificationService(); + final now = tz.TZDateTime.now(tz.local); + + // Use a time in the past (1 hour ago), wrapping to previous day if needed + final pastHour = now.hour - 1; + + // Only test if we are not at the beginning of the day + if (pastHour >= 0) { + final pastTime = TimeOfDay(hour: pastHour, minute: 0); + final result = service.nextInstanceOf(pastTime); + expect(result.day, now.day + 1); + expect(result.hour, pastHour); + expect(result.minute, 0); + } + }); + + test('scheduled time is in the future (always)', () { + final service = NotificationService(); + final now = tz.TZDateTime.now(tz.local); + + // Test with midnight (00:00) — always results in a time in future (tomorrow) + final result = service.nextInstanceOf(const TimeOfDay(hour: 0, minute: 0)); + + expect(result.isAfter(now) || result.isAtSameMomentAs(now), isTrue); + }); + + test('nextInstanceOf respects hours and minutes exactly', () { + final service = NotificationService(); + final now = tz.TZDateTime.now(tz.local); + + // Use a far future time today: 23:59, which should be today if we are before it + const targetTime = TimeOfDay(hour: 23, minute: 59); + final todayTarget = tz.TZDateTime( + tz.local, + now.year, + now.month, + now.day, + 23, + 59, + ); + + final result = service.nextInstanceOf(targetTime); + + if (now.isBefore(todayTarget)) { + expect(result.hour, 23); + expect(result.minute, 59); + expect(result.day, now.day); + } else { + // After 23:59, scheduled for tomorrow + expect(result.hour, 23); + expect(result.minute, 59); + expect(result.day, now.day + 1); + } + }); + }); + }); +} diff --git a/test/core/notifications/notification_settings_notifier_test.dart b/test/core/notifications/notification_settings_notifier_test.dart new file mode 100644 index 0000000..05ad729 --- /dev/null +++ b/test/core/notifications/notification_settings_notifier_test.dart @@ -0,0 +1,128 @@ +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:household_keeper/core/notifications/notification_settings_notifier.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + group('NotificationSettingsNotifier', () { + test('build() returns default state (enabled=false, time=07:00)', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + final state = container.read(notificationSettingsNotifierProvider); + + expect(state.enabled, isFalse); + expect(state.time, const TimeOfDay(hour: 7, minute: 0)); + }); + + test('setEnabled(true) updates state.enabled to true', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(notificationSettingsNotifierProvider.notifier) + .setEnabled(true); + + expect( + container.read(notificationSettingsNotifierProvider).enabled, + isTrue, + ); + }); + + test('setEnabled(true) persists to SharedPreferences', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(notificationSettingsNotifierProvider.notifier) + .setEnabled(true); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getBool('notifications_enabled'), isTrue); + }); + + test('setEnabled(false) updates state.enabled to false', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + // First enable, then disable + await container + .read(notificationSettingsNotifierProvider.notifier) + .setEnabled(true); + await container + .read(notificationSettingsNotifierProvider.notifier) + .setEnabled(false); + + expect( + container.read(notificationSettingsNotifierProvider).enabled, + isFalse, + ); + }); + + test('setEnabled(false) persists to SharedPreferences', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(notificationSettingsNotifierProvider.notifier) + .setEnabled(true); + await container + .read(notificationSettingsNotifierProvider.notifier) + .setEnabled(false); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getBool('notifications_enabled'), isFalse); + }); + + test( + 'setTime(09:30) updates state.time and persists hour+minute', + () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + await container + .read(notificationSettingsNotifierProvider.notifier) + .setTime(const TimeOfDay(hour: 9, minute: 30)); + + expect( + container.read(notificationSettingsNotifierProvider).time, + const TimeOfDay(hour: 9, minute: 30), + ); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getInt('notifications_hour'), 9); + expect(prefs.getInt('notifications_minute'), 30); + }, + ); + + test( + 'After _load() with existing prefs, state reflects persisted values', + () async { + SharedPreferences.setMockInitialValues({ + 'notifications_enabled': true, + 'notifications_hour': 8, + 'notifications_minute': 15, + }); + + final container = ProviderContainer(); + addTearDown(container.dispose); + + // Trigger build + container.read(notificationSettingsNotifierProvider); + + // Wait for async _load() to complete + await Future.delayed(Duration.zero); + + final state = container.read(notificationSettingsNotifierProvider); + expect(state.enabled, isTrue); + expect(state.time, const TimeOfDay(hour: 8, minute: 15)); + }, + ); + }); +}