fix(studyset): an exam's pace keeps its rate, and plans hold what they show
The pace kept a week for reviews only while more than two weeks were left: followed from further off, it halved its rate at two weeks and brought new cards until the exam. It now keeps a week, or half the days left when the exam is closer, and the sheet says so. An exam whose plan is gone said "Today", as the days left came from the pace, which needs the plan; they are now counted from the date alone. Once the exam's day was over, the study plan still asked for everything new at once and Study for the exam queued every new card; now neither shows, and a session for it studies as usual. Cards tagged HSK 3 alone offered HSK 1 and 2, plans covering nothing; a level offers a plan only when it covers some tag present. The plan editor kept topics deleted or moved out since and tags spelled in another case, chosen but not shown, and ticking such a tag added it twice; once it has read what a plan may cover, only that stays chosen, each tag as the flashcards spell it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
57240f01f3
commit
9c39155b94
3 changed files with 264 additions and 78 deletions
|
|
@ -1193,7 +1193,7 @@ void main() {
|
|||
.withExamPlans(const [
|
||||
ExamPlan(id: 'verbs', name: 'Verb test', tags: ['verb']),
|
||||
])
|
||||
.withExam(ExamChoice(plan: 'verbs', date: DateTime(2026, 10, 7))),
|
||||
.withExam(ExamChoice(plan: 'verbs', date: DateTime(2026, 10, 9))),
|
||||
);
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
|
|
@ -1201,7 +1201,8 @@ void main() {
|
|||
addTearDown(bloc.close);
|
||||
final first = await showing(bloc, '食べる');
|
||||
|
||||
// Two new verbs over the two days left: one a day.
|
||||
// Two new verbs over the first two of the four days left, the
|
||||
// last two kept for reviews: one a day.
|
||||
expect(first.pills, hasLength(1));
|
||||
});
|
||||
await studysets.updateContent(
|
||||
|
|
@ -1216,6 +1217,25 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('for an exam whose day is over, studies as usual rather than all '
|
||||
'its new cards at once', () async {
|
||||
await studysets.updateContent(
|
||||
'japanese',
|
||||
StudysetContent.titled('Japanese')
|
||||
.withExamPlans(const [
|
||||
ExamPlan(id: 'verbs', name: 'Verb test', tags: ['verb']),
|
||||
])
|
||||
.withExam(ExamChoice(plan: 'verbs', date: DateTime(2026, 9, 30))),
|
||||
);
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = focused(const StudyFocus(exam: true));
|
||||
addTearDown(bloc.close);
|
||||
|
||||
expect((await showing(bloc, '食べる')).pills, hasLength(3));
|
||||
});
|
||||
});
|
||||
|
||||
test('goes to the route and back as its query', () {
|
||||
const focus = StudyFocus(
|
||||
tag: (id: 'verb-id', name: 'verb'),
|
||||
|
|
|
|||
|
|
@ -25,20 +25,19 @@ void main() {
|
|||
[
|
||||
for (final plan in automaticExamPlans(['HSK-7-9', '중급'])) plan.name,
|
||||
],
|
||||
[
|
||||
'HSK 1',
|
||||
'HSK 2',
|
||||
'HSK 3',
|
||||
'HSK 4',
|
||||
'HSK 5',
|
||||
'HSK 6',
|
||||
'HSK 7–9',
|
||||
'Korean 초급',
|
||||
'Korean 중급',
|
||||
],
|
||||
['HSK 7–9', 'Korean 중급'],
|
||||
);
|
||||
expect(automaticExamPlans(['noun', 'verb']), isEmpty);
|
||||
});
|
||||
|
||||
test('leave out the levels below the lowest present, which would cover '
|
||||
'nothing', () {
|
||||
expect(automaticExamPlans(['HSK-3', 'HSK-5']), const [
|
||||
ExamPlan(id: 'auto:hsk:3', name: 'HSK 3', tags: ['HSK-3']),
|
||||
ExamPlan(id: 'auto:hsk:4', name: 'HSK 4', tags: ['HSK-3']),
|
||||
ExamPlan(id: 'auto:hsk:5', name: 'HSK 5', tags: ['HSK-3', 'HSK-5']),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('An exam plan', () {
|
||||
|
|
@ -76,6 +75,17 @@ void main() {
|
|||
expect(ExamChoice.fromJson({'plan': 'final', 'date': 'soon'}), isNull);
|
||||
expect(ExamChoice.fromJson({'plan': 'final'}), isNull);
|
||||
});
|
||||
|
||||
test('counts the days left by the calendar, and is past from the day '
|
||||
'after it on', () {
|
||||
final choice = ExamChoice(plan: 'final', date: DateTime(2026, 12, 6));
|
||||
|
||||
expect(choice.daysLeftAt(DateTime(2026, 10, 7, 23)), 60);
|
||||
expect(choice.daysLeftAt(DateTime(2026, 12, 6, 8)), 0);
|
||||
expect(choice.isPastAt(DateTime(2026, 12, 6, 23, 59)), isFalse);
|
||||
expect(choice.isPastAt(DateTime(2026, 12, 7)), isTrue);
|
||||
expect(choice.daysLeftAt(DateTime(2026, 12, 8)), 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('The pace to an exam', () {
|
||||
|
|
@ -98,7 +108,8 @@ void main() {
|
|||
));
|
||||
});
|
||||
|
||||
test('uses every day left when two weeks or fewer are', () {
|
||||
test('leaves half the days left for reviews when the exam is closer '
|
||||
'than two weeks', () {
|
||||
final pace = examPace(
|
||||
today: DateTime(2026, 10, 14),
|
||||
examDate: DateTime(2026, 10, 24),
|
||||
|
|
@ -107,14 +118,30 @@ void main() {
|
|||
pagesLeft: 101,
|
||||
);
|
||||
|
||||
// 10 days left, 5 of them to learn in.
|
||||
expect(pace, (
|
||||
daysLeft: 10,
|
||||
newPerDay: 3,
|
||||
newPerDay: 5,
|
||||
stepsPerWeek: 3,
|
||||
pagesPerDay: 11,
|
||||
pagesPerDay: 21,
|
||||
));
|
||||
});
|
||||
|
||||
test('keeps its rate when followed until two weeks are left', () {
|
||||
int newPerDay(DateTime today, int newCards) => examPace(
|
||||
today: today,
|
||||
examDate: DateTime(2026, 12, 6),
|
||||
newCards: newCards,
|
||||
stepsLeft: 0,
|
||||
pagesLeft: 0,
|
||||
).newPerDay;
|
||||
|
||||
// 30 days out, 230 cards over the 23 before the last week.
|
||||
expect(newPerDay(DateTime(2026, 11, 6), 230), 10);
|
||||
// 16 days on, 70 are left, with 14 days to go: not half the rate.
|
||||
expect(newPerDay(DateTime(2026, 11, 22), 70), 10);
|
||||
});
|
||||
|
||||
test('counts calendar days across a change of summer time', () {
|
||||
expect(
|
||||
examPace(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_opt
|
|||
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/exam_cubit.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/exam_plan.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/library_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_breadcrumb.dart';
|
||||
|
|
@ -119,6 +120,8 @@ void main() {
|
|||
String? frontLanguage,
|
||||
String? backLanguage,
|
||||
String? colorName,
|
||||
List<ExamPlan> examPlans = const [],
|
||||
ExamChoice? exam,
|
||||
}) => gateway.receiveRemoteChange(
|
||||
id,
|
||||
encodeStudysetPayload(
|
||||
|
|
@ -132,6 +135,8 @@ void main() {
|
|||
bibliography: bibliography,
|
||||
frontLanguage: frontLanguage,
|
||||
backLanguage: backLanguage,
|
||||
examPlans: examPlans,
|
||||
exam: exam,
|
||||
),
|
||||
),
|
||||
parentId: parent,
|
||||
|
|
@ -2105,6 +2110,103 @@ void main() {
|
|||
|
||||
expect(find.text('Prepare for an exam'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an exam whose plan is gone still says how far off it '
|
||||
'is', (tester) async {
|
||||
final today = clock.now();
|
||||
stored(
|
||||
'japanese',
|
||||
'Japanese',
|
||||
exam: ExamChoice(
|
||||
plan: 'gone',
|
||||
date: DateTime(today.year, today.month, today.day + 60),
|
||||
),
|
||||
);
|
||||
await showApp(tester, location: RoutePaths.studyset('japanese'));
|
||||
|
||||
await openPart(tester, 'Study plan');
|
||||
|
||||
expect(find.text('This exam plan is gone'), findsOneWidget);
|
||||
expect(find.textContaining('In 60 days'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("once the exam's day is over, it shows how far the person "
|
||||
'came, without a pace or studying for it', (tester) async {
|
||||
final today = clock.now();
|
||||
stored(
|
||||
'japanese',
|
||||
'Japanese',
|
||||
examPlans: const [
|
||||
ExamPlan(id: 'kana-quiz', name: 'Kana quiz', topics: ['kana']),
|
||||
],
|
||||
exam: ExamChoice(
|
||||
plan: 'kana-quiz',
|
||||
date: DateTime(today.year, today.month, today.day - 3),
|
||||
),
|
||||
);
|
||||
stored('kana', 'Kana', parent: 'japanese');
|
||||
await card('kana', 'あ');
|
||||
await showApp(tester, location: RoutePaths.studyset('japanese'));
|
||||
|
||||
await openPart(tester, 'Study plan');
|
||||
|
||||
expect(find.textContaining('It was on'), findsOneWidget);
|
||||
expect(find.text('0 of 1 flashcard started'), findsOneWidget);
|
||||
expect(find.text('Your pace'), findsNothing);
|
||||
expect(find.text('Study for the exam'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('editing a plan keeps only the topics and tags it shows, '
|
||||
'each tag as the flashcards spell it', (tester) async {
|
||||
stored(
|
||||
'japanese',
|
||||
'Japanese',
|
||||
examPlans: const [
|
||||
ExamPlan(
|
||||
id: 'final',
|
||||
name: 'Final',
|
||||
topics: ['kana', 'gone'],
|
||||
tags: ['VERB'],
|
||||
),
|
||||
],
|
||||
);
|
||||
stored('kana', 'Kana', parent: 'japanese');
|
||||
final taberu = await card('kana', '食べる');
|
||||
await showApp(tester, location: RoutePaths.studyset('japanese'));
|
||||
final person = PersonScope.of(
|
||||
tester.element(find.byType(StudysetScreen)),
|
||||
);
|
||||
await person.tags.setTagsOf(taberu, {await person.tags.create('Verb')});
|
||||
|
||||
await openPart(tester, 'Study plan');
|
||||
await tester.tap(find.text('Prepare for an exam'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Edit exam plan'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester
|
||||
.widget<CheckboxListTile>(
|
||||
find.widgetWithText(CheckboxListTile, 'Kana'),
|
||||
)
|
||||
.value,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<FilterChip>(find.widgetWithText(FilterChip, 'Verb'))
|
||||
.selected,
|
||||
isTrue,
|
||||
);
|
||||
await tester.tap(find.text('Save').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final japanese = (await person.studysets.all()).singleWhere(
|
||||
(studyset) => studyset.id == 'japanese',
|
||||
);
|
||||
expect(japanese.content.examPlans, const [
|
||||
ExamPlan(id: 'final', name: 'Final', topics: ['kana'], tags: ['Verb']),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('choosing another part ticks, as the navigation bar does; '
|
||||
|
|
|
|||
Loading…
Reference in a new issue