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_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/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 character', () { test('reads its meanings, readings, grade, strokes and frequency from ' 'the entry', () { final read = DictionaryCharacter.fromEntry( ref: (source: 'kanjidic', id: '食'), headword: '食', labels: ['jlpt:n5', 'grade:2'], entry: '{"meanings":["eat","food"],"on":["ショク","ジキ"],' '"kun":["く.う","た.べる"],"grade":2,"strokes":9,' '"frequency":328,"jlpt":5}', ); expect(read.meanings, ['eat', 'food']); expect(read.onReadings, ['ショク', 'ジキ']); expect(read.kunReadings, ['く.う', 'た.べる']); expect((read.grade, read.strokes, read.frequency), (2, 9, 328)); expect(read.jlpt, 5); expect(read.isKana, isFalse); }); test('becomes a character card that keeps where it is from; a kana ' 'means its romaji', () { final eat = character( '食', meanings: ['eat'], on: ['ショク'], kun: ['た.べる'], ).card; expect(eat.type, CardType.character); expect(eat.front.plainText.trim(), '食'); expect(eat.meanings, ['eat']); expect(eat.onReadings, ['ショク']); expect(eat.kunReadings, ['た.べる']); expect(eat.entry, (source: 'kanjidic', id: '食')); expect(eat.isComplete, isTrue); final a = character('あ', labels: ['hiragana'], romaji: 'a').card; expect(a.meanings, ['a']); expect(a.isComplete, isTrue); }); test('names the tags by JLPT level, school grade and script', () { expect( character('食', labels: ['jlpt:n5', 'grade:2'], grade: 2).tagNames, ['JLPT-N5', 'Grade 2'], ); expect( character( '亜', labels: ['jlpt:n1', 'grade:secondary'], grade: 8, ).tagNames, ['JLPT-N1', 'Secondary school'], ); expect(character('ア', labels: ['katakana']).tagNames, ['Katakana']); expect(isDictionaryTagName('grade 2'), isTrue); expect(isDictionaryTagName('Hiragana'), isTrue); expect(isDictionaryTagName('Kanji I like'), isFalse); }); }); group('a character of another language', () { test("Chinese: its pinyin as its readings, its HSK level and Hong Kong's " 'grade from its labels', () { final say = DictionaryCharacter.fromEntry( ref: (source: 'unihan-hant', id: '說'), headword: '說', labels: const ['hsk:1', 'common', 'grade:1', 'radical:149'], entry: '{"meanings": ["speak", "say"], "pinyin": ["shuō", "shuì"], ' '"strokes": 14, "simplified": ["说"]}', language: 'zh-Hant', ); expect(say.onReadings, ['shuō', 'shuì']); expect(say.kunReadings, isEmpty); expect((say.hsk, say.grade, say.strokes), (1, 1, 14)); expect(say.tagNames, ['HSK-1', 'Grade 1']); expect(say.card.onReadings, ['shuō', 'shuì']); }); test('Korean: a Hanja its sounds and native meanings, the 1,800 of ' 'education tagged; a letter its sound and name', () { final water = DictionaryCharacter.fromEntry( ref: (source: 'unihan-ko', id: '水'), headword: '水', labels: const ['education', 'radical:85'], entry: '{"meanings": ["water"], "eum": ["수"], "hun": ["물"], ' '"eumhun": ["물 수"], "strokes": 4}', language: 'ko', ); expect(water.onReadings, ['수']); expect(water.kunReadings, ['물']); expect(water.isEducationHanja, isTrue); expect(water.tagNames, ['Education Hanja']); final giyeok = DictionaryCharacter.fromEntry( ref: (source: 'unihan-ko', id: 'ㄱ'), headword: 'ㄱ', labels: const ['hangul'], entry: '{"romaji": "g, k", "script": "hangul", "name": "기역"}', language: 'ko', ); expect(giyeok.isKana, isTrue); expect(giyeok.meaningsShown, ['g, k', '기역']); expect(giyeok.tagNames, ['Hangul']); }); test('Greek: a letter with its capital, its transliteration and its ' 'names', () { final beta = DictionaryCharacter.fromEntry( ref: (source: 'greek-alphabet', id: 'β'), headword: 'Β β', labels: const ['greek'], entry: '{"romaji": "v", "script": "greek", "name": "βήτα", ' '"englishName": "beta", "sound": "v"}', language: 'el', ); expect(beta.card.front.plainText.trim(), 'Β β'); expect(beta.card.meanings, ['v', 'βήτα (beta)']); expect(beta.tagNames, ['Greek alphabet']); expect(isDictionaryTagName('Greek alphabet'), isTrue); }); test("a Korean word's KRDict level names its tag as KRDict does", () { final school = DictionaryWord.fromEntry( ref: (source: 'krdict', id: '73276'), headword: '학교', reading: '학꾜', labels: const ['level:beginner'], entry: '{"pos": "noun", "hanja": ["學校"], ' '"senses": [{"glosses": ["school"]}]}', language: 'ko', ); expect(school.koreanLevel, 1); expect(school.tagNames, ['초급', 'Noun']); expect(isDictionaryTagName('초급'), isTrue); expect(school.card.reading, '학꾜'); }); }); group('Cards behind the dictionary', () { late FakeCardGateway cardGateway; late CardRepository cards; setUp(() { cardGateway = FakeCardGateway(); cards = CardRepository(cardGateway); }); tearDown(() => cards.dispose()); test('are those whose entry the source corrected since, with the entry ' 'as it is now; cards of their own and made before versions are ' 'never asked about', () async { final gateway = FakeDictionaryGateway( words: [ word('1358280', '食べる', glosses: ['to eat'], version: 'v2'), word('1080510', 'テレビ', glosses: ['TV'], version: 'v1'), ], ); final updates = DictionaryUpdates(DictionaryRepository(gateway), cards); CardContent made(String id, String? version) => CardContent( type: CardType.vocabulary, front: RichContent.plain(id), back: RichContent.empty(), meanings: const ['x'], entry: (source: 'jmdict', id: id), entryVersion: version, ); final behind = await cards.create('japanese', made('1358280', 'v1')); await cards.create('japanese', made('1080510', 'v1')); await cards.create('japanese', made('1080510', null)); await cards.create('japanese', CardContent.empty()); final found = await updates.behind( await cards.cardsOf('japanese'), (front: 'ja', back: 'en'), ); expect(found.keys, [behind]); expect(found[behind]!.version, 'v2'); expect(gateway.changedAsked, [2]); }); test('of one entry keeping different versions are each compared', () async { final gateway = FakeDictionaryGateway( words: [ word('1358280', '食べる', glosses: ['to eat'], version: 'v2'), ], ); final updates = DictionaryUpdates(DictionaryRepository(gateway), cards); CardContent made(String version) => CardContent( type: CardType.vocabulary, front: RichContent.plain('食べる'), back: RichContent.empty(), meanings: const ['to eat'], entry: (source: 'jmdict', id: '1358280'), entryVersion: version, ); await cards.create('japanese', made('v2')); final old = await cards.create('japanese', made('v1')); final found = await updates.behind( await cards.cardsOf('japanese'), (front: 'ja', back: 'en'), ); expect(found.keys, [old]); }); }); 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, ''); }); test('its first three example sentences become hints, the word in bold ' 'and the English below', () { String example(int n) => '{"text":"食べ","ja":"$n: 魚を食べた。","en":"Sentence $n."}'; final word = DictionaryWord.fromEntry( ref: (source: 'jmdict', id: '1358280'), headword: '食べる', reading: 'たべる', labels: const [], entry: '{"senses":[{"glosses":["to eat"],"examples":[${example(1)},' '${example(2)}]},{"glosses":["to live on"],"examples":[' '${example(3)},${example(4)}]}]}', ); final hints = word.card.hints; expect(hints, hasLength(3)); expect(hints.first.plainText, '1: 魚を食べた。\nSentence 1.'); final document = hints.first.editorJson['document']! as Map; final first = (document['children']! as List).first as Map; expect((first['data']! as Map)['delta'], [ {'insert': '1: 魚を'}, { 'insert': '食べ', 'attributes': {'bold': true}, }, {'insert': 'た。'}, ]); }); }); 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; one of words unless a " 'kind is asked for', () async { const german = Dictionary( source: 'jmdict', sourceVersion: '1', kind: DictionaryKind.words, language: 'ja', glossLanguage: 'de', license: 'CC-BY-SA-4.0', credits: [], entryCount: 1, ); final repository = DictionaryRepository( FakeDictionaryGateway(available: [kanjidic, 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); expect( await repository.dictionaryFor(( front: 'ja', back: 'en', ), kind: DictionaryKind.characters), kanjidic, ); }); 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 = []; 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('makes one card of a word added twice at once, or twice in one ' 'list', () async { final both = await Future.wait([ adder.add('japanese', [taberu]), adder.add('japanese', [taberu]), ]); expect(both, [(added: 1, skipped: 0), (added: 0, skipped: 1)]); expect( await adder.add('japanese', [terebi, terebi]), (added: 1, skipped: 1), ); expect(await cards.cardsOf('japanese'), hasLength(2)); }); test('names the tags by level and word class', () { expect(taberu.tagNames, ['JLPT-N5', 'Verb']); expect( word('1', 'x', pos: ['adj-na', 'n']).tagNames, ['na-Adjective', 'Noun'], ); expect(word('2', 'y', pos: ['vt']).tagNames, isEmpty); }); }); }