feat: Implement husband features and fix iOS Safari web startup
Implement initial features for husband's companion app, including mock data service and husband notes screen. Refactor scripture and cycle services for improved stability and testability. Address iOS Safari web app startup issue by removing deprecated initialization. - Implemented MockDataService and HusbandNotesScreen. - Converted _DashboardTab and DevotionalScreen to StatefulWidgets for robust scripture provider initialization. - Refactored CycleService to use immutable CycleInfo class, reducing UI rebuilds. - Removed deprecated window.flutterConfiguration from index.html, resolving Flutter web app startup failure on iOS Safari. - Updated and fixed related tests.
This commit is contained in:
262
test/scripture_provider_test.dart
Normal file
262
test/scripture_provider_test.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:christian_period_tracker/models/scripture.dart';
|
||||
import 'package:christian_period_tracker/models/cycle_entry.dart';
|
||||
import 'package:christian_period_tracker/models/user_profile.dart';
|
||||
import 'package:christian_period_tracker/providers/scripture_provider.dart';
|
||||
import 'package:christian_period_tracker/providers/user_provider.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
// Fake ScriptureDatabase implementation for testing
|
||||
class FakeScriptureDatabase implements ScriptureDatabase {
|
||||
final int Function(String phase) getScriptureCountForPhaseFn;
|
||||
final Scripture? Function(String phase, int index) getScriptureForPhaseByIndexFn;
|
||||
final Scripture? Function(String phase)? getRandomScriptureForPhaseFn;
|
||||
|
||||
FakeScriptureDatabase({
|
||||
required this.getScriptureCountForPhaseFn,
|
||||
required this.getScriptureForPhaseByIndexFn,
|
||||
this.getRandomScriptureForPhaseFn,
|
||||
});
|
||||
|
||||
@override
|
||||
int getScriptureCountForPhase(String phase) => getScriptureCountForPhaseFn(phase);
|
||||
|
||||
@override
|
||||
Scripture? getScriptureForPhaseByIndex(String phase, int index) =>
|
||||
getScriptureForPhaseByIndexFn(phase, index);
|
||||
|
||||
@override
|
||||
Scripture? getRandomScriptureForPhase(String phase) =>
|
||||
getRandomScriptureForPhaseFn != null ? getRandomScriptureForPhaseFn!(phase) : null;
|
||||
|
||||
// Unimplemented methods (not used by ScriptureNotifier)
|
||||
@override
|
||||
List<Scripture> getAllScriptures() => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Scripture getHusbandScripture() => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> loadScriptures() => Future.value(); // Can be mocked to do nothing
|
||||
|
||||
@override
|
||||
Scripture? getRecommendedScripture(CycleEntry entry) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Scripture getScriptureForPhase(String phase) => throw UnimplementedError();
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('ScriptureNotifier', () {
|
||||
late ProviderContainer container;
|
||||
late String testPath;
|
||||
|
||||
setUpAll(() async {
|
||||
testPath = Directory.current.path + '/test_hive_temp_scripture_provider';
|
||||
final Directory tempDir = Directory(testPath);
|
||||
if (!await tempDir.exists()) {
|
||||
await tempDir.create(recursive: true);
|
||||
}
|
||||
Hive.init(testPath);
|
||||
Hive.registerAdapter(UserProfileAdapter());
|
||||
Hive.registerAdapter(RelationshipStatusAdapter());
|
||||
Hive.registerAdapter(FertilityGoalAdapter());
|
||||
Hive.registerAdapter(BibleTranslationAdapter());
|
||||
Hive.registerAdapter(UserRoleAdapter());
|
||||
await Hive.openBox<UserProfile>('user_profile');
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await Hive.close();
|
||||
await Directory(testPath).delete(recursive: true);
|
||||
});
|
||||
|
||||
final testScripture1 = Scripture(
|
||||
verses: {BibleTranslation.esv: "Verse 1"},
|
||||
reference: "Ref 1",
|
||||
applicablePhases: ['menstrual'],
|
||||
);
|
||||
final testScripture2 = Scripture(
|
||||
verses: {BibleTranslation.esv: "Verse 2"},
|
||||
reference: "Ref 2",
|
||||
applicablePhases: ['menstrual'],
|
||||
);
|
||||
final testScripture3 = Scripture(
|
||||
verses: {BibleTranslation.esv: "Verse 3"},
|
||||
reference: "Ref 3",
|
||||
applicablePhases: ['menstrual'],
|
||||
);
|
||||
|
||||
tearDown(() {
|
||||
container.dispose();
|
||||
});
|
||||
|
||||
test('initializes with correct scripture for phase', () async {
|
||||
final fakeDb = FakeScriptureDatabase(
|
||||
getScriptureCountForPhaseFn: (phase) => 3,
|
||||
getScriptureForPhaseByIndexFn: (phase, index) => testScripture1,
|
||||
);
|
||||
|
||||
container = ProviderContainer(
|
||||
overrides: [
|
||||
scriptureDatabaseProvider.overrideWithValue(fakeDb),
|
||||
userProfileProvider.overrideWith(
|
||||
(ref) => UserProfileNotifier(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final notifier = container.read(scriptureProvider.notifier);
|
||||
notifier.initializeScripture(CyclePhase.menstrual);
|
||||
|
||||
final state = container.read(scriptureProvider);
|
||||
expect(state.currentScripture, testScripture1);
|
||||
expect(state.currentPhase, CyclePhase.menstrual);
|
||||
expect(state.maxIndex, 3);
|
||||
// currentIndex will depend on dayOfYear % 3, which is hard to predict
|
||||
// So we'll just check it's within bounds.
|
||||
expect(state.currentIndex, isNonNegative);
|
||||
expect(state.currentIndex, lessThan(3));
|
||||
});
|
||||
|
||||
test('getNextScripture cycles correctly', () async {
|
||||
final scriptures = [testScripture1, testScripture2, testScripture3];
|
||||
final fakeDb = FakeScriptureDatabase(
|
||||
getScriptureCountForPhaseFn: (phase) => scriptures.length,
|
||||
getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index],
|
||||
);
|
||||
|
||||
container = ProviderContainer(
|
||||
overrides: [
|
||||
scriptureDatabaseProvider.overrideWithValue(fakeDb),
|
||||
userProfileProvider.overrideWith(
|
||||
(ref) => UserProfileNotifier(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final notifier = container.read(scriptureProvider.notifier);
|
||||
// Force initial state to 0 for predictable cycling
|
||||
notifier.initializeScripture(CyclePhase.menstrual);
|
||||
// Override currentIndex to 0 for predictable test
|
||||
container.read(scriptureProvider.notifier).state = container.read(scriptureProvider).copyWith(currentIndex: 0);
|
||||
|
||||
|
||||
// First next
|
||||
notifier.getNextScripture();
|
||||
expect(container.read(scriptureProvider).currentScripture, testScripture2);
|
||||
expect(container.read(scriptureProvider).currentIndex, 1);
|
||||
|
||||
// Second next
|
||||
notifier.getNextScripture();
|
||||
expect(container.read(scriptureProvider).currentScripture, testScripture3);
|
||||
expect(container.read(scriptureProvider).currentIndex, 2);
|
||||
|
||||
// Wrap around
|
||||
notifier.getNextScripture();
|
||||
expect(container.read(scriptureProvider).currentScripture, testScripture1);
|
||||
expect(container.read(scriptureProvider).currentIndex, 0);
|
||||
});
|
||||
|
||||
test('getPreviousScripture cycles correctly', () async {
|
||||
final scriptures = [testScripture1, testScripture2, testScripture3];
|
||||
final fakeDb = FakeScriptureDatabase(
|
||||
getScriptureCountForPhaseFn: (phase) => scriptures.length,
|
||||
getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index],
|
||||
);
|
||||
|
||||
container = ProviderContainer(
|
||||
overrides: [
|
||||
scriptureDatabaseProvider.overrideWithValue(fakeDb),
|
||||
userProfileProvider.overrideWith(
|
||||
(ref) => UserProfileNotifier(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final notifier = container.read(scriptureProvider.notifier);
|
||||
// Force initial state to 0 for predictable cycling
|
||||
notifier.initializeScripture(CyclePhase.menstrual);
|
||||
// Override currentIndex to 0 for predictable test
|
||||
container.read(scriptureProvider.notifier).state = container.read(scriptureProvider).copyWith(currentIndex: 0);
|
||||
|
||||
|
||||
// First previous (wraps around)
|
||||
notifier.getPreviousScripture();
|
||||
expect(container.read(scriptureProvider).currentScripture, testScripture3);
|
||||
expect(container.read(scriptureProvider).currentIndex, 2);
|
||||
|
||||
// Second previous
|
||||
notifier.getPreviousScripture();
|
||||
expect(container.read(scriptureProvider).currentScripture, testScripture2);
|
||||
expect(container.read(scriptureProvider).currentIndex, 1);
|
||||
|
||||
// Third previous
|
||||
notifier.getPreviousScripture();
|
||||
expect(container.read(scriptureProvider).currentScripture, testScripture1);
|
||||
expect(container.read(scriptureProvider).currentIndex, 0);
|
||||
});
|
||||
|
||||
test('getRandomScripture updates to a valid scripture', () async {
|
||||
final scriptures = [testScripture1, testScripture2, testScripture3];
|
||||
final fakeDb = FakeScriptureDatabase(
|
||||
getScriptureCountForPhaseFn: (phase) => scriptures.length,
|
||||
getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index % scriptures.length], // Ensure it always returns a valid one
|
||||
);
|
||||
|
||||
container = ProviderContainer(
|
||||
overrides: [
|
||||
scriptureDatabaseProvider.overrideWithValue(fakeDb),
|
||||
userProfileProvider.overrideWith(
|
||||
(ref) => UserProfileNotifier(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final notifier = container.read(scriptureProvider.notifier);
|
||||
notifier.initializeScripture(CyclePhase.menstrual);
|
||||
|
||||
// Perform a random selection
|
||||
notifier.getRandomScripture();
|
||||
final state = container.read(scriptureProvider);
|
||||
|
||||
expect(state.currentScripture, isNotNull);
|
||||
expect(state.currentScripture, isIn(scriptures)); // Ensure it's one of the valid scriptures
|
||||
expect(state.currentIndex, isNonNegative);
|
||||
expect(state.currentIndex, lessThan(scriptures.length));
|
||||
});
|
||||
|
||||
test('does not change state if maxIndex is 0', () async {
|
||||
final fakeDb = FakeScriptureDatabase(
|
||||
getScriptureCountForPhaseFn: (phase) => 0,
|
||||
getScriptureForPhaseByIndexFn: (phase, index) => null,
|
||||
);
|
||||
|
||||
container = ProviderContainer(
|
||||
overrides: [
|
||||
scriptureDatabaseProvider.overrideWithValue(fakeDb),
|
||||
userProfileProvider.overrideWith(
|
||||
(ref) => UserProfileNotifier(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final notifier = container.read(scriptureProvider.notifier);
|
||||
notifier.initializeScripture(CyclePhase.menstrual);
|
||||
|
||||
final initialState = container.read(scriptureProvider);
|
||||
expect(initialState.currentScripture, isNull);
|
||||
expect(initialState.maxIndex, 0);
|
||||
|
||||
notifier.getNextScripture();
|
||||
notifier.getPreviousScripture();
|
||||
notifier.getRandomScripture();
|
||||
|
||||
// State should remain unchanged
|
||||
expect(container.read(scriptureProvider), initialState);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user