halkyon/halkyon_learn_flutter/test/feature/dictionary/dictionary_data_test.dart
Tizian 「ティツィアーン」 c20578e421
feat(app): tags keep their case; dictionary tags read JLPT-N5, Noun
A tag is still its name, but no longer in lowercase: the name keeps the
case it was written in, and names that differ only in case are one tag
(ADR 0004, rule 2, amended). Two such tags from two devices fold into the
one with the smaller id, its case with it. Tags stored before stay
lowercase until renamed.

Words from the dictionary are tagged JLPT-N5 to JLPT-N1 and with their
word class (Noun, Verb, i-Adjective, na-Adjective, ...). Anki tags and
PDF chapter tags arrive as written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:27 +02:00

173 lines
5.5 KiB
Dart

import 'package:flutter_test/flutter_test.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/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/model/dictionary.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_dictionary_gateway.dart';
import '../../support/fake_tag_gateway.dart';
final taberu = word(
'1358280',
'食べる',
reading: 'たべる',
glosses: ['to eat'],
labels: ['common', 'jlpt:n5'],
pos: ['v1', 'vt'],
);
final terebi = word(
'1080510',
'テレビ',
glosses: ['television', 'TV'],
labels: ['common', 'jlpt:n5'],
pos: ['n'],
);
void main() {
group('a word', () {
test('reads its senses from the entry, the codes with them', () {
final read = DictionaryWord.fromEntry(
ref: (source: 'jmdict', id: '1358280'),
headword: '食べる',
reading: 'たべる',
labels: ['common', 'jlpt:n5'],
entry:
'{"forms":[{"text":"食べる"}],"senses":[{"glosses":["to eat"],'
'"pos":["v1","vt"]},{"glosses":["to live on"],"misc":["uk"]}]}',
);
expect(read.senses, const [
VocabularySense(glosses: ['to eat'], pos: ['v1', 'vt']),
VocabularySense(glosses: ['to live on'], misc: ['uk']),
]);
expect(read.jlpt, 5);
});
test('becomes a vocabulary card that keeps where it is from; a word in '
'kana has no reading', () {
final card = taberu.card;
expect(card.type, CardType.vocabulary);
expect(card.front.plainText.trim(), '食べる');
expect(card.reading, 'たべる');
expect(card.meanings, ['to eat']);
expect(card.entry, (source: 'jmdict', id: '1358280'));
expect(terebi.card.reading, '');
});
});
group('the repository', () {
test('asks for the dictionaries once, again after a failure', () async {
final gateway = FakeDictionaryGateway()..fail = Exception('offline');
final repository = DictionaryRepository(gateway);
await expectLater(repository.dictionaries(), throwsException);
gateway.fail = null;
expect(await repository.dictionaries(), [jmdict]);
await repository.dictionaries();
expect(gateway.asked, 2);
});
test("finds a studyset's dictionary by its front's language, with "
"meanings in its back's, else in English", () async {
const german = Dictionary(
source: 'jmdict',
sourceVersion: '1',
language: 'ja',
glossLanguage: 'de',
license: 'CC-BY-SA-4.0',
credits: [],
entryCount: 1,
);
final repository = DictionaryRepository(
FakeDictionaryGateway(available: [jmdict, german]),
);
expect(await repository.dictionaryFor((front: 'ja', back: 'de')), german);
expect(await repository.dictionaryFor((front: 'ja', back: 'fr')), jmdict);
expect(await repository.dictionaryFor((front: 'ja', back: null)), jmdict);
expect(await repository.dictionaryFor((front: 'ko', back: 'en')), isNull);
expect(await repository.dictionaryFor((front: null, back: 'en')), isNull);
});
test('brings every word of a level, a page at a time', () async {
final words = [
for (var i = 0; i < DictionaryRepository.page + 3; i++)
word('$i', 'w$i', labels: ['jlpt:n5']),
];
final counts = <int>[];
final all = await DictionaryRepository(
FakeDictionaryGateway(words: words),
).allLabelled(jmdict, 'jlpt:n5', onProgress: counts.add);
expect(all, words);
expect(counts, [
DictionaryRepository.page,
DictionaryRepository.page + 3,
]);
});
});
group('adding words', () {
late CardRepository cards;
late TagRepository tags;
late DictionaryAdder adder;
setUp(() {
cards = CardRepository(FakeCardGateway());
tags = TagRepository(FakeTagGateway());
adder = DictionaryAdder(cards, tags);
});
tearDown(() async {
await cards.dispose();
await tags.dispose();
});
test('makes each word a card once, tagged with its level and word '
'class, marked as from the dictionary', () async {
expect(await adder.add('japanese', [taberu]), (added: 1, skipped: 0));
expect(
await adder.add('japanese', [taberu, terebi]),
(added: 1, skipped: 1),
);
final made = await cards.cardsOf('japanese');
expect(
[for (final card in made) card.content.entry?.id],
[
'1358280',
'1080510',
],
);
final tagging = await tags.read();
expect(
[for (final tag in tagging.of(made.first.id)) tag.name],
[
'JLPT-N5',
'Verb',
],
);
expect(
[for (final tag in tagging.of(made.last.id)) tag.name],
[
'JLPT-N5',
'Noun',
],
);
expect((await cards.allCards()).every((card) => card.imported), isTrue);
});
test('names the tags by level and word class', () {
expect(tagNamesOf(taberu), ['JLPT-N5', 'Verb']);
expect(
tagNamesOf(word('1', 'x', pos: ['adj-na', 'n'])),
['na-Adjective', 'Noun'],
);
expect(tagNamesOf(word('2', 'y', pos: ['vt'])), isEmpty);
});
});
}