From 5556a73899eb2e581d0217162f733b523ce1e2b2 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 3 Jun 2026 17:01:53 +0900 Subject: [PATCH] test/models: test json serialization roundtrips for all models --- test/models/kanji_search_test.dart | 93 ++++++++++ test/models/word_search_test.dart | 282 +++++++++++++++++++++++++++++ 2 files changed, 375 insertions(+) create mode 100644 test/models/kanji_search_test.dart create mode 100644 test/models/word_search_test.dart diff --git a/test/models/kanji_search_test.dart b/test/models/kanji_search_test.dart new file mode 100644 index 0000000..01619eb --- /dev/null +++ b/test/models/kanji_search_test.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:jadb/models/kanji_search/kanji_search_radical.dart'; +import 'package:jadb/models/kanji_search/kanji_search_result.dart'; +import 'package:test/test.dart'; + +Object? _roundTripJson(Object? value) => jsonDecode(jsonEncode(value)); + +Map _roundTripMap(Object? json) => + Map.from(_roundTripJson(json) as Map); + +void main() { + test('KanjiSearchRadical JSON serialization roundtrip', () { + const radical = KanjiSearchRadical( + symbol: '人', + names: ['ひと', 'にんべん'], + forms: ['亻'], + meanings: ['person', 'human'], + ); + + final restored = KanjiSearchRadical.fromJson( + _roundTripMap(radical.toJson()), + ); + + expect(restored, equals(radical)); + }); + + test('KanjiSearchResult JSON serialization roundtrip', () { + const result = KanjiSearchResult( + kanji: '休', + taughtIn: 1, + jlptLevel: 'N5', + newspaperFrequencyRank: 1234, + strokeCount: 6, + meanings: ['rest', 'day off'], + kunyomi: ['やす.む', 'やす.まる'], + onyomi: ['キュウ'], + radical: KanjiSearchRadical( + symbol: '人', + names: ['ひと', 'にんべん'], + forms: ['亻'], + meanings: ['person', 'human'], + ), + parts: ['亻', '木'], + codepoints: {'ucs': '4F11', 'jis208': '1-22-57'}, + nanori: ['やす'], + alternativeLanguageReadings: { + 'korean': ['휴'], + 'pinyin': ['xiū'], + }, + strokeMiscounts: [5, 7], + queryCodes: { + 'skip': ['1-2-4'], + 'fourCorner': ['2429.0'], + }, + dictionaryReferences: {'nelson_c': '122', 'heisig': '457'}, + ); + + final restored = KanjiSearchResult.fromJson(_roundTripMap(result.toJson())); + + expect(restored, equals(result)); + }); + + test( + 'KanjiSearchResult JSON serialization roundtrip - nullable and empty fields', + () { + const result = KanjiSearchResult( + kanji: '々', + taughtIn: null, + jlptLevel: null, + newspaperFrequencyRank: null, + strokeCount: 3, + meanings: ['iteration mark'], + kunyomi: [], + onyomi: [], + radical: null, + parts: [], + codepoints: {'ucs': '3005'}, + nanori: [], + alternativeLanguageReadings: {}, + strokeMiscounts: [], + queryCodes: {}, + dictionaryReferences: {}, + ); + + final restored = KanjiSearchResult.fromJson( + _roundTripMap(result.toJson()), + ); + + expect(restored, equals(result)); + }, + ); +} diff --git a/test/models/word_search_test.dart b/test/models/word_search_test.dart new file mode 100644 index 0000000..893a213 --- /dev/null +++ b/test/models/word_search_test.dart @@ -0,0 +1,282 @@ +import 'dart:convert'; + +import 'package:jadb/models/common/jlpt_level.dart'; +import 'package:jadb/models/jmdict/jmdict_dialect.dart'; +import 'package:jadb/models/jmdict/jmdict_field.dart'; +import 'package:jadb/models/jmdict/jmdict_kanji_info.dart'; +import 'package:jadb/models/jmdict/jmdict_misc.dart'; +import 'package:jadb/models/jmdict/jmdict_pos.dart'; +import 'package:jadb/models/jmdict/jmdict_reading_info.dart'; +import 'package:jadb/models/word_search/word_search_match_span.dart'; +import 'package:jadb/models/word_search/word_search_result.dart'; +import 'package:jadb/models/word_search/word_search_ruby.dart'; +import 'package:jadb/models/word_search/word_search_sense.dart'; +import 'package:jadb/models/word_search/word_search_sense_language_source.dart'; +import 'package:jadb/models/word_search/word_search_sources.dart'; +import 'package:jadb/models/word_search/word_search_xref_entry.dart'; +import 'package:test/test.dart'; + +Object? _roundTripJson(Object? value) => jsonDecode(jsonEncode(value)); + +Map _roundTripMap(Object? json) => + Map.from(_roundTripJson(json) as Map); + +Map _roundTripObjectMap(Object? json) => + Map.from(_roundTripJson(json) as Map); + +void _expectScalarRoundTrip({ + required Iterable values, + required Object? Function(T value) toJson, + required T Function(Object? json) fromJson, +}) { + for (final value in values) { + expect( + fromJson(_roundTripJson(toJson(value))), + equals(value), + reason: 'Roundtrip failed for $value', + ); + } +} + +void _expectMapRoundTrip({ + required Iterable values, + required Map Function(T value) toJson, + required T Function(Map json) fromJson, +}) { + for (final value in values) { + expect( + fromJson(_roundTripObjectMap(toJson(value))), + equals(value), + reason: 'Roundtrip failed for $value', + ); + } +} + +WordSearchResult _buildNestedXrefResult() => WordSearchResult( + score: 7, + entryId: 300, + isCommon: false, + japanese: [WordSearchRuby(base: '補助', furigana: 'ほじょ')], + kanjiInfo: const {}, + readingInfo: const {}, + senses: const [], + jlptLevel: JlptLevel.none, + sources: const WordSearchSources(jmdict: true, jmnedict: false), +); + +WordSearchSense _buildSense() => WordSearchSense( + englishDefinitions: ['kana', 'syllabary'], + partsOfSpeech: [JMdictPOS.n], + seeAlso: [ + WordSearchXrefEntry( + entryId: 300, + ambiguous: false, + baseWord: '仮名遣い', + furigana: 'かなづかい', + xrefResult: _buildNestedXrefResult(), + ), + ], + antonyms: const [ + WordSearchXrefEntry( + entryId: 301, + ambiguous: true, + baseWord: '漢字', + furigana: 'かんじ', + xrefResult: null, + ), + ], + restrictedToReading: ['かな'], + restrictedToKanji: ['仮名'], + fields: [JMdictField.linguistics], + dialects: [JMdictDialect.kansai], + misc: [JMdictMisc.onlyKana, JMdictMisc.rare], + info: ['Typically written using kana alone.'], + languageSource: const [ + WordSearchSenseLanguageSource( + language: 'por', + phrase: 'canoa', + fullyDescribesSense: false, + constructedFromSmallerWords: true, + ), + ], +); + +WordSearchResult _buildWordSearchResult({ + List? matchSpans, +}) => WordSearchResult( + score: 42, + entryId: 123, + isCommon: true, + japanese: [ + WordSearchRuby(base: '仮名', furigana: 'かな'), + WordSearchRuby(base: 'かな'), + ], + kanjiInfo: {'仮名': JMdictKanjiInfo.rK, '仮': JMdictKanjiInfo.ateji}, + readingInfo: {'かな': JMdictReadingInfo.gikun, 'カナ': JMdictReadingInfo.rk}, + senses: [_buildSense()], + jlptLevel: JlptLevel.n5, + sources: const WordSearchSources(jmdict: true, jmnedict: true), + matchSpans: matchSpans, +); + +void main() { + test('JlptLevel JSON serialization roundtrip', () { + _expectScalarRoundTrip( + values: JlptLevel.values, + toJson: (value) => value.toJson(), + fromJson: JlptLevel.fromJson, + ); + }); + + test('JMdictDialect JSON serialization roundtrip', () { + _expectMapRoundTrip( + values: JMdictDialect.values, + toJson: (value) => value.toJson(), + fromJson: JMdictDialect.fromJson, + ); + }); + + test('JMdictField JSON serialization roundtrip', () { + _expectMapRoundTrip( + values: JMdictField.values, + toJson: (value) => value.toJson(), + fromJson: JMdictField.fromJson, + ); + }); + + test('JMdictKanjiInfo JSON serialization roundtrip', () { + _expectMapRoundTrip( + values: JMdictKanjiInfo.values, + toJson: (value) => value.toJson(), + fromJson: JMdictKanjiInfo.fromJson, + ); + }); + + test('JMdictMisc JSON serialization roundtrip', () { + _expectMapRoundTrip( + values: JMdictMisc.values, + toJson: (value) => value.toJson(), + fromJson: JMdictMisc.fromJson, + ); + }); + + test('JMdictPOS JSON serialization roundtrip', () { + _expectMapRoundTrip( + values: JMdictPOS.values, + toJson: (value) => value.toJson(), + fromJson: JMdictPOS.fromJson, + ); + }); + + test('JMdictReadingInfo JSON serialization roundtrip', () { + _expectMapRoundTrip( + values: JMdictReadingInfo.values, + toJson: (value) => value.toJson(), + fromJson: JMdictReadingInfo.fromJson, + ); + }); + + test('WordSearchMatchSpan JSON serialization roundtrip', () { + final span = WordSearchMatchSpan( + spanType: WordSearchMatchSpanType.sense, + index: 2, + subIndex: 3, + start: 4, + end: 8, + ); + + final restored = WordSearchMatchSpan.fromJson(_roundTripMap(span.toJson())); + + expect(restored, equals(span)); + expect(restored.subIndex, equals(span.subIndex)); + }); + + test('WordSearchRuby JSON serialization roundtrip', () { + final ruby = WordSearchRuby(base: '仮名', furigana: 'かな'); + + final restored = WordSearchRuby.fromJson(_roundTripMap(ruby.toJson())); + + expect(restored.toJson(), equals(ruby.toJson())); + }); + + test('WordSearchSenseLanguageSource JSON serialization roundtrip', () { + const source = WordSearchSenseLanguageSource( + language: 'por', + phrase: 'canoa', + fullyDescribesSense: false, + constructedFromSmallerWords: true, + ); + + final restored = WordSearchSenseLanguageSource.fromJson( + _roundTripMap(source.toJson()), + ); + + expect(restored.toJson(), equals(source.toJson())); + }); + + test('WordSearchSources JSON serialization roundtrip', () { + const sources = WordSearchSources(jmdict: false, jmnedict: true); + + final restored = WordSearchSources.fromJson( + _roundTripMap(sources.toJson()), + ); + + expect(restored.toJson(), equals(sources.toJson())); + expect(restored.sqlValue, equals(sources.sqlValue)); + }); + + test('WordSearchXrefEntry JSON serialization roundtrip', () { + final entry = WordSearchXrefEntry( + entryId: 300, + ambiguous: false, + baseWord: '仮名遣い', + furigana: 'かなづかい', + xrefResult: _buildNestedXrefResult(), + ); + + final restored = WordSearchXrefEntry.fromJson( + _roundTripMap(entry.toJson()), + ); + + expect(restored.toJson(), equals(entry.toJson())); + expect(restored.xrefResult?.toJson(), equals(entry.xrefResult?.toJson())); + }); + + test('WordSearchSense JSON serialization roundtrip', () { + final sense = _buildSense(); + + final restored = WordSearchSense.fromJson(_roundTripMap(sense.toJson())); + + expect(restored.toJson(), equals(sense.toJson())); + }); + + test('WordSearchResult JSON serialization roundtrip', () { + final result = _buildWordSearchResult(); + + final restored = WordSearchResult.fromJson(_roundTripMap(result.toJson())); + + expect(restored.toJson(), equals(result.toJson())); + expect(restored.matchSpans, isNull); + }); + + test('WordSearchResult leaves matchSpans out of JSON', () { + final result = _buildWordSearchResult( + matchSpans: [ + WordSearchMatchSpan( + spanType: WordSearchMatchSpanType.sense, + index: 0, + subIndex: 1, + start: 0, + end: 4, + ), + ], + ); + + final json = _roundTripMap(result.toJson()); + final restored = WordSearchResult.fromJson(json); + + expect(json.containsKey('matchSpans'), isFalse); + expect(restored.matchSpans, isNull); + expect(restored.toJson(), equals(result.toJson())); + }); +}