Files
Termind/FrameworkTests/AppReviewControllerTests.swift
T

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)
}
}
}