chore: Halkyon Learn moves into apps/learn
Its server, client, app and curator, and the tools that write its assets (maps, Anki fixtures, icons), as they are; paths are fixed in the commits that follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
11abd9afce
commit
649499be23
1 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,226 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/bloc/home_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/model/home_layout.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_goal.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_path_answer_gateway.dart';
|
||||
import '../../../support/fake_review_gateway.dart';
|
||||
import '../../../support/fake_settings_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
|
||||
void main() {
|
||||
late StudysetRepository studysets;
|
||||
late CardRepository cards;
|
||||
late ReviewRepository reviews;
|
||||
late SettingsRepository settings;
|
||||
late PathProgressRepository pathProgress;
|
||||
late HomeBloc bloc;
|
||||
// A Wednesday afternoon, local time.
|
||||
final now = DateTime(2026, 10, 7, 15);
|
||||
final today = DateTime(2026, 10, 7);
|
||||
|
||||
setUp(() {
|
||||
studysets = StudysetRepository(FakeStudysetGateway());
|
||||
cards = CardRepository(FakeCardGateway());
|
||||
reviews = ReviewRepository(FakeReviewGateway());
|
||||
settings = SettingsRepository(FakeSettingsGateway());
|
||||
pathProgress = PathProgressRepository(FakePathAnswerGateway());
|
||||
});
|
||||
tearDown(() async {
|
||||
await bloc.close();
|
||||
await studysets.dispose();
|
||||
await cards.dispose();
|
||||
await reviews.dispose();
|
||||
await settings.dispose();
|
||||
await pathProgress.dispose();
|
||||
});
|
||||
|
||||
CardContent card() =>
|
||||
CardContent(front: RichContent.empty(), back: RichContent.empty());
|
||||
|
||||
void start() =>
|
||||
bloc = HomeBloc(studysets, cards, reviews, settings, pathProgress)
|
||||
..add(const HomeEventStarted());
|
||||
|
||||
Future<HomeStateLoaded> showing(bool Function(HomeStateLoaded) test) async =>
|
||||
await bloc.stream.firstWhere((s) => s is HomeStateLoaded && test(s))
|
||||
as HomeStateLoaded;
|
||||
|
||||
test('a step of a path done counts as activity, and keeps the '
|
||||
'streak', () async {
|
||||
await withClock(Clock.fixed(now), () async {
|
||||
await withClock(
|
||||
Clock.fixed(now.subtract(const Duration(days: 1))),
|
||||
() => pathProgress.record('step-1', AnswerOutcome.continued),
|
||||
);
|
||||
start();
|
||||
final yesterday = await showing((s) => s.streak == 1);
|
||||
expect(yesterday.activity.on(today), 0);
|
||||
|
||||
await pathProgress.record('step-2', AnswerOutcome.continued);
|
||||
await pathProgress.record('step-3', AnswerOutcome.wrong);
|
||||
|
||||
final both = await showing((s) => s.streak == 2);
|
||||
expect(both.activity.on(today), 1);
|
||||
expect(both.goalProgress, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('counts ratings and cards written, keeps the streak and the goal, '
|
||||
'and follows new ratings', () async {
|
||||
await withClock(Clock.fixed(now), () async {
|
||||
final japanese = await studysets.create(
|
||||
StudysetContent.titled('Japanese'),
|
||||
);
|
||||
final neko = await cards.create(japanese, card());
|
||||
await reviews.record(
|
||||
cardId: neko,
|
||||
rating: Rating.good,
|
||||
reviewedAt: now.subtract(const Duration(days: 1)),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
await settings.update(
|
||||
(s) => s.withGoal(const StudyGoal(period: GoalPeriod.day, target: 5)),
|
||||
);
|
||||
start();
|
||||
|
||||
// Written today, rated yesterday: two days in a row.
|
||||
final first = await showing((s) => s.goal != null);
|
||||
expect(first.streak, 2);
|
||||
expect(first.activity.on(today), 1);
|
||||
expect(first.goalProgress, 0);
|
||||
|
||||
await reviews.record(
|
||||
cardId: neko,
|
||||
rating: Rating.easy,
|
||||
reviewedAt: now,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
final rated = await showing((s) => s.goalProgress == 1);
|
||||
expect(rated.activity.on(today), 2);
|
||||
});
|
||||
});
|
||||
|
||||
test('imported cards are no writing of the day', () async {
|
||||
await withClock(Clock.fixed(now), () async {
|
||||
final japanese = await studysets.create(
|
||||
StudysetContent.titled('Japanese'),
|
||||
);
|
||||
await cards.createAll(japanese, [card(), card()], origin: 'anki');
|
||||
await cards.create(japanese, card());
|
||||
start();
|
||||
|
||||
final state = await showing((s) => true);
|
||||
expect(state.activity.on(today), 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('an import alone starts the activity, without studying', () async {
|
||||
await withClock(Clock.fixed(now), () async {
|
||||
final japanese = await studysets.create(
|
||||
StudysetContent.titled('Japanese'),
|
||||
);
|
||||
await cards.createAll(japanese, [card()], origin: 'anki');
|
||||
start();
|
||||
|
||||
final state = await showing((s) => true);
|
||||
expect(state.activity.start, today);
|
||||
expect(state.activity.on(today), 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('cards fall due on their day, but those resting in an archived '
|
||||
'studyset', () async {
|
||||
await withClock(Clock.fixed(now), () async {
|
||||
final japanese = await studysets.create(
|
||||
StudysetContent.titled('Japanese'),
|
||||
);
|
||||
final german = await studysets.create(StudysetContent.titled('German'));
|
||||
for (final studyset in [japanese, german]) {
|
||||
final id = await cards.create(studyset, card());
|
||||
await reviews.record(
|
||||
cardId: id,
|
||||
rating: Rating.easy,
|
||||
reviewedAt: now,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
await studysets.archive(german);
|
||||
start();
|
||||
|
||||
final state = await showing((s) => s.due.isNotEmpty);
|
||||
expect(state.due.values.fold(0, (a, b) => a + b), 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('a rest day keeps the streak, and toggles off again', () async {
|
||||
await withClock(Clock.fixed(now), () async {
|
||||
final japanese = await studysets.create(
|
||||
StudysetContent.titled('Japanese'),
|
||||
);
|
||||
final neko = await cards.create(japanese, card());
|
||||
await reviews.record(
|
||||
cardId: neko,
|
||||
rating: Rating.good,
|
||||
reviewedAt: now.subtract(const Duration(days: 2)),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
start();
|
||||
// Written today, rated two days ago: yesterday breaks it.
|
||||
expect((await showing((s) => true)).streak, 1);
|
||||
|
||||
final yesterday = DateTime(2026, 10, 6);
|
||||
bloc.add(HomeEventRestDayToggled(yesterday));
|
||||
expect((await showing((s) => s.restDays.isNotEmpty)).streak, 3);
|
||||
|
||||
bloc.add(HomeEventRestDayToggled(yesterday));
|
||||
expect((await showing((s) => s.restDays.isEmpty)).streak, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('saves the goal and the layout in the settings', () async {
|
||||
start();
|
||||
await showing((s) => true);
|
||||
|
||||
bloc
|
||||
..add(
|
||||
const HomeEventGoalSet(StudyGoal(period: GoalPeriod.week, target: 50)),
|
||||
)
|
||||
..add(
|
||||
const HomeEventLayoutChanged(
|
||||
HomeLayout(
|
||||
order: [
|
||||
HomeWidget.week,
|
||||
HomeWidget.goal,
|
||||
HomeWidget.pomodoro,
|
||||
HomeWidget.activity,
|
||||
HomeWidget.deadlines,
|
||||
],
|
||||
hidden: {
|
||||
HomeWidget.pomodoro,
|
||||
HomeWidget.activity,
|
||||
HomeWidget.deadlines,
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
final state = await showing(
|
||||
(s) => s.goal != null && s.layout.hidden.isNotEmpty,
|
||||
);
|
||||
|
||||
expect(state.layout.shown, [HomeWidget.week, HomeWidget.goal]);
|
||||
expect((await settings.read()).goal?.target, 50);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue