perf(learn): a rated card gives way to the next one at once
Rating a card while studying waited for its review to be written, then for the card's schedule to be replayed, before the next card showed. StudySessionBloc now starts keeping the review and shows what comes next at once: the card's new place in the round is worked out from its reviews with the new one counted as if kept already (ReviewRepository.schedulesAsStudied's pending, each review counted once should it be kept by then). Should keeping it fail, the step leaves the session's Undo, the rating leaves its accuracy, and the card comes back where it stood, saying so (withRatingFailed), as a failed rating did before. An Undo of a rating still being kept waits for it and takes it back once kept; one that couldn't be kept has nothing to take back, and the Undo has brought the card back already. Whats-New: While studying, the next flashcard shows the moment you rate one. Whats-New-De: Beim Lernen kommt die nächste Karteikarte, sobald du eine bewertet hast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
d6b2bec113
commit
4940230ef1
2 changed files with 235 additions and 57 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
|
|
@ -528,6 +529,91 @@ void main() {
|
|||
expect((await showing(bloc, '犬')).ratingFailed, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
test('a rating shows the next card at once, before it is kept', () async {
|
||||
await cards.create('japanese', card('猫'));
|
||||
await cards.create('japanese', card('犬'));
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = session();
|
||||
addTearDown(bloc.close);
|
||||
await showing(bloc, '猫');
|
||||
final hold = reviewGateway.holdWrites = Completer<void>();
|
||||
|
||||
bloc.add(const StudySessionEventRated(Rating.easy));
|
||||
final dog = await showing(bloc, '犬');
|
||||
expect(reviewGateway.rows, isEmpty, reason: 'not kept yet');
|
||||
expect(dog.canUndo, isTrue);
|
||||
|
||||
hold.complete();
|
||||
await eventually(() => reviewGateway.rows.length == 1);
|
||||
expect(reviewGateway.rows.single.rating, Rating.easy.value);
|
||||
});
|
||||
});
|
||||
|
||||
test("a rating that can't be kept once the next card shows brings "
|
||||
'the card back, saying so, with nothing to undo', () async {
|
||||
await cards.create('japanese', card('猫'));
|
||||
await cards.create('japanese', card('犬'));
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = session();
|
||||
addTearDown(bloc.close);
|
||||
await showing(bloc, '猫');
|
||||
final hold = reviewGateway.holdWrites = Completer<void>();
|
||||
reviewGateway.failWrites = Exception('disk');
|
||||
|
||||
bloc.add(const StudySessionEventRated(Rating.easy));
|
||||
await showing(bloc, '犬');
|
||||
hold.complete();
|
||||
|
||||
final failed = await until(
|
||||
bloc,
|
||||
(s) => s is StudySessionStateShowing && s.ratingFailed,
|
||||
) as StudySessionStateShowing;
|
||||
expect(failed.card.content.front, text('猫'));
|
||||
expect(failed.canUndo, isFalse);
|
||||
expect(failed.pills.map((pill) => pill.state), [
|
||||
PillState.active,
|
||||
PillState.pending,
|
||||
]);
|
||||
|
||||
reviewGateway
|
||||
..holdWrites = null
|
||||
..failWrites = null;
|
||||
bloc.add(const StudySessionEventRated(Rating.easy));
|
||||
expect((await showing(bloc, '犬')).ratingFailed, isFalse);
|
||||
await eventually(() => reviewGateway.rows.length == 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Undo while a rating is still being kept takes it back once it '
|
||||
'is', () async {
|
||||
await cards.create('japanese', card('猫'));
|
||||
await cards.create('japanese', card('犬'));
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = session();
|
||||
addTearDown(bloc.close);
|
||||
await showing(bloc, '猫');
|
||||
final hold = reviewGateway.holdWrites = Completer<void>();
|
||||
|
||||
bloc.add(const StudySessionEventRated(Rating.easy));
|
||||
await showing(bloc, '犬');
|
||||
bloc.add(const StudySessionEventUndone());
|
||||
hold.complete();
|
||||
|
||||
final neko = await showing(bloc, '猫');
|
||||
expect(neko.ratingFailed, isFalse);
|
||||
await pumpEventQueue();
|
||||
expect(reviewGateway.rows, isEmpty);
|
||||
expect(bloc.state, isA<StudySessionStateShowing>());
|
||||
expect(
|
||||
(bloc.state as StudySessionStateShowing).card.content.front,
|
||||
text('猫'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('a round holds at most the cards per round', () async {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
|
||||
|
||||
/// A [ReviewGateway] in memory; ids sort by creation, as UUID v7 ids do.
|
||||
|
|
@ -10,6 +12,9 @@ class FakeReviewGateway() implements ReviewGateway {
|
|||
/// When set, storing a review fails with it.
|
||||
Exception? failWrites;
|
||||
|
||||
/// When set, storing a review waits for it to complete.
|
||||
Completer<void>? holdWrites;
|
||||
|
||||
@override
|
||||
Future<List<ReviewRow>> reviewsOf(Set<String> cardIds) async => [
|
||||
for (final row in rows)
|
||||
|
|
@ -21,6 +26,9 @@ class FakeReviewGateway() implements ReviewGateway {
|
|||
|
||||
@override
|
||||
Future<List<ReviewRow>> insert(Iterable<ReviewRow> reviews) async {
|
||||
if (holdWrites case final hold?) {
|
||||
await hold.future;
|
||||
}
|
||||
if (failWrites case final error?) {
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue