From 8aa111259cc49133cf7d04dd3657a09474ce2685 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Sun, 29 May 2022 21:46:40 +0700 Subject: [PATCH 1/4] Add intermediary class for switching schedulers (wip) --- .../ReminderScheduleCoordinator.swift | 97 +++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 6 ++ 2 files changed, 103 insertions(+) create mode 100644 WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift diff --git a/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift b/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift new file mode 100644 index 000000000000..96c41b895910 --- /dev/null +++ b/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift @@ -0,0 +1,97 @@ +import UserNotifications + +/// Abstraction layer between Blogging Reminders and Blogging Prompts. +/// +class ReminderScheduleCoordinator { + + // MARK: Dependencies + + private let notificationScheduler: NotificationScheduler + private let pushNotificationAuthorizer: PushNotificationAuthorizer + private let bloggingPromptsServiceFactory: BloggingPromptsServiceFactory + + private let bloggingRemindersScheduler: BloggingRemindersScheduler + private let promptRemindersScheduler: PromptRemindersScheduler + + // MARK: Public Methods + + init(notificationScheduler: NotificationScheduler = UNUserNotificationCenter.current(), + pushNotificationAuthorizer: PushNotificationAuthorizer = InteractiveNotificationsManager.shared, + bloggingPromptsServiceFactory: BloggingPromptsServiceFactory = .init()) throws { + + // initialize the dependencies + self.notificationScheduler = notificationScheduler + self.pushNotificationAuthorizer = pushNotificationAuthorizer + self.bloggingPromptsServiceFactory = bloggingPromptsServiceFactory + + // initialize the schedulers + self.bloggingRemindersScheduler = try .init(notificationCenter: notificationScheduler, + pushNotificationAuthorizer: pushNotificationAuthorizer) + self.promptRemindersScheduler = .init(bloggingPromptsServiceFactory: bloggingPromptsServiceFactory, + notificationScheduler: notificationScheduler, + pushAuthorizer: pushNotificationAuthorizer) + } + + + func schedule(for blog: Blog) -> BloggingRemindersScheduler.Schedule { + switch reminderType(for: blog) { + case .bloggingReminders: + return bloggingRemindersScheduler.schedule(for: blog) + + case .bloggingPrompts: + return .none + } + } + + func scheduledTime(for blog: Blog) -> Date { + switch reminderType(for: blog) { + case .bloggingReminders: + return bloggingRemindersScheduler.scheduledTime(for: blog) + + case .bloggingPrompts: + return Date() // TODO. + } + } + + func schedule(_ schedule: BloggingRemindersScheduler.Schedule, + for blog: Blog, + time: Date? = nil, + completion: @escaping (Result) -> ()) { + switch reminderType(for: blog) { + case .bloggingReminders: + bloggingRemindersScheduler.schedule(schedule, for: blog, time: time, completion: completion) + + case .bloggingPrompts: + promptRemindersScheduler.schedule(schedule, for: blog, time: time, completion: completion) + } + } + +} + +// MARK: - Private Helpers + +private extension ReminderScheduleCoordinator { + + enum ReminderType { + case bloggingReminders + case bloggingPrompts + } + + func promptReminderSettings(for blog: Blog) -> BloggingPromptSettings? { + guard Feature.enabled(.bloggingPrompts), + let service = bloggingPromptsServiceFactory.makeService(for: blog) else { + return nil + } + + return service.localSettings + } + + func reminderType(for blog: Blog) -> ReminderType { + if let settings = promptReminderSettings(for: blog), + settings.promptRemindersEnabled { + return .bloggingPrompts + } + + return .bloggingReminders + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3d18a5df1d9d..2ff644b7b3c1 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -4680,6 +4680,8 @@ FEDA1AD9269D475D0038EC98 /* ListTableViewCell+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDA1AD7269D475D0038EC98 /* ListTableViewCell+Comments.swift */; }; FEDDD46F26A03DE900F8942B /* ListTableViewCell+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */; }; FEDDD47026A03DE900F8942B /* ListTableViewCell+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */; }; + FEF4DC5528439357003806BE /* ReminderScheduleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */; }; + FEF4DC5628439357003806BE /* ReminderScheduleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */; }; FEFA263E26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFA263D26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift */; }; FEFA263F26C5AE9A009CCB7E /* ShareAppContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */; }; FEFA264026C5AE9E009CCB7E /* ShareAppTextActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */; }; @@ -7964,6 +7966,7 @@ FECA44312836647100D01F15 /* PromptRemindersSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptRemindersSchedulerTests.swift; sourceTree = ""; }; FEDA1AD7269D475D0038EC98 /* ListTableViewCell+Comments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTableViewCell+Comments.swift"; sourceTree = ""; }; FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTableViewCell+Notifications.swift"; sourceTree = ""; }; + FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinator.swift; sourceTree = ""; }; FEF93A01F06919BDB9486A8E /* Pods-WordPressHomeWidgetToday.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressHomeWidgetToday.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressHomeWidgetToday/Pods-WordPressHomeWidgetToday.release-alpha.xcconfig"; sourceTree = ""; }; FEFA263D26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppTextActivityItemSourceTests.swift; sourceTree = ""; }; FEFC0F872730510F001F7F1D /* WordPress 136.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 136.xcdatamodel"; sourceTree = ""; }; @@ -15481,6 +15484,7 @@ isa = PBXGroup; children = ( FECA442E28350B7800D01F15 /* PromptRemindersScheduler.swift */, + FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */, ); path = "Blogging Prompts"; sourceTree = ""; @@ -19336,6 +19340,7 @@ E66969E01B9E648100EC9C00 /* ReaderTopicToReaderDefaultTopic37to38.swift in Sources */, F1DB8D2B2288C24500906E2F /* UploadsManager.swift in Sources */, 9856A3E4261FD27A008D6354 /* UserProfileSectionHeader.swift in Sources */, + FEF4DC5528439357003806BE /* ReminderScheduleCoordinator.swift in Sources */, 82A062DE2017BCBA0084CE7C /* ActivityListSectionHeaderView.swift in Sources */, 73FEC871220B358500CEF791 /* WPAccount+RestApi.swift in Sources */, 93CDC72126CD342900C8A3A8 /* DestructiveAlertHelper.swift in Sources */, @@ -20674,6 +20679,7 @@ FABB225C2602FC2C00C8785C /* MenuHeaderViewController.m in Sources */, FABB225D2602FC2C00C8785C /* NotificationDetailsViewController.swift in Sources */, C3234F5527EBBACA004ADB29 /* SiteIntentVertical.swift in Sources */, + FEF4DC5628439357003806BE /* ReminderScheduleCoordinator.swift in Sources */, FABB225F2602FC2C00C8785C /* MediaURLExporter.swift in Sources */, FABB22602602FC2C00C8785C /* WordPress.xcdatamodeld in Sources */, 93CDC72226CD342900C8A3A8 /* DestructiveAlertHelper.swift in Sources */, From 63d55fb935fe52566fe0630411a973fa4d79eeda Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Mon, 30 May 2022 13:07:13 +0700 Subject: [PATCH 2/4] Finish implementation for scheduling methods, add some docs --- .../ReminderScheduleCoordinator.swift | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift b/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift index 96c41b895910..cf94c4922fb0 100644 --- a/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift +++ b/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift @@ -1,6 +1,10 @@ import UserNotifications -/// Abstraction layer between Blogging Reminders and Blogging Prompts. +/// Bridges the logic between Blogging Reminders and Blogging Prompts. +/// +/// Users can switch between receiving Blogging Reminders or Blogging Prompts based on the switch toggle in the reminder sheet. They are both delivered +/// through local notifications, but the mechanism between the two is differentiated due to technical limitations. Blogging Prompts requires the content +/// of each notification to be different, and this is not possible if we want to use a repeating `UNCalendarNotificationTrigger`. /// class ReminderScheduleCoordinator { @@ -33,39 +37,84 @@ class ReminderScheduleCoordinator { } + /// Returns the user's reminder schedule for the given `blog`, based on the current reminder type. + /// + /// - Parameter blog: The blog associated with the reminders. + /// - Returns: The user's preferred reminder schedule. func schedule(for blog: Blog) -> BloggingRemindersScheduler.Schedule { switch reminderType(for: blog) { case .bloggingReminders: return bloggingRemindersScheduler.schedule(for: blog) case .bloggingPrompts: - return .none + guard let settings = promptReminderSettings(for: blog), + let reminderDays = settings.reminderDays, + !reminderDays.getActiveWeekdays().isEmpty else { + return .none + } + + return .weekdays(reminderDays.getActiveWeekdays()) } } + /// Returns the user's preferred time for the given `blog`, based on the current reminder type. + /// + /// - Parameter blog: The blog associated with the reminders. + /// - Returns: The user's preferred time returned in `Date`. func scheduledTime(for blog: Blog) -> Date { switch reminderType(for: blog) { case .bloggingReminders: return bloggingRemindersScheduler.scheduledTime(for: blog) case .bloggingPrompts: - return Date() // TODO. + guard let settings = promptReminderSettings(for: blog), + let dateForTime = settings.reminderTimeDate() else { + return Constants.defaultTime + } + + return dateForTime } } + /// Schedules a reminder notification for the given `blog` based on the current reminder type. + /// + /// - Note: Calling this method will trigger the push notification authorization flow. + /// + /// - Parameters: + /// - schedule: The preferred notification schedule. + /// - blog: The blog that will upload the user's post. + /// - time: The user's preferred time to be notified. + /// - completion: Closure called after the process completes. func schedule(_ schedule: BloggingRemindersScheduler.Schedule, for blog: Blog, time: Date? = nil, completion: @escaping (Result) -> ()) { switch reminderType(for: blog) { case .bloggingReminders: - bloggingRemindersScheduler.schedule(schedule, for: blog, time: time, completion: completion) + bloggingRemindersScheduler.schedule(schedule, for: blog, time: time) { [weak self] result in + // always unschedule prompt reminders in case the user toggled the switch. + self?.promptRemindersScheduler.unschedule(for: blog) + completion(result) + } case .bloggingPrompts: - promptRemindersScheduler.schedule(schedule, for: blog, time: time, completion: completion) + promptRemindersScheduler.schedule(schedule, for: blog, time: time) { [weak self] result in + // always unschedule blogging reminders in case the user toggled the switch. + self?.bloggingRemindersScheduler.unschedule(for: blog) + completion(result) + } } } + /// Unschedules all future reminders from the given `blog`. + /// This applies to both Blogging Reminders and Blogging Prompts. + /// + /// - Parameter blog: The blog associated with the reminders. + func unschedule(for blog: Blog) { + bloggingRemindersScheduler.unschedule(for: blog) + promptRemindersScheduler.unschedule(for: blog) + } + } // MARK: - Private Helpers @@ -77,9 +126,18 @@ private extension ReminderScheduleCoordinator { case bloggingPrompts } + enum Constants { + static let defaultHour = 10 + static let defaultMinute = 0 + + static var defaultTime: Date { + let calendar = Calendar.current + return calendar.date(from: DateComponents(calendar: calendar, hour: defaultHour, minute: defaultMinute)) ?? Date() + } + } + func promptReminderSettings(for blog: Blog) -> BloggingPromptSettings? { - guard Feature.enabled(.bloggingPrompts), - let service = bloggingPromptsServiceFactory.makeService(for: blog) else { + guard let service = bloggingPromptsServiceFactory.makeService(for: blog) else { return nil } @@ -87,7 +145,8 @@ private extension ReminderScheduleCoordinator { } func reminderType(for blog: Blog) -> ReminderType { - if let settings = promptReminderSettings(for: blog), + if Feature.enabled(.bloggingPrompts), + let settings = promptReminderSettings(for: blog), settings.promptRemindersEnabled { return .bloggingPrompts } From 8c27b10e47926c217d7a5aea477256c1ccd5f6ba Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Mon, 30 May 2022 16:20:00 +0700 Subject: [PATCH 3/4] Add tests --- .../ReminderScheduleCoordinator.swift | 38 +-- WordPress/WordPress.xcodeproj/project.pbxproj | 14 +- .../ReminderScheduleCoordinatorTests.swift | 281 ++++++++++++++++++ 3 files changed, 314 insertions(+), 19 deletions(-) create mode 100644 WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift diff --git a/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift b/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift index cf94c4922fb0..4682d8dd4e02 100644 --- a/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift +++ b/WordPress/Classes/Utility/Blogging Prompts/ReminderScheduleCoordinator.swift @@ -10,32 +10,34 @@ class ReminderScheduleCoordinator { // MARK: Dependencies - private let notificationScheduler: NotificationScheduler - private let pushNotificationAuthorizer: PushNotificationAuthorizer - private let bloggingPromptsServiceFactory: BloggingPromptsServiceFactory - private let bloggingRemindersScheduler: BloggingRemindersScheduler private let promptRemindersScheduler: PromptRemindersScheduler + private let bloggingPromptsServiceFactory: BloggingPromptsServiceFactory // MARK: Public Methods - init(notificationScheduler: NotificationScheduler = UNUserNotificationCenter.current(), - pushNotificationAuthorizer: PushNotificationAuthorizer = InteractiveNotificationsManager.shared, - bloggingPromptsServiceFactory: BloggingPromptsServiceFactory = .init()) throws { - - // initialize the dependencies - self.notificationScheduler = notificationScheduler - self.pushNotificationAuthorizer = pushNotificationAuthorizer + init(bloggingRemindersScheduler: BloggingRemindersScheduler, + promptRemindersScheduler: PromptRemindersScheduler, + bloggingPromptsServiceFactory: BloggingPromptsServiceFactory = .init()) { + self.bloggingRemindersScheduler = bloggingRemindersScheduler + self.promptRemindersScheduler = promptRemindersScheduler self.bloggingPromptsServiceFactory = bloggingPromptsServiceFactory - - // initialize the schedulers - self.bloggingRemindersScheduler = try .init(notificationCenter: notificationScheduler, - pushNotificationAuthorizer: pushNotificationAuthorizer) - self.promptRemindersScheduler = .init(bloggingPromptsServiceFactory: bloggingPromptsServiceFactory, - notificationScheduler: notificationScheduler, - pushAuthorizer: pushNotificationAuthorizer) } + convenience init(notificationScheduler: NotificationScheduler = UNUserNotificationCenter.current(), + pushNotificationAuthorizer: PushNotificationAuthorizer = InteractiveNotificationsManager.shared, + bloggingPromptsServiceFactory: BloggingPromptsServiceFactory = .init()) throws { + + let bloggingRemindersScheduler = try BloggingRemindersScheduler(notificationCenter: notificationScheduler, + pushNotificationAuthorizer: pushNotificationAuthorizer) + let promptRemindersScheduler = PromptRemindersScheduler(bloggingPromptsServiceFactory: bloggingPromptsServiceFactory, + notificationScheduler: notificationScheduler, + pushAuthorizer: pushNotificationAuthorizer) + + self.init(bloggingRemindersScheduler: bloggingRemindersScheduler, + promptRemindersScheduler: promptRemindersScheduler, + bloggingPromptsServiceFactory: bloggingPromptsServiceFactory) + } /// Returns the user's reminder schedule for the given `blog`, based on the current reminder type. /// diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 2ff644b7b3c1..d709919744f1 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -4626,6 +4626,7 @@ FE25C235271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */; }; FE25C236271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */; }; FE2E3729281C839C00A1E82A /* BloggingPromptsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */; }; + FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */; }; FE32EFFF275914390040BE67 /* MenuSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */; }; FE32F000275914390040BE67 /* MenuSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */; }; FE32F002275F602E0040BE67 /* CommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32F001275F602E0040BE67 /* CommentContentRenderer.swift */; }; @@ -7934,6 +7935,7 @@ FE23EB4826E7C91F005A1698 /* richCommentStyle.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = richCommentStyle.css; path = Resources/HTML/richCommentStyle.css; sourceTree = ""; }; FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCommentsNotificationSheetViewController.swift; sourceTree = ""; }; FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsServiceTests.swift; sourceTree = ""; }; + FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinatorTests.swift; sourceTree = ""; }; FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetViewController.swift; sourceTree = ""; }; FE32F001275F602E0040BE67 /* CommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentContentRenderer.swift; sourceTree = ""; }; FE32F005275F62620040BE67 /* WebCommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebCommentContentRenderer.swift; sourceTree = ""; }; @@ -11473,6 +11475,7 @@ children = ( DC06DFF727BD52A100969974 /* BackgroundTasks */, F15D1FB8265C40EA00854EE5 /* Blogging Reminders */, + FE32E7EF284496F500744D80 /* Blogging Prompts */, 174C116D2624601000346EC6 /* Deep Linking */, F93735F422D53C1800A3C312 /* Logging */, 93B853211B44165B0064FE72 /* Analytics */, @@ -14902,7 +14905,6 @@ children = ( F15D1FB9265C41A900854EE5 /* BloggingRemindersStoreTests.swift */, F151EC822665271200AEA89E /* BloggingRemindersSchedulerTests.swift */, - FECA44312836647100D01F15 /* PromptRemindersSchedulerTests.swift */, ); name = "Blogging Reminders"; sourceTree = ""; @@ -15434,6 +15436,15 @@ path = Prompts; sourceTree = ""; }; + FE32E7EF284496F500744D80 /* Blogging Prompts */ = { + isa = PBXGroup; + children = ( + FECA44312836647100D01F15 /* PromptRemindersSchedulerTests.swift */, + FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */, + ); + name = "Blogging Prompts"; + sourceTree = ""; + }; FE32F004275F60360040BE67 /* ContentRenderer */ = { isa = PBXGroup; children = ( @@ -20076,6 +20087,7 @@ E1C545801C6C79BB001CEB0E /* MediaSettingsTests.swift in Sources */, C3439B5F27FE3A3C0058DA55 /* SiteCreationWizardLauncherTests.swift in Sources */, 7E987F5A2108122A00CAFB88 /* NotificationUtility.swift in Sources */, + FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */, 4688E6CC26AB571D00A5D894 /* RequestAuthenticatorTests.swift in Sources */, 7E442FC720F677CB00DEACA5 /* ActivityLogRangesTest.swift in Sources */, 938466B92683CA0E00A538DC /* ReferrerDetailsViewModelTests.swift in Sources */, diff --git a/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift b/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift new file mode 100644 index 000000000000..354c1ed8a090 --- /dev/null +++ b/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift @@ -0,0 +1,281 @@ +import XCTest +import OHHTTPStubs + +@testable import WordPress + +class ReminderScheduleCoordinatorTests: CoreDataTestCase { + + private let timeout = TimeInterval(1) + private var flagOverrideStore: FeatureFlagOverrideStore! + private var accountService: AccountService! + private var bloggingPromptsServiceFactory: BloggingPromptsServiceFactory! + private var blog: Blog! + private var mockBloggingScheduler: MockBloggingRemindersScheduler! + private var mockPromptScheduler: MockPromptRemindersScheduler! + private var coordinator: ReminderScheduleCoordinator! + + override func setUp() { + flagOverrideStore = FeatureFlagOverrideStore() + blog = makeBlog() + accountService = makeAccountService() + bloggingPromptsServiceFactory = BloggingPromptsServiceFactory(contextManager: contextManager) + mockBloggingScheduler = try! MockBloggingRemindersScheduler() + mockBloggingScheduler.behavior = behaviorForBloggingScheduler() + mockPromptScheduler = MockPromptRemindersScheduler() + mockPromptScheduler.behavior = behaviorForPromptScheduler() + coordinator = ReminderScheduleCoordinator(bloggingRemindersScheduler: mockBloggingScheduler, + promptRemindersScheduler: mockPromptScheduler, + bloggingPromptsServiceFactory: bloggingPromptsServiceFactory) + + super.setUp() + } + + override func tearDown() { + flagOverrideStore = nil + accountService = nil + bloggingPromptsServiceFactory = nil + blog = nil + mockBloggingScheduler = nil + mockPromptScheduler = nil + coordinator = nil + + super.tearDown() + } + + // MARK: - Tests + + // scheduled + + func test_scheduled_withPromptsDisabled_returnsScheduleForBloggingReminders() { + disableBloggingPrompts() + let expectedSchedule = behaviorForBloggingScheduler().scheduleToReturn + + let returnedSchedule = coordinator.schedule(for: blog) + + XCTAssertEqual(returnedSchedule, expectedSchedule) + } + + func test_scheduled_withPromptsEnabled_returnsScheduleForBloggingPrompts() { + let behavior = behaviorForPromptScheduler() + let expectedSchedule = behavior.scheduleToReturn + enableBloggingPrompts(with: behavior) + + let returnedSchedule = coordinator.schedule(for: blog) + + XCTAssertEqual(returnedSchedule, expectedSchedule) + } + + // scheduledTime + + func test_scheduledTime_withPromptsDisabled_returnsScheduledTimeForBloggingReminders() { + let (expectedHour, expectedMinute) = behaviorForBloggingScheduler().timeComponents + disableBloggingPrompts() + + let timeDate = coordinator.scheduledTime(for: blog) + let components = Calendar.current.dateComponents([.hour, .minute], from: timeDate) + + XCTAssertEqual(components.hour!, expectedHour) + XCTAssertEqual(components.minute!, expectedMinute) + } + + func test_scheduledTime_withPromptsEnabled_returnsScheduledTimeForBloggingPrompts() { + let behavior = behaviorForPromptScheduler() + let (expectedHour, expectedMinute) = behavior.timeComponents + enableBloggingPrompts(with: behavior) + + let timeDate = coordinator.scheduledTime(for: blog) + let components = Calendar.current.dateComponents([.hour, .minute], from: timeDate) + + XCTAssertEqual(components.hour!, expectedHour) + XCTAssertEqual(components.minute!, expectedMinute) + } + + // schedule + + func test_schedule_withPromptsDisabled_shouldScheduleBloggingReminders() { + let behavior = behaviorForBloggingScheduler() + disableBloggingPrompts() + + let expect = expectation(description: "Scheduling should succeed") + coordinator.schedule(behavior.scheduleToReturn, for: blog, time: behavior.scheduledTimeToReturn) { result in + guard case .success = result else { + XCTFail("Expected a success result") + expect.fulfill() + return + } + + // scheduling blogging reminders should automatically unschedule pending notifications from blogging prompts. + XCTAssertEqual(self.mockPromptScheduler.behavior.blogToUnschedule, self.blog) + + // ensure that `schedule` is called on the right scheduler. + XCTAssertTrue(self.mockBloggingScheduler.behavior.scheduleCalled) + + expect.fulfill() + } + wait(for: [expect], timeout: timeout) + } + + func test_schedule_withPromptsEnabled_shouldSchedulePromptReminders() { + let behavior = behaviorForPromptScheduler() + enableBloggingPrompts(with: behavior) + + let expect = expectation(description: "Scheduling should succeed") + coordinator.schedule(behavior.scheduleToReturn, for: blog, time: behavior.scheduledTimeToReturn) { result in + guard case .success = result else { + XCTFail("Expected a success result") + expect.fulfill() + return + } + + // scheduling blogging prompts should automatically unschedule pending notifications from blogging reminders. + XCTAssertEqual(self.mockBloggingScheduler.behavior.blogToUnschedule, self.blog) + + // ensure that `schedule` is called on the right scheduler. + XCTAssertTrue(self.mockPromptScheduler.behavior.scheduleCalled) + + expect.fulfill() + } + wait(for: [expect], timeout: timeout) + } + + // unschedule + + func test_unschedule_shouldUnscheduleRemindersFromBoth() { + coordinator.unschedule(for: blog) + + XCTAssertNotNil(mockPromptScheduler.behavior.blogToUnschedule) + XCTAssertNotNil(mockBloggingScheduler.behavior.blogToUnschedule) + XCTAssertEqual(mockPromptScheduler.behavior.blogToUnschedule, mockBloggingScheduler.behavior.blogToUnschedule) + } +} + +// MARK: Helpers + +private extension ReminderScheduleCoordinatorTests { + + func disableBloggingPrompts() { + try! flagOverrideStore.override(FeatureFlag.bloggingPrompts, withValue: false) + } + + func enableBloggingPrompts(with behavior: MockSchedulerBehavior) { + try! flagOverrideStore.override(FeatureFlag.bloggingPrompts, withValue: true) + let (hour, minute) = behavior.timeComponents + makePromptSettings(enabled: true, schedule: behavior.scheduleToReturn, hour: hour, minute: minute) + } + + @discardableResult + func makePromptSettings(enabled: Bool = true, schedule: BloggingRemindersScheduler.Schedule = .none, hour: Int, minute: Int) -> BloggingPromptSettings { + let settings = NSEntityDescription.insertNewObject(forEntityName: "BloggingPromptSettings", + into: mainContext) as! WordPress.BloggingPromptSettings + settings.promptRemindersEnabled = enabled + settings.siteID = blog.dotComID!.int32Value + + let reminderDays = NSEntityDescription.insertNewObject(forEntityName: "BloggingPromptSettingsReminderDays", + into: mainContext) as! WordPress.BloggingPromptSettingsReminderDays + if case .weekdays(let weekdays) = schedule { + reminderDays.sunday = weekdays.contains(.sunday) + reminderDays.monday = weekdays.contains(.monday) + reminderDays.tuesday = weekdays.contains(.tuesday) + reminderDays.wednesday = weekdays.contains(.wednesday) + reminderDays.thursday = weekdays.contains(.thursday) + reminderDays.friday = weekdays.contains(.friday) + reminderDays.saturday = weekdays.contains(.saturday) + } + settings.reminderDays = reminderDays + settings.reminderTime = String(format: "%02d.%02d", hour, minute) + + return settings + } + + func makeBlog() -> Blog { + return BlogBuilder(mainContext).isHostedAtWPcom().build() + } + + func makeAccountService() -> AccountService { + let service = AccountService(managedObjectContext: mainContext) + let account = service.createOrUpdateAccount(withUsername: "testuser", authToken: "authtoken") + account.defaultBlog = blog + account.primaryBlogID = blog.dotComID! + account.userID = NSNumber(value: 1) + service.setDefaultWordPressComAccount(account) + + return service + } + + func behaviorForBloggingScheduler() -> MockSchedulerBehavior { + var behavior = MockSchedulerBehavior() + behavior.scheduleToReturn = .weekdays([.monday, .tuesday, .friday]) + behavior.scheduledTimeToReturn = Calendar.current.date(from: DateComponents(hour: 15, minute: 30))! + return behavior + } + + func behaviorForPromptScheduler() -> MockSchedulerBehavior { + var behavior = MockSchedulerBehavior() + behavior.scheduleToReturn = .weekdays([.thursday, .wednesday, .saturday]) + behavior.scheduledTimeToReturn = Calendar.current.date(from: DateComponents(hour: 20, minute: 15))! + return behavior + } + + struct MockSchedulerBehavior { + // states + var scheduleToReturn: BloggingRemindersScheduler.Schedule = .none + var scheduledTimeToReturn: Date = Date() + + // schedule + var scheduleCalled = false + var scheduleReturnsSuccess = true + + // unschedule + var blogToUnschedule: Blog? = nil + + enum Errors: Error { + case intended + } + + var timeComponents: (Int, Int) { + let components = Calendar.current.dateComponents([.hour, .minute], from: scheduledTimeToReturn) + return (components.hour!, components.minute!) + } + } + + class MockPromptRemindersScheduler: PromptRemindersScheduler { + var behavior = MockSchedulerBehavior() + + override func schedule(_ schedule: BloggingRemindersScheduler.Schedule, + for blog: Blog, + time: Date? = nil, + completion: @escaping (Result) -> ()) { + behavior.scheduleCalled = true + completion(behavior.scheduleReturnsSuccess ? .success(()) : .failure(MockSchedulerBehavior.Errors.intended)) + } + + override func unschedule(for blog: Blog) { + behavior.blogToUnschedule = blog + } + } + + + class MockBloggingRemindersScheduler: BloggingRemindersScheduler { + var behavior = MockSchedulerBehavior() + + override func schedule(for blog: Blog) -> BloggingRemindersScheduler.Schedule { + return behavior.scheduleToReturn + } + + override func scheduledTime(for blog: Blog) -> Date { + return behavior.scheduledTimeToReturn + } + + override func schedule(_ schedule: BloggingRemindersScheduler.Schedule, + for blog: Blog, + time: Date? = nil, + completion: @escaping (Result) -> ()) { + behavior.scheduleCalled = true + completion(behavior.scheduleReturnsSuccess ? .success(()) : .failure(MockSchedulerBehavior.Errors.intended)) + } + + override func unschedule(for blog: Blog) { + behavior.blogToUnschedule = blog + } + } +} From bf4c0c82557de59dd16baa72910d356c2c048bf1 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Tue, 31 May 2022 00:07:01 +0700 Subject: [PATCH 4/4] Fix broken tests --- .../ReminderScheduleCoordinatorTests.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift b/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift index 354c1ed8a090..32bc6b51734a 100644 --- a/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift +++ b/WordPress/WordPressTest/ReminderScheduleCoordinatorTests.swift @@ -194,11 +194,19 @@ private extension ReminderScheduleCoordinatorTests { func makeAccountService() -> AccountService { let service = AccountService(managedObjectContext: mainContext) let account = service.createOrUpdateAccount(withUsername: "testuser", authToken: "authtoken") - account.defaultBlog = blog - account.primaryBlogID = blog.dotComID! account.userID = NSNumber(value: 1) service.setDefaultWordPressComAccount(account) + /// NOTE: When the whole suite is run, somehow the defaultWordPress account set here is wiped. + /// The account is created and stored successfully, but the UUID stored in the user defaults is somehow always nil. + /// + /// The strangest thing is, it works just fine when this suite is run exclusively; but only fails when the whole suite is run. + /// My suspicion is that some part may have wiped the user defaults, resetting the default account to nil. + /// + /// Until there's a better solution, setting the account UUID directly here seemed to fix the problem. + /// + UserDefaults.standard.set(account.uuid, forKey: "AccountDefaultDotcomUUID") + return service }