refactor: rename app from Severed to OpenAppLock

Full rename ahead of App Store submission: project, targets, bundle
identifiers (dev.bchen.Severed -> dev.bchen.OpenAppLock), entitlements
file, app entry point, user-facing onboarding strings, test helpers,
and docs. Verified: all 62 unit tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:05:06 -04:00
parent b8473383d3
commit 1d5e79ba7c
42 changed files with 145 additions and 145 deletions

View File

@@ -0,0 +1,31 @@
//
// OnboardingUITests.swift
// OpenAppLockUITests
//
import XCTest
final class OnboardingUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testOnboardingWalksThroughPermissionToHome() throws {
let app = XCUIApplication.launchOpenAppLock(onboardingCompleted: false)
// Welcome step.
app.staticTexts["OpenAppLock"].waitToAppear()
app.buttons["onboardingContinueButton"].waitToAppear().tap()
// Permission step: granting (mocked) lands on the Apps home screen.
app.buttons["allowScreenTimeButton"].waitToAppear().tap()
app.buttons["newRuleButton"].waitToAppear()
XCTAssertTrue(app.staticTexts["Blocked Apps"].exists)
}
func testCompletedOnboardingIsSkipped() throws {
let app = XCUIApplication.launchOpenAppLock(onboardingCompleted: true)
app.buttons["newRuleButton"].waitToAppear()
XCTAssertFalse(app.buttons["onboardingContinueButton"].exists)
}
}

View File

@@ -0,0 +1,160 @@
//
// 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)")
}
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)
}
}

View File

@@ -0,0 +1,136 @@
//
// RuleManagementUITests.swift
// OpenAppLockUITests
//
import XCTest
/// Detail sheet, editing, disabling, deleting, and unblocking seeded with
/// one actively-blocking rule ("Work Time") and one upcoming rule ("Sleep").
final class RuleManagementUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testDetailShowsLiveStatusAndFacts() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Work Time"].waitToAppear().tap()
XCTAssertEqual(app.staticTexts["detailRuleName"].waitToAppear().label, "Work Time")
XCTAssertTrue(app.staticTexts["detailStatusLabel"].label.contains("left"))
app.element("detailRow-During this time").waitToAppear()
XCTAssertTrue(app.element("detailRow-On these days").exists)
XCTAssertTrue(app.element("detailRow-Unblocks allowed").exists)
app.buttons["editRuleButton"].waitToAppear()
app.buttons["closeDetailButton"].tap()
app.buttons["newRuleButton"].waitToAppear()
}
func testEditRuleTogglesHardModeOn() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap()
app.switches["hardModeToggle"].waitToAppear().tap()
app.buttons["doneButton"].tap()
// Back on the detail view, unblocks are no longer allowed.
let row = app.element("detailRow-Unblocks allowed").waitToAppear()
XCTAssertTrue(row.label.contains("No"), "Expected 'Unblocks allowed: No', got: \(row.label)")
}
func testDisableRule() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap()
// Disable lives in the ellipsis menu in the navigation bar.
let actionsMenu = app.navigationBars.buttons["ruleActionsMenu"].waitToAppear()
XCTAssertEqual(actionsMenu.label, "Rule Actions")
actionsMenu.tap()
app.buttons["Disable Rule"].waitToAppear().tap()
// The detail caption now reports the rule as disabled.
let status = app.staticTexts["detailStatusLabel"].waitToAppear()
XCTAssertTrue(status.label.contains("Disabled"), "Got: \(status.label)")
app.buttons["closeDetailButton"].tap()
let cardStatus = app.staticTexts["ruleStatus-Sleep"].waitToAppear()
XCTAssertEqual(cardStatus.label, "Disabled")
}
func testDeleteRuleRemovesCard() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap()
// Delete lives in the ellipsis menu in the navigation bar.
app.navigationBars.buttons["ruleActionsMenu"].waitToAppear().tap()
app.buttons["Delete Rule"].waitToAppear().tap()
app.buttons["newRuleButton"].waitToAppear()
XCTAssertFalse(
app.buttons["ruleCard-Sleep"].waitForExistence(timeout: 2),
"Deleted rule's card should disappear"
)
XCTAssertTrue(app.buttons["ruleCard-Work Time"].exists)
}
func testUnblockActiveSoftRule() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
// The active rule surfaces in Blocked Apps; unblocking pauses it.
app.buttons["blockedTile-Work Time"].waitToAppear().tap()
app.sheets.buttons["Unblock"].waitToAppear().tap()
app.staticTexts["nothingBlockedLabel"].waitToAppear()
XCTAssertEqual(app.staticTexts["ruleStatus-Work Time"].waitToAppear().label, "Paused")
}
}
/// Hard block behavior seeded with an actively-blocking Hard Mode rule.
final class HardModeUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testHardLockedRuleCannotBeEdited() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "hard-mode-active")
app.buttons["ruleCard-Locked In"].waitToAppear().tap()
// The lock notice replaces Edit Rule entirely.
app.element("hardModeLockedNotice").waitToAppear()
XCTAssertFalse(app.buttons["editRuleButton"].exists)
XCTAssertTrue(app.element("detailRow-Unblocks allowed").label.contains("No"))
}
func testHardLockedRuleCannotBeUnblocked() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "hard-mode-active")
app.buttons["blockedTile-Locked In"].waitToAppear().tap()
// No unblock dialog just the refusal alert.
let alert = app.alerts["Hard Mode is on"].waitToAppear()
XCTAssertTrue(alert.staticTexts["This block can't be lifted until it ends."].exists)
alert.buttons["OK"].tap()
// Still blocked.
XCTAssertTrue(app.buttons["blockedTile-Locked In"].exists)
XCTAssertFalse(app.staticTexts["nothingBlockedLabel"].exists)
}
func testSoftRuleUnblockOfferedButHardRuleRefused() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["blockedTile-Work Time"].waitToAppear().tap()
// Soft rule: the confirmation dialog appears instead of the refusal alert.
app.sheets.buttons["Unblock"].waitToAppear()
XCTAssertFalse(app.alerts["Hard Mode is on"].exists)
}
}

View File

@@ -0,0 +1,48 @@
//
// UITestSupport.swift
// OpenAppLockUITests
//
import XCTest
extension XCUIApplication {
/// Launches the app in UI-testing mode: in-memory storage, mocked Screen
/// Time authorization, and no shield side effects.
static func launchOpenAppLock(
onboardingCompleted: Bool = true,
seedScenario: String? = nil
) -> XCUIApplication {
let app = XCUIApplication()
var arguments = ["-ui-testing"]
arguments.append(onboardingCompleted ? "-onboarding-completed" : "-onboarding-required")
if let seedScenario {
arguments.append("-seed-scenario=\(seedScenario)")
}
app.launchArguments = arguments
app.launch()
return app
}
}
extension XCUIApplication {
/// Finds an element by accessibility identifier regardless of how SwiftUI
/// exposes it (Other, StaticText, Button, ).
func element(_ identifier: String) -> XCUIElement {
descendants(matching: .any).matching(identifier: identifier).firstMatch
}
}
extension XCUIElement {
/// Asserts the element appears within the timeout, then returns it.
@discardableResult
func waitToAppear(
timeout: TimeInterval = 5, file: StaticString = #filePath, line: UInt = #line
) -> XCUIElement {
XCTAssertTrue(
waitForExistence(timeout: timeout),
"Expected \(self) to exist within \(timeout)s",
file: file, line: line
)
return self
}
}