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>
This commit is contained in:
99
SeveredUITests/RuleCreationUITests.swift
Normal file
99
SeveredUITests/RuleCreationUITests.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// RuleCreationUITests.swift
|
||||
// SeveredUITests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class RuleCreationUITests: XCTestCase {
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
func testCreateScheduleRuleFromTypeCard() throws {
|
||||
let app = XCUIApplication.launchSevered()
|
||||
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)
|
||||
|
||||
// Hold to commit saves the rule and returns home.
|
||||
app.buttons["holdToCommitButton"].waitToAppear().press(forDuration: 2.0)
|
||||
app.buttons["ruleCard-In the Zone"].waitToAppear()
|
||||
XCTAssertFalse(app.element("emptyRulesCard").exists)
|
||||
}
|
||||
|
||||
func testCreateRuleFromPreset() throws {
|
||||
let app = XCUIApplication.launchSevered()
|
||||
app.buttons["newRuleButton"].waitToAppear().tap()
|
||||
|
||||
app.buttons["preset-work-time"].waitToAppear().tap()
|
||||
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "Work Time")
|
||||
|
||||
app.buttons["holdToCommitButton"].waitToAppear().press(forDuration: 2.0)
|
||||
app.buttons["ruleCard-Work Time"].waitToAppear()
|
||||
}
|
||||
|
||||
func testRenameRuleInEditor() throws {
|
||||
let app = XCUIApplication.launchSevered()
|
||||
app.buttons["newRuleButton"].waitToAppear().tap()
|
||||
app.buttons["ruleKind-schedule"].waitToAppear().tap()
|
||||
|
||||
app.buttons["renameButton"].waitToAppear().tap()
|
||||
let nameField = app.textFields.firstMatch.waitToAppear()
|
||||
nameField.tap()
|
||||
// Clear the prefilled name, then type the new one.
|
||||
let deletions = String(repeating: XCUIKeyboardKey.delete.rawValue, count: 24)
|
||||
nameField.typeText(deletions + "My Focus")
|
||||
app.buttons["OK"].tap()
|
||||
|
||||
XCTAssertEqual(app.staticTexts["ruleEditorTitle"].label, "My Focus")
|
||||
app.buttons["holdToCommitButton"].waitToAppear().press(forDuration: 2.0)
|
||||
app.buttons["ruleCard-My Focus"].waitToAppear()
|
||||
}
|
||||
|
||||
func testDayTogglesUpdateSummary() throws {
|
||||
let app = XCUIApplication.launchSevered()
|
||||
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.launchSevered()
|
||||
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["holdToCommitButton"].waitToAppear().press(forDuration: 2.0)
|
||||
app.buttons["ruleCard-Time Keeper"].waitToAppear()
|
||||
}
|
||||
|
||||
func testNewRuleSheetShowsTypesAndPresets() throws {
|
||||
let app = XCUIApplication.launchSevered()
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user