From 9ebd2e389324b08663dc00f2b18f04aec2c875c2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Sep 2024 16:08:37 +0200 Subject: [PATCH 1/6] fix: enable some event tests without libolm Otherwise we were skipping the emoji tests without crypto set up and similar, which was not intentional. --- test/event_test.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/event_test.dart b/test/event_test.dart index b9439503..593cfe7f 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -28,7 +28,7 @@ import 'fake_client.dart'; void main() { /// All Tests related to the Event - group('Event', tags: 'olm', () { + group('Event', () { Logs().level = Level.error; final timestamp = DateTime.now().millisecondsSinceEpoch; @@ -269,7 +269,7 @@ void main() { await matrix.dispose(closeDatabase: true); }); - test('requestKey', () async { + test('requestKey', tags: 'olm', () async { final matrix = Client('testclient', httpClient: FakeMatrixApi()); await matrix.checkHomeserver(Uri.parse('https://fakeserver.notexisting'), checkWellKnown: false); @@ -310,7 +310,7 @@ void main() { await matrix.dispose(closeDatabase: true); }); - test('requestKey', () async { + test('requestKey', tags: 'olm', () async { jsonObj['state_key'] = '@alice:example.com'; final event = Event.fromJson( jsonObj, Room(id: '!localpart:server.abc', client: client)); @@ -1394,6 +1394,7 @@ void main() { }); test( 'encrypted attachments', + tags: 'olm', () async { final FILE_BUFF_ENC = Uint8List.fromList([0x3B, 0x6B, 0xB2, 0x8C, 0xAF]); @@ -1504,7 +1505,7 @@ void main() { await room.client.dispose(closeDatabase: true); }, ); - test('downloadAndDecryptAttachment store', () async { + test('downloadAndDecryptAttachment store', tags: 'olm', () async { final FILE_BUFF = Uint8List.fromList([0]); var serverHits = 0; Future downloadCallback(Uri uri) async { @@ -1546,7 +1547,7 @@ void main() { await room.client.dispose(closeDatabase: true); }); - test('downloadAndDecryptAttachment store only', () async { + test('downloadAndDecryptAttachment store only', tags: 'olm', () async { final FILE_BUFF = Uint8List.fromList([0]); var serverHits = 0; Future downloadCallback(Uri uri) async { @@ -1595,7 +1596,8 @@ void main() { await room.client.dispose(closeDatabase: true); }); - test('downloadAndDecryptAttachment store only without file', () async { + test('downloadAndDecryptAttachment store only without file', tags: 'olm', + () async { final FILE_BUFF = Uint8List.fromList([0]); var serverHits = 0; Future downloadCallback(Uri uri) async { From c7d49695d56cd59f517b315dc5e8adb3cb933a45 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Sep 2024 18:15:01 +0200 Subject: [PATCH 2/6] fix: prevent body (and plaintextBody) from return html by accident It is not clear why we ever would want to return the formatted_body when we ask for the body, but it seems to not be used anywhere and there are no tests covering that functionality. However it leads to suprising results, where the plaintextBody can be tricked into returning html without applying conversions. So we just get rid of that functionality. --- lib/src/event.dart | 1 - test/event_test.dart | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index f086a1e9..5768f01d 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -292,7 +292,6 @@ class Event extends MatrixEvent { String get body { if (redacted) return 'Redacted'; if (text != '') return text; - if (formattedText != '') return formattedText; return type; } diff --git a/test/event_test.dart b/test/event_test.dart index 593cfe7f..3351182c 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -1200,6 +1200,35 @@ void main() { }, room); expect(event.plaintextBody, '**blah**'); }); + + test('body', () { + final event = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'body': 'blah', + 'msgtype': 'm.text', + 'format': 'org.matrix.custom.html', + 'formatted_body': 'blub', + }, + 'event_id': '\$source', + 'sender': '@alice:example.org', + }, room); + expect(event.body, 'blah'); + + final event2 = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'body': '', + 'msgtype': 'm.text', + 'format': 'org.matrix.custom.html', + 'formatted_body': 'blub', + }, + 'event_id': '\$source', + 'sender': '@alice:example.org', + }, room); + expect(event2.body, 'm.room.message'); + }); + test('getDisplayEvent', () { final room = Room(id: '!1234', client: client); var event = Event.fromJson({ From a497a6601269f9bc16b31871262c3fce6c258e64 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 27 Sep 2024 16:32:08 +0200 Subject: [PATCH 3/6] chore: Add more (un)localized body tests Covers a few edge cases that still fail. Changes to the unlocalizedBody function shouldn't cause behavioural changes apart from fixing a few edge cases. --- lib/src/event.dart | 35 ++-- test/event_test.dart | 447 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 468 insertions(+), 14 deletions(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index 5768f01d..4e9710d7 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -761,7 +761,10 @@ class Event extends MatrixEvent { /// Returns a localized String representation of this event. For a /// room list you may find [withSenderNamePrefix] useful. Set [hideReply] to /// crop all lines starting with '>'. With [plaintextBody] it'll use the - /// plaintextBody instead of the normal body. + /// plaintextBody instead of the normal body which in practice will convert + /// the html body to a plain text body before falling back to the body. In + /// either case this function won't return the html body without converting + /// it to plain text. /// [removeMarkdown] allow to remove the markdown formating from the event body. /// Usefull form message preview or notifications text. Future calcLocalizedBody(MatrixLocalizations i18n, @@ -850,37 +853,41 @@ class Event extends MatrixEvent { } /// Calculating the body of an event regardless of localization. - String calcUnlocalizedBody( - {bool hideReply = false, - bool hideEdit = false, - bool plaintextBody = false, - bool removeMarkdown = false}) { + String calcUnlocalizedBody({ + bool hideReply = false, + bool hideEdit = false, + bool plaintextBody = false, + bool removeMarkdown = false, + }) { if (redacted) { return 'Removed by ${senderFromMemoryOrFallback.displayName ?? senderId}'; } var body = plaintextBody ? this.plaintextBody : this.body; - // we need to know if the message is an html message to be able to determine - // if we need to strip the reply fallback. - var htmlMessage = content['format'] != 'org.matrix.custom.html'; + // Html messages will already have their reply fallback removed during the Html to Text conversion. + var mayHaveReplyFallback = + !plaintextBody || content['format'] != 'org.matrix.custom.html'; + // If we have an edit, we want to operate on the new content final newContent = content.tryGetMap('m.new_content'); if (hideEdit && relationshipType == RelationshipTypes.edit && newContent != null) { if (plaintextBody && newContent['format'] == 'org.matrix.custom.html') { - htmlMessage = true; - body = HtmlToText.convert( - newContent.tryGet('formatted_body') ?? formattedText); + final newBody = newContent.tryGet('formatted_body'); + if (newBody != null) { + mayHaveReplyFallback = false; + body = HtmlToText.convert(newBody); + } } else { - htmlMessage = false; + mayHaveReplyFallback = true; body = newContent.tryGet('body') ?? body; } } // Hide reply fallback // Be sure that the plaintextBody already stripped teh reply fallback, // if the message is formatted - if (hideReply && (!plaintextBody || htmlMessage)) { + if (hideReply && mayHaveReplyFallback) { body = body.replaceFirst( RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), ''); } diff --git a/test/event_test.dart b/test/event_test.dart index 3351182c..748e3c6c 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -1229,6 +1229,453 @@ void main() { expect(event2.body, 'm.room.message'); }); + group('unlocalized body reply stripping', () { + int i = 0; + + void testUnlocalizedBody({ + required Object? body, + required Object? formattedBody, + required bool html, + Object? editBody, + Object? editFormattedBody, + bool editHtml = false, + bool isEdit = false, + required String expectation, + required bool plaintextBody, + }) { + i += 1; + test('$i', () { + final event = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'msgtype': 'm.text', + if (body != null) 'body': body, + if (formattedBody != null) 'formatted_body': formattedBody, + if (html) 'format': 'org.matrix.custom.html', + if (isEdit) ...{ + 'm.new_content': { + if (editBody != null) 'body': editBody, + if (editFormattedBody != null) + 'formatted_body': editFormattedBody, + if (editHtml) 'format': 'org.matrix.custom.html', + }, + 'm.relates_to': { + 'event_id': '\$source2', + 'rel_type': RelationshipTypes.edit, + }, + }, + }, + 'event_id': '\$source', + 'sender': '@alice:example.org', + }, room); + + expect( + event.calcUnlocalizedBody( + hideReply: true, hideEdit: true, plaintextBody: plaintextBody), + expectation, + reason: + 'event was ${event.toJson()} and plaintextBody ${plaintextBody ? "was" : "was not"} set', + ); + }); + } + + // everything where we expect the body to be returned + testUnlocalizedBody( + expectation: 'body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + // not sure we actually want m.room.message here and not an empty string + expectation: 'm.room.message', + plaintextBody: false, + body: '', + formattedBody: 'formatted body', + html: false, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'm.room.message', + plaintextBody: false, + body: null, + formattedBody: 'formatted body', + html: false, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'm.room.message', + plaintextBody: false, + body: 5, + formattedBody: 'formatted body', + html: false, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: false, + body: 'body', + formattedBody: null, + html: true, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: true, + body: 'body', + formattedBody: null, + html: true, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: true, + body: 'body', + // do we actually expect this to then use the body? + formattedBody: '', + html: true, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: true, + body: 'body', + formattedBody: 5, + html: true, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: null, + editFormattedBody: null, + editHtml: true, + ); + testUnlocalizedBody( + expectation: '**formatted body**', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: '**formatted body**', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: null, + editFormattedBody: null, + editHtml: true, + ); + + // everything where we expect the formatted body to be returned + testUnlocalizedBody( + expectation: '**formatted body**', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: '**formatted body**', + plaintextBody: true, + body: 5, + formattedBody: 'formatted body', + html: true, + isEdit: false, + editBody: null, + editFormattedBody: null, + editHtml: false, + ); + + // everything where we expect the edit body to be returned + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 5, + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: false, + ); + + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: false, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: false, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: null, + editHtml: false, + ); + + // everything where we expect the edit formatted body to be returned + testUnlocalizedBody( + expectation: '**edit formatted body**', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: null, + editFormattedBody: 'edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: '**edit formatted body**', + plaintextBody: true, + body: 'body', + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: '**edit formatted body**', + plaintextBody: true, + body: null, + formattedBody: 'formatted body', + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: '**edit formatted body**', + plaintextBody: true, + body: 'body', + formattedBody: null, + html: true, + isEdit: true, + editBody: 'edit body', + editFormattedBody: 'edit formatted body', + editHtml: true, + ); + }); + test('getDisplayEvent', () { final room = Room(id: '!1234', client: client); var event = Event.fromJson({ From e1f0d9c0ad099d4dbdf2f00bad8b2dc7381464bc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 27 Sep 2024 17:01:43 +0200 Subject: [PATCH 4/6] fix: edge cases when calculating (un)localized body We used to randomly return an empty string when the formatted body was empty, even though we never return an empty string usually. Similarly we used to return the original formatted body in an edit, when the new event has no formatted body. --- lib/src/event.dart | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index 4e9710d7..3fbc6354 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -297,9 +297,13 @@ class Event extends MatrixEvent { /// Use this to get a plain-text representation of the event, stripping things /// like spoilers and thelike. Useful for plain text notifications. - String get plaintextBody => content['format'] == 'org.matrix.custom.html' - ? HtmlToText.convert(formattedText) - : body; + String get plaintextBody => switch (formattedText) { + // if the formattedText is empty, fallback to body + '' => body, + final String s when content['format'] == 'org.matrix.custom.html' => + HtmlToText.convert(s), + _ => body, + }; /// Returns a list of [Receipt] instances for this event. List get receipts { @@ -873,12 +877,14 @@ class Event extends MatrixEvent { if (hideEdit && relationshipType == RelationshipTypes.edit && newContent != null) { - if (plaintextBody && newContent['format'] == 'org.matrix.custom.html') { - final newBody = newContent.tryGet('formatted_body'); - if (newBody != null) { - mayHaveReplyFallback = false; - body = HtmlToText.convert(newBody); - } + final newBody = + newContent.tryGet('formatted_body', TryGet.silent); + if (plaintextBody && + newContent['format'] == 'org.matrix.custom.html' && + newBody != null && + newBody.isNotEmpty) { + mayHaveReplyFallback = false; + body = HtmlToText.convert(newBody); } else { mayHaveReplyFallback = true; body = newContent.tryGet('body') ?? body; From 43bc0e19635c9ef3dfb99903917fb2dcb96a8626 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 28 Sep 2024 13:52:48 +0200 Subject: [PATCH 5/6] chore: tear down clients in event tests properly Fixes some noise around logs when I was trying to enable branch coverage. --- test/event_test.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/event_test.dart b/test/event_test.dart index 748e3c6c..c55f3d3e 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -56,6 +56,8 @@ void main() { final event = Event.fromJson( jsonObj, Room(id: '!testroom:example.abc', client: client)); + tearDownAll(() async => client.dispose()); + test('Create from json', () async { jsonObj['content'] = json.decode(contentJson); expect(event.toJson(), jsonObj); @@ -237,6 +239,8 @@ void main() { expect(event.content.isEmpty, true); redactionEventJson.remove('redacts'); expect(event.unsigned?['redacted_because'], redactionEventJson); + + await client.dispose(); } }); @@ -248,6 +252,8 @@ void main() { expect(() async => await event.cancelSend(), throwsException); event.status = EventStatus.sending; await event.cancelSend(); + + await room.client.dispose(); }); test('sendAgain', () async { @@ -327,6 +333,8 @@ void main() { ), ); expect(event.canRedact, true); + + await client.dispose(); }); test('getLocalizedBody, isEventKnown', () async { final matrix = Client('testclient', httpClient: FakeMatrixApi()); @@ -961,6 +969,8 @@ void main() { expect(await event.calcLocalizedBody(MatrixDefaultLocalizations()), 'Unknown event unknown.event.type'); expect(event.isEventTypeKnown, false); + + await matrix.dispose(closeDatabase: true); }); test('getLocalizedBody, parameters', () async { @@ -1126,6 +1136,8 @@ void main() { await event.calcLocalizedBody(MatrixDefaultLocalizations()), 'Example accepted key verification request', ); + + await matrix.dispose(closeDatabase: true); }); test('aggregations', () { From e3d41bb4492c6ef74b9b8ea5b35ed9abc5940668 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 28 Sep 2024 14:40:22 +0200 Subject: [PATCH 6/6] fix: properly remove reply fallback from (un)localized body --- lib/src/event.dart | 5 ++- test/event_test.dart | 99 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index 3fbc6354..a257eb24 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -869,8 +869,9 @@ class Event extends MatrixEvent { var body = plaintextBody ? this.plaintextBody : this.body; // Html messages will already have their reply fallback removed during the Html to Text conversion. - var mayHaveReplyFallback = - !plaintextBody || content['format'] != 'org.matrix.custom.html'; + var mayHaveReplyFallback = !plaintextBody || + (content['format'] != 'org.matrix.custom.html' || + formattedText.isEmpty); // If we have an edit, we want to operate on the new content final newContent = content.tryGetMap('m.new_content'); diff --git a/test/event_test.dart b/test/event_test.dart index c55f3d3e..7e06e810 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -1686,6 +1686,105 @@ void main() { editFormattedBody: 'edit formatted body', editHtml: true, ); + + // test with reply fallback + testUnlocalizedBody( + expectation: 'body', + plaintextBody: false, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: + '
abc
formatted body', + html: true, + isEdit: false, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: true, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: + '
abc
formatted body', + html: false, + isEdit: false, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'body', + plaintextBody: true, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: null, + html: true, + isEdit: false, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: '**formatted body**', + plaintextBody: true, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: + '
abc
formatted body', + html: true, + isEdit: false, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: false, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: + '
abc
formatted body', + html: true, + isEdit: true, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: true, + ); + testUnlocalizedBody( + expectation: 'edit body', + plaintextBody: true, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: + '
abc
formatted body', + html: true, + isEdit: true, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: false, + ); + testUnlocalizedBody( + expectation: '**edit formatted body**', + plaintextBody: true, + body: '> <@some:user.id> acb def\n\nbody', + formattedBody: + '
abc
formatted body', + html: true, + isEdit: true, + // strictly speaking there is no quote in edits, but we have to handle them anyway because of other clients doing it wrong + editBody: '> <@some:user.id> acb def\n\nedit body', + editFormattedBody: + '
abc
edit formatted body', + editHtml: true, + ); }); test('getDisplayEvent', () {