feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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 01:33:12 +02:00
|
|
|
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],
|
|
|
|
|
[
|
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 10:48:06 +02:00
|
|
|
'JLPT-N5',
|
|
|
|
|
'Verb',
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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 01:33:12 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
[for (final tag in tagging.of(made.last.id)) tag.name],
|
|
|
|
|
[
|
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 10:48:06 +02:00
|
|
|
'JLPT-N5',
|
|
|
|
|
'Noun',
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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 01:33:12 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
expect((await cards.allCards()).every((card) => card.imported), isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('names the tags by level and word class', () {
|
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 10:48:06 +02:00
|
|
|
expect(tagNamesOf(taberu), ['JLPT-N5', 'Verb']);
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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 01:33:12 +02:00
|
|
|
expect(
|
|
|
|
|
tagNamesOf(word('1', 'x', pos: ['adj-na', 'n'])),
|
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 10:48:06 +02:00
|
|
|
['na-Adjective', 'Noun'],
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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 01:33:12 +02:00
|
|
|
);
|
|
|
|
|
expect(tagNamesOf(word('2', 'y', pos: ['vt'])), isEmpty);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|