diff --git a/WordPress/Classes/Models/AbstractPost.swift b/WordPress/Classes/Models/AbstractPost.swift index 63cd8849580b..aeba69af1c33 100644 --- a/WordPress/Classes/Models/AbstractPost.swift +++ b/WordPress/Classes/Models/AbstractPost.swift @@ -17,6 +17,11 @@ extension AbstractPost { // MARK: - Status + /// Returns `true` is the post has one of the given statuses. + func isStatus(in statuses: Set) -> Bool { + statuses.contains(status ?? .draft) + } + /// - note: deprecated (kahu-offline-mode) @objc var statusTitle: String? { diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index 36c4dd82d972..d15936ecccad 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -11,9 +11,13 @@ protocol PostCoordinatorDelegate: AnyObject { class PostCoordinator: NSObject { - enum SavingError: Error { + enum SavingError: Error, LocalizedError { case mediaFailure(AbstractPost) case unknown + + var errorDescription: String? { + Strings.genericErrorTitle + } } @objc static let shared = PostCoordinator() @@ -153,7 +157,7 @@ class PostCoordinator: NSObject { @MainActor func _publish(_ post: AbstractPost, options: PublishingOptions) async throws { assert(post.isOriginal()) - assert(post.status == .draft) + assert(post.isStatus(in: [.draft, .pending])) await pauseSyncing(for: post) defer { resumeSyncing(for: post) } @@ -217,13 +221,10 @@ class PostCoordinator: NSObject { /// Patches the post. /// - /// - warning: Can only be used on posts with no unsynced revisions. - /// /// - warning: Work-in-progress (kahu-offline-mode) @MainActor func _update(_ post: AbstractPost, changes: RemotePostUpdateParameters) async throws { assert(post.isOriginal()) - assert(post.revision == nil, "Can only be used on posts with no unsynced changes") let post = post.original() do { @@ -238,7 +239,7 @@ class PostCoordinator: NSObject { guard let topViewController = UIApplication.shared.mainWindow?.topmostPresentedViewController else { return } - let alert = UIAlertController(title: Strings.errorTitle, message: error.localizedDescription, preferredStyle: .alert) + let alert = UIAlertController(title: Strings.genericErrorTitle, message: error.localizedDescription, preferredStyle: .alert) if let error = error as? PostRepository.PostSaveError { switch error { case .conflict: @@ -298,7 +299,7 @@ class PostCoordinator: NSObject { /// Returns `true` if the post is eligible for syncing. func isSyncAllowed(for post: AbstractPost) -> Bool { - post.status == .draft + post.status == .draft || post.status == .pending } /// Returns `true` if post has any revisions that need to be synced. @@ -1215,6 +1216,7 @@ private enum Strings { static let deletePost = NSLocalizedString("postsList.deletePost.message", value: "Post deleted permanently", comment: "A short message explaining that a post was deleted permanently.") static let movePageToTrash = NSLocalizedString("postsList.movePageToTrash.message", value: "Page moved to trash", comment: "A short message explaining that a page was moved to the trash bin.") static let deletePage = NSLocalizedString("postsList.deletePage.message", value: "Page deleted permanently", comment: "A short message explaining that a page was deleted permanently.") - static let errorTitle = NSLocalizedString("postNotice.errorTitle", value: "An error occured", comment: "A generic error message title") + static let genericErrorTitle = NSLocalizedString("postNotice.errorTitle", value: "An error occured", comment: "A generic error message title") static let buttonOK = NSLocalizedString("postNotice.ok", value: "OK", comment: "Button OK") + static let errorUnsyncedChangesMessage = NSLocalizedString("postNotice.errorUnsyncedChangesMessage", value: "The app is uploading previously made changes to the server. Please try again later.", comment: "An error message") } diff --git a/WordPress/Classes/Services/PostRepository+Helpers.swift b/WordPress/Classes/Services/PostRepository+Helpers.swift index f486cba7e7a7..1919fddc61ab 100644 --- a/WordPress/Classes/Services/PostRepository+Helpers.swift +++ b/WordPress/Classes/Services/PostRepository+Helpers.swift @@ -9,7 +9,10 @@ extension RemotePostCreateParameters { status: (post.status ?? .draft).rawValue ) date = post.dateCreated - authorID = post.authorID?.intValue + // - warning: the currnet Core Data model defaults to `0` + if let authorID = post.authorID?.intValue, authorID > 0 { + self.authorID = authorID + } title = post.postTitle content = post.content password = post.password diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index 0a7c31e83705..237075693ca4 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -3,8 +3,18 @@ import WordPressKit final class PostRepository { - enum Error: Swift.Error { + enum Error: Swift.Error, LocalizedError { case remoteAPIUnavailable + case hasUnsyncedChanges + case patchingUnsyncedPost // Should never happen + + var errorDescription: String? { + switch self { + case .remoteAPIUnavailable: return Strings.genericErrorMessage + case .hasUnsyncedChanges: return Strings.errorUnsyncedChangesMessage + case .patchingUnsyncedPost: return Strings.genericErrorMessage + } + } } private let coreDataStack: CoreDataStackSwift @@ -170,15 +180,19 @@ final class PostRepository { /// revisions are used only for content. @MainActor func _update(_ post: AbstractPost, changes: RemotePostUpdateParameters) async throws { - let original = post.original ?? post - guard let postID = original.postID, postID.intValue > 0 else { + assert(post.isOriginal()) + + guard post.revision == nil else { + throw PostRepository.Error.hasUnsyncedChanges + } + guard let postID = post.postID, postID.intValue > 0 else { assertionFailure("Trying to patch a non-existent post") - return + throw PostRepository.Error.patchingUnsyncedPost } let uploadedPost = try await _patch(post, postID: postID, changes: changes, overwrite: true) let context = coreDataStack.mainContext - PostHelper.update(original, with: uploadedPost, in: context, overwrite: true) + PostHelper.update(post, with: uploadedPost, in: context, overwrite: true) ContextManager.shared.saveContextAndWait(context) } @@ -213,7 +227,8 @@ final class PostRepository { case .conflict: // Fetch the latest post to consolidate the changes let remotePost = try await service.post(withID: postID) - if changes.content != nil && remotePost.content != original.content { + // Check for false positives + if changes.content != nil && remotePost.content != changes.content && remotePost.content != original.content { // The conflict in content can be resolved only manually throw PostSaveError.conflict(latest: remotePost) } @@ -670,3 +685,8 @@ extension PostRepository { } } + +private enum Strings { + static let genericErrorMessage = NSLocalizedString("postList.genericErrorMessage", value: "Something went wrong", comment: "A generic error message title") + static let errorUnsyncedChangesMessage = NSLocalizedString("postList.errorUnsyncedChangesMessage", value: "The app is uploading previously made changes to the server. Please try again later.", comment: "An error message") +} diff --git a/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift b/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift index b04eb8e0c9f6..9a2f86bba46d 100644 --- a/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift +++ b/WordPress/Classes/ViewRelated/Pages/PageMenuViewModelTests.swift @@ -154,7 +154,7 @@ final class PageMenuViewModelTests: CoreDataTestCase { .map { $0.buttons } let expectedButtons: [[AbstractPostButton]] = [ [.view], - [.duplicate, .publish], + [.publish, .duplicate], [.setParent], [.trash] ] @@ -174,7 +174,7 @@ final class PageMenuViewModelTests: CoreDataTestCase { .map { $0.buttons } let expectedButtons: [[AbstractPostButton]] = [ [.view], - [.moveToDraft, .publish], + [.moveToDraft], [.setParent], [.trash] ] diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift index 291d810aab62..c533e2a44fd6 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift @@ -1,6 +1,7 @@ import Foundation enum AbstractPostHelper { + /// - warning: deprecated (kahu-offline-mode) static func editorPublishAction(for post: AbstractPost) -> PostEditorAction { post.blog.isPublishingPostsAllowed() ? .publish : .submitForReview } diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift index 507504c1b38c..7a94386b41f3 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostMenuHelper.swift @@ -123,7 +123,11 @@ extension AbstractPostButton: AbstractPostMenuAction { switch self { case .retry: return Strings.retry case .view: return post.status == .publish ? Strings.view : Strings.preview - case .publish: return AbstractPostHelper.editorPublishAction(for: post).publishActionLabel + case .publish: + guard RemoteFeatureFlag.syncPublishing.enabled() else { + return AbstractPostHelper.editorPublishAction(for: post).publishActionLabel + } + return Strings.publish case .stats: return Strings.stats case .duplicate: return Strings.duplicate case .moveToDraft: return Strings.draft @@ -194,6 +198,7 @@ extension AbstractPostButton: AbstractPostMenuAction { static let trash = NSLocalizedString("posts.trash.actionTitle", value: "Move to trash", comment: "Label for a option that moves a post to the trash folder") static let view = NSLocalizedString("posts.view.actionTitle", value: "View", comment: "Label for the view post button. Tapping displays the post as it appears on the web.") static let preview = NSLocalizedString("posts.preview.actionTitle", value: "Preview", comment: "Label for the preview post button. Tapping displays the post as it appears on the web.") + static let publish = NSLocalizedString("posts.publish.actionTitle", value: "Publish", comment: "Label for the publish post button.") static let retry = NSLocalizedString("posts.retry.actionTitle", value: "Retry", comment: "Retry uploading the post.") static let share = NSLocalizedString("posts.share.actionTitle", value: "Share", comment: "Share the post.") static let blaze = NSLocalizedString("posts.blaze.actionTitle", value: "Promote with Blaze", comment: "Promote the post with Blaze.") diff --git a/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift b/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift index f37bd146cd57..64cc334c20cf 100644 --- a/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift @@ -7,6 +7,7 @@ final class PageMenuViewModel: AbstractPostMenuViewModel { private let isSitePostsPage: Bool private let isJetpackFeaturesEnabled: Bool private let isBlazeFlagEnabled: Bool + private let isSyncPublishingEnabled: Bool var buttonSections: [AbstractPostButtonSection] { [ @@ -27,13 +28,15 @@ final class PageMenuViewModel: AbstractPostMenuViewModel { isSiteHomepage: Bool, isSitePostsPage: Bool, isJetpackFeaturesEnabled: Bool = JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled(), - isBlazeFlagEnabled: Bool = BlazeHelper.isBlazeFlagEnabled() + isBlazeFlagEnabled: Bool = BlazeHelper.isBlazeFlagEnabled(), + isSyncPublishingEnabled: Bool = RemoteFeatureFlag.syncPublishing.enabled() ) { self.page = page self.isSiteHomepage = isSiteHomepage self.isSitePostsPage = isSitePostsPage self.isJetpackFeaturesEnabled = isJetpackFeaturesEnabled self.isBlazeFlagEnabled = isBlazeFlagEnabled + self.isSyncPublishingEnabled = isSyncPublishingEnabled } private func createPrimarySection() -> AbstractPostButtonSection { @@ -49,6 +52,10 @@ final class PageMenuViewModel: AbstractPostMenuViewModel { private func createSecondarySection() -> AbstractPostButtonSection { var buttons = [AbstractPostButton]() + if canPublish { + buttons.append(.publish) + } + if page.status != .draft && !isSiteHomepage { buttons.append(.moveToDraft) } @@ -61,17 +68,23 @@ final class PageMenuViewModel: AbstractPostMenuViewModel { buttons.append(.share) } - if page.status != .trash && page.isFailed { - buttons.append(.retry) - } - - if !page.isFailed, page.status != .publish && page.status != .trash { - buttons.append(.publish) + if !isSyncPublishingEnabled { + if page.status != .trash && page.isFailed { + buttons.append(.retry) + } } return AbstractPostButtonSection(buttons: buttons) } + private var canPublish: Bool { + guard isSyncPublishingEnabled else { + return !page.isFailed && page.status != .publish && page.status != .trash + } + let userCanPublish = page.blog.capabilities != nil ? page.blog.isPublishingPostsAllowed() : true + return page.isStatus(in: [.draft, .pending]) && userCanPublish + } + private func createBlazeSection() -> AbstractPostButtonSection { var buttons = [AbstractPostButton]() diff --git a/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift index 6379d90b8cbe..31a4e59a2677 100644 --- a/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift @@ -15,6 +15,7 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { private let isBlazeFlagEnabled: Bool private let isSyncPublishingEnabled: Bool + /// - warning: deprecated (kahu-offline-mode) var progressBlock: ((Float) -> Void)? = nil { didSet { if let _ = oldValue, let uuid = progressObserverUUID { @@ -86,6 +87,23 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { } var statusColor: UIColor { + guard isSyncPublishingEnabled else { + return _statusColor + } + switch post.status ?? .draft { + case .pending: + return .success + case .scheduled: + return .primary(.shade40) + case .trash: + return .error + default: + return .neutral(.shade70) + } + } + + /// - warning: deprecated (kahu-offline-mode) + var _statusColor: UIColor { guard let status = postStatus else { return .neutral(.shade70) } @@ -166,6 +184,10 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { private func createSecondarySection() -> AbstractPostButtonSection { var buttons = [AbstractPostButton]() + if canPublish { + buttons.append(.publish) + } + if post.status != .draft { buttons.append(.moveToDraft) } @@ -190,10 +212,6 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { } } - if canPublish { - buttons.append(.publish) - } - return AbstractPostButtonSection(buttons: buttons) } @@ -230,21 +248,26 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { return autoUploadInteractor.canCancelAutoUpload(of: post) } - /// Returns true if any of the following conditions are true: - /// - /// * The post is a draft. - /// * The post failed to upload and has local changes but the user canceled auto-uploading - /// * The upload failed and the user cannot Cancel it anymore. This happens when we reached the maximum number of retries. private var canPublish: Bool { + guard isSyncPublishingEnabled else { + return _canPublish + } + let userCanPublish = post.blog.capabilities != nil ? post.blog.isPublishingPostsAllowed() : true + return (post.status == .draft || post.status == .pending) && userCanPublish + } + + /// - warning: deprecated (kahu-offline-mode) + private var _canPublish: Bool { let isNotCancelableWithFailedToUploadChanges: Bool = post.isFailed && post.hasLocalChanges() && !autoUploadInteractor.canCancelAutoUpload(of: post) return post.isDraft() || isNotCancelableWithFailedToUploadChanges } func statusAndBadges(separatedBy separator: String) -> String { - let sticky = post.isStickyPost && !isUploadingOrFailed ? Constants.stickyLabel : "" + let sticky = post.isStickyPost ? Constants.stickyLabel : "" + let pending = (post.status == .pending && isSyncPublishingEnabled) ? Constants.pendingReview : "" let status = self.status ?? "" - return [status, sticky].filter { !$0.isEmpty }.joined(separator: separator) + return [status, pending, sticky].filter { !$0.isEmpty }.joined(separator: separator) } /// Determine what the failed status message should be and return it. @@ -266,6 +289,7 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { private enum Constants { static let stickyLabel = NSLocalizedString("Sticky", comment: "Label text that defines a post marked as sticky") + static let pendingReview = NSLocalizedString("postList.badgePendingReview", value: "Pending review", comment: "Badge for post cells") } enum StatusMessages { diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift index 78599f3dd2e4..75e4ba99e632 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift @@ -133,10 +133,17 @@ extension PublishingEditor { case .schedule, .publish: showPrepublishingSheet(for: action, analyticsStat: analyticsStat) case .update: - performUpdatePostAction() + guard !isUploadingMedia else { + return displayMediaIsUploadingAlert() + } + performUpdateAction() case .submitForReview: - // TODO: Show Prepublishing VC - fatalError("Not implemented (kahu-offline-mode)") + guard !isUploadingMedia else { + return displayMediaIsUploadingAlert() + } + var changes = RemotePostUpdateParameters() + changes.status = Post.Status.pending.rawValue + performUpdateAction(changes: changes) case .save, .saveAsDraft, .continueFromHomepageEditing: assertionFailure("No longer used and supported") break @@ -171,15 +178,16 @@ extension PublishingEditor { dismissOrPopView() } - private func performUpdatePostAction() { + private func performUpdateAction(changes: RemotePostUpdateParameters? = nil) { SVProgressHUD.setDefaultMaskType(.clear) SVProgressHUD.show() postEditorStateContext.updated(isBeingPublished: true) Task { @MainActor in do { - try await PostCoordinator.shared._save(post) - dismissOrPopView() + let post = try await PostCoordinator.shared._save(post, changes: changes) + self.post = post + self.createRevisionOfPost() } catch { postEditorStateContext.updated(isBeingPublished: false) } @@ -363,7 +371,7 @@ extension PublishingEditor { return discardAndDismiss() } - if post.original().status == .draft { + if post.original().isStatus(in: [.draft, .pending]) { showCloseDraftConfirmationAlert() } else { showClosePublishedPostConfirmationAlert() diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorState.swift b/WordPress/Classes/ViewRelated/Post/PostEditorState.swift index 90cb7f86ab1b..b1be436bb658 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditorState.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditorState.swift @@ -281,7 +281,7 @@ public class PostEditorStateContext { case .draft: return makePublishAction() case .pending: - return .update + return userCanPublish ? .publish : .update case .publishPrivate, .publish, .scheduled: return .update case .trash, .deleted: diff --git a/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift index 67996a035bec..2acfef217038 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift @@ -105,6 +105,9 @@ private func makeBadgesString(for post: Post, syncStateViewModel: PostSyncStateV if !shouldHideAuthor, let author = post.authorForDisplay() { badges.append((author, nil)) } + if !syncStateViewModel.isEditable { + badges = badges.map { ($0.0, UIColor.textTertiary) } + } return AbstractPostHelper.makeBadgesString(with: badges) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 1dd8505e8549..c729a20030f5 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -25,7 +25,7 @@ extension PostSettingsViewController { } @objc var isDraftOrPending: Bool { - [Post.Status.draft, Post.Status.draft].contains(apost.original().status) + apost.original().isStatus(in: [.draft, .pending]) } @objc func setupStandaloneEditor() { diff --git a/WordPress/WordPressTest/PostRepositorySaveTests.swift b/WordPress/WordPressTest/PostRepositorySaveTests.swift index 695fe5a9adbd..8895dbf3aca3 100644 --- a/WordPress/WordPressTest/PostRepositorySaveTests.swift +++ b/WordPress/WordPressTest/PostRepositorySaveTests.swift @@ -871,6 +871,92 @@ class PostRepositorySaveTests: CoreDataTestCase { XCTAssertNil(revision.managedObjectContext) } + /// Scenario: the use sends the updated content to the server, the content + /// gets updated on the server, but the app never recieves a response. + func testSaveConflictFalsePositiveNoResponseFromTheServer() async throws { + // GIVEN a draft post (client behind, server has "content-c") + let clientDateModified = Date(timeIntervalSince1970: 1709852440) + let serverDateModified = Date(timeIntervalSince1970: 1709852440) + .addingTimeInterval(30) + + let post = makePost { + $0.status = .draft + $0.postID = 974 + $0.authorID = 29043 + $0.dateCreated = clientDateModified + $0.dateModified = clientDateModified + $0.postTitle = "Hello" + $0.content = "content-a" + } + + // GIVEN a modified content + let revision = post.createRevision() + revision.content = "content-b" + + try mainContext.save() + + // GIVEN server revision that's ahead and has a new `content-b` that + // was saved after the first "partially successfull" request + let serverPost = { + var post = WordPressComPost.mock + post.modified = serverDateModified + post.title = "title-c" + post.content = "content-b" + return post + }() + + var requestCount = 0 + // GIVEN a server where the post is ahead and has a new content + stub(condition: isPath("/rest/v1.2/sites/80511/posts/974")) { request in + requestCount += 1 + + if requestCount == 1 { + // THEN the first request contains an `if_not_modified_since` parameter + try assertRequestBody(request, expected: """ + { + "content" : "content-b", + "if_not_modified_since" : "2024-03-07T23:00:40+0000" + } + """) + // GIVEN the first request fails with 409 (Conflict) + let ifNotModifiedSince = try request.getIfNotModifiedSince() + XCTAssertTrue(ifNotModifiedSince < serverDateModified) + return HTTPStubsResponse(jsonObject: [ + "error": "old-revision", + "message": "There is a revision of this post that is more recent." + ], statusCode: 409, headers: nil) + } + if requestCount == 2 { + // THEN the second request contains only the delta + try assertRequestBody(request, expected: """ + { + "content" : "content-b" + } + """) + var serverPost = serverPost + serverPost.content = "content-b" + return try HTTPStubsResponse(value: serverPost, statusCode: 200) + } + + throw URLError(.unknown) + } + + stub(condition: isPath("/rest/v1.1/sites/80511/posts/974")) { request in + try HTTPStubsResponse(value: serverPost, statusCode: 200) + } + + try await repository._save(post) + + // THEN the content got updated to the version from the local revision + XCTAssertEqual(post.content, "content-b") + // THEN the rest of the changes are consolidated on the server and the + // new changes made elsewhere are not overwritten thanks to the detla update + XCTAssertEqual(post.postTitle, "title-c") + // THEN the local revision got deleted + XCTAssertNil(post.revision) + XCTAssertNil(revision.managedObjectContext) + } + // MARK: - XMLRPC /// Scenario: saving a post that was deleted on the remote. diff --git a/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift b/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift index 5f9373b6d094..7101488aa721 100644 --- a/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift +++ b/WordPress/WordPressTest/ViewRelated/Post/Views/PostCardStatusViewModelTests.swift @@ -81,7 +81,7 @@ class PostCardStatusViewModelTests: CoreDataTestCase { .map { $0.buttons } let expectedButtons: [[AbstractPostButton]] = [ [.view], - [.duplicate, .publish], + [.publish, .duplicate], [.settings], [.trash] ]