Replace the custom themed presentation with the bare native iOS design language across a three-tab TabView (Home / Rules / Settings): - MainTabView hosts the enforcement lifecycle (refresh loop, rule-change and scene-active reconcile) so it runs regardless of the selected tab - HomeView: "Currently Blocking" + "Usage" sections (replaces AppsHomeView) - RulesListView: rules grouped into Schedule / Time Limit / Open Limit - SettingsView: Uninstall Protection toggle + Manage App Lists - AppListLibraryView / ManageAppListsView: shared App List management - AppSettings: app-group-backed settings store Uninstall Protection: while enabled and any Hard Mode rule is actively blocking, deny device app-removal via a dedicated ManagedSettingsStore (RulePolicy.shouldDenyAppRemoval -> RuleEnforcer -> ShieldController). Slim AppListPickerSheet to the picker role; drop AppsHomeView and the custom Theme/HoldToCommitButton/RuleCardView chrome. Update spec section 6 and the unit/UI test suites accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.3 KiB
Swift
139 lines
5.3 KiB
Swift
//
|
|
// RulePolicyTests.swift
|
|
// OpenAppLockTests
|
|
//
|
|
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import OpenAppLock
|
|
|
|
@MainActor
|
|
@Suite("Hard Mode policy")
|
|
struct RulePolicyTests {
|
|
let mondayDuringWork = date(2025, 1, 6, 10, 0)
|
|
let mondayEvening = date(2025, 1, 6, 19, 0)
|
|
|
|
func rule(hardMode: Bool) -> BlockingRule {
|
|
BlockingRule(name: "Work Time", hardMode: hardMode)
|
|
}
|
|
|
|
@Test("An active Hard Mode rule is locked")
|
|
func hardLockedWhileActive() {
|
|
let rule = rule(hardMode: true)
|
|
#expect(RulePolicy.isHardLocked(rule, at: mondayDuringWork, calendar: utc))
|
|
#expect(!RulePolicy.canEdit(rule, at: mondayDuringWork, calendar: utc))
|
|
#expect(!RulePolicy.canDisable(rule, at: mondayDuringWork, calendar: utc))
|
|
#expect(!RulePolicy.canDelete(rule, at: mondayDuringWork, calendar: utc))
|
|
#expect(!RulePolicy.canUnblock(rule, at: mondayDuringWork, calendar: utc))
|
|
#expect(!RulePolicy.canTurnOffHardMode(rule, at: mondayDuringWork, calendar: utc))
|
|
}
|
|
|
|
@Test("A Hard Mode rule unlocks once its window ends")
|
|
func unlockedOutsideWindow() {
|
|
let rule = rule(hardMode: true)
|
|
#expect(!RulePolicy.isHardLocked(rule, at: mondayEvening, calendar: utc))
|
|
#expect(RulePolicy.canEdit(rule, at: mondayEvening, calendar: utc))
|
|
#expect(RulePolicy.canDisable(rule, at: mondayEvening, calendar: utc))
|
|
#expect(RulePolicy.canDelete(rule, at: mondayEvening, calendar: utc))
|
|
#expect(RulePolicy.canTurnOffHardMode(rule, at: mondayEvening, calendar: utc))
|
|
}
|
|
|
|
@Test("A disabled Hard Mode rule is not locked")
|
|
func disabledRuleNotLocked() {
|
|
let rule = rule(hardMode: true)
|
|
rule.isEnabled = false
|
|
#expect(!RulePolicy.isHardLocked(rule, at: mondayDuringWork, calendar: utc))
|
|
#expect(RulePolicy.canEdit(rule, at: mondayDuringWork, calendar: utc))
|
|
}
|
|
|
|
@Test("Active non-Hard-Mode rules may be unblocked")
|
|
func softRuleUnblockable() {
|
|
let rule = rule(hardMode: false)
|
|
#expect(RulePolicy.canUnblock(rule, at: mondayDuringWork, calendar: utc))
|
|
}
|
|
|
|
@Test("Unblocking pauses the rule until its window ends")
|
|
func unblockPausesUntilWindowEnd() {
|
|
let rule = rule(hardMode: false)
|
|
let didUnblock = RulePolicy.unblock(rule, at: mondayDuringWork, calendar: utc)
|
|
#expect(didUnblock)
|
|
#expect(rule.pausedUntil == date(2025, 1, 6, 17, 0))
|
|
#expect(rule.status(at: mondayDuringWork, calendar: utc)
|
|
== .paused(until: date(2025, 1, 6, 17, 0)))
|
|
}
|
|
|
|
@Test("Unblocking a Hard Mode rule is refused and changes nothing")
|
|
func hardModeUnblockRefused() {
|
|
let rule = rule(hardMode: true)
|
|
let didUnblock = RulePolicy.unblock(rule, at: mondayDuringWork, calendar: utc)
|
|
#expect(!didUnblock)
|
|
#expect(rule.pausedUntil == nil)
|
|
#expect(rule.status(at: mondayDuringWork, calendar: utc).isActive)
|
|
}
|
|
|
|
@Test("Unblocking an inactive rule is refused")
|
|
func inactiveUnblockRefused() {
|
|
let rule = rule(hardMode: false)
|
|
#expect(!RulePolicy.unblock(rule, at: mondayEvening, calendar: utc))
|
|
#expect(rule.pausedUntil == nil)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
@Suite("Uninstall protection policy")
|
|
struct UninstallProtectionPolicyTests {
|
|
let mondayDuringWork = date(2025, 1, 6, 10, 0)
|
|
let mondayEvening = date(2025, 1, 6, 19, 0)
|
|
|
|
func scheduleRule(hardMode: Bool) -> BlockingRule {
|
|
BlockingRule(name: "Work Time", hardMode: hardMode)
|
|
}
|
|
|
|
func hardLimitRule() -> BlockingRule {
|
|
BlockingRule(
|
|
name: "Time Keeper",
|
|
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 45)),
|
|
hardMode: true,
|
|
days: Weekday.everyDay)
|
|
}
|
|
|
|
@Test("App removal is only denied with the setting on AND a hard rule active")
|
|
func deniedOnlyWhenEnabledAndHardLocked() {
|
|
let hard = scheduleRule(hardMode: true)
|
|
// Setting off: never deny, even with an active hard rule.
|
|
#expect(
|
|
!RulePolicy.shouldDenyAppRemoval(
|
|
rules: [hard], enabled: false, at: mondayDuringWork, calendar: utc))
|
|
// Setting on + active hard rule: deny.
|
|
#expect(
|
|
RulePolicy.shouldDenyAppRemoval(
|
|
rules: [hard], enabled: true, at: mondayDuringWork, calendar: utc))
|
|
// Setting on but the hard rule is outside its window: do not deny.
|
|
#expect(
|
|
!RulePolicy.shouldDenyAppRemoval(
|
|
rules: [hard], enabled: true, at: mondayEvening, calendar: utc))
|
|
}
|
|
|
|
@Test("A soft rule never triggers app-removal denial")
|
|
func softRuleNeverDenies() {
|
|
let soft = scheduleRule(hardMode: false)
|
|
#expect(
|
|
!RulePolicy.shouldDenyAppRemoval(
|
|
rules: [soft], enabled: true, at: mondayDuringWork, calendar: utc))
|
|
}
|
|
|
|
@Test("A spent hard-mode limit rule triggers denial; unspent does not")
|
|
func spentHardLimitDenies() {
|
|
let rule = hardLimitRule()
|
|
#expect(
|
|
RulePolicy.shouldDenyAppRemoval(
|
|
rules: [rule], enabled: true, usageFor: { _ in RuleUsage(minutesUsed: 45) },
|
|
at: mondayDuringWork, calendar: utc))
|
|
#expect(
|
|
!RulePolicy.shouldDenyAppRemoval(
|
|
rules: [rule], enabled: true, usageFor: { _ in RuleUsage(minutesUsed: 10) },
|
|
at: mondayDuringWork, calendar: utc))
|
|
}
|
|
}
|