diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index 6c324c855e..ccd7f46d89 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -14,6 +14,7 @@ import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; abstract class ThreadDataSource { Future getAllEmail( @@ -28,6 +29,18 @@ abstract class ThreadDataSource { } ); + Future searchEmails( + Session session, + AccountId accountId, + { + UnsignedInt? limit, + int? position, + Set? sort, + Filter? filter, + Properties? properties + } + ); + Future getChanges( Session session, AccountId accountId, diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index b18202dabc..769df93776 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dar import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class LocalThreadDataSourceImpl extends ThreadDataSource { @@ -40,6 +41,21 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { throw UnimplementedError(); } + @override + Future searchEmails( + Session session, + AccountId accountId, + { + UnsignedInt? limit, + int? position, + Set? sort, + Filter? filter, + Properties? properties + } + ) { + throw UnimplementedError(); + } + @override Future getChanges( Session session, diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 1f1974c2a3..48c317357a 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class ThreadDataSourceImpl extends ThreadDataSource { @@ -56,6 +57,30 @@ class ThreadDataSourceImpl extends ThreadDataSource { }).catchError(_exceptionThrower.throwException); } + @override + Future searchEmails( + Session session, + AccountId accountId, + { + UnsignedInt? limit, + int? position, + Set? sort, + Filter? filter, + Properties? properties, + } + ) { + return Future.sync(() async { + return await threadAPI.searchEmails( + session, + accountId, + limit: limit, + position: position, + sort: sort, + filter: filter, + properties: properties); + }).catchError(_exceptionThrower.throwException); + } + @override Future getChanges( Session session, diff --git a/lib/features/thread/data/network/thread_api.dart b/lib/features/thread/data/network/thread_api.dart index 93cc0ff5c3..72a2b2ffbb 100644 --- a/lib/features/thread/data/network/thread_api.dart +++ b/lib/features/thread/data/network/thread_api.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/response/response_object.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; @@ -16,8 +19,12 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/get/get_email_method.dart'; import 'package:jmap_dart_client/jmap/mail/email/get/get_email_response.dart'; import 'package:jmap_dart_client/jmap/mail/email/query/query_email_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet.dart'; +import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet_get_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet_get_response.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; class ThreadAPI { @@ -83,6 +90,96 @@ class ThreadAPI { return EmailsResponse(emailList: resultList?.list, state: resultList?.state); } + Future searchEmails( + Session session, + AccountId accountId, + { + UnsignedInt? limit, + int? position, + Set? sort, + Filter? filter, + Properties? properties + } + ) async { + final processingInvocation = ProcessingInvocation(); + + final jmapRequestBuilder = JmapRequestBuilder(httpClient, processingInvocation); + + // Email/query + final queryEmailMethod = QueryEmailMethod(accountId); + + if (limit != null) queryEmailMethod.addLimit(limit); + + if (position != null) queryEmailMethod.addPosition(position); + + if (sort != null) queryEmailMethod.addSorts(sort); + + if (filter != null) queryEmailMethod.addFilters(filter); + + final queryEmailInvocation = jmapRequestBuilder.invocation(queryEmailMethod); + + // SearchSnippet/get + final getSearchSnippetMethod = SearchSnippetGetMethod(accountId); + if (filter != null) getSearchSnippetMethod.addFilters(filter); + getSearchSnippetMethod.addReferenceEmailIds( + processingInvocation.createResultReference( + queryEmailInvocation.methodCallId, + ReferencePath.idsPath)); + final getSearchSnippetInvocation = jmapRequestBuilder.invocation( + getSearchSnippetMethod); + + // Email/get + final getEmailMethod = GetEmailMethod(accountId); + + if (properties != null) getEmailMethod.addProperties(properties); + + getEmailMethod.addReferenceIds(processingInvocation.createResultReference( + queryEmailInvocation.methodCallId, + ReferencePath.idsPath)); + + final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); + + final capabilities = getEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final result = await (jmapRequestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final emailResultList = result.parse( + getEmailInvocation.methodCallId, GetEmailResponse.deserialize); + + if (sort != null && emailResultList != null) { + for (var comparator in sort) { + emailResultList.sortEmails(comparator); + } + } + + final searchSnippets = _getSearchSnippetsFromResponse( + result, + getSearchSnippetMethodCallId: getSearchSnippetInvocation.methodCallId, + ); + return SearchEmailsResponse( + emailList: emailResultList?.list, + state: emailResultList?.state, + searchSnippets: searchSnippets); + } + + List? _getSearchSnippetsFromResponse( + ResponseObject response, + {required MethodCallId getSearchSnippetMethodCallId} + ) { + try { + return response.parse( + getSearchSnippetMethodCallId, + SearchSnippetGetResponse.fromJson)?.list; + } catch (e) { + logError('ThreadAPI::searchEmails:getSearchSnippetsFromResponse: Exception = $e'); + return null; + } + } + Future getChanges( Session session, AccountId accountId, diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index b7a5482e9a..3541a07f63 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_email.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; class ThreadRepositoryImpl extends ThreadRepository { @@ -287,7 +288,7 @@ class ThreadRepositoryImpl extends ThreadRepository { } @override - Future> searchEmails( + Future> searchEmails( Session session, AccountId accountId, { @@ -298,7 +299,7 @@ class ThreadRepositoryImpl extends ThreadRepository { Properties? properties } ) async { - final emailResponse = await mapDataSource[DataSourceType.network]!.getAllEmail( + final searchEmailsResponse = await mapDataSource[DataSourceType.network]!.searchEmails( session, accountId, limit: limit, @@ -307,7 +308,7 @@ class ThreadRepositoryImpl extends ThreadRepository { filter: filter, properties: properties); - return emailResponse.emailList ?? List.empty(); + return searchEmailsResponse.toSearchEmails ?? []; } @override diff --git a/lib/features/thread/domain/model/email_response.dart b/lib/features/thread/domain/model/email_response.dart index 87c054e417..dd72318e8e 100644 --- a/lib/features/thread/domain/model/email_response.dart +++ b/lib/features/thread/domain/model/email_response.dart @@ -7,7 +7,7 @@ class EmailsResponse with EquatableMixin { final List? emailList; final State? state; - EmailsResponse({ + const EmailsResponse({ this.emailList, this.state }); diff --git a/lib/features/thread/domain/model/search_email.dart b/lib/features/thread/domain/model/search_email.dart new file mode 100644 index 0000000000..4b995ac08b --- /dev/null +++ b/lib/features/thread/domain/model/search_email.dart @@ -0,0 +1,91 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +class SearchEmail extends Email { + final String? searchSnippetSubject; + final String? searchSnippetPreview; + + SearchEmail({ + super.id, + super.blobId, + super.threadId, + super.mailboxIds, + super.keywords, + super.size, + super.receivedAt, + super.headers, + super.messageId, + super.inReplyTo, + super.references, + super.subject, + super.sentAt, + super.hasAttachment, + super.preview, + super.sender, + super.from, + super.to, + super.cc, + super.bcc, + super.replyTo, + super.textBody, + super.htmlBody, + super.attachments, + super.bodyStructure, + super.bodyValues, + super.headerUserAgent, + super.headerMdn, + super.headerCalendarEvent, + super.sMimeStatusHeader, + super.identityHeader, + required this.searchSnippetSubject, + required this.searchSnippetPreview + }); + + @override + List get props => [ + ...super.props, + searchSnippetSubject, + searchSnippetPreview, + ]; + + factory SearchEmail.fromEmail( + Email email, { + String? searchSnippetSubject, + String? searchSnippetPreview, + }) { + return SearchEmail( + id: email.id, + blobId: email.blobId, + threadId: email.threadId, + mailboxIds: email.mailboxIds, + keywords: email.keywords, + size: email.size, + receivedAt: email.receivedAt, + headers: email.headers, + messageId: email.messageId, + inReplyTo: email.inReplyTo, + references: email.references, + subject: email.subject, + sentAt: email.sentAt, + hasAttachment: email.hasAttachment, + preview: email.preview, + sender: email.sender, + from: email.from, + to: email.to, + cc: email.cc, + bcc: email.bcc, + replyTo: email.replyTo, + textBody: email.textBody, + htmlBody: email.htmlBody, + attachments: email.attachments, + bodyStructure: email.bodyStructure, + bodyValues: email.bodyValues, + headerUserAgent: email.headerUserAgent, + headerMdn: email.headerMdn, + headerCalendarEvent: email.headerCalendarEvent, + sMimeStatusHeader: email.sMimeStatusHeader, + identityHeader: email.identityHeader, + searchSnippetSubject: searchSnippetSubject, + searchSnippetPreview: searchSnippetPreview, + ); + } +} \ No newline at end of file diff --git a/lib/features/thread/domain/model/search_emails_response.dart b/lib/features/thread/domain/model/search_emails_response.dart new file mode 100644 index 0000000000..f17a110881 --- /dev/null +++ b/lib/features/thread/domain/model/search_emails_response.dart @@ -0,0 +1,35 @@ +import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_email.dart'; + +class SearchEmailsResponse extends EmailsResponse { + const SearchEmailsResponse({ + super.emailList, + super.state, + required this.searchSnippets, + }); + + final List? searchSnippets; + + @override + List get props => [...super.props, searchSnippets]; + + List? get toSearchEmails { + if (searchSnippets == null) { + return emailList?.map(SearchEmail.fromEmail).toList(); + } + + final mapSearchSnippet = Map.fromEntries( + searchSnippets!.map((searchSnippet) => MapEntry( + searchSnippet.emailId, + searchSnippet))); + + return emailList?.map((email) { + final searchSnippet = mapSearchSnippet[email.id]; + return SearchEmail.fromEmail( + email, + searchSnippetSubject: searchSnippet?.subject, + searchSnippetPreview: searchSnippet?.preview); + }).toList(); + } +} \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index c1ba1e1808..87e6ea37bb 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -13,6 +13,7 @@ import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_email.dart'; abstract class ThreadRepository { Stream getAllEmail( @@ -41,7 +42,7 @@ abstract class ThreadRepository { Stream loadMoreEmails(GetEmailRequest emailRequest); - Future> searchEmails( + Future> searchEmails( Session session, AccountId accountId, { diff --git a/test/features/thread/data/network/thread_api_test.dart b/test/features/thread/data/network/thread_api_test.dart new file mode 100644 index 0000000000..67b2a759f8 --- /dev/null +++ b/test/features/thread/data/network/thread_api_test.dart @@ -0,0 +1,228 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http_mock_adapter/http_mock_adapter.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; +import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; +import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet.dart'; +import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_email.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; + +void main() { + final baseOption = BaseOptions(method: 'POST'); + final dio = Dio(baseOption)..options.baseUrl = 'http://domain.com/jmap'; + final dioAdapter = DioAdapter(dio: dio); + final httpClient = HttpClient(dio); + final threadApi = ThreadAPI(httpClient); + + final sessionState = State('some-session-state'); + final state = State('some-state'); + final filter = EmailFilterCondition(text: 'some-text'); + + group('thread api test:', () { + group('searchEmails:', () { + Map generateRequest({required Filter filter}) => { + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail", + "urn:apache:james:params:jmap:mail:shares", + ], + "methodCalls": [ + [ + "Email/query", + { + "accountId": AccountFixtures.aliceAccountId.id.value, + "filter": filter.toJson(), + }, + "c0" + ], + [ + "SearchSnippet/get", + { + "accountId": AccountFixtures.aliceAccountId.id.value, + "filter": filter.toJson(), + "#emailIds": { + "resultOf": "c0", + "name": "Email/query", + "path": "/ids/*" + }, + }, + "c1" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.id.value, + "#ids": { + "resultOf": "c0", + "name": "Email/query", + "path": "/ids/*" + }, + }, + "c2" + ] + ] + }; + + Map generateResponse({ + required List foundSearchEmails, + required List notFoundEmailIds, + ErrorMethodResponse? searchSnippetError, + }) => { + "sessionState": sessionState.value, + "methodResponses": [ + [ + "Email/query", + { + "accountId": AccountFixtures.aliceAccountId.id.value, + "ids": foundSearchEmails + .map((searchEmail) => searchEmail.id?.id.value) + .toList() + ..addAll(notFoundEmailIds.map((emailId) => emailId.id.value)), + }, + "c0" + ], + if (searchSnippetError == null) + [ + "SearchSnippet/get", + { + "accountId": AccountFixtures.aliceAccountId.id.value, + "notFound": notFoundEmailIds + .map((emailId) => emailId.id.value) + .toList(), + "state": state.value, + "list": foundSearchEmails + .map((searchEmail) => SearchSnippet( + emailId: searchEmail.id!, + subject: searchEmail.searchSnippetSubject, + preview: searchEmail.searchSnippetPreview).toJson()) + .toList(), + }, + "c1" + ] + else + [ + "error", + { + "type": searchSnippetError.type.value, + }, + "c1" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.id.value, + "state": state.value, + "list": foundSearchEmails + .map((searchEmail) => searchEmail.toJson()) + .toList(), + "notFound": notFoundEmailIds + .map((emailId) => emailId.id.value) + .toList(), + }, + "c2" + ] + ] + }; + + test( + 'should return SearchEmailResponse including search snippets ' + 'when search snippet method return values', + () async { + // arrange + final searchEmail = SearchEmail( + id: EmailId(Id('someEmailId')), + searchSnippetSubject: 'searchSnippetSubject', + searchSnippetPreview: 'searchSnippetPreview', + ); + dioAdapter.onPost( + '', + (server) => server.reply( + 200, + generateResponse( + foundSearchEmails: [searchEmail], + notFoundEmailIds: [], + ), + ), + data: generateRequest(filter: filter), + ); + + // act + final result = await threadApi.searchEmails( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + filter: filter, + ); + + // assert + expect( + result, + equals( + SearchEmailsResponse( + searchSnippets: [ + SearchSnippet( + emailId: searchEmail.id!, + subject: searchEmail.searchSnippetSubject, + preview: searchEmail.searchSnippetPreview, + ), + ], + emailList: [Email(id: searchEmail.id)], + state: state + ), + ), + ); + }); + + test( + 'should return SearchEmailResponse without search snippets ' + 'when search snippet method return error', + () async { + // arrange + final searchEmail = SearchEmail( + id: EmailId(Id('someEmailId')), + searchSnippetSubject: 'searchSnippetSubject', + searchSnippetPreview: 'searchSnippetPreview', + ); + dioAdapter.onPost( + '', + (server) => server.reply( + 200, + generateResponse( + foundSearchEmails: [searchEmail], + notFoundEmailIds: [], + searchSnippetError: UnknownMethodResponse(), + ), + ), + data: generateRequest(filter: filter), + ); + + // act + final result = await threadApi.searchEmails( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + filter: filter, + ); + + // assert + expect( + result, + equals( + SearchEmailsResponse( + searchSnippets: null, + emailList: [Email(id: searchEmail.id)], + state: state + ), + ), + ); + }); + }); + }); +} \ No newline at end of file diff --git a/test/features/thread/domain/model/search_emails_response_test.dart b/test/features/thread/domain/model/search_emails_response_test.dart new file mode 100644 index 0000000000..7acff7d1e7 --- /dev/null +++ b/test/features/thread/domain/model/search_emails_response_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_email.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; + +void main() { + group('search emails response test:', () { + test( + 'should return list of search emails ' + 'when toSearchEmails is called', + () { + // arrange + final emailId = EmailId(Id('someId')); + const searchSnippetSubject = 'search-snippet-subject'; + const searchSnippetPreview = 'search-snippet-preview'; + final email = Email(id: emailId); + final searchSnippet = SearchSnippet( + emailId: emailId, + subject: searchSnippetSubject, + preview: searchSnippetPreview, + ); + final searchEmailsResponse = SearchEmailsResponse( + emailList: [email], + searchSnippets: [searchSnippet], + ); + + // act + final result = searchEmailsResponse.toSearchEmails; + + // assert + expect( + result, + [SearchEmail( + id: emailId, + searchSnippetSubject: searchSnippetSubject, + searchSnippetPreview: searchSnippetPreview)] + ); + }); + + test( + 'should map list of search emails according to emailId ' + 'when toSearchEmails is called', + () { + // arrange + final emailId1 = EmailId(Id('someId1')); + final emailId2 = EmailId(Id('someId2')); + const searchSnippetSubject1 = 'search-snippet-subject-1'; + const searchSnippetPreview1 = 'search-snippet-preview-1'; + const searchSnippetSubject2 = 'search-snippet-subject-2'; + const searchSnippetPreview2 = 'search-snippet-preview-2'; + final email1 = Email(id: emailId1, subject: 'subject-1'); + final searchSnippet1 = SearchSnippet( + emailId: emailId1, + subject: searchSnippetSubject1, + preview: searchSnippetPreview1, + ); + final email2 = Email(id: emailId2, subject: 'subject-2'); + final searchSnippet2 = SearchSnippet( + emailId: emailId2, + subject: searchSnippetSubject2, + preview: searchSnippetPreview2, + ); + final searchEmailsResponse = SearchEmailsResponse( + emailList: [email1, email2], + searchSnippets: [searchSnippet1, searchSnippet2], + ); + + // act + final result = searchEmailsResponse.toSearchEmails; + + // assert + expect( + result, + [ + SearchEmail( + id: emailId1, + subject: email1.subject, + searchSnippetSubject: searchSnippetSubject1, + searchSnippetPreview: searchSnippetPreview1, + ), + SearchEmail( + id: emailId2, + subject: email2.subject, + searchSnippetSubject: searchSnippetSubject2, + searchSnippetPreview: searchSnippetPreview2, + ), + ] + ); + }); + }); +} \ No newline at end of file