feat: add local database persistence

This commit is contained in:
2026-06-12 01:01:50 +08:00
parent 9ba681e3c7
commit b88b947e66
10 changed files with 4322 additions and 28 deletions
+115
View File
@@ -0,0 +1,115 @@
import 'dart:convert';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:focus_seed/src/data/focus_seed_store.dart';
import 'package:focus_seed/src/data/local/focus_seed_database.dart';
import 'package:focus_seed/src/models/app_state.dart';
import 'package:focus_seed/src/models/focus_session.dart';
import 'package:focus_seed/src/models/habit.dart';
import 'package:focus_seed/src/models/mood.dart';
import 'package:focus_seed/src/models/reflection_entry.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('new installs seed localized starter habits into sqlite', () async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final state = await store.load(
defaultHabitTitles: const ['Plan', 'Focus', 'Reflect'],
);
expect(state.anchor, isEmpty);
expect(state.habits.map((habit) => habit.title), [
'Plan',
'Focus',
'Reflect',
]);
});
test(
'sqlite snapshot round-trips focus, habit, and reflection data',
() async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final now = DateTime(2026, 6, 12, 10, 30);
final state = AppState(
anchor: 'Ship the local database',
anchorDateKey: '2026-06-12',
habits: [
Habit(
id: 'habit-1',
title: 'Review the plan',
createdAt: now.subtract(const Duration(days: 2)),
completedDates: const {'2026-06-11', '2026-06-12'},
),
],
sessions: [
FocusSession(id: 'session-1', minutes: 25, completedAt: now),
],
reflections: [
ReflectionEntry(
id: 'reflection-1',
dateKey: '2026-06-12',
note: 'Small scope kept it clean.',
mood: Mood.clear,
createdAt: now.add(const Duration(minutes: 1)),
),
],
);
await store.save(state);
final loaded = await store.load();
expect(loaded.anchor, 'Ship the local database');
expect(loaded.anchorDateKey, '2026-06-12');
expect(loaded.habits.single.completedDates, {'2026-06-11', '2026-06-12'});
expect(loaded.sessions.single.minutes, 25);
expect(loaded.reflections.single.note, 'Small scope kept it clean.');
},
);
test('legacy shared preferences json migrates once and is removed', () async {
final legacyState = AppState(
anchor: 'Migrate safely',
anchorDateKey: '2026-06-12',
habits: [
Habit(
id: 'habit-legacy',
title: 'Keep existing data',
createdAt: DateTime(2026, 6, 10),
completedDates: const {'2026-06-12'},
),
],
sessions: [
FocusSession(
id: 'session-legacy',
minutes: 15,
completedAt: DateTime(2026, 6, 12, 8),
),
],
reflections: const [],
);
SharedPreferences.setMockInitialValues({
'focus_seed_state_v1': jsonEncode(legacyState.toJson()),
});
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final migrated = await store.load();
final preferences = await SharedPreferences.getInstance();
expect(migrated.anchor, 'Migrate safely');
expect(migrated.habits.single.title, 'Keep existing data');
expect(migrated.sessions.single.minutes, 15);
expect(preferences.getString('focus_seed_state_v1'), isNull);
});
}
+20 -10
View File
@@ -1,6 +1,9 @@
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:focus_seed/src/app/focus_seed_app.dart';
import 'package:focus_seed/src/data/focus_seed_store.dart';
import 'package:focus_seed/src/data/local/focus_seed_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
@@ -14,8 +17,7 @@ void main() {
});
testWidgets('Today guides the user from intention to focus', (tester) async {
await tester.pumpWidget(const FocusSeedApp(locale: Locale('en')));
await tester.pumpAndSettle();
await pumpFocusSeedApp(tester, locale: const Locale('en'));
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.text("TODAY'S INTENTION"), findsOneWidget);
@@ -35,8 +37,7 @@ void main() {
});
testWidgets('bottom navigation exposes Journey and Me', (tester) async {
await tester.pumpWidget(const FocusSeedApp(locale: Locale('en')));
await tester.pumpAndSettle();
await pumpFocusSeedApp(tester, locale: const Locale('en'));
await tester.tap(find.text('Journey'));
await tester.pumpAndSettle();
@@ -60,8 +61,7 @@ void main() {
tester.platformDispatcher.clearTextScaleFactorTestValue();
});
await tester.pumpWidget(const FocusSeedApp(locale: Locale('en')));
await tester.pumpAndSettle();
await pumpFocusSeedApp(tester, locale: const Locale('en'));
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.byType(Scrollable), findsWidgets);
@@ -69,8 +69,7 @@ void main() {
});
testWidgets('development default uses concise Chinese copy', (tester) async {
await tester.pumpWidget(const FocusSeedApp());
await tester.pumpAndSettle();
await pumpFocusSeedApp(tester);
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.text('\u4eca\u65e5\u76ee\u6807'), findsOneWidget);
@@ -88,8 +87,7 @@ void main() {
testWidgets('development language switch changes the active locale', (
tester,
) async {
await tester.pumpWidget(const FocusSeedApp());
await tester.pumpAndSettle();
await pumpFocusSeedApp(tester);
await tester.tap(find.text('\u6211\u7684'));
await tester.pumpAndSettle();
@@ -102,3 +100,15 @@ void main() {
expect(find.text('Language'), findsOneWidget);
});
}
Future<void> pumpFocusSeedApp(WidgetTester tester, {Locale? locale}) async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
await tester.pumpWidget(
FocusSeedApp(
locale: locale,
focusSeedStore: FocusSeedStore(database: database),
),
);
await tester.pumpAndSettle();
}