From 835dc11679f1729e99a016f6748907777a9c1212 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 06:55:14 -0400 Subject: [PATCH 1/7] Show RS Unified Comments experimental flag in release builds Lift the debug-only restriction so the new wordpress-rs comments screens can be opted into outside debug builds for broader soak testing. The flag still defaults off and routing is unchanged. --- .../experimentalfeatures/ExperimentalFeaturesViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt index cfb992be9876..3ce5fe462ef7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt @@ -39,10 +39,9 @@ internal class ExperimentalFeaturesViewModel @Inject constructor( } private fun shouldShowFeature(feature: Feature): Boolean { - // These features are only shown in debug builds + // This feature is only shown in debug builds return when (feature) { - Feature.EXPERIMENTAL_POST_TYPES, - Feature.RS_UNIFIED_COMMENTS -> BuildConfig.DEBUG + Feature.EXPERIMENTAL_POST_TYPES -> BuildConfig.DEBUG else -> true } } From 3dd3726aa93963136c843e1d70125b1528c1ae94 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 08:47:34 -0400 Subject: [PATCH 2/7] Load and save comment edits via wordpress-rs without a FluxC row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On rs-capable sites (WP.com REST or application password) the comment editor now loads via the edit-context retrieve (raw content + author email) and saves via wordpress-rs without requiring a pre-existing FluxC comment row — the post-save cache mirror is best-effort, applied only when a row exists. Non-rs sites (XML-RPC via the legacy/reader launch points) keep the FluxC load and save path unchanged. --- .../comments/unified/CommentsRsDataSource.kt | 45 ++++-- .../unified/UnifiedCommentDetailsViewModel.kt | 10 +- .../unified/UnifiedCommentsEditViewModel.kt | 144 ++++++++++-------- .../unified/CommentsRsDataSourceTest.kt | 84 +++++++++- .../UnifiedCommentsEditViewModelTest.kt | 83 +++++++++- 5 files changed, 279 insertions(+), 87 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSource.kt index e25dce457ddb..b602101b793c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSource.kt @@ -11,6 +11,7 @@ import uniffi.wp_api.CommentDeleteParams import uniffi.wp_api.CommentListParams import uniffi.wp_api.CommentRetrieveParams import uniffi.wp_api.CommentUpdateParams +import uniffi.wp_api.CommentWithEditContext import uniffi.wp_api.CommentWithViewContext import uniffi.wp_api.PostEndpointType import uniffi.wp_api.PostListParams @@ -88,6 +89,24 @@ class CommentsRsDataSource @Inject constructor( } } + /** + * Fetches a comment's editable state for pre-filling the edit screen. Uses the edit context — + * unlike the view context, it returns the raw (unrendered) content and the author email. + * Requires the `edit_comment` capability; a denial returns null, which the editor surfaces + * as its load-error snackbar. + */ + suspend fun getCommentForEdit(site: SiteModel, commentId: Long): RsEditedComment? = safe(errorValue = null) { + val client = wpApiClientProvider.getWpApiClient(site) + when ( + val result = client.request { + it.comments().retrieveWithEditContext(commentId, CommentRetrieveParams()) + } + ) { + is WpRequestResult.Success -> result.response.data.toRsEditedComment() + else -> null + } + } + /** * Fetches one page of the site's comments. Pass [firstPageParams] for the first page and the * previous page's [RsCommentsPageResult.Success.nextPageParams] for subsequent pages. @@ -210,9 +229,10 @@ class CommentsRsDataSource @Inject constructor( data class CommentAuthor(val name: String, val email: String, val url: String) /** - * The server's post-save state of an edited comment. Mirroring this (rather than the values - * that were sent) into the FluxC cache keeps the cache faithful when the server normalises - * fields — e.g. content passes through KSES filtering. + * A comment's editable state (edit context): the edit screen's pre-fill and the server's + * post-save echo. Mirroring the echo (rather than the values that were sent) into the FluxC + * cache keeps the cache faithful when the server normalises fields — e.g. content passes + * through KSES filtering. */ data class RsEditedComment( val authorName: String, @@ -247,22 +267,19 @@ class CommentsRsDataSource @Inject constructor( ) } when (result) { - is WpRequestResult.Success -> { - val serverComment = result.response.data - RsEditResult.Success( - RsEditedComment( - authorName = serverComment.authorName, - authorEmail = serverComment.authorEmail, - authorUrl = serverComment.authorUrl, - contentRaw = serverComment.content.raw - ) - ) - } + is WpRequestResult.Success -> RsEditResult.Success(result.response.data.toRsEditedComment()) is WpRequestResult.WpError -> RsEditResult.Error(result.errorMessage) else -> RsEditResult.Error(null) } } + private fun CommentWithEditContext.toRsEditedComment() = RsEditedComment( + authorName = authorName, + authorEmail = authorEmail, + authorUrl = authorUrl, + contentRaw = content.raw + ) + suspend fun delete(site: SiteModel, commentId: Long): RsResult = write(site) { it.comments().delete(commentId, CommentDeleteParams()) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt index 271bbc5614b0..a7ab8bf6331e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt @@ -50,8 +50,7 @@ import javax.inject.Named * - **Liking** (wordpress-rs has no comment like action). * - **Keeping the FluxC-backed comment list in sync**: after each rs write we mirror the change * into the local comment cache and poke [LocalCommentCacheUpdateHandler] so the (still FluxC) - * list reflects it. The cache row is also the source of the post title, like state and local id - * used to launch the (still FluxC) edit screen. + * list reflects it. The cache row is also the source of the post title and like state. * * **Note mode**: when [start] receives a `noteId` the comment was opened from a notification. * Every successful write then also refreshes the note DB (so the notifications list stays fresh) @@ -102,7 +101,8 @@ class UnifiedCommentDetailsViewModel @Inject constructor( private var loadedComment: RsComment? = null private var isLikeInProgress = false - // From the FluxC cache row: the local id used to launch the (still FluxC) edit screen. + // From the FluxC cache row: kept only to fill SiteCommentIdentifier's shape for the shared + // edit screen, which no longer reads it (it loads and saves via wordpress-rs on rs sites). private var localCommentId: Int = 0 fun start(site: SiteModel, remoteCommentId: Long, noteId: String? = null) { @@ -139,8 +139,8 @@ class UnifiedCommentDetailsViewModel @Inject constructor( var local = commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId).firstOrNull() // Opened from the rs list the FluxC cache may not have this comment at all (the // legacy list guaranteed a row before the detail could open). Fetch it so the - // post title, like state and the edit screen's local id are available — WP.com - // only, since FluxC has no application-password transport. + // post title and like state are available — WP.com only, since FluxC has no + // application-password transport. if (local == null && site.isUsingWpComRestApi) { commentsStore.fetchComment(site, remoteCommentId, null) local = commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId).firstOrNull() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt index 027f75b93b10..1984e55a7eea 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt @@ -10,7 +10,6 @@ import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat.COMMENT_EDITED import org.wordpress.android.datasets.wrappers.ReaderCommentTableWrapper import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.persistence.comments.CommentsDao.CommentEntity import org.wordpress.android.fluxc.store.CommentsStore import org.wordpress.android.models.usecases.LocalCommentCacheUpdateHandler import org.wordpress.android.modules.BG_THREAD @@ -229,70 +228,85 @@ class UnifiedCommentsEditViewModel @Inject constructor( } private suspend fun mapCommentEssentials(): CommentEssentials { - val commentEntity = getCommentUseCase.execute(site, commentIdentifier.remoteCommentId) - return if (commentEntity != null) { - CommentEssentials( - commentId = commentEntity.id, - userName = commentEntity.authorName ?: "", - commentText = commentEntity.content ?: "", - userUrl = commentEntity.authorUrl ?: "", - userEmail = commentEntity.authorEmail ?: "" - ) + // A failed load returns default CommentEssentials, which fails isValid() and surfaces + // the load-error snackbar in initViews(). + return if (canUseRs()) { + // Edit context returns the raw (unrendered) content the editor must show — the same + // thing the FluxC entity stored — plus the author email the view context omits. + val rsComment = commentsRsDataSource.getCommentForEdit(site, commentIdentifier.remoteCommentId) + if (rsComment != null) { + CommentEssentials( + commentId = commentIdentifier.remoteCommentId, + userName = rsComment.authorName, + commentText = rsComment.contentRaw, + userUrl = rsComment.authorUrl, + userEmail = rsComment.authorEmail + ) + } else { + CommentEssentials() + } } else { - CommentEssentials() - } - } - - private suspend fun updateComment(editedCommentEssentials: CommentEssentials) { - val commentEntity = - commentsStore.getCommentByLocalSiteAndRemoteId(site.id, commentIdentifier.remoteCommentId).firstOrNull() - commentEntity?.run { - val isCommentEntityUpdated = updateCommentEntity(this, editedCommentEssentials) - if (isCommentEntityUpdated) { - analyticsUtilsWrapper.trackCommentActionWithSiteDetails( - COMMENT_EDITED, - commentIdentifier.toCommentActionSource(), - site + val commentEntity = getCommentUseCase.execute(site, commentIdentifier.remoteCommentId) + if (commentEntity != null) { + CommentEssentials( + commentId = commentEntity.id, + userName = commentEntity.authorName ?: "", + commentText = commentEntity.content ?: "", + userUrl = commentEntity.authorUrl ?: "", + userEmail = commentEntity.authorEmail ?: "" ) - when (commentIdentifier) { - is NotificationCommentIdentifier -> { - updateNotificationEntity() - } - is ReaderCommentIdentifier -> { - updateReaderEntity(editedCommentEssentials) - } - else -> { - _uiActionEvent.postValue(Event(DONE)) - localCommentCacheUpdateHandler.requestCommentsUpdate() - } - } } else { - showUpdateCommentError() + CommentEssentials() } - } ?: showUpdateCommentError() + } } - private suspend fun updateCommentEntity( - comment: CommentEntity, - editedCommentEssentials: CommentEssentials - ): Boolean { + private suspend fun updateComment(editedCommentEssentials: CommentEssentials) { // Prefer wordpress-rs, which can edit comments on both WP.com and self-hosted // application-password sites (FluxC's updateEditComment can't reach app-password sites). // Fall back to FluxC for sites rs can't serve — e.g. XML-RPC-only self-hosted comments // still reachable through the legacy detail/reader launch points. - return if (canUseRs()) { - updateCommentViaRs(comment, editedCommentEssentials) + val saved = if (canUseRs()) { + updateCommentViaRs(editedCommentEssentials) } else { - val updatedComment = comment.copy( - authorUrl = editedCommentEssentials.userUrl, - authorName = editedCommentEssentials.userName, - authorEmail = editedCommentEssentials.userEmail, - content = editedCommentEssentials.commentText + updateCommentViaFluxC(editedCommentEssentials) + } + if (saved) { + analyticsUtilsWrapper.trackCommentActionWithSiteDetails( + COMMENT_EDITED, + commentIdentifier.toCommentActionSource(), + site ) - !commentsStore.updateEditComment(site, updatedComment).isError + when (commentIdentifier) { + is NotificationCommentIdentifier -> { + updateNotificationEntity() + } + is ReaderCommentIdentifier -> { + updateReaderEntity(editedCommentEssentials) + } + else -> { + _uiActionEvent.postValue(Event(DONE)) + localCommentCacheUpdateHandler.requestCommentsUpdate() + } + } + } else { + showUpdateCommentError() } } + private suspend fun updateCommentViaFluxC(editedCommentEssentials: CommentEssentials): Boolean { + val comment = commentsStore + .getCommentByLocalSiteAndRemoteId(site.id, commentIdentifier.remoteCommentId) + .firstOrNull() ?: return false + val updatedComment = comment.copy( + authorUrl = editedCommentEssentials.userUrl, + authorName = editedCommentEssentials.userName, + authorEmail = editedCommentEssentials.userEmail, + content = editedCommentEssentials.commentText + ) + return !commentsStore.updateEditComment(site, updatedComment).isError + } + private fun canUseRs(): Boolean = site.isUsingWpComRestApi || site.hasApplicationPassword() /** @@ -302,11 +316,12 @@ class UnifiedCommentsEditViewModel @Inject constructor( * server echo (not the values that were sent) is what's cached so server-side normalisation * (e.g. KSES content filtering) can't diverge from the cache; the legacy FluxC path also * cached the server response. + * + * The mirror is best-effort: comments opened from the rs list on an application-password + * site may have no FluxC row (FluxC can't fetch there), and the rs screens read the server, + * not the cache. */ - private suspend fun updateCommentViaRs( - comment: CommentEntity, - editedCommentEssentials: CommentEssentials - ): Boolean { + private suspend fun updateCommentViaRs(editedCommentEssentials: CommentEssentials): Boolean { // The endpoint applies author fields to the comment record for any comment (registered // author or not), same as the legacy FluxC POST and wp-admin's comment editor. val result = commentsRsDataSource.updateComment( @@ -320,14 +335,19 @@ class UnifiedCommentsEditViewModel @Inject constructor( ) ) if (result !is RsEditResult.Success) return false - val serverComment = comment.copy( - authorName = result.comment.authorName, - authorEmail = result.comment.authorEmail, - authorUrl = result.comment.authorUrl, - content = result.comment.contentRaw - ) - // Local-only cache write (isError = false persists the entity without a network round-trip). - commentsStore.updateComment(isError = false, commentId = serverComment.id, comment = serverComment) + commentsStore + .getCommentByLocalSiteAndRemoteId(site.id, commentIdentifier.remoteCommentId) + .firstOrNull()?.let { cached -> + val serverComment = cached.copy( + authorName = result.comment.authorName, + authorEmail = result.comment.authorEmail, + authorUrl = result.comment.authorUrl, + content = result.comment.contentRaw + ) + // Local-only cache write (isError = false persists the entity without a network + // round-trip). + commentsStore.updateComment(isError = false, commentId = serverComment.id, comment = serverComment) + } return true } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt index 2b511b068e9b..a860987786b0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt @@ -6,6 +6,7 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.stub @@ -14,6 +15,12 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider import rs.wordpress.api.kotlin.WpApiClient import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.CommentContentWithEditContext +import uniffi.wp_api.CommentStatus +import uniffi.wp_api.CommentType +import uniffi.wp_api.CommentWithEditContext +import uniffi.wp_api.CommentsRequestExecutor +import uniffi.wp_api.CommentsRequestRetrieveWithEditContextResponse import uniffi.wp_api.PostEndpointType import uniffi.wp_api.PostListParams import uniffi.wp_api.PostsRequestExecutor @@ -23,10 +30,12 @@ import uniffi.wp_api.SparseAnyPostWithViewContext import uniffi.wp_api.SparsePostTitleWithViewContext import uniffi.wp_api.UniffiWpApiClient import uniffi.wp_api.WpErrorCode +import java.util.Date /** - * Tests for [CommentsRsDataSource.fetchPostTitles]: the per-site title cache, the posts→pages - * endpoint fallback, negative caching of unresolvable ids, and request chunking. + * Tests for [CommentsRsDataSource.fetchPostTitles] — the per-site title cache, the posts→pages + * endpoint fallback, negative caching of unresolvable ids, and request chunking — and for + * [CommentsRsDataSource.getCommentForEdit]'s edit-context mapping and error handling. * * The [wpApiClient] stub executes each request's builder lambda against a mocked * [UniffiWpApiClient], recording the endpoint and paging params actually sent — so the tests @@ -43,6 +52,7 @@ class CommentsRsDataSourceTest { private val wpApiClient: WpApiClient = mock() private val uniffiClient: UniffiWpApiClient = mock() private val postsExecutor: PostsRequestExecutor = mock() + private val commentsExecutor: CommentsRequestExecutor = mock() private lateinit var dataSource: CommentsRsDataSource private val recordedRequests = mutableListOf() @@ -59,6 +69,12 @@ class CommentsRsDataSourceTest { dataSource = CommentsRsDataSource(wpApiClientProvider) whenever(wpApiClientProvider.getWpApiClient(any(), anyOrNull())).thenReturn(wpApiClient) whenever(uniffiClient.posts()).thenReturn(postsExecutor) + whenever(uniffiClient.comments()).thenReturn(commentsExecutor) + commentsExecutor.stub { + // The payload is decided by the request-level stub below; this value is unused. + on { retrieveWithEditContext(any(), any()) } doReturn + CommentsRequestRetrieveWithEditContextResponse(editContextComment(), mock()) + } postsExecutor.stub { on { filterListWithViewContext(any(), any(), any()) } doSuspendableAnswer { invocation -> val params = invocation.getArgument(1) @@ -232,6 +248,70 @@ class CommentsRsDataSourceTest { ) } + @Test + fun `getCommentForEdit maps the edit-context response`() = runTest { + val serverComment = editContextComment( + authorName = "author", + authorEmail = "author@example.com", + authorUrl = "https://example.com", + contentRaw = "raw content" + ) + stubRequests(WpRequestResult.Success(CommentsRequestRetrieveWithEditContextResponse(serverComment, mock()))) + + val result = dataSource.getCommentForEdit(siteA, 42L) + + assertThat(result).isEqualTo( + CommentsRsDataSource.RsEditedComment( + authorName = "author", + authorEmail = "author@example.com", + authorUrl = "https://example.com", + contentRaw = "raw content" + ) + ) + } + + @Test + fun `getCommentForEdit returns null on a server error`() = runTest { + stubRequests(wpError()) + + assertThat(dataSource.getCommentForEdit(siteA, 42L)).isNull() + } + + @Test + fun `getCommentForEdit returns null when the request throws`() = runTest { + wpApiClient.stub { + on { request(any()) } doSuspendableAnswer { throw IllegalStateException("boom") } + } + + assertThat(dataSource.getCommentForEdit(siteA, 42L)).isNull() + } + + // An edit-context comment with the four fields the editor consumes; everything else dummy. + private fun editContextComment( + authorName: String = "", + authorEmail: String = "", + authorUrl: String = "", + contentRaw: String = "" + ) = CommentWithEditContext( + id = 42L, + author = 1L, + authorEmail = authorEmail, + authorIp = "", + authorName = authorName, + authorUrl = authorUrl, + authorUserAgent = "", + content = CommentContentWithEditContext(raw = contentRaw, rendered = ""), + date = "", + dateGmt = Date(0), + link = "", + parent = 0L, + post = 0L, + status = CommentStatus.Approved, + commentType = CommentType.Comment, + authorAvatarUrls = emptyMap(), + additionalFields = mock() + ) + // A sparse post as returned by the id+title sparse-field request: everything else null. private fun sparsePost(id: Long, title: String) = SparseAnyPostWithViewContext( id = id, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt index 87525e37cd69..e479b2340a16 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt @@ -224,7 +224,7 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { @Test fun `onActionMenuClicked saves via wordpress-rs and mirrors to cache for rs-capable site`() = test { - whenever(getCommentUseCase.execute(rsSite, remoteCommentId)).thenReturn(COMMENT_ENTITY) + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(RS_COMMENT_FOR_EDIT) whenever(commentsStore.getCommentByLocalSiteAndRemoteId(rsSite.id, remoteCommentId)) .thenReturn(listOf(COMMENT_ENTITY)) whenever(commentsRsDataSource.updateComment(any(), any(), any(), any())) @@ -266,9 +266,7 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { @Test fun `onActionMenuClicked surfaces error and skips DONE when wordpress-rs update fails`() = test { - whenever(getCommentUseCase.execute(rsSite, remoteCommentId)).thenReturn(COMMENT_ENTITY) - whenever(commentsStore.getCommentByLocalSiteAndRemoteId(rsSite.id, remoteCommentId)) - .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(RS_COMMENT_FOR_EDIT) whenever(commentsRsDataSource.updateComment(any(), any(), any(), any())) .thenReturn(RsEditResult.Error("boom")) @@ -280,6 +278,74 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { verify(commentsStore, never()).updateComment(any(), any(), any()) } + @Test + fun `Should load comment via wordpress-rs for rs-capable site`() = test { + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(RS_COMMENT_FOR_EDIT) + + viewModel.start(rsSite, siteCommentIdentifier) + + assertThat(uiState[1].editedComment).isEqualTo( + CommentEssentials( + commentId = remoteCommentId, + userName = RS_COMMENT_FOR_EDIT.authorName, + commentText = RS_COMMENT_FOR_EDIT.contentRaw, + userUrl = RS_COMMENT_FOR_EDIT.authorUrl, + userEmail = RS_COMMENT_FOR_EDIT.authorEmail + ) + ) + verify(getCommentUseCase, never()).execute(any(), any()) + } + + @Test + fun `Should display error SnackBar when wordpress-rs load fails`() = test { + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(null) + + viewModel.start(rsSite, siteCommentIdentifier) + + val expected = UiStringRes(R.string.error_load_comment) + assertEquals(expected, onSnackbarMessage.first().message) + verify(getCommentUseCase, never()).execute(any(), any()) + } + + @Test + fun `onActionMenuClicked saves via wordpress-rs without a FluxC row and skips the mirror`() = test { + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(RS_COMMENT_FOR_EDIT) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(rsSite.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsRsDataSource.updateComment(any(), any(), any(), any())) + .thenReturn(RsEditResult.Success(SERVER_EDITED_COMMENT)) + + viewModel.start(rsSite, siteCommentIdentifier) + viewModel.onActionMenuClicked() + + assertThat(uiActionEvent.firstOrNull()).isEqualTo(DONE) + verify(localCommentCacheUpdateHandler).requestCommentsUpdate() + verify(analyticsUtilsWrapper).trackCommentActionWithSiteDetails( + Stat.COMMENT_EDITED, + AnalyticsCommentActionSource.SITE_COMMENTS, + rsSite + ) + verify(commentsStore, never()).updateComment(any(), any(), any()) + verify(commentsStore, never()).updateEditComment(any(), any()) + } + + @Test + fun `Should update notification entity on wordpress-rs save if NotificationCommentIdentifier`() = test { + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(RS_COMMENT_FOR_EDIT) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(rsSite.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsRsDataSource.updateComment(any(), any(), any(), any())) + .thenReturn(RsEditResult.Success(SERVER_EDITED_COMMENT)) + whenever(notificationActionsWrapper.downloadNoteAndUpdateDB(noteId)) + .thenReturn(true) + + viewModel.start(rsSite, notificationCommentIdentifier) + viewModel.onActionMenuClicked() + + verify(notificationActionsWrapper).downloadNoteAndUpdateDB(noteId) + assertThat(uiActionEvent.firstOrNull()).isEqualTo(DONE) + } + @Test fun `onBackPressed triggers CLOSE when no edits`() { viewModel.start(site, siteCommentIdentifier) @@ -570,5 +636,14 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { authorUrl = "server authorUrl", contentRaw = "server content" ) + + // Pre-fill fixture for rs loads, mirroring COMMENT_ENTITY's editable fields so save-path + // verifications match regardless of which transport loaded the comment. + private val RS_COMMENT_FOR_EDIT = RsEditedComment( + authorName = COMMENT_ENTITY.authorName!!, + authorEmail = COMMENT_ENTITY.authorEmail!!, + authorUrl = COMMENT_ENTITY.authorUrl!!, + contentRaw = COMMENT_ENTITY.content!! + ) } } From d924c964cce812dc8b7fce632af925f5b384f2c9 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 13:02:10 -0400 Subject: [PATCH 3/7] Address review findings: reader rs-path test, load dedup, drop dead localCommentId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a test covering the reader identifier on an rs-capable site (the real-world reader edit path: rs load + rs save + reader table update) - Collapse mapCommentEssentials' duplicated fallback into loadCommentViaRs/ loadCommentViaFluxC helpers, matching the save-side naming - Remove SiteCommentIdentifier.localCommentId and the detail screen pipeline that fed it — nothing reads it now that the editor addresses comments by remote id --- .../ui/comments/CommentDetailFragment.java | 2 +- .../ui/comments/unified/CommentIdentifier.kt | 1 - .../unified/UnifiedCommentDetailsViewModel.kt | 7 +-- .../unified/UnifiedCommentsEditViewModel.kt | 55 +++++++++---------- .../UnifiedCommentDetailsViewModelTest.kt | 4 +- .../UnifiedCommentsEditViewModelTest.kt | 20 ++++++- 6 files changed, 48 insertions(+), 41 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java index a3069dcd5dcb..b2c1179ca472 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java @@ -705,7 +705,7 @@ private CommentIdentifier mapCommentIdentifier() { switch (mCommentSource) { case SITE_COMMENTS: if (mComment != null) { - return new SiteCommentIdentifier(mComment.getId(), mComment.getRemoteCommentId()); + return new SiteCommentIdentifier(mComment.getRemoteCommentId()); } else { return null; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt index f3da9c995512..c70bf9b44d37 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt @@ -8,7 +8,6 @@ sealed class CommentIdentifier : Parcelable { @Parcelize data class SiteCommentIdentifier( - val localCommentId: Int, override val remoteCommentId: Long ) : CommentIdentifier() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt index a7ab8bf6331e..931cb7cb6deb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt @@ -101,10 +101,6 @@ class UnifiedCommentDetailsViewModel @Inject constructor( private var loadedComment: RsComment? = null private var isLikeInProgress = false - // From the FluxC cache row: kept only to fill SiteCommentIdentifier's shape for the shared - // edit screen, which no longer reads it (it loads and saves via wordpress-rs on rs sites). - private var localCommentId: Int = 0 - fun start(site: SiteModel, remoteCommentId: Long, noteId: String? = null) { if (isStarted) return isStarted = true @@ -165,7 +161,6 @@ class UnifiedCommentDetailsViewModel @Inject constructor( when { loaded.rsComment != null -> { loadedComment = loaded.rsComment - localCommentId = loaded.cached?.id?.toInt() ?: 0 _uiState.value = loaded.rsComment.toUiState( loaded.cached, loaded.fallbackPostTitle, @@ -236,7 +231,7 @@ class UnifiedCommentDetailsViewModel @Inject constructor( // In note mode edit through the note identifier so the edit screen refreshes the note DB // after saving, keeping the notifications list consistent with the edited comment. val identifier = noteId?.let { NotificationCommentIdentifier(it, remoteCommentId) } - ?: SiteCommentIdentifier(localCommentId, remoteCommentId) + ?: SiteCommentIdentifier(remoteCommentId) _uiActionEvent.value = Event(LaunchEditComment(site, identifier)) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt index 1984e55a7eea..d3d83647607a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt @@ -230,37 +230,34 @@ class UnifiedCommentsEditViewModel @Inject constructor( private suspend fun mapCommentEssentials(): CommentEssentials { // A failed load returns default CommentEssentials, which fails isValid() and surfaces // the load-error snackbar in initViews(). - return if (canUseRs()) { - // Edit context returns the raw (unrendered) content the editor must show — the same - // thing the FluxC entity stored — plus the author email the view context omits. - val rsComment = commentsRsDataSource.getCommentForEdit(site, commentIdentifier.remoteCommentId) - if (rsComment != null) { - CommentEssentials( - commentId = commentIdentifier.remoteCommentId, - userName = rsComment.authorName, - commentText = rsComment.contentRaw, - userUrl = rsComment.authorUrl, - userEmail = rsComment.authorEmail - ) - } else { - CommentEssentials() - } - } else { - val commentEntity = getCommentUseCase.execute(site, commentIdentifier.remoteCommentId) - if (commentEntity != null) { - CommentEssentials( - commentId = commentEntity.id, - userName = commentEntity.authorName ?: "", - commentText = commentEntity.content ?: "", - userUrl = commentEntity.authorUrl ?: "", - userEmail = commentEntity.authorEmail ?: "" - ) - } else { - CommentEssentials() - } - } + val essentials = if (canUseRs()) loadCommentViaRs() else loadCommentViaFluxC() + return essentials ?: CommentEssentials() } + // Edit context returns the raw (unrendered) content the editor must show — the same + // thing the FluxC entity stored — plus the author email the view context omits. + private suspend fun loadCommentViaRs(): CommentEssentials? = + commentsRsDataSource.getCommentForEdit(site, commentIdentifier.remoteCommentId)?.let { rsComment -> + CommentEssentials( + commentId = commentIdentifier.remoteCommentId, + userName = rsComment.authorName, + commentText = rsComment.contentRaw, + userUrl = rsComment.authorUrl, + userEmail = rsComment.authorEmail + ) + } + + private suspend fun loadCommentViaFluxC(): CommentEssentials? = + getCommentUseCase.execute(site, commentIdentifier.remoteCommentId)?.let { commentEntity -> + CommentEssentials( + commentId = commentEntity.id, + userName = commentEntity.authorName ?: "", + commentText = commentEntity.content ?: "", + userUrl = commentEntity.authorUrl ?: "", + userEmail = commentEntity.authorEmail ?: "" + ) + } + private suspend fun updateComment(editedCommentEssentials: CommentEssentials) { // Prefer wordpress-rs, which can edit comments on both WP.com and self-hosted // application-password sites (FluxC's updateEditComment can't reach app-password sites). diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt index 2d2db5f04462..ac13a6fd0c6e 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt @@ -280,7 +280,7 @@ class UnifiedCommentDetailsViewModelTest : BaseUnitTest() { } @Test - fun `onEditClicked emits launch edit event with local id from cache`() = test { + fun `onEditClicked emits launch edit event with site comment identifier`() = test { viewModel.start(site, REMOTE_COMMENT_ID) viewModel.onEditClicked() @@ -288,7 +288,7 @@ class UnifiedCommentDetailsViewModelTest : BaseUnitTest() { val event = uiActionEvents.last() assertThat(event).isInstanceOf(LaunchEditComment::class.java) assertThat((event as LaunchEditComment).commentIdentifier) - .isEqualTo(SiteCommentIdentifier(LOCAL_COMMENT_ID, REMOTE_COMMENT_ID)) + .isEqualTo(SiteCommentIdentifier(REMOTE_COMMENT_ID)) } @Test diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt index e479b2340a16..ceaa2f06ca9b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt @@ -98,10 +98,9 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { setIsWPCom(true) } - private val localCommentId = 1000 private val remoteCommentId = 4321L private val postId = 678L - private val siteCommentIdentifier = SiteCommentIdentifier(localCommentId, remoteCommentId) + private val siteCommentIdentifier = SiteCommentIdentifier(remoteCommentId) private val noteId = "noteId" private val notificationCommentIdentifier = NotificationCommentIdentifier(noteId, remoteCommentId) private val readerCommentIdentifier = ReaderCommentIdentifier(REMOTE_SITE_ID, postId, remoteCommentId) @@ -346,6 +345,23 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { assertThat(uiActionEvent.firstOrNull()).isEqualTo(DONE) } + // Reader comments live on WP.com blogs, so real-world reader edits take the rs path. + @Test + fun `Should update reader entity on wordpress-rs save if ReaderCommentIdentifier`() = test { + whenever(commentsRsDataSource.getCommentForEdit(rsSite, remoteCommentId)).thenReturn(RS_COMMENT_FOR_EDIT) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(rsSite.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsRsDataSource.updateComment(any(), any(), any(), any())) + .thenReturn(RsEditResult.Success(SERVER_EDITED_COMMENT)) + + viewModel.start(rsSite, readerCommentIdentifier) + viewModel.onActionMenuClicked() + + verify(readerCommentTableWrapper).addOrUpdateComment(any()) + assertThat(uiActionEvent.firstOrNull()).isEqualTo(DONE) + verify(commentsStore, never()).updateEditComment(any(), any()) + } + @Test fun `onBackPressed triggers CLOSE when no edits`() { viewModel.start(site, siteCommentIdentifier) From e229412976513fc67456b65238494824c5692ff6 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 14:09:04 -0400 Subject: [PATCH 4/7] Add test for the FluxC save path's missing-row guard The rs empty-list tests hit the opposite-behaving rs mirror guard (save succeeds, skip mirror); nothing exercised the FluxC branch where a missing cache row must fail the save with no DONE and no analytics. --- .../viewmodels/UnifiedCommentsEditViewModelTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt index ceaa2f06ca9b..75e1503f6c77 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt @@ -209,6 +209,20 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { assertThat(onSnackbarMessage.firstOrNull()).isNotNull } + @Test + fun `onActionMenuClicked triggers snackbar and skips DONE when FluxC row is missing at save time`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(emptyList()) + + viewModel.start(site, siteCommentIdentifier) + viewModel.onActionMenuClicked() + + assertThat(onSnackbarMessage.firstOrNull()).isNotNull + assertThat(uiActionEvent).isEmpty() + verify(commentsStore, never()).updateEditComment(any(), any()) + verify(analyticsUtilsWrapper, never()).trackCommentActionWithSiteDetails(any(), any(), any()) + } + @Test fun `onActionMenuClicked triggers DONE action if comment update successfully`() = test { whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) From 2444bc01304759ddcf8dc58af866a291242632ed Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 14:22:47 -0400 Subject: [PATCH 5/7] Don't open the comment editor when offline The editor loads the comment from the network, so opening it offline showed an empty screen with a load-error snackbar. Guard onEditClicked with the detail screen's existing isOffline() check, matching moderation and reply. --- .../unified/UnifiedCommentDetailsViewModel.kt | 2 ++ .../viewmodels/UnifiedCommentDetailsViewModelTest.kt | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt index 931cb7cb6deb..ed93d6c47511 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentDetailsViewModel.kt @@ -227,6 +227,8 @@ class UnifiedCommentDetailsViewModel @Inject constructor( fun onEditClicked() { if (loadedComment == null) return + // The editor loads the comment from the network, so don't open it offline. + if (isOffline()) return trackCommentAction(Stat.COMMENT_EDITOR_OPENED) // In note mode edit through the note identifier so the edit screen refreshes the note DB // after saving, keeping the notifications list consistent with the edited comment. diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt index ac13a6fd0c6e..093edfc0d600 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentDetailsViewModelTest.kt @@ -291,6 +291,17 @@ class UnifiedCommentDetailsViewModelTest : BaseUnitTest() { .isEqualTo(SiteCommentIdentifier(REMOTE_COMMENT_ID)) } + @Test + fun `onEditClicked shows snackbar and does not open the editor when offline`() = test { + viewModel.start(site, REMOTE_COMMENT_ID) + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + + viewModel.onEditClicked() + + assertThat(uiActionEvents.filterIsInstance()).isEmpty() + assertThat(snackbarMessages).isNotEmpty + } + @Test fun `onPostTitleClicked emits open post in reader event`() = test { viewModel.start(site, REMOTE_COMMENT_ID) From 7e96eef0ea2f8cfc603a6f8a636e55f5c92bd229 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 14:29:35 -0400 Subject: [PATCH 6/7] Guard the remaining comment editor launch points against offline The rs detail was guarded in the previous commit, but the legacy detail (which hosts comment notifications when the rs detail isn't used) and the reader could still open the editor offline, showing an empty screen with a load-error snackbar. Use NetworkUtils.checkConnection, the same idiom both files already use for reply and moderation. --- .../wordpress/android/ui/comments/CommentDetailFragment.java | 4 ++++ .../wordpress/android/ui/reader/ReaderCommentListActivity.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java index b2c1179ca472..f4f29c7ebb9b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java @@ -675,6 +675,10 @@ private void editComment(@NonNull SiteModel site) { if (!isAdded()) { return; } + // The editor loads and saves the comment over the network, so don't open it offline. + if (!NetworkUtils.checkConnection(getActivity())) { + return; + } if (mCommentSource != null) { AnalyticsUtils.trackCommentActionWithSiteDetails( Stat.COMMENT_EDITOR_OPENED, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.kt index b6a49042e732..e2ebeab9ff54 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.kt @@ -605,6 +605,10 @@ class ReaderCommentListActivity : BaseAppCompatActivity(), comment: ReaderComment, postSite: SiteModel ) { + // The editor loads and saves the comment over the network, so don't open it offline. + if (!NetworkUtils.checkConnection(this)) { + return + } val intent = createIntent( this, ReaderCommentIdentifier(comment.blogId, comment.postId, comment.commentId), From 42c6f615fc8bceb3b77e7ca4163bdca032002079 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 14 Jul 2026 14:35:36 -0400 Subject: [PATCH 7/7] Fix misleading comment on the edit-context setup stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stub is load-bearing — without it Mockito returns null for the non-null suspend return and the happy-path test fails. Only the payload is unused, not the stub itself. --- .../android/ui/comments/unified/CommentsRsDataSourceTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt index a860987786b0..2939dfdc5854 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentsRsDataSourceTest.kt @@ -71,7 +71,8 @@ class CommentsRsDataSourceTest { whenever(uniffiClient.posts()).thenReturn(postsExecutor) whenever(uniffiClient.comments()).thenReturn(commentsExecutor) commentsExecutor.stub { - // The payload is decided by the request-level stub below; this value is unused. + // Must return non-null to satisfy the mock's suspend contract; the payload each test + // actually sees comes from the request-level stub below. on { retrieveWithEditContext(any(), any()) } doReturn CommentsRequestRetrieveWithEditContextResponse(editContextComment(), mock()) }