177 lines
7.5 KiB
Swift
177 lines
7.5 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import TermindApplication
|
|
|
|
@Suite @MainActor
|
|
struct AppReviewControllerTests {
|
|
@Test
|
|
func startsAtFirstLaunchAndRequiresSevenElapsedDaysAndAHost() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
#expect(h.defaults.object(forKey: SettingsKey.reviewFirstUse) as? Date == h.date)
|
|
h.date.addTimeInterval(7 * 86_400 - 1)
|
|
#expect(!controller.isEligible(hasHosts: true))
|
|
h.date.addTimeInterval(1)
|
|
#expect(controller.isEligible(hasHosts: true))
|
|
#expect(!controller.isEligible(hasHosts: false))
|
|
}
|
|
|
|
@Test
|
|
func reopeningPreservesFirstUseAndIgnoresLegacyAttemptBudgets() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
_ = h.makeController()
|
|
let firstUse = h.date
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
h.defaults.set(h.date, forKey: "app.review.lastAttempt")
|
|
h.defaults.set(["1.0"], forKey: "app.review.requestedVersions")
|
|
h.defaults.set(["one-day"], forKey: "app.review.activeDays")
|
|
let controller = h.makeController()
|
|
#expect(controller.isEligible(hasHosts: true))
|
|
#expect(h.defaults.object(forKey: SettingsKey.reviewFirstUse) as? Date == firstUse)
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func bothResponsesSurviveRelaunchAndDoNotClaimACompletedRating(opened: Bool) throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
if opened { controller.openReview { _, completion in completion(true) } } else { controller.decline() }
|
|
h.date.addTimeInterval(366 * 86_400)
|
|
let restored = h.makeController()
|
|
#expect(restored.response == (opened ? .openedReview : .declined))
|
|
#expect(!restored.isEligible(hasHosts: true))
|
|
}
|
|
|
|
@Test
|
|
func aRejectedURLOpenDoesNotConsumeTheInvitation() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
controller.openReview { _, completion in completion(false) }
|
|
#expect(controller.response == nil)
|
|
#expect(controller.isEligible(hasHosts: true))
|
|
#expect(h.defaults.object(forKey: SettingsKey.reviewInvitationResponse) == nil)
|
|
}
|
|
|
|
@Test
|
|
func timeAndNewHostsDoNotInsertAnInvitationUntilTheNextVisit() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
var visit = AppReviewInvitationVisit()
|
|
visit.enter(controller: controller, hasHosts: true)
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
visit.enter(controller: controller, hasHosts: false)
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
visit.enter(controller: controller, hasHosts: true)
|
|
#expect(visit.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func allWindowsHideImmediatelyAfterEitherResponse(opened: Bool) throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
var first = AppReviewInvitationVisit()
|
|
var second = AppReviewInvitationVisit()
|
|
first.enter(controller: controller, hasHosts: true)
|
|
second.enter(controller: controller, hasHosts: true)
|
|
#expect(first.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
#expect(second.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
if opened { controller.openReview { _, completion in completion(true) } } else { controller.decline() }
|
|
#expect(!first.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
#expect(!second.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
}
|
|
|
|
@Test
|
|
func filteringAndDeletingTheLastHostHideWithoutConsumingTheInvitation() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
var visit = AppReviewInvitationVisit()
|
|
visit.enter(controller: controller, hasHosts: true)
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: true, isFiltering: true))
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: false, isFiltering: false))
|
|
#expect(visit.isVisible(controller: controller, hasHosts: true, isFiltering: false))
|
|
#expect(controller.response == nil)
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func developerSwitchBypassesGatesAndNeverChangesTheRealResponse(opened: Bool) throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
controller.decline()
|
|
var visit = AppReviewInvitationVisit()
|
|
visit.enter(controller: controller, hasHosts: false)
|
|
controller.setPreviewEnabled(true)
|
|
#expect(visit.isVisible(controller: controller, hasHosts: false, isFiltering: false))
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: false, isFiltering: true))
|
|
if opened {
|
|
controller.openReview { _, _ in Issue.record("Developer preview must not open external URLs.") }
|
|
} else {
|
|
controller.decline()
|
|
}
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: false, isFiltering: false))
|
|
#expect(controller.response == .declined)
|
|
#expect(h.makeController().response == .declined)
|
|
#expect(!h.makeController().isPreviewEnabled)
|
|
controller.setPreviewEnabled(false)
|
|
controller.setPreviewEnabled(true)
|
|
#expect(visit.isVisible(controller: controller, hasHosts: false, isFiltering: false))
|
|
controller.setPreviewEnabled(false)
|
|
#expect(!visit.isVisible(controller: controller, hasHosts: false, isFiltering: false))
|
|
}
|
|
|
|
@Test
|
|
func disabledControllersLeaveDefaultsUntouched() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = AppReviewController(defaults: h.defaults, now: { h.date })
|
|
h.date.addTimeInterval(8 * 86_400)
|
|
controller.decline()
|
|
controller.openReview { _, completion in completion(true) }
|
|
#expect(!controller.isEligible(hasHosts: true))
|
|
#expect(h.defaults.object(forKey: SettingsKey.reviewFirstUse) == nil)
|
|
#expect(h.defaults.object(forKey: SettingsKey.reviewInvitationResponse) == nil)
|
|
}
|
|
|
|
@Test
|
|
func clockRollbackDoesNotMakeANewInstallationEligible() throws {
|
|
let h = try Harness()
|
|
defer { h.clear() }
|
|
let controller = h.makeController()
|
|
h.date.addTimeInterval(-86_400)
|
|
#expect(!controller.isEligible(hasHosts: true))
|
|
}
|
|
|
|
private final class Harness {
|
|
let suite = "AppReviewControllerTests.\(UUID())"
|
|
let defaults: UserDefaults
|
|
var date = Date(timeIntervalSince1970: 1_735_689_600)
|
|
|
|
init() throws {
|
|
defaults = try #require(UserDefaults(suiteName: suite))
|
|
}
|
|
|
|
func makeController() -> AppReviewController {
|
|
AppReviewController(defaults: defaults, isEnabled: true, now: { [unowned self] in self.date })
|
|
}
|
|
|
|
func clear() {
|
|
defaults.removePersistentDomain(forName: suite)
|
|
}
|
|
}
|
|
}
|