- Add Block Adult Content toggle to all three rule editors, persisted as BlockingRule.blockAdultContent (inline default for clean migration of existing stores) and surfaced in the detail sheet as an 'Adult websites: Blocked/Allowed' row - Engage Screen Time's adult-website filter (webContent.blockedByFilter = .auto()) alongside the rule's shield and clear it when the shield clears - Replace the New Rule sheet's view-swap with a NavigationStack push (navigationDestination(item:)); the editor uses native chrome there (system back button, inline title, toolbar rename), enabling the push animation and edge-swipe back - Tests: 93 passing (+2 unit: draft round-trip and enforcer forwarding; +3 UI: toggle-to-detail flow, default-allowed row, swipe-back as a behavioral proof of native navigation) - Spec updated accordingly (editor sections, behavior, data model, navigation note)
152 lines
4.8 KiB
Swift
152 lines
4.8 KiB
Swift
//
|
|
// RuleModelTests.swift
|
|
// SeveredTests
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
import Testing
|
|
|
|
@testable import Severed
|
|
|
|
@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 container = try ModelContainer(
|
|
for: BlockingRule.self,
|
|
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
|
)
|
|
let context = container.mainContext
|
|
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() {
|
|
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.selectionCount = 3
|
|
|
|
let rule = draft.makeRule()
|
|
#expect(rule.blockAdultContent)
|
|
#expect(RuleDraft(rule: rule) == draft)
|
|
}
|
|
|
|
@Test("Applying a draft updates an existing rule")
|
|
func applyToExisting() {
|
|
let rule = BlockingRule(name: "Old Name")
|
|
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("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"])
|
|
}
|
|
}
|