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)
This commit is contained in:
2026-03-16 14:54:39 +01:00
parent 878767138c
commit 0f6789becd
2 changed files with 225 additions and 0 deletions

View File

@@ -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);
}
});
});
});
}

View File

@@ -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<void>.delayed(Duration.zero);
final state = container.read(notificationSettingsNotifierProvider);
expect(state.enabled, isTrue);
expect(state.time, const TimeOfDay(hour: 8, minute: 15));
},
);
});
}