- AppList @Model with launch migration from legacy inline selections (rules with identical selections share one list; idempotent) - rules point at one app list; Block/Allow Only belongs to the rule and is offered only in the Schedule editor (limit kinds sanitize to Block) - app-list picker sheet (select / create / edit / delete with in-use protection) replaces the per-rule selection sheet - Hard Mode locks app-list editing while any hard rule is blocking - SwiftData stability: relationships are only wired between managed models, and unit tests share one in-memory container (fresh context + data wipe per test) — per-test container creation trapped intermittently inside SwiftData
168 lines
5.4 KiB
Swift
168 lines
5.4 KiB
Swift
//
|
|
// RuleModelTests.swift
|
|
// OpenAppLockTests
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
import Testing
|
|
|
|
@testable import OpenAppLock
|
|
|
|
@MainActor
|
|
@Suite("BlockingRule model & persistence")
|
|
struct RuleModelTests {
|
|
@Test("Defaults match the reference app's new-rule defaults")
|
|
func defaults() {
|
|
let rule = BlockingRule(name: "In the Zone")
|
|
#expect(rule.kind == .schedule)
|
|
#expect(rule.isEnabled)
|
|
#expect(!rule.hardMode)
|
|
#expect(!rule.blockAdultContent)
|
|
#expect(rule.selectionMode == .block)
|
|
#expect(rule.days == Weekday.weekdays)
|
|
#expect(rule.startMinutes == 9 * 60)
|
|
#expect(rule.endMinutes == 17 * 60)
|
|
#expect(rule.pausedUntil == nil)
|
|
}
|
|
|
|
@Test("Days survive the raw-storage round trip")
|
|
func daysRoundTrip() {
|
|
let rule = BlockingRule(name: "Test")
|
|
rule.days = [.sunday, .wednesday, .saturday]
|
|
#expect(rule.days == [.sunday, .wednesday, .saturday])
|
|
#expect(rule.dayNumbers == [1, 4, 7])
|
|
}
|
|
|
|
@Test("Kind and selection mode survive raw storage, with safe fallbacks")
|
|
func enumRoundTrip() {
|
|
let rule = BlockingRule(name: "Test", kind: .openLimit, selectionMode: .allowOnly)
|
|
#expect(rule.kind == .openLimit)
|
|
#expect(rule.selectionMode == .allowOnly)
|
|
rule.kindRaw = "garbage"
|
|
rule.selectionModeRaw = "garbage"
|
|
#expect(rule.kind == .schedule)
|
|
#expect(rule.selectionMode == .block)
|
|
}
|
|
|
|
@Test("Rules persist and fetch through SwiftData")
|
|
func persistence() throws {
|
|
let context = try makeInMemoryContext()
|
|
let rule = BlockingRule(
|
|
name: "Deep Sleep", hardMode: true,
|
|
days: Weekday.everyDay, startMinutes: 22 * 60, endMinutes: 6 * 60
|
|
)
|
|
context.insert(rule)
|
|
try context.save()
|
|
|
|
let fetched = try context.fetch(FetchDescriptor<BlockingRule>())
|
|
#expect(fetched.count == 1)
|
|
let saved = try #require(fetched.first)
|
|
#expect(saved.name == "Deep Sleep")
|
|
#expect(saved.hardMode)
|
|
#expect(saved.days == Weekday.everyDay)
|
|
#expect(saved.schedule.crossesMidnight)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
@Suite("RuleDraft")
|
|
struct RuleDraftTests {
|
|
@Test("New drafts use the kind's defaults")
|
|
func newDraftDefaults() {
|
|
let draft = RuleDraft(kind: .timeLimit)
|
|
#expect(draft.name == "Time Keeper")
|
|
#expect(draft.kind == .timeLimit)
|
|
#expect(draft.dailyLimitMinutes == 45)
|
|
#expect(draft.days == Weekday.weekdays)
|
|
#expect(!draft.hardMode)
|
|
}
|
|
|
|
@Test("Draft → rule → draft round-trips every field")
|
|
func roundTrip() throws {
|
|
let context = try makeInMemoryContext()
|
|
let list = AppList(name: "Distractions", selectionCount: 3)
|
|
context.insert(list)
|
|
|
|
var draft = RuleDraft(kind: .schedule)
|
|
draft.name = "Locked In"
|
|
draft.days = Weekday.everyDay
|
|
draft.startMinutes = 22 * 60
|
|
draft.endMinutes = 6 * 60
|
|
draft.hardMode = true
|
|
draft.blockAdultContent = true
|
|
draft.selectionMode = .allowOnly
|
|
draft.appList = list
|
|
|
|
let rule = draft.insertRule(into: context)
|
|
#expect(rule.blockAdultContent)
|
|
#expect(RuleDraft(rule: rule) == draft)
|
|
}
|
|
|
|
@Test("Applying a draft updates an existing rule")
|
|
func applyToExisting() throws {
|
|
let context = try makeInMemoryContext()
|
|
let rule = BlockingRule(name: "Old Name")
|
|
context.insert(rule)
|
|
var draft = RuleDraft(rule: rule)
|
|
draft.name = "New Name"
|
|
draft.hardMode = true
|
|
draft.apply(to: rule)
|
|
#expect(rule.name == "New Name")
|
|
#expect(rule.hardMode)
|
|
}
|
|
|
|
@Test("Sanitizing trims whitespace and falls back to the kind default")
|
|
func sanitizedName() {
|
|
var draft = RuleDraft(kind: .schedule)
|
|
draft.name = " Deep Work "
|
|
#expect(draft.sanitized().name == "Deep Work")
|
|
|
|
draft.name = " "
|
|
#expect(draft.sanitized().name == "In the Zone")
|
|
|
|
var limitDraft = RuleDraft(kind: .timeLimit)
|
|
limitDraft.name = ""
|
|
#expect(limitDraft.sanitized().name == "Time Keeper")
|
|
}
|
|
|
|
@Test("Preset drafts copy the preset's schedule")
|
|
func presetDraft() throws {
|
|
let preset = try #require(
|
|
RulePresetSection.all
|
|
.flatMap(\.presets)
|
|
.first { $0.id == "deep-sleep" }
|
|
)
|
|
let draft = RuleDraft(preset: preset)
|
|
#expect(draft.name == "Deep Sleep")
|
|
#expect(draft.startMinutes == 22 * 60)
|
|
#expect(draft.endMinutes == 6 * 60)
|
|
#expect(draft.kind == .schedule)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
@Suite("Weekday summaries")
|
|
struct WeekdayTests {
|
|
@Test("Named sets")
|
|
func namedSets() {
|
|
#expect(Weekday.weekdays.summary == "Weekdays")
|
|
#expect(Weekday.weekends.summary == "Weekends")
|
|
#expect(Weekday.everyDay.summary == "Every day")
|
|
#expect(Set<Weekday>().summary == "Never")
|
|
}
|
|
|
|
@Test("Custom sets list days in display order")
|
|
func customSets() {
|
|
let days: Set<Weekday> = [.friday, .monday, .wednesday]
|
|
#expect(days.summary == "Mon, Wed, Fri")
|
|
}
|
|
|
|
@Test("Picker display order starts on Sunday")
|
|
func displayOrder() {
|
|
#expect(Weekday.displayOrder.first == .sunday)
|
|
#expect(Weekday.displayOrder.count == 7)
|
|
#expect(Weekday.displayOrder.map(\.shortLabel) == ["S", "M", "T", "W", "T", "F", "S"])
|
|
}
|
|
}
|