Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically upload published and draft posts #12466

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions WordPress/Classes/Services/PostAutoUploadInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ final class PostAutoUploadInteractor {
func autoUploadAction(for post: AbstractPost) -> AutoUploadAction {
guard post.isFailed,
let status = post.status,
PostAutoUploadInteractor.allowedStatuses.contains(status),
!post.hasRemote() else {
PostAutoUploadInteractor.allowedStatuses.contains(status) else {
return .nothing
}

Expand Down
28 changes: 24 additions & 4 deletions WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,30 @@ class PostCardStatusViewModel: NSObject {
if MediaCoordinator.shared.isUploadingMedia(for: post) {
return NSLocalizedString("Uploading media...", comment: "Message displayed on a post's card while the post is uploading media")
} else if post.isFailed {
if canCancelAutoUpload {
return StatusMessages.postWillBePublished
let defaultFailedMessage = StatusMessages.uploadFailed
guard let postStatus = post.status else {
return defaultFailedMessage
}

if post.wasAutoUploadCancelled {
return StatusMessages.localChanges
}

return StatusMessages.uploadFailed
if autoUploadInteractor.autoUploadAction(for: post) != .upload {
leandroalonso marked this conversation as resolved.
Show resolved Hide resolved
return defaultFailedMessage
}
if post.hasRemote() {
return StatusMessages.changesWillBeUploaded
}

switch postStatus {
leandroalonso marked this conversation as resolved.
Show resolved Hide resolved
case .draft:
return StatusMessages.draftWillBeUploaded
case .publish:
return StatusMessages.postWillBePublished
default:
return defaultFailedMessage
}
} else if post.remoteStatus == .pushing {
return NSLocalizedString("Uploading post...", comment: "Message displayed on a post's card when the post has failed to upload")
} else {
Expand Down Expand Up @@ -98,7 +113,8 @@ class PostCardStatusViewModel: NSObject {
}

if post.isFailed {
return (canCancelAutoUpload || post.wasAutoUploadCancelled) ? .warning : .error
let autoUploadAction = autoUploadInteractor.autoUploadAction(for: post)
return (autoUploadAction == .upload || post.wasAutoUploadCancelled) ? .warning : .error
}

switch status {
Expand Down Expand Up @@ -225,5 +241,9 @@ class PostCardStatusViewModel: NSObject {
static let postWillBePublished = NSLocalizedString("Post will be published next time your device is online",
comment: "Message shown in the posts list when a post is scheduled for publishing")
static let localChanges = NSLocalizedString("Local changes", comment: "A status label for a post that only exists on the user's iOS device, and has not yet been published to their blog.")
static let draftWillBeUploaded = NSLocalizedString("Draft will be uploaded next time your device is online",
comment: "Message shown in post list when a draft is scheduled to be automatically uploaded.")
static let changesWillBeUploaded = NSLocalizedString("Changes will be uploaded next time your device is online",
comment: "Message shown in post list when a post's local changes will be automatically uploaded when the device is online.")
}
}
41 changes: 32 additions & 9 deletions WordPress/Classes/ViewRelated/Post/PostNoticeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum PostNoticeUserInfoKey {
struct PostNoticeViewModel {
private let post: AbstractPost
private let postCoordinator: PostCoordinator
private let autoUploadInteractor = PostAutoUploadInteractor()
leandroalonso marked this conversation as resolved.
Show resolved Hide resolved

init(post: AbstractPost, postCoordinator: PostCoordinator = PostCoordinator.shared) {
self.post = post
Expand Down Expand Up @@ -103,16 +104,31 @@ struct PostNoticeViewModel {
}
}

// TODO Move strings to FailureTitles enum
private var failureTitle: String {
if post.status == .publish {
return FailureTitles.postWillBePublished
var defaultTitle: String {
if post is Page {
return FailureTitles.pageFailedToUpload
} else {
return FailureTitles.postFailedToUpload
}
}

if post is Page {
return NSLocalizedString("Page failed to upload", comment: "Title of notification displayed when a page has failed to upload.")
} else {
return NSLocalizedString("Post failed to upload", comment: "Title of notification displayed when a post has failed to upload.")
guard let postStatus = post.status,
autoUploadInteractor.autoUploadAction(for: post) == .upload else {
return defaultTitle
}

if post.hasRemote() {
return FailureTitles.changesWillBeUploaded
}

switch postStatus {
case .draft:
return FailureTitles.draftWillBeUploaded
case .publish:
return FailureTitles.postWillBePublished
default:
return defaultTitle
}
}

Expand Down Expand Up @@ -201,8 +217,7 @@ struct PostNoticeViewModel {
}

private var failureAction: FailureAction {
let interactor = PostAutoUploadInteractor()
return interactor.canCancelAutoUpload(of: post) ? .cancel : .retry
return autoUploadInteractor.canCancelAutoUpload(of: post) ? .cancel : .retry
}

// MARK: - Action Handlers
Expand Down Expand Up @@ -247,6 +262,14 @@ struct PostNoticeViewModel {
enum FailureTitles {
static let postWillBePublished = NSLocalizedString("Post will be published the next time your device is online",
comment: "Text displayed in notice after a post if published while offline.")
static let draftWillBeUploaded = NSLocalizedString("Draft will be uploaded next time your device is online",
comment: "Text displayed in notice after the app fails to upload a draft.")
static let pageFailedToUpload = NSLocalizedString("Page failed to upload",
comment: "Title of notification displayed when a page has failed to upload.")
static let postFailedToUpload = NSLocalizedString("Post failed to upload",
comment: "Title of notification displayed when a post has failed to upload.")
static let changesWillBeUploaded = NSLocalizedString("Changes will be uploaded next time your device is online",
comment: "Text displayed in notice after the app fails to upload a post.")
}

enum FailureActionTitles {
Expand Down
5 changes: 5 additions & 0 deletions WordPress/WordPressTest/PostBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class PostBuilder {
return self
}

func with(status: BasePost.Status) -> PostBuilder {
post.status = status
return self
}

func with(title: String) -> PostBuilder {
post.postTitle = title
return self
Expand Down
41 changes: 39 additions & 2 deletions WordPress/WordPressTest/PostCardCellTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class PostCardCellTests: XCTestCase {
private var postActionSheetDelegateMock: PostActionSheetDelegateMock!

override func setUp() {
super.setUp()
contextManager = TestContextManager()
context = contextManager.newDerivedContext()

Expand Down Expand Up @@ -263,9 +264,21 @@ class PostCardCellTests: XCTestCase {
XCTAssertFalse(postCell.separatorLabel.isHidden)
}

func testShowsWarningMessageForFailedPublishedPosts() {
func testShowsChangesWillBeUploadedWarningForFailedPublishedPostsWithRemote() {
// Given
let post = PostBuilder().published().with(remoteStatus: .failed).confirmedAutoUpload().build()
let post = PostBuilder(context).published().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build()

// When
leandroalonso marked this conversation as resolved.
Show resolved Hide resolved
postCell.configure(with: post)

// Then
XCTAssertEqual(postCell.statusLabel.text, StatusMessages.changesWillBeUploaded)
XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning)
}

func testShowsPostWillBePublishedWarningForLocallyPublishedPosts() {
// Given
let post = PostBuilder(context).published().with(remoteStatus: .failed).confirmedAutoUpload().build()

// When
postCell.configure(with: post)
Expand Down Expand Up @@ -326,6 +339,30 @@ class PostCardCellTests: XCTestCase {
XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning)
}

func testShowsChangesWillBeUploadedMessageForDraftsWithRemote() {
// Given
let post = PostBuilder(context).drafted().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build()

// When
postCell.configure(with: post)

// Then
XCTAssertEqual(postCell.statusLabel.text, StatusMessages.changesWillBeUploaded)
XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning)
}

func testShowsDraftWillBeUploadedMessageForLocalDrafts() {
// Given
let post = PostBuilder(context).drafted().with(remoteStatus: .failed).build()

// When
postCell.configure(with: post)

// Then
XCTAssertEqual(postCell.statusLabel.text, StatusMessages.draftWillBeUploaded)
XCTAssertEqual(postCell.statusLabel.textColor, UIColor.warning)
}

private func postCellFromNib() -> PostCardCell {
let bundle = Bundle(for: PostCardCell.self)
guard let postCell = bundle.loadNibNamed("PostCardCell", owner: nil)?.first as? PostCardCell else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ class PostCoordinatorUploadActionUseCaseTests: XCTestCase {
createPost(.publish, confirmedAutoUpload: true): .upload,
// Published local drafts with no confirmation will be remote auto-saved
createPost(.publish): .autoSave,
// Draft and published posts with remote are currently unsupported. This will be
// fixed soon.
createPost(.draft, hasRemote: true): .nothing,
createPost(.publish, hasRemote: true): .nothing,
// Posts with remote that do not have confirmation will be remote auto-saved
shiki marked this conversation as resolved.
Show resolved Hide resolved
createPost(.draft, hasRemote: true): .autoSave,
createPost(.publish, hasRemote: true): .autoSave,
// Posts with remote that have confirmation will be automatically uploaded
createPost(.draft, hasRemote: true, confirmedAutoUpload: true): .upload,
createPost(.publish, hasRemote: true, confirmedAutoUpload: true): .upload,
// Other statuses are currently ignored
createPost(.publishPrivate, confirmedAutoUpload: true): .nothing,
createPost(.scheduled, confirmedAutoUpload: true): .nothing,
Expand Down Expand Up @@ -67,7 +69,7 @@ class PostCoordinatorUploadActionUseCaseTests: XCTestCase {
}

/// Test which auto-uploaded post types can be canceled by the user
func testCancelAutoUploadMethodAppliesToLocalDraftsAndConfirmedUploads() {
func testCancelAutoUploadMethodAppliesToDraftsAndConfirmedUploads() {
// Arrange
let postsAndExpectedCancelableResult: [Post: Bool] = [
// Local drafts are automatically uploaded and do not need to be canceled. We consider
Expand All @@ -79,10 +81,9 @@ class PostCoordinatorUploadActionUseCaseTests: XCTestCase {
// auto-saved is considered safe and do not need to be canceled. It should also happen
// in the background without any interaction from the user.
createPost(.publish): false,
// Draft and published posts with remote are currently unsupported. These should
// allow cancelation. This will be fixed soon.
createPost(.draft, hasRemote: true, confirmedAutoUpload: true): false,
createPost(.publish, hasRemote: true, confirmedAutoUpload: true): false,
// Confirmed draft and published posts with remote will be automatically uploaded.
createPost(.draft, hasRemote: true, confirmedAutoUpload: true): true,
createPost(.publish, hasRemote: true, confirmedAutoUpload: true): true,
// Posts with remote that have no confirmation will be remote auto-saved.
createPost(.draft, hasRemote: true): false,
createPost(.publish, hasRemote: true): false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ class PostCoordinatorFailedPostsFetcherTests: XCTestCase {
createPost(status: .draft),
createPost(status: .draft),
createPost(status: .draft),
createPost(status: .publish)
]
let unexpectedPosts = [
createPost(status: .publish),
createPost(status: .draft, hasRemote: true),
createPost(status: .publish, hasRemote: true),
]
let unexpectedPosts = [
createPost(status: .publishPrivate),
createPost(status: .publishPrivate, hasRemote: true),
createPost(status: .scheduled),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import Nimble

@testable import WordPress

private typealias FailureActionTitles = PostNoticeViewModel.FailureActionTitles
private typealias FailureTitles = PostNoticeViewModel.FailureTitles

class PostNoticeViewModelTests: XCTestCase {
private var contextManager: TestContextManager!
private var context: NSManagedObjectContext!
Expand All @@ -22,22 +25,73 @@ class PostNoticeViewModelTests: XCTestCase {
super.tearDown()
}

func testCreatesNoticeWithFailureMessageForFailedPublishedPosts() {
// Given
let post = PostBuilder(context)
.published()
.with(title: "Darby Ritchie")
.with(remoteStatus: .failed)
.confirmedAutoUpload()
.build()
func testNoticesToBeShownAfterFailingToUploadPosts() {
struct Expectation {
let scenario: String
let post: Post
let title: String
let actionTitle: String
}

// When
let notice = PostNoticeViewModel(post: post).notice
// Arrange
let expectations: [Expectation] = [
Expectation(
scenario: "Local draft",
post: createPost(.draft),
title: FailureTitles.draftWillBeUploaded,
actionTitle: FailureActionTitles.retry
),
Expectation(
scenario: "Draft with confirmed local changes",
post: createPost(.draft, hasRemote: true),
title: FailureTitles.changesWillBeUploaded,
actionTitle: FailureActionTitles.cancel
),
Expectation(
scenario: "Local published draft",
post: createPost(.publish),
title: FailureTitles.postWillBePublished,
actionTitle: FailureActionTitles.cancel
),
Expectation(
scenario: "Published post with confirmed local changes",
post: createPost(.publish, hasRemote: true),
title: FailureTitles.changesWillBeUploaded,
actionTitle: FailureActionTitles.cancel
),
Expectation(
scenario: "Currently unsupported: Locally scheduled post",
post: createPost(.scheduled),
title: FailureTitles.postFailedToUpload,
actionTitle: FailureActionTitles.retry
),
Expectation(
scenario: "Currently unsupported: Scheduled post with confirmed local changes",
post: createPost(.scheduled, hasRemote: true),
title: FailureTitles.postFailedToUpload,
actionTitle: FailureActionTitles.retry
),
]

// Then
expect(notice.title).to(equal(PostNoticeViewModel.FailureTitles.postWillBePublished))
expect(notice.message).to(equal(post.postTitle))
expect(notice.actionTitle).to(equal(PostNoticeViewModel.FailureActionTitles.cancel))
expectations.forEach { expectation in
// Act
let notice = PostNoticeViewModel(post: expectation.post).notice

// Assert
expect({
guard notice.title == expectation.title else {
return .failed(reason: "Scenario “\(expectation.scenario)” failed. Expected notice.title to equal ”\(expectation.title)”. Actual is ”\(notice.title)”.")
}
guard notice.actionTitle == expectation.actionTitle else {
return .failed(reason: "Scenario ”\(expectation.scenario)” failed. Expected notice.actionTitle to equal ”\(expectation.actionTitle)”. Actual is ”\(String(describing: notice.actionTitle))”.")
}
guard notice.message == expectation.post.postTitle else {
return .failed(reason: "Scenario ”\(expectation.scenario)” failed. Expected notice.message to equal ”\(String(describing: expectation.post.postTitle))”. Actual is ”\(String(describing: notice.message))”.")
}

return .succeeded
}).to(succeed())
}
}

func testFailedPublishedPostsCancelButtonWillCancelAutoUpload() {
Expand All @@ -60,6 +114,21 @@ class PostNoticeViewModelTests: XCTestCase {
expect(postCoordinator.cancelAutoUploadOfInvocations).to(equal(1))
}

private func createPost(_ status: BasePost.Status, hasRemote: Bool = false) -> Post {
var builder = PostBuilder(context)
.with(title: UUID().uuidString)
.with(status: status)
.with(remoteStatus: .failed)

if hasRemote {
builder = builder.withRemote()
}

builder = builder.confirmedAutoUpload()

return builder.build()
}

private final class MockPostCoordinator: PostCoordinator {
private(set) var cancelAutoUploadOfInvocations: Int = 0

Expand Down
Loading