首次远程推送

This commit is contained in:
2026-09-27 23:40:22 +08:00
parent 2e0bf3c784
commit 8afa47952a
70 changed files with 8603 additions and 4502 deletions
+97
View File
@@ -12,6 +12,8 @@ import 'package:focus_seed/src/models/reflection_entry.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
@@ -77,4 +79,99 @@ void main() {
'reflection-yesterday',
]);
});
test('default focus duration persists and survives record cleanup', () async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final controller = FocusSeedController(store: store);
await controller.load();
final changed = await controller.setDefaultFocusMinutes(45);
expect(changed, isTrue);
expect(controller.state!.defaultFocusMinutes, 45);
final reloaded = FocusSeedController(store: store);
await reloaded.load();
expect(reloaded.state!.defaultFocusMinutes, 45);
final cleared = await reloaded.clearData();
expect(cleared, isTrue);
expect(reloaded.state!.defaultFocusMinutes, 45);
});
test('completed focus sessions snapshot the active daily focus', () async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final controller = FocusSeedController(store: store);
await controller.load();
final savedFocus = await controller.setTodayAnchor('Review launch notes');
final savedSession = await controller.completeFocusSession(
10,
actualSeconds: 60,
);
expect(savedFocus, isTrue);
expect(savedSession, isTrue);
expect(controller.state!.sessions.single.intention, 'Review launch notes');
final reloaded = FocusSeedController(store: store);
await reloaded.load();
expect(reloaded.state!.sessions.single.intention, 'Review launch notes');
});
test('progress notes can be backfilled for a past record date', () async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final controller = FocusSeedController(store: store);
await controller.load();
const pastDate = '2026-06-12';
final saved = await controller.saveReflectionForDate(
pastDate,
'Recovered the next step.',
Mood.clear,
);
expect(saved, isTrue);
expect(controller.state!.reflections.single.dateKey, pastDate);
expect(
controller.state!.reflections.single.note,
'Recovered the next step.',
);
});
test('today anchor does not create a daily action object', () async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final controller = FocusSeedController(store: store);
await controller.load();
final savedToday = await controller.setTodayAnchor('Review launch notes');
expect(savedToday, isTrue);
expect(controller.state!.activeAnchor(todayKey()), 'Review launch notes');
expect(controller.state!.actions, isEmpty);
});
test('focus can complete without a today anchor', () async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final controller = FocusSeedController(store: store);
await controller.load();
final savedSession = await controller.completeFocusSession(
10,
actualSeconds: 120,
);
expect(savedSession, isTrue);
expect(controller.state!.sessions.single.intention, isEmpty);
expect(controller.state!.actions, isEmpty);
});
}
+16 -1
View File
@@ -52,7 +52,13 @@ void main() {
),
],
sessions: [
FocusSession(id: 'session-1', minutes: 25, completedAt: now),
FocusSession(
id: 'session-1',
minutes: 25,
actualSeconds: 90,
completedAt: now,
intention: 'Ship the local database',
),
],
reflections: [
ReflectionEntry(
@@ -63,6 +69,7 @@ void main() {
createdAt: now.add(const Duration(minutes: 1)),
),
],
defaultFocusMinutes: 45,
);
await store.save(state);
@@ -72,7 +79,15 @@ void main() {
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.sessions.single.actualSeconds, 90);
expect(loaded.sessions.single.actualMinutes, 2);
expect(loaded.sessions.single.intention, 'Ship the local database');
expect(loaded.reflections.single.note, 'Small scope kept it clean.');
expect(loaded.defaultFocusMinutes, 45);
final row = await database.select(database.focusSessionRows).getSingle();
expect(row.plannedMinutes, 25);
expect(row.actualSeconds, 90);
expect(row.intentionText, 'Ship the local database');
},
);
-65
View File
@@ -1,65 +0,0 @@
import 'package:flutter_test/flutter_test.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/growth_stage.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';
void main() {
const key = '2026-06-11';
final date = DateTime(2026, 6, 11, 10);
test('growth stage follows real daily progress', () {
const seed = AppState(
anchor: '',
anchorDateKey: key,
habits: [],
sessions: [],
reflections: [],
);
expect(growthStageFor(seed, key), GrowthStage.seed);
final sprout = seed.copyWith(anchor: 'Ship one useful thing');
expect(growthStageFor(sprout, key), GrowthStage.sprout);
final leaves = sprout.copyWith(
sessions: [FocusSession(id: 'session-1', minutes: 25, completedAt: date)],
habits: [
Habit(
id: 'habit-1',
title: 'Plan one task',
createdAt: date,
completedDates: const {key},
),
],
);
expect(growthStageFor(leaves, key), GrowthStage.leaves);
final flower = leaves.copyWith(
reflections: [
ReflectionEntry(
id: 'reflection-1',
dateKey: key,
note: 'A smaller scope made the work clearer.',
mood: Mood.clear,
createdAt: date,
),
],
);
expect(growthStageFor(flower, key), GrowthStage.flower);
});
test('version one anchor data migrates to the current day', () {
final state = AppState.fromJson(const {
'version': 1,
'anchor': 'Legacy intention',
'habits': <Object>[],
'sessions': <Object>[],
'reflections': <Object>[],
});
expect(state.anchorDateKey, isNotEmpty);
expect(state.toJson()['version'], 2);
});
}
+388 -62
View File
@@ -1,9 +1,13 @@
import 'dart:io';
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:focus_seed/src/models/app_state.dart';
import 'package:focus_seed/src/models/focus_session.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
@@ -16,45 +20,352 @@ void main() {
expect(focusSeedDefaultLocale(), const Locale('zh'));
});
testWidgets('Today guides the user from intention to focus', (tester) async {
test('welcome art uses managed onboarding assets', () {
final welcomeSource =
File(
'lib/src/features/onboarding/welcome_screen.dart',
).readAsStringSync();
final landingSource =
File(
'lib/src/features/onboarding/landing_setup_screen.dart',
).readAsStringSync();
final recordsSource =
File('lib/src/features/records/records_screen.dart').readAsStringSync();
expect(welcomeSource, contains('Image.asset'));
expect(welcomeSource, contains('AppAssets.welcomePath'));
expect(welcomeSource, contains('AppAssets.focusBlock'));
expect(welcomeSource, contains('AppAssets.reflectionNote'));
expect(landingSource, contains('AppAssets.landingFocusStart'));
expect(recordsSource, contains('AppAssets.recordsEmpty'));
expect(welcomeSource, isNot(contains('KineticIllustration')));
expect(welcomeSource, isNot(contains('SingleChildScrollView')));
expect(welcomeSource, isNot(contains('Parallax')));
expect(landingSource, isNot(contains('KineticIllustration')));
expect(recordsSource, isNot(contains('KineticIllustration')));
expect(welcomeSource, isNot(contains('SeedIllustration')));
expect(welcomeSource, isNot(contains('MiniTerrainMark')));
});
testWidgets('Today keeps the core loop free of saved plan noise', (
tester,
) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.text("TODAY'S FOCUS"), findsOneWidget);
expect(find.text("Set today's focus"), findsOneWidget);
expect(find.text('Start 10 min'), findsWidgets);
expect(find.text('Name today\'s focus'), findsOneWidget);
expect(find.text('Pick a focus plan'), findsNothing);
expect(find.text('Deep work sprint'), findsNothing);
});
await tester.tap(find.text("Name today's focus"));
testWidgets('first launch shows welcome before Today', (tester) async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
await tester.pumpWidget(
FocusSeedApp(
locale: const Locale('en'),
focusSeedStore: FocusSeedStore(database: database),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Plant just one thing each day.'), findsOneWidget);
expect(find.text('Continue'), findsOneWidget);
expect(find.byType(Image), findsWidgets);
await tester.tap(find.text('Continue'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(find.text('Protect 10 minutes first.'), findsOneWidget);
expect(find.byType(Image), findsWidgets);
await tester.tap(find.text('Continue'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(find.text('Leave one progress note.'), findsOneWidget);
expect(find.byType(Image), findsWidgets);
await tester.tap(find.text('Set today\'s focus'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 700));
expect(find.text('Start with 10 minutes.'), findsOneWidget);
expect(find.text('Write down what you will do'), findsOneWidget);
expect(find.text('What do you want to do first?'), findsNothing);
await tester.tap(find.text('Write down what you will do'));
await tester.pumpAndSettle();
expect(find.text('What do you want to do first?'), findsOneWidget);
await tester.ensureVisible(find.text('Start 10 minutes'));
await tester.pumpAndSettle();
final enterTodayButton = find.ancestor(
of: find.text('Start 10 minutes'),
matching: find.byWidgetPredicate((widget) => widget is ButtonStyleButton),
);
expect(enterTodayButton, findsOneWidget);
expect(
(tester.widget(enterTodayButton) as ButtonStyleButton).onPressed,
isNotNull,
);
await tester.tap(enterTodayButton);
await tester.pumpAndSettle();
expect(find.text('Deep work in progress'), findsOneWidget);
expect(find.text('Start this session first'), findsOneWidget);
});
testWidgets('landing setup can save typed action before starting', (
tester,
) async {
SharedPreferences.setMockInitialValues({
'focus_seed_onboarding_complete': true,
'focus_seed_landing_setup_complete': false,
});
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
await tester.pumpWidget(
FocusSeedApp(
locale: const Locale('en'),
focusSeedStore: FocusSeedStore(database: database),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Write down what you will do'));
await tester.pumpAndSettle();
await tester.enterText(
find.byType(TextField),
'Finish the launch checklist',
);
await tester.ensureVisible(find.text('Start 10 minutes'));
await tester.pumpAndSettle();
await tester.tap(find.text('Start 10 minutes'));
await tester.pumpAndSettle();
expect(
find.text('Today\'s focus: Finish the launch checklist'),
findsOneWidget,
);
expect(find.text('10 min'), findsWidgets);
});
testWidgets('finished onboarding restores landing until it is completed', (
tester,
) async {
SharedPreferences.setMockInitialValues({
'focus_seed_onboarding_complete': true,
'focus_seed_landing_setup_complete': false,
});
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
await tester.pumpWidget(
FocusSeedApp(
locale: const Locale('en'),
focusSeedStore: FocusSeedStore(database: database),
),
);
await tester.pumpAndSettle();
expect(find.text('Start with 10 minutes.'), findsOneWidget);
expect(find.text('Focus Seed'), findsNothing);
await tester.ensureVisible(find.text('Enter Today'));
await tester.pumpAndSettle();
await tester.tap(find.text('Enter Today'));
await tester.pumpAndSettle();
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.text('Start with 10 minutes.'), findsNothing);
await tester.pumpWidget(
FocusSeedApp(
locale: const Locale('en'),
focusSeedStore: FocusSeedStore(database: database),
),
);
await tester.pumpAndSettle();
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.text('Start with 10 minutes.'), findsNothing);
});
testWidgets('bottom navigation exposes Focus, Records, and Me', (
tester,
) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
await tester.tap(_navDestination('Focus'));
await tester.pumpAndSettle();
expect(find.text('Ready when you are'), findsOneWidget);
await tester.tap(_navDestination('Records'));
await tester.pumpAndSettle();
expect(find.text('Records'), findsWidgets);
expect(find.text('No progress records yet'), findsOneWidget);
await tester.tap(_navDestination('Me'));
await tester.pumpAndSettle();
expect(find.text('Me'), findsWidgets);
expect(find.text('Preferences'), findsWidgets);
await tester.tap(_navDestination('Today'));
await tester.pumpAndSettle();
expect(find.text('Name today\'s focus'), findsOneWidget);
});
testWidgets('V1 action loop records a session and capture', (tester) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
final startButton = find.ancestor(
of: find.text('Start 10 min').last,
matching: find.byWidgetPredicate((widget) => widget is ButtonStyleButton),
);
await tester.ensureVisible(startButton);
await tester.pumpAndSettle();
await tester.tap(startButton);
await tester.pumpAndSettle();
expect(find.text('Deep work in progress'), findsOneWidget);
await tester.tap(find.text('Pause'));
await tester.pumpAndSettle();
expect(find.text('Paused - your place is kept'), findsOneWidget);
await tester.tap(find.text('Resume'));
await tester.pumpAndSettle();
expect(find.text('Deep work in progress'), findsOneWidget);
await tester.tap(find.text('Complete session'));
await tester.pumpAndSettle();
expect(find.text('Session complete'), findsOneWidget);
expect(find.text('Write progress note'), findsOneWidget);
await tester.tap(find.text('Write progress note'));
await tester.pumpAndSettle();
expect(find.text('Leave one progress note'), findsOneWidget);
await tester.enterText(find.byType(TextField).last, 'Finished one pass.');
await tester.tap(find.text('Save note'));
await tester.pumpAndSettle();
await tester.tap(_navDestination('Records'));
await tester.pumpAndSettle();
expect(find.text('Records'), findsWidgets);
expect(find.text('Finished one pass.'), findsOneWidget);
expect(find.text('No progress records yet'), findsNothing);
});
testWidgets('Records can backfill reflection for a past daily record', (
tester,
) async {
final database = FocusSeedDatabase(NativeDatabase.memory());
addTearDown(database.close);
final store = FocusSeedStore(database: database);
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
await store.save(
AppState(
anchor: '',
anchorDateKey: '',
habits: const [],
sessions: [
FocusSession(
id: 'session-yesterday',
minutes: 10,
actualSeconds: 600,
completedAt: yesterday,
intention: 'Draft launch notes',
),
],
reflections: const [],
),
);
await tester.pumpWidget(
FocusSeedApp(
locale: const Locale('en'),
focusSeedStore: store,
showWelcomeOnFirstRun: false,
),
);
await tester.pumpAndSettle();
await tester.tap(_navDestination('Records'));
await tester.pumpAndSettle();
expect(find.text('Draft launch notes'), findsWidgets);
expect(find.text('No progress note yet'), findsWidgets);
expect(find.text('Write one sentence to close today'), findsOneWidget);
expect(find.text('Continue this'), findsNothing);
final reflectionButton = find.ancestor(
of: find.text('Write one sentence to close today'),
matching: find.byWidgetPredicate((widget) => widget is ButtonStyleButton),
);
await tester.ensureVisible(reflectionButton);
await tester.pumpAndSettle();
await tester.tap(reflectionButton);
await tester.pumpAndSettle();
expect(find.text('Leave one progress note'), findsOneWidget);
expect(find.text('TODAY\'S FOCUS'), findsWidgets);
expect(find.text('Draft launch notes'), findsWidgets);
await tester.enterText(find.byType(TextField).last, 'Next: polish copy.');
final saveNoteButton = find.ancestor(
of: find.text('Save note'),
matching: find.byWidgetPredicate((widget) => widget is ButtonStyleButton),
);
await tester.ensureVisible(saveNoteButton);
await tester.pumpAndSettle();
await tester.tap(saveNoteButton);
await tester.pumpAndSettle();
expect(find.text('Next: polish copy.'), findsOneWidget);
expect(find.text('Write one sentence to close today'), findsNothing);
expect(find.text('Continue this'), findsNothing);
});
testWidgets('settings manages local data without JSON tools', (tester) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
await tester.tap(_navDestination('Me'));
await tester.pumpAndSettle();
var meScroll = find.byType(Scrollable).first;
expect(find.text('Personal rhythm'), findsNothing);
expect(find.text('Saved plans'), findsNothing);
await tester.scrollUntilVisible(
find.text('Theme, language, and default focus'),
180,
scrollable: meScroll,
);
final preferencesRow = find.ancestor(
of: find.text('Theme, language, and default focus'),
matching: find.byType(InkWell),
);
expect(preferencesRow, findsOneWidget);
await tester.tap(preferencesRow);
await tester.pumpAndSettle();
expect(find.text('Default focus'), findsOneWidget);
expect(find.text('10 min'), findsWidgets);
expect(find.text('1'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.text('45'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add_rounded).last);
await tester.pumpAndSettle();
expect(find.text('Custom length'), findsOneWidget);
await tester.enterText(find.byType(TextField), '7');
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
expect(find.text('Finish the launch checklist'), findsOneWidget);
expect(find.text('Start 25 min'), findsOneWidget);
});
testWidgets('bottom navigation exposes Journey and Me', (tester) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
await tester.tap(find.text('Journey'));
expect(find.text('7 min'), findsOneWidget);
await tester.pageBack();
await tester.pumpAndSettle();
expect(find.text('Progress'), findsOneWidget);
await tester.tap(find.text('Me'));
await tester.pumpAndSettle();
expect(find.text('Your garden'), findsOneWidget);
});
testWidgets('Me manages local data without JSON tools', (tester) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
await tester.tap(find.text('Me'));
await tester.pumpAndSettle();
final meScroll = find.byType(Scrollable).first;
meScroll = find.byType(Scrollable).first;
await tester.scrollUntilVisible(
find.text('Local records'),
180,
scrollable: meScroll,
);
expect(find.text('Local records'), findsOneWidget);
expect(find.text('Clear today'), findsOneWidget);
await tester.scrollUntilVisible(
@@ -67,35 +378,17 @@ void main() {
expect(find.text('Import JSON'), findsNothing);
});
testWidgets('Journey opens history detail with empty states', (tester) async {
testWidgets('Records shows empty focus and reflection states', (
tester,
) async {
await pumpFocusSeedApp(tester, locale: const Locale('en'));
await tester.tap(find.text('Journey'));
await tester.tap(_navDestination('Records'));
await tester.pumpAndSettle();
final journeyScroll = find.byType(Scrollable).first;
await tester.scrollUntilVisible(
find.text('7-day rhythm'),
180,
scrollable: journeyScroll,
);
expect(find.text('7-day rhythm'), findsOneWidget);
await tester.scrollUntilVisible(
find.text('Record library'),
180,
scrollable: journeyScroll,
);
expect(find.text('Record library'), findsOneWidget);
await tester.tap(find.text('Record library'));
await tester.pumpAndSettle();
expect(find.text('History'), findsOneWidget);
expect(find.text('No focus sessions yet'), findsOneWidget);
await tester.tap(find.text('Notes'));
await tester.pumpAndSettle();
expect(find.text('No notes yet'), findsOneWidget);
expect(find.text('Records'), findsWidgets);
expect(find.text('No progress records yet'), findsOneWidget);
expect(find.text('Back to Today'), findsOneWidget);
});
testWidgets('Today remains usable at narrow width and 200 percent text', (
@@ -121,16 +414,16 @@ void main() {
await pumpFocusSeedApp(tester);
expect(find.text('Focus Seed'), findsOneWidget);
expect(find.text('\u4eca\u65e5\u91cd\u70b9'), findsOneWidget);
expect(find.text('\u8bbe\u7f6e\u4eca\u65e5\u91cd\u70b9'), findsOneWidget);
await tester.scrollUntilVisible(
find.text('\u8ba1\u5212\u4e00\u4e2a\u660e\u786e\u4efb\u52a1'),
180,
);
expect(
find.text('\u8ba1\u5212\u4e00\u4e2a\u660e\u786e\u4efb\u52a1'),
find.text('\u5199\u4e0b\u4eca\u5929\u7684\u91cd\u70b9'),
findsOneWidget,
);
expect(find.text('\u5f00\u59cb 10 \u5206\u949f'), findsWidgets);
expect(
find.text('\u9009\u62e9\u4e00\u4e2a\u4e13\u6ce8\u8ba1\u5212'),
findsNothing,
);
expect(find.text('\u6df1\u5ea6\u5de5\u4f5c'), findsNothing);
});
testWidgets('development language switch changes the active locale', (
@@ -138,15 +431,36 @@ void main() {
) async {
await pumpFocusSeedApp(tester);
await tester.tap(find.text('\u6211\u7684'));
await tester.tap(_navDestination('\u6211\u7684'));
await tester.pumpAndSettle();
final settingsScroll = find.byType(Scrollable).first;
await tester.scrollUntilVisible(
find.text('\u4e3b\u9898\u3001\u8bed\u8a00\u548c\u9ed8\u8ba4\u4e13\u6ce8'),
180,
scrollable: settingsScroll,
);
final preferencesRow = find.ancestor(
of: find.text(
'\u4e3b\u9898\u3001\u8bed\u8a00\u548c\u9ed8\u8ba4\u4e13\u6ce8',
),
matching: find.byType(InkWell),
);
await tester.tap(preferencesRow);
await tester.pumpAndSettle();
expect(find.text('\u8bed\u8a00'), findsOneWidget);
await tester.tap(find.byIcon(Icons.language_rounded));
final languageRow = find.ancestor(
of: find.text('\u8bed\u8a00'),
matching: find.byType(InkWell),
);
expect(languageRow, findsOneWidget);
await tester.tap(languageRow);
await tester.pumpAndSettle();
await tester.tap(find.text('English'));
await tester.pumpAndSettle();
expect(find.text('Language'), findsOneWidget);
expect(find.text('Development only'), findsOneWidget);
expect(find.text('English'), findsOneWidget);
});
}
@@ -157,7 +471,19 @@ Future<void> pumpFocusSeedApp(WidgetTester tester, {Locale? locale}) async {
FocusSeedApp(
locale: locale,
focusSeedStore: FocusSeedStore(database: database),
showWelcomeOnFirstRun: false,
),
);
await tester.pumpAndSettle();
}
Finder _navDestination(String label) {
final key = switch (label) {
'Today' || '\u4eca\u65e5' => 'today',
'Focus' || '\u4e13\u6ce8' => 'focus',
'Records' || '\u8bb0\u5f55' => 'records',
'Me' || '\u6211\u7684' => 'me',
_ => label.toLowerCase(),
};
return find.byKey(ValueKey('focus-seed-nav-$key'));
}