import 'dart:convert'; import 'package:halkyon_content/rich_text.dart'; import 'package:test/test.dart'; void main() { /// The editor's JSON for a page with one paragraph per text. Map paragraphs(List texts) => { 'document': { 'type': 'page', 'children': [ for (final text in texts) { 'type': 'paragraph', 'data': { 'delta': [ {'insert': text}, ], }, }, ], }, }; group('RichContent', () { test('without a first heading that repeats a title, whatever its case ' 'and spaces; anything else stays', () { RichContent page(List> children) => RichContent({ 'document': {'type': 'page', 'children': children}, }); Map heading(String text) => { 'type': 'heading', 'data': { 'level': 1, 'delta': [ {'insert': text}, ], }, }; final text = { 'type': 'paragraph', 'data': { 'delta': [ {'insert': 'In Japanese…'}, ], }, }; expect( page([heading('What is Kanji?'), text]) .withoutHeading('what is kanji?'), page([text]), ); final other = page([heading('Kanji'), text]); expect(other.withoutHeading('What is Kanji?'), other); final later = page([text, heading('What is Kanji?')]); expect(later.withoutHeading('What is Kanji?'), later); expect(other.withoutHeading(''), other); }); test('equals content with the same JSON, whatever the key order', () { final a = RichContent({ 'document': {'type': 'page', 'children': []}, }); final b = RichContent({ 'document': {'children': [], 'type': 'page'}, }); expect(a, b); expect(a.hashCode, b.hashCode); }); test('differs when the text differs', () { expect( RichContent(paragraphs(['猫'])), isNot(RichContent(paragraphs(['犬']))), ); }); test('differs when the format version differs', () { expect( RichContent(paragraphs(['猫'])), isNot(RichContent(paragraphs(['猫']), formatVersion: 0)), ); }); test('is not affected by later changes to the JSON it was made from', () { final json = paragraphs(['猫']); final content = RichContent(json); final document = json['document']! as Map; (document['children']! as List).clear(); expect(content, RichContent(paragraphs(['猫']))); }); test('cannot be changed through its JSON', () { final content = RichContent(paragraphs(['猫'])); final document = content.editorJson['document']! as Map; final children = document['children']! as List; expect(() => document['type'] = 'heading', throwsUnsupportedError); expect(children.clear, throwsUnsupportedError); }); test('survives a JSON round trip, Japanese text and marks included', () { final content = RichContent({ 'document': { 'type': 'page', 'children': [ { 'type': 'heading', 'data': { 'level': 2, 'delta': [ {'insert': '日本語'}, ], }, }, { 'type': 'paragraph', 'data': { 'delta': [ { 'insert': '猫', 'attributes': {'bold': true}, }, {'insert': 'は動物です。'}, ], }, }, ], }, }); final stored = jsonEncode(content.toJson()); final read = RichContent.fromJson( jsonDecode(stored) as Map, ); expect(read, content); }); test('rejects content in a newer format than this app knows', () { expect( () => RichContent.fromJson({ 'formatVersion': RichContent.currentFormatVersion + 1, 'editor': RichContent.empty().editorJson, }), throwsFormatException, ); }); test('rejects JSON that is not rich content', () { expect( () => RichContent.fromJson({'formatVersion': 1, 'editor': 'text'}), throwsFormatException, ); expect( () => RichContent.fromJson({'editor': RichContent.empty().editorJson}), throwsFormatException, ); }); }); test("RichContent.imageIds lists the person's images it shows, in order " 'and once each; no sound, nor an image from elsewhere', () { Map block(String type, String url) => { 'type': type, 'data': {'url': url}, }; final content = RichContent({ 'document': { 'type': 'page', 'children': [ block('image', 'asset:b'), block('audio', 'asset:sound'), block('image', 'https://example.org/cat.png'), block('image', 'asset:a'), block('image', 'asset:b'), ], }, }); expect(content.imageIds, ['b', 'a']); }); group('RichContent.plainText', () { test('is the text without formatting, one line per block', () { expect( RichContent(paragraphs(['ひらがな', 'かたかな'])).plainText, 'ひらがな\nかたかな', ); }); test('joins the pieces of a formatted block', () { final content = RichContent({ 'document': { 'type': 'page', 'children': [ { 'type': 'paragraph', 'data': { 'delta': [ {'insert': 'big '}, { 'insert': 'cat', 'attributes': {'bold': true}, }, ], }, }, ], }, }); expect(content.plainText, 'big cat'); }); test('is empty for empty content', () { expect(RichContent.empty().plainText, isEmpty); }); }); group('files', () { RichContent blocks(List> children) => RichContent({ 'document': {'type': 'page', 'children': children}, }); test('names the asset rows its images and sounds show', () { final content = blocks([ { 'type': 'image', 'data': {'url': assetUrl('cat')}, }, { 'type': 'audio', 'data': {'url': assetUrl('neko')}, }, { 'type': 'image', 'data': {'url': 'https://example.com/dog.png'}, }, ]); expect(content.assetIds, {'cat', 'neko'}); expect(content.hasImages, isTrue); expect(content.hasSounds, isTrue); }); test('content of only an image or a sound is not blank', () { expect( blocks([ { 'type': 'image', 'data': {'url': assetUrl('cat')}, }, ]).isBlank, isFalse, ); expect( blocks([ { 'type': 'audio', 'data': {'url': assetUrl('neko')}, }, ]).isBlank, isFalse, ); expect(RichContent.plain(' ').isBlank, isTrue); expect(RichContent.empty().isBlank, isTrue); }); test("a block's text counts as text: a formula, a callout's title, a " "video's caption, a study button's label, a spoiler's hidden text", () { final content = blocks([ { 'type': mathBlockType, 'data': {mathFormulaKey: 'E = mc^2'}, }, { 'type': calloutBlockType, 'data': {calloutTitleKey: 'Mind the particle'}, 'children': [ { 'type': 'paragraph', 'data': { 'delta': [ {'insert': 'は, not わ'}, ], }, }, ], }, { 'type': youtubeBlockType, 'data': {youtubeVideoKey: 'dQw4w9WgXcQ', youtubeCaptionKey: 'Listen'}, }, { 'type': studyButtonBlockType, 'data': { studyButtonStudysetKey: 'japanese', studyButtonLabelKey: 'Go', }, }, { 'type': spoilerBlockType, 'data': { 'delta': [ {'insert': 'ねこ'}, ], }, }, ]); expect( content.plainText, 'E = mc^2\nMind the particle\nは, not わ\nListen\nGo\nねこ', ); }); test('a video or a study button is something without any text; an ' 'empty formula or callout is nothing', () { expect( blocks([ { 'type': youtubeBlockType, 'data': {youtubeVideoKey: 'dQw4w9WgXcQ'}, }, ]).isBlank, isFalse, ); expect( blocks([ { 'type': studyButtonBlockType, 'data': {studyButtonStudysetKey: 'japanese'}, }, ]).isBlank, isFalse, ); expect( blocks([ { 'type': mathBlockType, 'data': {mathFormulaKey: ' '}, }, {'type': calloutBlockType, 'data': {}}, ]).isBlank, isTrue, ); }); test('reads only asset URLs as files of the person', () { expect(assetIdOf(assetUrl('cat')), 'cat'); expect(assetIdOf('asset:'), isNull); expect(assetIdOf('https://example.com/cat.png'), isNull); expect(assetIdOf(42), isNull); }); }); }