Files
OpenAppLock/OpenAppLockUITests/RuleCreationUITests.swift
Brendan Chen 783a8571a7 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).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 12:51:26 -04:00

187 lines
7.9 KiB
Swift

//
// RuleCreationUITests.swift
// OpenAppLockUITests
//
import XCTest
final class RuleCreationUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testCreateScheduleRuleFromTypeCard() throws {
let app = XCUIApplication.launchOpenAppLock()
app.element("emptyRulesCard").waitToAppear()
app.buttons["newRuleButton"].tap()
app.staticTexts["New Rule"].waitToAppear()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
// Editor opens with the schedule defaults.
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "In the Zone")
XCTAssertTrue(app.staticTexts["During this time"].exists)
// The confirmation checkmark lives in the navigation bar and carries
// a descriptive accessibility label.
let commit = app.navigationBars.buttons["commitRuleButton"].waitToAppear()
XCTAssertEqual(commit.label, "Add Rule")
commit.tap()
app.buttons["ruleCard-In the Zone"].waitToAppear()
XCTAssertFalse(app.element("emptyRulesCard").exists)
}
func testCreateRuleFromPreset() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["preset-work-time"].waitToAppear().tap()
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "Work Time")
app.buttons["commitRuleButton"].waitToAppear().tap()
app.buttons["ruleCard-Work Time"].waitToAppear()
}
func testRenameRuleInEditor() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
// The rule name is an inline text field at the top of the editor
// no separate edit/rename button.
XCTAssertFalse(app.buttons["renameButton"].exists)
let nameField = app.textFields["ruleNameField"].waitToAppear()
// Tap at the trailing edge so the cursor lands after the last character.
nameField.coordinate(withNormalizedOffset: CGVector(dx: 0.95, dy: 0.5)).tap()
let deletions = String(repeating: XCUIKeyboardKey.delete.rawValue, count: 24)
nameField.typeText(deletions + "My Focus\n")
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].label, "My Focus")
app.buttons["commitRuleButton"].waitToAppear().tap()
app.buttons["ruleCard-My Focus"].waitToAppear()
}
func testDayTogglesFillRowAndHaveLargeTapTargets() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
let first = app.buttons["dayToggle-1"].waitToAppear()
let last = app.buttons["dayToggle-7"].waitToAppear()
let span = last.frame.maxX - first.frame.minX
XCTAssertGreaterThan(
span, app.frame.width * 0.75,
"Day toggles should span the full row width, got \(span) of \(app.frame.width)"
)
XCTAssertGreaterThanOrEqual(
first.frame.height, 44,
"Day toggle tap target should be at least 44pt tall"
)
}
func testDayTogglesUpdateSummary() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
// Default is Weekdays; enabling Saturday and Sunday makes it Every day.
XCTAssertTrue(app.staticTexts["Weekdays"].waitToAppear().exists)
app.buttons["dayToggle-1"].tap()
app.buttons["dayToggle-7"].tap()
XCTAssertTrue(app.staticTexts["Every day"].waitToAppear().exists)
}
func testCreateTimeLimitRule() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap()
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "Time Keeper")
XCTAssertTrue(app.staticTexts["When I use"].exists)
XCTAssertEqual(app.staticTexts["dailyLimitStepperValue"].label, "45m")
app.steppers["dailyLimitStepper"].buttons["Increment"].tap()
XCTAssertEqual(app.staticTexts["dailyLimitStepperValue"].label, "60m")
app.buttons["commitRuleButton"].waitToAppear().tap()
app.buttons["ruleCard-Time Keeper"].waitToAppear()
}
func testAdultContentToggleFlowsToDetail() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
// The toggle lives at the bottom of the form; scroll it clear of the
// commit bar before tapping.
app.staticTexts["ruleEditorTitle"].waitToAppear()
app.swipeUp()
app.switches["adultContentToggle"].waitToAppear().tap()
app.buttons["commitRuleButton"].waitToAppear().tap()
app.buttons["ruleCard-In the Zone"].waitToAppear().tap()
let row = app.element("detailRow-Adult websites").waitToAppear()
XCTAssertTrue(row.label.contains("Blocked"), "Got: \(row.label)")
}
func testAdultContentDefaultsToAllowed() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Work Time"].waitToAppear().tap()
let row = app.element("detailRow-Adult websites").waitToAppear()
XCTAssertTrue(row.label.contains("Allowed"), "Got: \(row.label)")
}
/// Block Adult Content is a Schedule-only option: the Time Limit editor must
/// not offer the toggle, and the rule's detail must not show the row.
func testTimeLimitOmitsAdultContent() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap()
// Scroll to the bottom of the form, where Hard Mode lives. The adult
// toggle (which would sit beside it on a Schedule rule) is absent.
app.staticTexts["ruleEditorTitle"].waitToAppear()
app.swipeUp()
app.switches["hardModeToggle"].waitToAppear()
XCTAssertFalse(
app.switches["adultContentToggle"].exists,
"Time-limit rules must not offer Block Adult Content"
)
app.buttons["commitRuleButton"].waitToAppear().tap()
app.buttons["ruleCard-Time Keeper"].waitToAppear().tap()
app.element("detailRuleName").waitToAppear()
XCTAssertFalse(
app.element("detailRow-Adult websites").exists,
"Time-limit detail must not show the Adult websites row"
)
}
func testEditorSupportsNativeSwipeBack() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
app.staticTexts["ruleEditorTitle"].waitToAppear()
// Native push navigation supports the edge-swipe back gesture.
let edge = app.coordinate(withNormalizedOffset: CGVector(dx: 0.02, dy: 0.5))
let middle = app.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))
edge.press(forDuration: 0.05, thenDragTo: middle)
app.buttons["ruleKind-schedule"].waitToAppear()
XCTAssertTrue(app.staticTexts["New Rule"].exists)
}
func testNewRuleSheetShowsTypesAndPresets() throws {
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear()
XCTAssertTrue(app.buttons["ruleKind-timeLimit"].exists)
XCTAssertTrue(app.buttons["ruleKind-openLimit"].exists)
XCTAssertTrue(app.staticTexts["Get More Done"].exists)
XCTAssertTrue(app.buttons["preset-work-time"].exists)
XCTAssertTrue(app.buttons["preset-laser-focus"].exists)
}
}