From a7090f9734d2796c07bed05d6e91595c3346a548 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sat, 26 Sep 2026 21:09:25 -0400 Subject: [PATCH 1/3] feat: stretch app-list picker tap targets to the trailing button In the rule editor's app-list picker, the selection hit area previously covered only the checkmark and text, leaving the gap before the trailing Edit/View button untappable. The toggle button label now stretches across the whole row (.frame(maxWidth: .infinity, minHeight: 44) + .contentShape(Rectangle()), mirroring DayOfWeekPicker) so tapping the title, description, or anywhere left of the button toggles selection, and the row exposes its checked state as the .isSelected accessibility trait for VoiceOver and the UI suite. UI test taps the title and the previous dead zone to verify toggling both ways, and asserts the 44pt minimum height and span to the Edit button. Co-Authored-By: opencode-go (GLM, via opencode) --- .../Views/AppLists/AppListLibraryView.swift | 13 +++-- OpenAppLockUITests/AppListUITests.swift | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/OpenAppLock/Views/AppLists/AppListLibraryView.swift b/OpenAppLock/Views/AppLists/AppListLibraryView.swift index 2c319f7..817df55 100644 --- a/OpenAppLock/Views/AppLists/AppListLibraryView.swift +++ b/OpenAppLock/Views/AppLists/AppListLibraryView.swift @@ -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. @@ -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) @@ -163,9 +166,11 @@ struct AppListLibraryView: View { .frame(width: 28) rowText(list) } + .frame(maxWidth: .infinity, minHeight: 44) + .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 { diff --git a/OpenAppLockUITests/AppListUITests.swift b/OpenAppLockUITests/AppListUITests.swift index 0d20aca..4dcbcab 100644 --- a/OpenAppLockUITests/AppListUITests.swift +++ b/OpenAppLockUITests/AppListUITests.swift @@ -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. From cfa72855db76f6e06161859c6f0729170f23e7d7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sat, 26 Sep 2026 21:44:48 -0400 Subject: [PATCH 2/3] docs: spell out Xcode MCP preview/simulator UI validation step Make the manual-UI-validation workflow bullet explicit about the mechanism (RenderPreview snapshot or DeviceInteraction run with screenshot inspection) so agents don't stop at passing tests. Co-Authored-By: opencode-go (GLM, via opencode) --- AGENTS.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c01f552..23c4541 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 From 95f20a4c572bde28f12564db1c46651891cd503c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sat, 26 Sep 2026 21:56:03 -0400 Subject: [PATCH 3/3] fix: align picker row content to the leading edge The stretched row frame defaulted to center alignment, floating the checkbox and text inward and showing a gap before each checkbox. Anchor the frame to .leading so content sits at the standard list inset while the 44pt tap target still spans the row. Co-Authored-By: opencode-go (GLM, via opencode) --- OpenAppLock/Views/AppLists/AppListLibraryView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAppLock/Views/AppLists/AppListLibraryView.swift b/OpenAppLock/Views/AppLists/AppListLibraryView.swift index 817df55..583e370 100644 --- a/OpenAppLock/Views/AppLists/AppListLibraryView.swift +++ b/OpenAppLock/Views/AppLists/AppListLibraryView.swift @@ -166,7 +166,7 @@ struct AppListLibraryView: View { .frame(width: 28) rowText(list) } - .frame(maxWidth: .infinity, minHeight: 44) + .frame(maxWidth: .infinity, minHeight: 44, alignment: .leading) .contentShape(Rectangle()) } .accessibilityIdentifier("appListRow-\(list.name)")