refactor: model rule options as a per-kind sum type
Replace the wide BlockingRule/RuleDraft "god struct" — where every option existed for every kind — with a RuleConfiguration sum type that carries only the options each kind actually has: .schedule(ScheduleConfig: window + selectionMode + blockAdultContent) .timeLimit(TimeLimitConfig: dailyLimitMinutes) .openLimit(OpenLimitConfig: maxOpens) Name, days, Hard Mode, app list, and pause stay common to all kinds. This makes illegal states unrepresentable: Block / Allow Only and Block Adult Content are now structurally Schedule-only. User-visible behavior change: the Time Limit and Open Limit editors no longer offer a Block Adult Content toggle, and their detail sheets drop the "Adult websites" row — those never made sense for a usage budget. Limit rules are always Block and never engage the web-content filter. BlockingRule keeps flat columns as raw persistence behind a computed `configuration` bridge (lowest SwiftData risk; the cross-process RuleSnapshot wire format and the Screen Time extensions are untouched). Logic, the editors, and the detail sheet all switch on the sum type. Spec (§1, §3.5/§3.6, §5.2) updated first; tests reworked to the new API with new structural-guarantee unit tests and a UI test asserting the Time Limit editor/detail omit adult content. Full suite green (180 tests).
This commit is contained in:
@@ -86,10 +86,21 @@ struct AppListModelTests {
|
||||
@MainActor
|
||||
@Suite("Legacy selection → AppList migration")
|
||||
struct AppListMigrationTests {
|
||||
/// Simulates a rule decoded from a pre-app-list store: a plain rule whose
|
||||
/// legacy inline-selection columns are populated.
|
||||
private func legacyRule(name: String, selectionData: Data, selectionCount: Int) -> BlockingRule {
|
||||
let rule = BlockingRule(name: name)
|
||||
rule.selectionData = selectionData
|
||||
rule.selectionCount = selectionCount
|
||||
return rule
|
||||
}
|
||||
|
||||
@Test("Rules with legacy inline selections get a list named after them")
|
||||
func createsListsFromLegacySelections() throws {
|
||||
let context = try makeInMemoryContext()
|
||||
let rule = BlockingRule(name: "Work Time", selectionData: Data([1]), selectionCount: 3)
|
||||
let rule = BlockingRule(name: "Work Time")
|
||||
rule.selectionData = Data([1])
|
||||
rule.selectionCount = 3
|
||||
context.insert(rule)
|
||||
try context.save()
|
||||
|
||||
@@ -108,9 +119,9 @@ struct AppListMigrationTests {
|
||||
@Test("Rules with identical selections share one list")
|
||||
func sharesListForIdenticalSelections() throws {
|
||||
let context = try makeInMemoryContext()
|
||||
let first = BlockingRule(name: "Work Time", selectionData: Data([7]), selectionCount: 2)
|
||||
let second = BlockingRule(name: "Sleep", selectionData: Data([7]), selectionCount: 2)
|
||||
let different = BlockingRule(name: "Gym", selectionData: Data([9]), selectionCount: 1)
|
||||
let first = legacyRule(name: "Work Time", selectionData: Data([7]), selectionCount: 2)
|
||||
let second = legacyRule(name: "Sleep", selectionData: Data([7]), selectionCount: 2)
|
||||
let different = legacyRule(name: "Gym", selectionData: Data([9]), selectionCount: 1)
|
||||
context.insert(first)
|
||||
context.insert(second)
|
||||
context.insert(different)
|
||||
@@ -127,7 +138,7 @@ struct AppListMigrationTests {
|
||||
@Test("Migration is idempotent and skips selection-less rules")
|
||||
func idempotentAndSkipsEmpty() throws {
|
||||
let context = try makeInMemoryContext()
|
||||
let legacy = BlockingRule(name: "Work Time", selectionData: Data([1]), selectionCount: 1)
|
||||
let legacy = legacyRule(name: "Work Time", selectionData: Data([1]), selectionCount: 1)
|
||||
let empty = BlockingRule(name: "No Apps")
|
||||
context.insert(legacy)
|
||||
context.insert(empty)
|
||||
@@ -163,22 +174,24 @@ struct AppListDraftTests {
|
||||
#expect(other.name == "Other")
|
||||
}
|
||||
|
||||
@Test("Sanitizing forces Block mode for time- and open-limit rules")
|
||||
func sanitizedForcesBlockForLimitKinds() {
|
||||
var timeDraft = RuleDraft(kind: .timeLimit)
|
||||
timeDraft.selectionMode = .allowOnly
|
||||
#expect(timeDraft.sanitized().selectionMode == .block)
|
||||
@Test("Limit drafts structurally cannot carry a selection mode")
|
||||
func limitDraftsHaveNoSelectionMode() {
|
||||
// The sum type makes Block / Allow Only a Schedule-only option: a limit
|
||||
// draft's configuration has no selection mode to force back to Block.
|
||||
#expect(RuleDraft(kind: .timeLimit).configuration.scheduleConfig == nil)
|
||||
#expect(RuleDraft(kind: .openLimit).configuration.scheduleConfig == nil)
|
||||
|
||||
var openDraft = RuleDraft(kind: .openLimit)
|
||||
openDraft.selectionMode = .allowOnly
|
||||
#expect(openDraft.sanitized().selectionMode == .block)
|
||||
// And the rule built from a limit draft is always Block.
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", configuration: RuleDraft(kind: .timeLimit).configuration)
|
||||
#expect(rule.selectionMode == .block)
|
||||
}
|
||||
|
||||
@Test("Sanitizing keeps Allow Only on schedule rules")
|
||||
func sanitizedKeepsAllowOnlyForSchedule() {
|
||||
@Test("Allow Only survives on schedule rules")
|
||||
func keepsAllowOnlyForSchedule() {
|
||||
var draft = RuleDraft(kind: .schedule)
|
||||
draft.selectionMode = .allowOnly
|
||||
#expect(draft.sanitized().selectionMode == .allowOnly)
|
||||
draft.scheduleConfig.selectionMode = .allowOnly
|
||||
#expect(draft.sanitized().scheduleConfig.selectionMode == .allowOnly)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
44
OpenAppLockTests/RuleConfigurationTests.swift
Normal file
44
OpenAppLockTests/RuleConfigurationTests.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// RuleConfigurationTests.swift
|
||||
// OpenAppLockTests
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import OpenAppLock
|
||||
|
||||
@MainActor
|
||||
@Suite("RuleConfiguration sum type")
|
||||
struct RuleConfigurationTests {
|
||||
@Test("Each case reports its kind")
|
||||
func kindDerivation() {
|
||||
#expect(RuleConfiguration.schedule(ScheduleConfig()).kind == .schedule)
|
||||
#expect(RuleConfiguration.timeLimit(TimeLimitConfig()).kind == .timeLimit)
|
||||
#expect(RuleConfiguration.openLimit(OpenLimitConfig()).kind == .openLimit)
|
||||
}
|
||||
|
||||
@Test("Defaults match the reference app's new-rule defaults")
|
||||
func defaults() {
|
||||
let schedule = RuleConfiguration.default(for: .schedule).scheduleConfig
|
||||
#expect(schedule?.startMinutes == 9 * 60)
|
||||
#expect(schedule?.endMinutes == 17 * 60)
|
||||
#expect(schedule?.selectionMode == .block)
|
||||
#expect(schedule?.blockAdultContent == false)
|
||||
|
||||
#expect(RuleConfiguration.default(for: .timeLimit).timeLimitConfig?.dailyLimitMinutes == 45)
|
||||
#expect(RuleConfiguration.default(for: .openLimit).openLimitConfig?.maxOpens == 5)
|
||||
}
|
||||
|
||||
@Test("Typed projections only unwrap the matching case")
|
||||
func projections() {
|
||||
let schedule = RuleConfiguration.schedule(ScheduleConfig(blockAdultContent: true))
|
||||
#expect(schedule.scheduleConfig?.blockAdultContent == true)
|
||||
#expect(schedule.timeLimitConfig == nil)
|
||||
#expect(schedule.openLimitConfig == nil)
|
||||
|
||||
let openLimit = RuleConfiguration.openLimit(OpenLimitConfig(maxOpens: 7))
|
||||
#expect(openLimit.openLimitConfig?.maxOpens == 7)
|
||||
#expect(openLimit.scheduleConfig == nil)
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ struct RuleEnforcerTests {
|
||||
func skipsTimeLimitRules() {
|
||||
let shields = MockShieldController()
|
||||
let enforcer = RuleEnforcer(shields: shields)
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit)
|
||||
let rule = BlockingRule(name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig()))
|
||||
|
||||
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
||||
|
||||
@@ -104,7 +104,8 @@ struct RuleEnforcerTests {
|
||||
func forwardsSelectionMode() {
|
||||
let shields = MockShieldController()
|
||||
let enforcer = RuleEnforcer(shields: shields)
|
||||
let rule = BlockingRule(name: "Focus", selectionMode: .allowOnly)
|
||||
let rule = BlockingRule(
|
||||
name: "Focus", configuration: .schedule(ScheduleConfig(selectionMode: .allowOnly)))
|
||||
|
||||
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
||||
|
||||
@@ -115,7 +116,8 @@ struct RuleEnforcerTests {
|
||||
func forwardsAdultContentFlag() {
|
||||
let shields = MockShieldController()
|
||||
let enforcer = RuleEnforcer(shields: shields)
|
||||
let filtered = BlockingRule(name: "Clean Mode", blockAdultContent: true)
|
||||
let filtered = BlockingRule(
|
||||
name: "Clean Mode", configuration: .schedule(ScheduleConfig(blockAdultContent: true)))
|
||||
let unfiltered = BlockingRule(name: "Plain")
|
||||
|
||||
enforcer.refresh(rules: [filtered, unfiltered], at: mondayDuringWork, calendar: utc)
|
||||
|
||||
@@ -36,8 +36,9 @@ struct RuleModelTests {
|
||||
|
||||
@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)
|
||||
let rule = BlockingRule(
|
||||
name: "Test", configuration: .schedule(ScheduleConfig(selectionMode: .allowOnly)))
|
||||
#expect(rule.kind == .schedule)
|
||||
#expect(rule.selectionMode == .allowOnly)
|
||||
rule.kindRaw = "garbage"
|
||||
rule.selectionModeRaw = "garbage"
|
||||
@@ -45,12 +46,43 @@ struct RuleModelTests {
|
||||
#expect(rule.selectionMode == .block)
|
||||
}
|
||||
|
||||
@Test("Limit kinds can never carry Schedule-only options")
|
||||
func limitKindsHaveNoScheduleOptions() {
|
||||
let timeLimit = BlockingRule(
|
||||
name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 30)))
|
||||
#expect(timeLimit.kind == .timeLimit)
|
||||
#expect(timeLimit.selectionMode == .block)
|
||||
#expect(!timeLimit.blockAdultContent)
|
||||
#expect(timeLimit.dailyLimitMinutes == 30)
|
||||
|
||||
let openLimit = BlockingRule(
|
||||
name: "Gate Keeper", configuration: .openLimit(OpenLimitConfig(maxOpens: 3)))
|
||||
#expect(openLimit.kind == .openLimit)
|
||||
#expect(openLimit.selectionMode == .block)
|
||||
#expect(!openLimit.blockAdultContent)
|
||||
#expect(openLimit.maxOpens == 3)
|
||||
}
|
||||
|
||||
@Test("Configuration round-trips through the model's raw storage")
|
||||
func configurationRoundTrip() {
|
||||
let config = RuleConfiguration.schedule(
|
||||
ScheduleConfig(
|
||||
startMinutes: 22 * 60, endMinutes: 6 * 60,
|
||||
selectionMode: .allowOnly, blockAdultContent: true))
|
||||
let rule = BlockingRule(name: "Deep Sleep", configuration: config)
|
||||
#expect(rule.configuration == config)
|
||||
#expect(rule.startMinutes == 22 * 60)
|
||||
#expect(rule.blockAdultContent)
|
||||
}
|
||||
|
||||
@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
|
||||
name: "Deep Sleep",
|
||||
configuration: .schedule(ScheduleConfig(startMinutes: 22 * 60, endMinutes: 6 * 60)),
|
||||
hardMode: true,
|
||||
days: Weekday.everyDay
|
||||
)
|
||||
context.insert(rule)
|
||||
try context.save()
|
||||
@@ -73,7 +105,7 @@ struct RuleDraftTests {
|
||||
let draft = RuleDraft(kind: .timeLimit)
|
||||
#expect(draft.name == "Time Keeper")
|
||||
#expect(draft.kind == .timeLimit)
|
||||
#expect(draft.dailyLimitMinutes == 45)
|
||||
#expect(draft.timeLimitConfig.dailyLimitMinutes == 45)
|
||||
#expect(draft.days == Weekday.weekdays)
|
||||
#expect(!draft.hardMode)
|
||||
}
|
||||
@@ -87,18 +119,32 @@ struct RuleDraftTests {
|
||||
var draft = RuleDraft(kind: .schedule)
|
||||
draft.name = "Locked In"
|
||||
draft.days = Weekday.everyDay
|
||||
draft.startMinutes = 22 * 60
|
||||
draft.endMinutes = 6 * 60
|
||||
draft.configuration = .schedule(
|
||||
ScheduleConfig(
|
||||
startMinutes: 22 * 60, endMinutes: 6 * 60,
|
||||
selectionMode: .allowOnly, blockAdultContent: true))
|
||||
draft.hardMode = true
|
||||
draft.blockAdultContent = true
|
||||
draft.selectionMode = .allowOnly
|
||||
draft.appList = list
|
||||
|
||||
let rule = draft.insertRule(into: context)
|
||||
#expect(rule.blockAdultContent)
|
||||
#expect(rule.selectionMode == .allowOnly)
|
||||
#expect(RuleDraft(rule: rule) == draft)
|
||||
}
|
||||
|
||||
@Test("A limit draft cannot carry Schedule-only options")
|
||||
func limitDraftHasNoScheduleOptions() {
|
||||
let draft = RuleDraft(kind: .openLimit)
|
||||
// The configuration is an open-limit case, so there is structurally no
|
||||
// selection mode or adult-content flag to set.
|
||||
#expect(draft.configuration.scheduleConfig == nil)
|
||||
#expect(draft.openLimitConfig.maxOpens == 5)
|
||||
|
||||
let rule = BlockingRule(name: draft.name, configuration: draft.configuration)
|
||||
#expect(!rule.blockAdultContent)
|
||||
#expect(rule.selectionMode == .block)
|
||||
}
|
||||
|
||||
@Test("Applying a draft updates an existing rule")
|
||||
func applyToExisting() throws {
|
||||
let context = try makeInMemoryContext()
|
||||
@@ -135,8 +181,8 @@ struct RuleDraftTests {
|
||||
)
|
||||
let draft = RuleDraft(preset: preset)
|
||||
#expect(draft.name == "Deep Sleep")
|
||||
#expect(draft.startMinutes == 22 * 60)
|
||||
#expect(draft.endMinutes == 6 * 60)
|
||||
#expect(draft.scheduleConfig.startMinutes == 22 * 60)
|
||||
#expect(draft.scheduleConfig.endMinutes == 6 * 60)
|
||||
#expect(draft.kind == .schedule)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ struct RuleStatusTests {
|
||||
|
||||
@Test("Time-limit rules are never schedule-active")
|
||||
func timeLimitNeverActive() {
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit)
|
||||
let rule = BlockingRule(name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig()))
|
||||
let status = rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc)
|
||||
#expect(!status.isActive)
|
||||
if case .upcoming = status {} else {
|
||||
@@ -116,7 +116,8 @@ struct RuleStatusTests {
|
||||
/// daily budget — never the vestigial 09:00 start as "Starts in 22h".
|
||||
@Test("Idle time-limit rule shows its daily budget, not a clock countdown")
|
||||
func timeLimitDisplayLabel() {
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit, dailyLimitMinutes: 15)
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 15)))
|
||||
let now = date(2025, 1, 6, 11, 38) // past the vestigial 09:00 window start
|
||||
let status = rule.status(at: now, calendar: utc)
|
||||
#expect(rule.statusLabel(for: status, relativeTo: now) == "15m / day")
|
||||
@@ -124,7 +125,8 @@ struct RuleStatusTests {
|
||||
|
||||
@Test("Idle open-limit rule shows its daily opens budget")
|
||||
func openLimitDisplayLabel() {
|
||||
let rule = BlockingRule(name: "Gate Keeper", kind: .openLimit, maxOpens: 5)
|
||||
let rule = BlockingRule(
|
||||
name: "Gate Keeper", configuration: .openLimit(OpenLimitConfig(maxOpens: 5)))
|
||||
let now = date(2025, 1, 6, 11, 38)
|
||||
let status = rule.status(at: now, calendar: utc)
|
||||
#expect(rule.statusLabel(for: status, relativeTo: now) == "5 opens / day")
|
||||
@@ -140,7 +142,8 @@ struct RuleStatusTests {
|
||||
|
||||
@Test("A spent time-limit budget still shows the blocked countdown")
|
||||
func timeLimitBlockingDisplayLabel() {
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit, dailyLimitMinutes: 15)
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 15)))
|
||||
let now = date(2025, 1, 6, 11, 38)
|
||||
let status = rule.status(at: now, calendar: utc, usage: RuleUsage(minutesUsed: 15))
|
||||
#expect(status.isActive)
|
||||
|
||||
@@ -41,7 +41,9 @@ struct RuleSnapshotTests {
|
||||
let context = try makeInMemoryContext()
|
||||
let list = AppList(name: "Distractions", selectionData: Data([9]), selectionCount: 1)
|
||||
let rule = BlockingRule(
|
||||
name: "Gate Keeper", kind: .openLimit, days: Weekday.weekends, maxOpens: 3)
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: 3)),
|
||||
days: Weekday.weekends)
|
||||
context.insert(list)
|
||||
context.insert(rule)
|
||||
rule.appList = list
|
||||
@@ -135,7 +137,8 @@ struct RuleSchedulerTests {
|
||||
private func limitRule(kind: RuleKind, name: String) throws -> BlockingRule {
|
||||
let context = try makeInMemoryContext()
|
||||
let list = AppList(name: "Apps", selectionData: Data([1]), selectionCount: 1)
|
||||
let rule = BlockingRule(name: name, kind: kind, days: Weekday.everyDay)
|
||||
let rule = BlockingRule(
|
||||
name: name, configuration: .default(for: kind), days: Weekday.everyDay)
|
||||
context.insert(list)
|
||||
context.insert(rule)
|
||||
rule.appList = list
|
||||
@@ -148,7 +151,9 @@ struct RuleSchedulerTests {
|
||||
) throws -> BlockingRule {
|
||||
let context = try makeInMemoryContext()
|
||||
let rule = BlockingRule(
|
||||
name: name, kind: .schedule, days: days, startMinutes: start, endMinutes: end)
|
||||
name: name,
|
||||
configuration: .schedule(ScheduleConfig(startMinutes: start, endMinutes: end)),
|
||||
days: days)
|
||||
context.insert(rule)
|
||||
if withApps {
|
||||
let list = AppList(name: "Apps", selectionData: Data([7]), selectionCount: 1)
|
||||
@@ -186,7 +191,7 @@ struct RuleSchedulerTests {
|
||||
let (scheduler, monitor, _) = makeScheduler()
|
||||
let context = try makeInMemoryContext()
|
||||
let applessSchedule = BlockingRule(name: "Work Time")
|
||||
let applessLimit = BlockingRule(name: "Empty", kind: .timeLimit)
|
||||
let applessLimit = BlockingRule(name: "Empty", configuration: .timeLimit(TimeLimitConfig()))
|
||||
context.insert(applessSchedule)
|
||||
context.insert(applessLimit)
|
||||
|
||||
|
||||
@@ -79,13 +79,17 @@ struct UsageStatusTests {
|
||||
let mondayMorning = date(2025, 1, 6, 10, 0)
|
||||
|
||||
private func timeLimitRule(limit: Int = 45) -> BlockingRule {
|
||||
BlockingRule(name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay,
|
||||
dailyLimitMinutes: limit)
|
||||
BlockingRule(
|
||||
name: "Time Keeper",
|
||||
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: limit)),
|
||||
days: Weekday.everyDay)
|
||||
}
|
||||
|
||||
private func openLimitRule(maxOpens: Int = 5) -> BlockingRule {
|
||||
BlockingRule(name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay,
|
||||
maxOpens: maxOpens)
|
||||
BlockingRule(
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: maxOpens)),
|
||||
days: Weekday.everyDay)
|
||||
}
|
||||
|
||||
@Test("A time-limit rule with budget left is not active")
|
||||
@@ -153,13 +157,17 @@ struct UsageEnforcementTests {
|
||||
let ledger = MockUsageLedger()
|
||||
let enforcer = RuleEnforcer(shields: shields, usage: ledger)
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay, dailyLimitMinutes: 45)
|
||||
name: "Time Keeper",
|
||||
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 45)),
|
||||
days: Weekday.everyDay)
|
||||
ledger.usageByRule[rule.id] = RuleUsage(minutesUsed: 45)
|
||||
|
||||
enforcer.refresh(rules: [rule], at: mondayMorning, calendar: utc)
|
||||
|
||||
#expect(shields.shieldedRuleIDs == [rule.id])
|
||||
#expect(shields.appliedModes[rule.id] == .block)
|
||||
// Limit rules never engage the adult-content filter (Schedule-only).
|
||||
#expect(shields.appliedAdultContentFlags[rule.id] == false)
|
||||
}
|
||||
|
||||
@Test("A limit rule with budget left is not shielded")
|
||||
@@ -168,7 +176,9 @@ struct UsageEnforcementTests {
|
||||
let ledger = MockUsageLedger()
|
||||
let enforcer = RuleEnforcer(shields: shields, usage: ledger)
|
||||
let rule = BlockingRule(
|
||||
name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay, maxOpens: 5)
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: 5)),
|
||||
days: Weekday.everyDay)
|
||||
ledger.usageByRule[rule.id] = RuleUsage(opensUsed: 2)
|
||||
|
||||
enforcer.refresh(rules: [rule], at: mondayMorning, calendar: utc)
|
||||
@@ -181,9 +191,13 @@ struct UsageEnforcementTests {
|
||||
@Suite("Usage display strings")
|
||||
struct UsageDisplayTests {
|
||||
let timeRule = BlockingRule(
|
||||
name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay, dailyLimitMinutes: 45)
|
||||
name: "Time Keeper",
|
||||
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 45)),
|
||||
days: Weekday.everyDay)
|
||||
let openRule = BlockingRule(
|
||||
name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay, maxOpens: 5)
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: 5)),
|
||||
days: Weekday.everyDay)
|
||||
|
||||
@Test("Time-limit rows show minutes used and remaining")
|
||||
func timeLimitStrings() {
|
||||
|
||||
Reference in New Issue
Block a user