Files
OpenAppLock/SeveredTests/RuleStatusTests.swift
Brendan Chen e6c87baeba feat: clone Opal's rules feature with hard block enforcement
Rebuild the app around recurring screen-time blocking rules modeled on
Opal's My Apps tab:

- Onboarding with FamilyControls Screen Time authorization
- Apps home with Blocked Apps tiles and live rule cards
- New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery
- Rule editors: time windows (incl. overnight), day picker, app selection
  via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit
- Hard Mode: active hard rules cannot be edited, disabled, deleted, or
  unblocked until their window ends; soft rules pause until next window
- Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer
- 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg
  harness for in-memory storage, mocked authorization, seeded scenarios
- docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording

Known gap: background window transitions and time/open-limit thresholds
need a DeviceActivityMonitor extension; shields currently sync while the
app is running.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-12 12:35:52 -04:00

113 lines
4.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// RuleStatusTests.swift
// SeveredTests
//
import Foundation
import Testing
@testable import Severed
@MainActor
@Suite("Rule status derivation and labels")
struct RuleStatusTests {
/// 09:0017:00 weekdays schedule rule, like the video's "Work Time".
func workTime(hardMode: Bool = false) -> BlockingRule {
BlockingRule(name: "Work Time", hardMode: hardMode)
}
@Test("Disabled rules report disabled regardless of the clock")
func disabled() {
let rule = workTime()
rule.isEnabled = false
#expect(rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc) == .disabled)
}
@Test("Enabled rule inside its window is active until the window ends")
func active() {
let status = workTime().status(at: date(2025, 1, 6, 10, 0), calendar: utc)
#expect(status == .active(until: date(2025, 1, 6, 17, 0)))
#expect(status.isActive)
}
@Test("Enabled rule outside its window is upcoming")
func upcoming() {
let status = workTime().status(at: date(2025, 1, 6, 18, 0), calendar: utc)
#expect(status == .upcoming(startsAt: date(2025, 1, 7, 9, 0)))
#expect(!status.isActive)
}
@Test("A rule with no days is dormant")
func dormant() {
let rule = workTime()
rule.days = []
#expect(rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc) == .dormant)
}
@Test("A paused rule reports paused until the window ends")
func paused() {
let rule = workTime()
rule.pausedUntil = date(2025, 1, 6, 17, 0)
let status = rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc)
#expect(status == .paused(until: date(2025, 1, 6, 17, 0)))
#expect(!status.isActive)
}
@Test("An expired pause no longer affects status")
func expiredPause() {
let rule = workTime()
rule.pausedUntil = date(2025, 1, 6, 9, 30)
let status = rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc)
#expect(status == .active(until: date(2025, 1, 6, 17, 0)))
}
@Test("Time-limit rules are never schedule-active")
func timeLimitNeverActive() {
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit)
let status = rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc)
#expect(!status.isActive)
if case .upcoming = status {} else {
Issue.record("Expected upcoming, got \(status)")
}
}
@Test("Active label matches the reference style: hours round up")
func activeLabel() {
// 11:28 17:00 is 5h32m; the reference app shows "6h left".
let status = workTime().status(at: date(2025, 1, 6, 11, 28), calendar: utc)
#expect(status.label(relativeTo: date(2025, 1, 6, 11, 28)) == "6h left")
}
@Test("Upcoming label matches the reference style")
func upcomingLabel() {
// Friday 11:28 Saturday 09:00 is 21h32m; the reference shows "Starts in 22h".
let weekend = BlockingRule(name: "Weekend Zen", days: Weekday.weekends)
let friday = date(2025, 1, 10, 11, 28)
#expect(weekend.status(at: friday, calendar: utc).label(relativeTo: friday) == "Starts in 22h")
}
@Test("Countdown formatting tiers", arguments: [
(30, "30m"), (59, "59m"), (60, "1h"), (90, "2h"),
(6 * 60, "6h"), (47 * 60, "47h"), (49 * 60, "2d"), (75 * 60, "3d"),
])
func countdownTiers(minutes: Int, expected: String) {
let now = date(2025, 1, 6, 0, 0)
let target = utc.date(byAdding: .minute, value: minutes, to: now)!
#expect(RuleStatus.countdown(from: now, to: target) == expected)
}
@Test("Countdown never reads below one minute")
func countdownFloor() {
let now = date(2025, 1, 6, 0, 0)
#expect(RuleStatus.countdown(from: now, to: now.addingTimeInterval(5)) == "1m")
}
@Test("Static labels")
func staticLabels() {
let now = date(2025, 1, 6, 0, 0)
#expect(RuleStatus.disabled.label(relativeTo: now) == "Disabled")
#expect(RuleStatus.dormant.label(relativeTo: now) == "No days selected")
#expect(RuleStatus.paused(until: now).label(relativeTo: now) == "Paused")
}
}