feat(app): study a studyset with everything inside it
Studying a studyset covers its subtree (decided with the tree, ADR 0004): the studysets of its subtree in tree order, without archived studysets and what is inside them (studiedIn); an archived studyset studies nothing. The study queue takes the cards of each studyset as a group: due cards of every studyset first, earliest first, then each studyset's new cards in tree order, at most 20 started per studyset and local day, as before for one. The library's cards count the same way, so a studyset whose cards sit in its sub-studysets still invites a session, and the badge counts what the session would study; an archived studyset, paused, still counts all it holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7cc28c3cdc
commit
9c2960f49d
12 changed files with 224 additions and 55 deletions
|
|
@ -91,6 +91,7 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
|
|||
final person = PersonScope.of(context);
|
||||
return BlocProvider(
|
||||
create: (context) => StudySessionBloc(
|
||||
person.studysets,
|
||||
person.cards,
|
||||
person.reviews,
|
||||
state.pathParameters['studysetId']!,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@ import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.d
|
|||
import 'package:halkyon_learn_flutter/src/feature/study/model/card_schedule.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_queue.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
|
||||
/// A study session over a studyset's cards: shows the next card of the study
|
||||
/// queue, its answer on request, and records each rating with how long
|
||||
/// answering took; the queue is worked out anew after every rating.
|
||||
/// A study session over a studyset with everything inside it, but for
|
||||
/// archived studysets (`studiedIn`): shows the next card of the study queue,
|
||||
/// its answer on request, and records each rating with how long answering
|
||||
/// took; the queue is worked out anew after every rating.
|
||||
class StudySessionBloc(
|
||||
final StudysetRepository _studysets,
|
||||
final CardRepository _cards,
|
||||
final ReviewRepository _reviews,
|
||||
final String _studysetId,
|
||||
|
|
@ -57,13 +61,19 @@ class StudySessionBloc(
|
|||
}
|
||||
|
||||
Future<void> _showNext(Emitter<StudySessionState> emit) async {
|
||||
final cards = await _cards.cardsOf(_studysetId);
|
||||
final schedules = await _reviews.schedulesOf([
|
||||
for (final card in cards) card.id,
|
||||
]);
|
||||
final groups = <List<String>>[];
|
||||
final cards = <String, Card>{};
|
||||
for (final studysetId in studiedIn(_studysetId, await _studysets.all())) {
|
||||
final studysetCards = await _cards.cardsOf(studysetId);
|
||||
groups.add([for (final card in studysetCards) card.id]);
|
||||
for (final card in studysetCards) {
|
||||
cards[card.id] = card;
|
||||
}
|
||||
}
|
||||
final schedules = await _reviews.schedulesOf([...cards.keys]);
|
||||
final now = clock.now().toUtc();
|
||||
final queue = studyQueue(
|
||||
cardOrder: [for (final card in cards) card.id],
|
||||
groups: groups,
|
||||
schedules: schedules,
|
||||
now: now,
|
||||
startOfToday: startOfLocalDay(now),
|
||||
|
|
@ -75,7 +85,7 @@ class StudySessionBloc(
|
|||
emit(StudySessionStateDone(next: upcoming.firstOrNull));
|
||||
return;
|
||||
}
|
||||
final card = cards.firstWhere((card) => card.id == queue.first);
|
||||
final card = cards[queue.first]!;
|
||||
emit(
|
||||
StudySessionStateShowing(
|
||||
card: card,
|
||||
|
|
|
|||
|
|
@ -24,25 +24,27 @@ final class const StudyProgress({
|
|||
List<Object?> get props => [cards, due, reviewsDue, mature];
|
||||
}
|
||||
|
||||
/// The progress of the cards in [cardOrder] with their [schedules] at [now],
|
||||
/// in the local day that began at [startOfToday]: what a session started now
|
||||
/// would study, as [studyQueue] puts it together, and how much is mature.
|
||||
/// The progress of the cards in [groups] (each studyset's, in tree order, as
|
||||
/// [studyQueue] takes them) with their [schedules] at [now], in the local day
|
||||
/// that began at [startOfToday]: what a session started now would study, and
|
||||
/// how much is mature.
|
||||
StudyProgress studyProgress({
|
||||
required List<String> cardOrder,
|
||||
required List<List<String>> groups,
|
||||
required Map<String, CardSchedule> schedules,
|
||||
required DateTime now,
|
||||
required DateTime startOfToday,
|
||||
}) {
|
||||
final cards = [for (final group in groups) ...group];
|
||||
final queue = studyQueue(
|
||||
cardOrder: cardOrder,
|
||||
groups: groups,
|
||||
schedules: schedules,
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
);
|
||||
return StudyProgress(
|
||||
cards: cardOrder.length,
|
||||
cards: cards.length,
|
||||
due: queue.length,
|
||||
reviewsDue: queue.where((id) => !(schedules[id]?.isNew ?? true)).length,
|
||||
mature: cardOrder.where((id) => schedules[id]?.isMature ?? false).length,
|
||||
mature: cards.where((id) => schedules[id]?.isMature ?? false).length,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ import 'dart:math' as math;
|
|||
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/card_schedule.dart';
|
||||
|
||||
/// The cards of a studyset to study at [now], in order (decided 2026-09-30).
|
||||
/// The cards to study at [now], in order (decided 2026-09-30; over a studyset
|
||||
/// with what is inside it, 2026-10-01).
|
||||
///
|
||||
/// First the cards due for review or in their learning steps, earliest first;
|
||||
/// then new cards in the studyset's order ([cardOrder]), at most [newPerDay]
|
||||
/// started per local day, the day that began at [startOfToday]. When nothing
|
||||
/// else is due, a learning card due within [learnAhead] comes early.
|
||||
/// [groups] holds the cards of every studyset studied, in tree order, each in
|
||||
/// its own order. First the cards due for review or in their learning steps,
|
||||
/// earliest first, from every studyset; then each studyset's new cards, in
|
||||
/// tree order, at most [newPerDay] started per studyset and local day, the day
|
||||
/// that began at [startOfToday]. When nothing else is due, a learning card due
|
||||
/// within [learnAhead] comes early.
|
||||
List<String> studyQueue({
|
||||
required List<String> cardOrder,
|
||||
required List<List<String>> groups,
|
||||
required Map<String, CardSchedule> schedules,
|
||||
required DateTime now,
|
||||
required DateTime startOfToday,
|
||||
|
|
@ -18,26 +21,27 @@ List<String> studyQueue({
|
|||
}) {
|
||||
DateTime? dueOf(String id) => schedules[id]?.due;
|
||||
int byDue(String a, String b) => dueOf(a)!.compareTo(dueOf(b)!);
|
||||
bool startedToday(String id) =>
|
||||
schedules[id]?.firstReviewedAt?.isBefore(startOfToday) == false;
|
||||
final cards = [for (final group in groups) ...group];
|
||||
|
||||
final due = [
|
||||
for (final id in cardOrder)
|
||||
for (final id in cards)
|
||||
if (dueOf(id) case final due? when !due.isAfter(now)) id,
|
||||
]..sort(byDue);
|
||||
final startedToday = schedules.values
|
||||
.where(
|
||||
(schedule) => schedule.firstReviewedAt?.isBefore(startOfToday) == false,
|
||||
)
|
||||
.length;
|
||||
final fresh = [
|
||||
for (final id in cardOrder)
|
||||
for (final group in groups)
|
||||
...[
|
||||
for (final id in group)
|
||||
if (schedules[id]?.isNew ?? true) id,
|
||||
].take(math.max(0, newPerDay - startedToday));
|
||||
].take(math.max(0, newPerDay - group.where(startedToday).length)),
|
||||
];
|
||||
final queue = [...due, ...fresh];
|
||||
if (queue.isNotEmpty) {
|
||||
return queue;
|
||||
}
|
||||
final soon = [
|
||||
for (final id in cardOrder)
|
||||
for (final id in cards)
|
||||
if (schedules[id] case final schedule?
|
||||
when schedule.isLearning &&
|
||||
!schedule.due!.isAfter(now.add(learnAhead)))
|
||||
|
|
|
|||
|
|
@ -225,12 +225,20 @@ class StudysetListBloc(
|
|||
final progress = <String, StudyProgress>{};
|
||||
try {
|
||||
for (final studyset in studysets) {
|
||||
final cardIds = [
|
||||
for (final card in await _cards.cardsOf(studyset.id)) card.id,
|
||||
// What a session on the studyset would study; an archived one
|
||||
// rests, paused, and still counts all it holds.
|
||||
final studied = studyset.isArchived
|
||||
? subtreeOf(studyset.id, _studysetsRead)
|
||||
: studiedIn(studyset.id, _studysetsRead);
|
||||
final groups = [
|
||||
for (final id in studied)
|
||||
[for (final card in await _cards.cardsOf(id)) card.id],
|
||||
];
|
||||
progress[studyset.id] = studyProgress(
|
||||
cardOrder: cardIds,
|
||||
schedules: await _reviews.schedulesOf(cardIds),
|
||||
groups: groups,
|
||||
schedules: await _reviews.schedulesOf([
|
||||
for (final group in groups) ...group,
|
||||
]),
|
||||
now: now,
|
||||
startOfToday: startOfLocalDay(now),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,28 @@ List<String> subtreeOf(String id, List<Studyset> all) {
|
|||
return subtree;
|
||||
}
|
||||
|
||||
/// The ids of the studysets a session on the studyset [id] studies: its
|
||||
/// subtree in tree order, without archived studysets and what is inside them
|
||||
/// (ADR 0004); nothing when it is archived itself.
|
||||
List<String> studiedIn(String id, List<Studyset> all) {
|
||||
final byId = {for (final studyset in all) studyset.id: studyset};
|
||||
final studied = <String>[];
|
||||
final seen = <String>{};
|
||||
void visit(String id) {
|
||||
final studyset = byId[id];
|
||||
if (studyset == null || studyset.isArchived || !seen.add(id)) {
|
||||
return;
|
||||
}
|
||||
studied.add(id);
|
||||
for (final child in childrenOf(id, all)) {
|
||||
visit(child.id);
|
||||
}
|
||||
}
|
||||
|
||||
visit(id);
|
||||
return studied;
|
||||
}
|
||||
|
||||
/// The studysets the studyset [id] sits in, from the one showing at the top
|
||||
/// level down to its parent; empty for a studyset at the top level itself.
|
||||
List<Studyset> ancestorsOf(String id, List<Studyset> all) {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/study/bloc/study_session_bloc.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_payload.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_review_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
|
||||
CardContent card(String front) => CardContent(
|
||||
front: RichContent({
|
||||
|
|
@ -18,18 +22,28 @@ CardContent card(String front) => CardContent(
|
|||
);
|
||||
|
||||
void main() {
|
||||
late StudysetRepository studysets;
|
||||
late CardRepository cards;
|
||||
late FakeReviewGateway reviewGateway;
|
||||
late ReviewRepository reviews;
|
||||
late DateTime now;
|
||||
|
||||
setUp(() {
|
||||
studysets = StudysetRepository(
|
||||
FakeStudysetGateway()..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
);
|
||||
cards = CardRepository(FakeCardGateway());
|
||||
reviewGateway = FakeReviewGateway();
|
||||
reviews = ReviewRepository(reviewGateway);
|
||||
now = DateTime.utc(2026, 10, 5, 9);
|
||||
});
|
||||
tearDown(() => cards.dispose());
|
||||
tearDown(() async {
|
||||
await studysets.dispose();
|
||||
await cards.dispose();
|
||||
});
|
||||
|
||||
TypeMatcher<StudySessionStateShowing> showing(
|
||||
String front, {
|
||||
|
|
@ -44,7 +58,7 @@ void main() {
|
|||
await cards.create('japanese', card('犬'));
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = StudySessionBloc(cards, reviews, 'japanese')
|
||||
final bloc = StudySessionBloc(studysets, cards, reviews, 'japanese')
|
||||
..add(const StudySessionEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
await expectLater(bloc.stream, emits(showing('猫')));
|
||||
|
|
@ -74,7 +88,7 @@ void main() {
|
|||
await cards.create('japanese', card('猫'));
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = StudySessionBloc(cards, reviews, 'japanese')
|
||||
final bloc = StudySessionBloc(studysets, cards, reviews, 'japanese')
|
||||
..add(const StudySessionEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
await expectLater(bloc.stream, emits(showing('猫')));
|
||||
|
|
@ -99,7 +113,7 @@ void main() {
|
|||
await cards.create('japanese', card('猫'));
|
||||
|
||||
await withClock(Clock(() => now), () async {
|
||||
final bloc = StudySessionBloc(cards, reviews, 'japanese')
|
||||
final bloc = StudySessionBloc(studysets, cards, reviews, 'japanese')
|
||||
..add(const StudySessionEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
await expectLater(bloc.stream, emits(showing('猫')));
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ void main() {
|
|||
test('counts the cards, those a session would study now, the reviews '
|
||||
'among them, and the mature ones', () {
|
||||
final progress = studyProgress(
|
||||
cardOrder: ['due', 'new', 'later', 'mature'],
|
||||
groups: [
|
||||
['due', 'new', 'later', 'mature'],
|
||||
],
|
||||
schedules: {
|
||||
'due': reviewed(now.subtract(const Duration(hours: 1))),
|
||||
'new': fresh,
|
||||
|
|
@ -41,7 +43,7 @@ void main() {
|
|||
final order = [for (var i = 0; i < 30; i++) 'n$i'];
|
||||
|
||||
final progress = studyProgress(
|
||||
cardOrder: order,
|
||||
groups: [order],
|
||||
schedules: {for (final id in order) id: fresh},
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
|
|
@ -53,7 +55,7 @@ void main() {
|
|||
|
||||
test('has no mastery without cards', () {
|
||||
final progress = studyProgress(
|
||||
cardOrder: const [],
|
||||
groups: const [],
|
||||
schedules: const {},
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ void main() {
|
|||
test('due cards come first, earliest first, then new cards in their '
|
||||
'studyset order', () {
|
||||
final queue = studyQueue(
|
||||
cardOrder: ['n1', 'late', 'n2', 'early'],
|
||||
groups: [
|
||||
['n1', 'late', 'n2', 'early'],
|
||||
],
|
||||
schedules: {
|
||||
'n1': fresh,
|
||||
'late': dueAt(now.subtract(const Duration(hours: 1))),
|
||||
|
|
@ -34,7 +36,9 @@ void main() {
|
|||
|
||||
test('leaves out cards due later', () {
|
||||
final queue = studyQueue(
|
||||
cardOrder: ['later'],
|
||||
groups: [
|
||||
['later'],
|
||||
],
|
||||
schedules: {'later': dueAt(now.add(const Duration(days: 1)))},
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
|
|
@ -54,7 +58,7 @@ void main() {
|
|||
);
|
||||
|
||||
final queue = studyQueue(
|
||||
cardOrder: order,
|
||||
groups: [order],
|
||||
schedules: schedules,
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
|
|
@ -67,7 +71,9 @@ void main() {
|
|||
test('when nothing else is due, a learning card due within 20 minutes '
|
||||
'comes early', () {
|
||||
final queue = studyQueue(
|
||||
cardOrder: ['soon', 'later'],
|
||||
groups: [
|
||||
['soon', 'later'],
|
||||
],
|
||||
schedules: {
|
||||
'soon': dueAt(now.add(const Duration(minutes: 5)), learning: true),
|
||||
'later': dueAt(now.add(const Duration(minutes: 30)), learning: true),
|
||||
|
|
@ -78,4 +84,42 @@ void main() {
|
|||
|
||||
expect(queue, ['soon']);
|
||||
});
|
||||
|
||||
group('over several studysets, as a studyset with what is inside it', () {
|
||||
test('due cards of every studyset come first, earliest first, then each '
|
||||
"studyset's new cards in tree order", () {
|
||||
final queue = studyQueue(
|
||||
groups: [
|
||||
['parent-new', 'parent-late'],
|
||||
['child-new', 'child-early'],
|
||||
],
|
||||
schedules: {
|
||||
'parent-new': fresh,
|
||||
'parent-late': dueAt(now.subtract(const Duration(hours: 1))),
|
||||
'child-new': fresh,
|
||||
'child-early': dueAt(now.subtract(const Duration(days: 2))),
|
||||
},
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
);
|
||||
|
||||
expect(queue, ['child-early', 'parent-late', 'parent-new', 'child-new']);
|
||||
});
|
||||
|
||||
test('each studyset starts its own 20 new cards a local day', () {
|
||||
final parent = [for (var i = 0; i < 25; i++) 'p$i'];
|
||||
final child = [for (var i = 0; i < 25; i++) 'c$i'];
|
||||
|
||||
final queue = studyQueue(
|
||||
groups: [parent, child],
|
||||
schedules: {
|
||||
for (final id in [...parent, ...child]) id: fresh,
|
||||
},
|
||||
now: now,
|
||||
startOfToday: startOfToday,
|
||||
);
|
||||
|
||||
expect(queue, [...parent.take(20), ...child.take(20)]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:material_ui/material_ui.dart' hide Card;
|
||||
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
||||
|
|
@ -44,7 +47,14 @@ void main() {
|
|||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(cardGateway: gateway),
|
||||
testDependencies(
|
||||
studysetGateway: FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
cardGateway: gateway,
|
||||
),
|
||||
location: RoutePaths.study('japanese'),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,31 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test("counts what is inside a studyset, but for archived studysets' "
|
||||
'cards, which rest', () async {
|
||||
final japanese = await studysets.create(StudysetContent.titled('Japanese'));
|
||||
final kana = await studysets.create(
|
||||
StudysetContent.titled('Kana'),
|
||||
parentId: japanese,
|
||||
);
|
||||
final kanji = await studysets.create(
|
||||
StudysetContent.titled('Kanji'),
|
||||
parentId: japanese,
|
||||
);
|
||||
await studysets.archive(kanji);
|
||||
await cards.create(japanese, newCard());
|
||||
await cards.create(kana, newCard());
|
||||
await cards.create(kanji, newCard());
|
||||
|
||||
start();
|
||||
final state = await listing(['Japanese']);
|
||||
|
||||
expect(
|
||||
state.progress[japanese],
|
||||
const StudyProgress(cards: 2, due: 2, reviewsDue: 0, mature: 0),
|
||||
);
|
||||
});
|
||||
|
||||
test('lists the starred studysets first', () async {
|
||||
await studysets.create(StudysetContent.titled('Japanese'));
|
||||
final german = await studysets.create(StudysetContent.titled('German'));
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
|
||||
Studyset studyset(String id, {String? parent, String position = 'a0'}) =>
|
||||
Studyset(
|
||||
Studyset studyset(
|
||||
String id, {
|
||||
String? parent,
|
||||
String position = 'a0',
|
||||
DateTime? archivedAt,
|
||||
}) => Studyset(
|
||||
id: id,
|
||||
content: StudysetContent.titled(id),
|
||||
parentId: parent,
|
||||
position: position,
|
||||
);
|
||||
archivedAt: archivedAt,
|
||||
);
|
||||
|
||||
Iterable<String> ids(List<Studyset> studysets) =>
|
||||
studysets.map((studyset) => studyset.id);
|
||||
|
|
@ -121,4 +126,26 @@ void main() {
|
|||
expect(ancestorsOf('a', all), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('what a session on a studyset studies', () {
|
||||
test('is its subtree in tree order, without archived studysets and '
|
||||
'what is inside them', () {
|
||||
final archived = DateTime.utc(2026, 10);
|
||||
final all = [
|
||||
studyset('japanese'),
|
||||
studyset('kana', parent: 'japanese', position: 'a1'),
|
||||
studyset('hiragana', parent: 'kana'),
|
||||
studyset(
|
||||
'kanji',
|
||||
parent: 'japanese',
|
||||
position: 'a2',
|
||||
archivedAt: archived,
|
||||
),
|
||||
studyset('jouyou', parent: 'kanji'),
|
||||
];
|
||||
|
||||
expect(studiedIn('japanese', all), ['japanese', 'kana', 'hiragana']);
|
||||
expect(studiedIn('kanji', all), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue