Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,19 @@ when reminded:
inaccurate, update it in the same commit (see Documentation above). Then write
the failing test, run it (compile failure counts as red), implement, re-run
focused tests, then the full suite. Run tests often and fail fast.
- **Always attempt to validate the UI manually before committing.** Build and
run the app (simulator/device) and visually confirm the change behaves as
intended. This step **may be skipped only when such tooling is unavailable**
(e.g. the Xcode MCP / a simulator is not reachable in the session) — in that
case, say so explicitly and hand the verification back to the user rather
than silently skipping it.
- **Always attempt to validate the UI manually before committing.** Every
change that touches what a screen renders (views, layout, hit targets,
strings) must be visually confirmed via the Xcode MCP before committing —
either an `xcode_RenderPreview` snapshot of the affected view, or better a
`DeviceInteraction*` run on a simulator: build & install the app, drive the
affected flow with synthesized taps, and inspect the captured screenshots
directly (subagents can drive the simulator; see the `device-interaction`
skill). Read the screenshots yourself, don't only trust interaction logs.
Manual verification is what catches "the right element exists but the layout
looks wrong" failures that tests can't. This step **may be skipped only when
such tooling is unavailable** (e.g. the Xcode MCP / a simulator is not
reachable in the session) — in that case, say so explicitly and hand the
verification back to the user rather than silently skipping it.
- **Branch and open a PR for every change.** New features and bug fixes do not
go directly onto `main`. Create a topic branch (`feat/…`, `fix/…`,
`chore/…`), push it, and open a GitHub PR with `gh pr create` for the
Expand Down
13 changes: 9 additions & 4 deletions OpenAppLock/Views/AppLists/AppListLibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import SwiftUI
///
/// - **Picker** (`selection` non-nil): each row's tap **toggles** its membership
/// in the rule's selection, so multiple lists can be combined into one rule.
/// A trailing button opens the list for editing/viewing as before. Creating
/// a list appends it to the selection.
/// The toggle target spans the whole row left of the trailing button — icon,
/// title, description, and the stretch in between — so tapping the title or
/// description selects/deselects too; the row exposes `.isSelected` to
/// VoiceOver. A trailing button opens the list for editing/viewing as before.
/// Creating a list appends it to the selection.
/// - **Management** (`selection` nil): no checkmark; tapping the row opens it —
/// the editor sheet when unlocked, the read-only `AppListDetailView` while
/// locked. Used by Settings ▸ Manage App Lists.
Expand Down Expand Up @@ -150,7 +153,7 @@ struct AppListLibraryView: View {
@ViewBuilder
private func listRow(_ list: AppList) -> some View {
if isPicking {
// Picker mode: tapping the row toggles the list's membership.
// Whole-row tap target (see the picker-mode doc comment above).
HStack {
Button {
toggle(list)
Expand All @@ -163,9 +166,11 @@ struct AppListLibraryView: View {
.frame(width: 28)
rowText(list)
}
.frame(maxWidth: .infinity, minHeight: 44, alignment: .leading)
.contentShape(Rectangle())
}
.accessibilityIdentifier("appListRow-\(list.name)")
Spacer()
.accessibilityAddTraits(isSelected(list) ? .isSelected : [])
// Locked lists stay read-only (no "Edit"), but can still be
// opened to view their apps; unlocked lists open the editor.
if listsLocked {
Expand Down
52 changes: 52 additions & 0 deletions OpenAppLockUITests/AppListUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,58 @@ final class AppListUITests: XCTestCase {
)
}

func testPickerRowTouchTargetExtendsToEditButton() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap()

// The App List row pushes the selection screen onto the editor's stack.
app.element("selectedAppsRow").waitToAppear()
.tap(untilAppears: app.buttons["editAppListButton-Distractions"])
let row = app.element("appListRow-Distractions").waitToAppear()
let edit = app.buttons["editAppListButton-Distractions"].waitToAppear()

XCTAssertGreaterThanOrEqual(
row.frame.height, 44,
"Row tap target should be at least 44pt tall"
)
XCTAssertGreaterThanOrEqual(
row.frame.maxX, edit.frame.minX - 12,
"Row tap target should extend to the Edit button, got \(row.frame) vs \(edit.frame)"
)

// Tapping the title (the leading stretch of the row) selects the list.
row.tapAtNormalizedOffset(0.15, 0.5)
waitForSelection(row, expected: true)

// Tapping further along the row — well past the text, in the stretch
// before the Edit button that used to be an untappable gap — deselects.
row.tapAtNormalizedOffset(0.8, 0.5)
waitForSelection(row, expected: false)

// The title still toggles back on after the dead-zone tap.
row.tapAtNormalizedOffset(0.15, 0.5)
waitForSelection(row, expected: true)
}

/// Polls until the element's `isSelected` accessibility trait matches,
/// rather than reading the property straight after a tap — the SwiftUI
/// re-render that recomputes the trait updates asynchronously on a slow
/// runner.
private func waitForSelection(
_ element: XCUIElement, expected: Bool,
file: StaticString = #filePath, line: UInt = #line
) {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(format: "isSelected == %@", NSNumber(value: expected)),
object: element)
XCTAssertEqual(
XCTWaiter().wait(for: [expectation], timeout: 5), .completed,
"Expected isSelected to become \(expected)",
file: file, line: line)
}

/// Asserts the read-only `AppListDetailView` is showing: its lock notice is
/// present and neither edit affordance (the apps picker, the Save button)
/// exists — the "no editing" rule holds while a list is merely viewable.
Expand Down
Loading