fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5efa552489
commit
6737a42ff7
6 changed files with 954 additions and 77 deletions
|
|
@ -1,16 +1,28 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
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.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_adder.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_updates.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/entry_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.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/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 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
import '../../support/fake_settings_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/fake_tag_gateway.dart';
|
||||
|
||||
final taberu = word(
|
||||
|
|
@ -381,16 +393,156 @@ void main() {
|
|||
group('adding words', () {
|
||||
late CardRepository cards;
|
||||
late TagRepository tags;
|
||||
late StudysetRepository studysets;
|
||||
late SettingsRepository settings;
|
||||
late DictionaryAdder adder;
|
||||
|
||||
final omae = word(
|
||||
'1002830',
|
||||
'お前',
|
||||
reading: 'おまえ',
|
||||
glosses: ['you'],
|
||||
labels: ['jlpt:n3'],
|
||||
pos: ['pn'],
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
cards = CardRepository(FakeCardGateway());
|
||||
tags = TagRepository(FakeTagGateway());
|
||||
adder = DictionaryAdder(cards, tags);
|
||||
// Japanese › Vocabulary › N5, N3 and an archived Old; Other beside.
|
||||
ByteData titled(String title) =>
|
||||
encodeStudysetPayload(StudysetContent.titled(title));
|
||||
studysets = StudysetRepository(
|
||||
FakeStudysetGateway()
|
||||
..receiveRemoteChange('japanese', titled('Japanese'))
|
||||
..receiveRemoteChange(
|
||||
'vocabulary',
|
||||
titled('Vocabulary'),
|
||||
parentId: 'japanese',
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'n5',
|
||||
titled('N5'),
|
||||
parentId: 'vocabulary',
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'n3',
|
||||
titled('N3'),
|
||||
parentId: 'vocabulary',
|
||||
position: 'a1',
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'old',
|
||||
titled('Old'),
|
||||
parentId: 'vocabulary',
|
||||
position: 'a2',
|
||||
archivedAt: DateTime.utc(2026, 10),
|
||||
)
|
||||
..receiveRemoteChange('other', titled('Other'), position: 'a1'),
|
||||
);
|
||||
settings = SettingsRepository(FakeSettingsGateway());
|
||||
adder = DictionaryAdder(cards, tags, studysets, settings);
|
||||
});
|
||||
tearDown(() async {
|
||||
await cards.dispose();
|
||||
await tags.dispose();
|
||||
await studysets.dispose();
|
||||
await settings.dispose();
|
||||
});
|
||||
|
||||
List<String?> entryIds(List<Card> cards) => [
|
||||
for (final card in cards) card.content.entry?.id,
|
||||
];
|
||||
|
||||
test('a word held anywhere in the studyset or its topics, archived ones '
|
||||
'too, is never added again; one held outside it is', () async {
|
||||
await adder.add('n5', [taberu]);
|
||||
await adder.add('old', [terebi]);
|
||||
await adder.add('other', [omae]);
|
||||
|
||||
expect(
|
||||
await adder.add('vocabulary', [taberu, terebi, omae]),
|
||||
(added: 1, skipped: 2),
|
||||
);
|
||||
expect(entryIds(await cards.cardsOf('vocabulary')), ['1002830']);
|
||||
expect(await adder.entriesIn('vocabulary'), {
|
||||
taberu.ref,
|
||||
terebi.ref,
|
||||
omae.ref,
|
||||
});
|
||||
});
|
||||
|
||||
test('words go to the studyset or any topic inside it but archived '
|
||||
'ones, in the order of the tree', () async {
|
||||
expect(
|
||||
[
|
||||
for (final studyset in await adder.destinationsOf('vocabulary'))
|
||||
studyset.id,
|
||||
],
|
||||
['vocabulary', 'n5', 'n3'],
|
||||
);
|
||||
expect(
|
||||
[for (final studyset in await adder.destinationsOf('n5')) studyset.id],
|
||||
['n5'],
|
||||
);
|
||||
});
|
||||
|
||||
test('new words go where chosen for the studyset, a choice that follows '
|
||||
'the person; while none is, or once it is no longer inside the '
|
||||
'studyset, to the studyset itself', () async {
|
||||
expect(await adder.destinationOf('vocabulary'), 'vocabulary');
|
||||
|
||||
await adder.chooseDestination('vocabulary', 'n3');
|
||||
expect(
|
||||
(await settings.read()).addDestinationOf('vocabulary'),
|
||||
'n3',
|
||||
);
|
||||
await adder.add('vocabulary', [taberu]);
|
||||
expect(entryIds(await cards.cardsOf('n3')), ['1358280']);
|
||||
expect(await cards.cardsOf('vocabulary'), isEmpty);
|
||||
|
||||
await studysets.move('n3', parentId: null);
|
||||
expect(await adder.destinationOf('vocabulary'), 'vocabulary');
|
||||
await adder.add('vocabulary', [terebi]);
|
||||
expect(entryIds(await cards.cardsOf('vocabulary')), ['1080510']);
|
||||
});
|
||||
|
||||
test(
|
||||
"the cards of a video's words held anywhere inside get its tag",
|
||||
() async {
|
||||
await adder.add('n3', [taberu]);
|
||||
await adder.add('n5', [terebi]);
|
||||
|
||||
final tag = await adder.tagHeld('vocabulary', {
|
||||
taberu.ref,
|
||||
}, 'あかね噺 S1 E1');
|
||||
|
||||
final tagging = await tags.read();
|
||||
final [eat] = await cards.cardsOf('n3');
|
||||
final [tv] = await cards.cardsOf('n5');
|
||||
expect([
|
||||
for (final each in tagging.of(eat.id)) each.id,
|
||||
], contains(tag.id));
|
||||
expect([
|
||||
for (final each in tagging.of(tv.id)) each.id,
|
||||
], isNot(contains(tag.id)));
|
||||
},
|
||||
);
|
||||
|
||||
test("a word's progress comes from its card anywhere in the studyset "
|
||||
'and its topics', () async {
|
||||
await adder.add('n3', [taberu]);
|
||||
await adder.add('other', [terebi]);
|
||||
final reviews = ReviewRepository(FakeReviewGateway());
|
||||
|
||||
final progress = await EntryProgress(
|
||||
cards,
|
||||
reviews,
|
||||
studysets,
|
||||
).of('vocabulary');
|
||||
|
||||
expect(progress.keys, [taberu.ref]);
|
||||
expect(progress[taberu.ref]?.isNew, isTrue);
|
||||
});
|
||||
|
||||
test('makes each word a card once, tagged with its level and word '
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
|
|
@ -82,6 +84,9 @@ void main() {
|
|||
FakeDictionaryGateway? dictionaries,
|
||||
StrokeRepository? strokes,
|
||||
String? location,
|
||||
|
||||
/// Gives Japanese the topics Vocabulary, and N5 and N3 inside it.
|
||||
bool topics = false,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1400, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -131,6 +136,23 @@ void main() {
|
|||
),
|
||||
),
|
||||
);
|
||||
if (topics) {
|
||||
ByteData titled(String title) =>
|
||||
encodeStudysetPayload(StudysetContent.titled(title));
|
||||
studysets
|
||||
..receiveRemoteChange(
|
||||
'vocabulary',
|
||||
titled('Vocabulary'),
|
||||
parentId: 'japanese',
|
||||
)
|
||||
..receiveRemoteChange('n5', titled('N5'), parentId: 'vocabulary')
|
||||
..receiveRemoteChange(
|
||||
'n3',
|
||||
titled('N3'),
|
||||
parentId: 'vocabulary',
|
||||
position: 'a1',
|
||||
);
|
||||
}
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
|
|
@ -193,6 +215,60 @@ void main() {
|
|||
expect(card.content.reading, 'たべる');
|
||||
});
|
||||
|
||||
testWidgets('from a studyset with topics, a word a topic holds shows as '
|
||||
'held, and new words go where Adds to says: the studyset itself, '
|
||||
'until a topic is chosen in its menu', (tester) async {
|
||||
await cards.createAll('n5', [toukyou.card], origin: 'jmdict');
|
||||
await show(
|
||||
tester,
|
||||
topics: true,
|
||||
location: RoutePaths.dictionary('vocabulary'),
|
||||
);
|
||||
await tester.enterText(find.byType(TextField), 'たべ');
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Add'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Adds to Vocabulary'), findsOneWidget);
|
||||
expect(
|
||||
[
|
||||
for (final card in await cards.cardsOf('vocabulary'))
|
||||
card.content.entry?.id,
|
||||
],
|
||||
['1358280'],
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Adds to Vocabulary'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('N3').last);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Adds to N3'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'テレビ');
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Add'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
[for (final card in await cards.cardsOf('n3')) card.content.entry?.id],
|
||||
['1080510'],
|
||||
);
|
||||
|
||||
// N5's word, seen from Vocabulary: held, nothing to add.
|
||||
await tester.enterText(find.byType(TextField), '東京');
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('In this studyset'), findsOneWidget);
|
||||
expect(find.text('Add'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a studyset without topics shows no Adds to', (tester) async {
|
||||
await show(tester, location: RoutePaths.dictionary('japanese'));
|
||||
expect(find.textContaining('Adds to'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('tapping Add twice makes one card', (tester) async {
|
||||
await show(tester);
|
||||
await addMaterial(tester, 'From the dictionary');
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
|||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/widget/watch_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.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 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -26,6 +28,7 @@ import '../../support/fake_device_dictionaries.dart';
|
|||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_jellyfin.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
import '../../support/fake_settings_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/fake_tag_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
|
|
@ -39,8 +42,23 @@ void main() {
|
|||
late TagRepository tags;
|
||||
late FakeJellyfinGateway jellyfin;
|
||||
late FakeMediaPlayers players;
|
||||
late FakeStudysetGateway studysets;
|
||||
late FakeSettingsGateway settingsGateway;
|
||||
|
||||
setUp(() {
|
||||
studysets = FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(
|
||||
StudysetContent(
|
||||
title: 'Japanese',
|
||||
description: RichContent.empty(),
|
||||
frontLanguage: 'ja',
|
||||
backLanguage: 'en',
|
||||
),
|
||||
),
|
||||
);
|
||||
settingsGateway = FakeSettingsGateway();
|
||||
cardGateway = FakeCardGateway();
|
||||
tagGateway = FakeTagGateway();
|
||||
cards = CardRepository(cardGateway);
|
||||
|
|
@ -86,18 +104,6 @@ void main() {
|
|||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final studysets = FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(
|
||||
StudysetContent(
|
||||
title: 'Japanese',
|
||||
description: RichContent.empty(),
|
||||
frontLanguage: 'ja',
|
||||
backLanguage: 'en',
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!playerAvailable) {
|
||||
players = FakeMediaPlayers(available: false);
|
||||
}
|
||||
|
|
@ -105,6 +111,7 @@ void main() {
|
|||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: studysets,
|
||||
settingsGateway: settingsGateway,
|
||||
cardGateway: cardGateway,
|
||||
tagGateway: tagGateway,
|
||||
dictionaryGateway:
|
||||
|
|
@ -123,6 +130,14 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Adds words as the screen does, into [studysets].
|
||||
DictionaryAdder adder() => DictionaryAdder(
|
||||
cards,
|
||||
tags,
|
||||
StudysetRepository(studysets),
|
||||
SettingsRepository(settingsGateway),
|
||||
);
|
||||
|
||||
/// Plays on to [position].
|
||||
Future<void> playTo(WidgetTester tester, Duration position) async {
|
||||
players.last!.moveTo(position);
|
||||
|
|
@ -537,7 +552,7 @@ void main() {
|
|||
testWidgets('a word the studyset holds says how far it is learnt, in the '
|
||||
'list of the words it holds', (tester) async {
|
||||
final station = sampleWords.firstWhere((word) => word.headword == '駅');
|
||||
await DictionaryAdder(cards, tags).add('japanese', [station]);
|
||||
await adder().add('japanese', [station]);
|
||||
final card = (await cards.cardsOf('japanese')).single;
|
||||
final reviews = FakeReviewGateway();
|
||||
await reviews.insert([
|
||||
|
|
@ -561,6 +576,55 @@ void main() {
|
|||
expect(find.textContaining('Learned · again'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("watching from a studyset with topics, a topic's word shows "
|
||||
'as held, and a new one goes where Adds to says, shown as added at '
|
||||
'once', (tester) async {
|
||||
studysets.receiveRemoteChange(
|
||||
'n3',
|
||||
encodeStudysetPayload(StudysetContent.titled('N3')),
|
||||
parentId: 'japanese',
|
||||
);
|
||||
final station = sampleWords.firstWhere((word) => word.headword == '駅');
|
||||
await adder().add('n3', [station]);
|
||||
await watch(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Filter the words'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Words in the studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('駅', findRichText: true), findsOneWidget);
|
||||
// The filter's menu stays open for more choices.
|
||||
await tester.tap(find.byTooltip('Filter the words'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Adds to Japanese'));
|
||||
await tester.pumpAndSettle();
|
||||
// The words' levels say N3 too.
|
||||
await tester.tap(find.widgetWithText(MenuItemButton, 'N3'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Adds to N3'), findsOneWidget);
|
||||
|
||||
await players.last!.play();
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
await tester.tap(find.text('いました'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Add'));
|
||||
await tester.pump();
|
||||
// Held and new at once, before the card is written.
|
||||
expect(find.text('Add'), findsNothing);
|
||||
expect(find.text('New'), findsOneWidget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('会う added'), findsOneWidget);
|
||||
expect(
|
||||
[for (final card in await cards.cardsOf('n3')) card.content.entry?.id],
|
||||
containsAll(['6']),
|
||||
);
|
||||
expect(await cards.cardsOf('japanese'), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('the filter narrows the words by their class', (tester) async {
|
||||
await watch(tester);
|
||||
expect(find.textContaining('駅', findRichText: true), findsOneWidget);
|
||||
|
|
@ -752,7 +816,7 @@ void main() {
|
|||
testWidgets("the episode's words can be studied or crammed: the cards of "
|
||||
'its words, held before it was watched too, get its tag', (tester) async {
|
||||
final station = sampleWords.firstWhere((word) => word.headword == '駅');
|
||||
await DictionaryAdder(cards, tags).add('japanese', [station]);
|
||||
await adder().add('japanese', [station]);
|
||||
final card = (await cards.cardsOf('japanese')).single;
|
||||
await watch(tester);
|
||||
await tester.tap(find.byTooltip('Play'));
|
||||
|
|
|
|||
|
|
@ -310,6 +310,74 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
group('where words added from a studyset go', () {
|
||||
test('is the studyset itself until another is chosen; choosing the '
|
||||
'studyset itself forgets the choice', () {
|
||||
final settings = const PersonSettings().withAddDestination(
|
||||
'vocabulary',
|
||||
'n3',
|
||||
);
|
||||
|
||||
expect(const PersonSettings().addDestinationOf('vocabulary'), isNull);
|
||||
expect(settings.addDestinationOf('vocabulary'), 'n3');
|
||||
expect(settings.addDestinationOf('japanese'), isNull);
|
||||
expect(
|
||||
settings.withAddDestination('vocabulary', 'vocabulary').addDestinations,
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('survives a round trip, in the tenth schema, left out while none '
|
||||
'is chosen; a payload from before has none', () {
|
||||
final settings = const PersonSettings().withAddDestination(
|
||||
'vocabulary',
|
||||
'n3',
|
||||
);
|
||||
|
||||
expect(settingsSchemaVersion, 10);
|
||||
expect(
|
||||
decodePayload(
|
||||
encodeSettingsPayload(settings),
|
||||
newestSchema: settingsSchemaVersion,
|
||||
)['addDestinations'],
|
||||
{'vocabulary': 'n3'},
|
||||
);
|
||||
expect(decodeSettingsPayload(encodeSettingsPayload(settings)), settings);
|
||||
expect(
|
||||
decodePayload(
|
||||
encodeSettingsPayload(const PersonSettings()),
|
||||
newestSchema: settingsSchemaVersion,
|
||||
).keys,
|
||||
isNot(contains('addDestinations')),
|
||||
);
|
||||
expect(
|
||||
decodeSettingsPayload(
|
||||
encodePayload(9, {
|
||||
'studyTags': <String, Object?>{},
|
||||
'addDestinations': 'n3',
|
||||
}),
|
||||
).addDestinations,
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('fold as ties do: for a studyset these settings win, the choices '
|
||||
'only the other has are added', () {
|
||||
final here = const PersonSettings().withAddDestination(
|
||||
'vocabulary',
|
||||
'n3',
|
||||
);
|
||||
final there = const PersonSettings()
|
||||
.withAddDestination('vocabulary', 'n5')
|
||||
.withAddDestination('kanji', 'grade-1');
|
||||
|
||||
expect(here.foldedWith(there).addDestinations, {
|
||||
'vocabulary': 'n3',
|
||||
'kanji': 'grade-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('the Jellyfin shows tied to a studyset', () {
|
||||
const show = JellyfinTie(serverId: 'server-1', itemId: 'show-1');
|
||||
const film = JellyfinTie(serverId: 'server-1', itemId: 'film-1');
|
||||
|
|
@ -347,7 +415,7 @@ void main() {
|
|||
newestSchema: settingsSchemaVersion,
|
||||
);
|
||||
|
||||
expect(settingsSchemaVersion, 9);
|
||||
expect(settingsSchemaVersion, greaterThanOrEqualTo(9));
|
||||
expect(payload['jellyfinTies'], {
|
||||
'japanese': [
|
||||
{'server': 'server-1', 'item': 'show-1'},
|
||||
|
|
|
|||
|
|
@ -91,6 +91,60 @@ void main() {
|
|||
expect((await repository.read()).byId(own)?.fromJellyfin, isFalse);
|
||||
});
|
||||
|
||||
test('tags cards by name in one go: the tags missing are made, those '
|
||||
'named for Jellyfin as made for it, a name in any case is the tag it '
|
||||
'names, and the tags are read once for it and once after', () async {
|
||||
final level = await repository.create('JLPT-N5');
|
||||
final reads = gateway.cardTagReads;
|
||||
|
||||
final ids = await repository.addTagsNamed(
|
||||
{
|
||||
'card-1': {'jlpt-n5', 'Noun', 'あかね噺'},
|
||||
'card-2': {'Noun', 'あかね噺 S1 E1'},
|
||||
},
|
||||
fromJellyfin: {'あかね噺', 'あかね噺 S1 E1'},
|
||||
);
|
||||
|
||||
expect(gateway.cardTagReads - reads, lessThanOrEqualTo(2));
|
||||
expect(ids['jlpt-n5'], level);
|
||||
expect(ids.keys, containsAll(['Noun', 'あかね噺', 'あかね噺 S1 E1']));
|
||||
final tagging = await repository.read();
|
||||
expect(
|
||||
{for (final tag in tagging.of('card-1')) tag.name},
|
||||
{'JLPT-N5', 'Noun', 'あかね噺'},
|
||||
);
|
||||
expect(
|
||||
{for (final tag in tagging.of('card-2')) tag.name},
|
||||
{'Noun', 'あかね噺 S1 E1'},
|
||||
);
|
||||
expect(
|
||||
[
|
||||
for (final tag in tagging.all)
|
||||
if (tag.fromJellyfin) tag.name,
|
||||
],
|
||||
unorderedEquals(['あかね噺', 'あかね噺 S1 E1']),
|
||||
);
|
||||
});
|
||||
|
||||
test('tagging by name gives a card a tag it has once, and refuses a '
|
||||
'blank name before writing anything', () async {
|
||||
await repository.addTagsNamed({
|
||||
'card-1': {'Noun'},
|
||||
});
|
||||
await repository.addTagsNamed({
|
||||
'card-1': {'noun'},
|
||||
});
|
||||
expect(gateway.cardTagCount, 1);
|
||||
|
||||
await expectLater(
|
||||
repository.addTagsNamed({
|
||||
'card-2': {'Verb', ' '},
|
||||
}),
|
||||
throwsArgumentError,
|
||||
);
|
||||
expect((await repository.read()).named('Verb'), isNull);
|
||||
});
|
||||
|
||||
test('lists the tags by name, whatever their case', () async {
|
||||
await repository.create('verb');
|
||||
await repository.create('adjective');
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ class FakeTagGateway() implements TagGateway {
|
|||
/// When set, reading the tags fails with it.
|
||||
Exception? failReads;
|
||||
|
||||
/// How often the cards' tags were read: every card's, so reading them is
|
||||
/// what costs.
|
||||
int cardTagReads = 0;
|
||||
|
||||
/// How many tags cards have, in all.
|
||||
int get cardTagCount => _cardTags.length;
|
||||
|
||||
/// Stores the tag [id] as if another device had written it and a sync had
|
||||
/// brought it here.
|
||||
void receiveRemoteTag(String id, ByteData payload) {
|
||||
|
|
@ -67,7 +74,10 @@ class FakeTagGateway() implements TagGateway {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<List<CardTagRow>> cardTags() async => [..._cardTags.values];
|
||||
Future<List<CardTagRow>> cardTags() async {
|
||||
cardTagReads++;
|
||||
return [..._cardTags.values];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NoteTagRow>> noteTags() async => [..._noteTags.values];
|
||||
|
|
|
|||
Loading…
Reference in a new issue