Commit ced7e9a361
ced7e9a3613e96fc61d1acfd0da94c58c17497dd
parent: 4cb642b1b7
Verified · cmc
cmc <hello@cleberg.net> · 2026-10-18 06:29 UTC
Fix accessibility audit findings; add audit UI test
- MoonriseUITests runs Xcode's accessibility audit on Sky, Settings,
System, Sun, Launches, Library and a Library page, printing every
issue (A11Y lines) without failing.
- Launch and Library rows wrap instead of truncating; Today summary
wraps freely.
- 44-point targets for Now, Today, the summary bar and spacecraft
names; dimmed controls at 0.6 opacity.
- Library fact rows read as one element (label and value).
- Settings keeps the default tint instead of night-mode red.
Audit: 60 issues before, 46 after. Remaining are Dynamic Type
'partially unsupported' notes (also raised on system controls),
contrast on text under the translucent tab bar, and the system Done
button.
Layout: unified · split
App/Sources/LaunchesScreen.swift
+2 −2
| @@ -64,9 +64,9 @@ struct LaunchesScreen: View { |
| 64 | StatusTag(status: l.status, ink: ink, accent: accent) |
64 | StatusTag(status: l.status, ink: ink, accent: accent) |
| 65 | } |
65 | } |
| 66 | Text(l.rocket).font(.title2.weight(.medium)) |
66 | Text(l.rocket).font(.title2.weight(.medium)) |
| 67 | if let m = l.mission { Text(m).font(.subheadline).lineLimit(1) } |
67 | if let m = l.mission { Text(m).font(.subheadline) } |
| 68 | HStack { |
68 | HStack { |
| 69 | Text("\(l.provider) · \(l.location)").font(.caption2).opacity(0.6).lineLimit(1) |
69 | Text("\(l.provider) · \(l.location)").font(.caption2).opacity(0.7) |
| 70 | Spacer() |
70 | Spacer() |
| 71 | Countdown(to: l.net).font(.footnote.weight(.medium).monospacedDigit()) |
71 | Countdown(to: l.net).font(.footnote.weight(.medium).monospacedDigit()) |
| 72 | } |
72 | } |
App/Sources/LibraryScreen.swift
+2 −1
| @@ -66,7 +66,7 @@ struct PageRow: View { |
| 66 | var body: some View { |
66 | var body: some View { |
| 67 | VStack(alignment: .leading, spacing: 2) { |
67 | VStack(alignment: .leading, spacing: 2) { |
| 68 | Text(page.title).font(.body) |
68 | Text(page.title).font(.body) |
| 69 | Text(page.subtitle).font(.caption).opacity(0.6).lineLimit(1) |
69 | Text(page.subtitle).font(.caption).opacity(0.7) |
| 70 | } |
70 | } |
| 71 | .padding(.vertical, 2) |
71 | .padding(.vertical, 2) |
| 72 | } |
72 | } |
| @@ -111,6 +111,7 @@ struct LibraryPageView: View { |
| 111 | Text(f.label).font(.footnote).opacity(0.6) |
111 | Text(f.label).font(.footnote).opacity(0.6) |
| 112 | Text(f.value).font(.subheadline.monospacedDigit()) |
112 | Text(f.value).font(.subheadline.monospacedDigit()) |
| 113 | } |
113 | } |
| |
114 | .accessibilityElement(children: .combine) |
| 114 | Divider().overlay(ink.opacity(0.15)) |
115 | Divider().overlay(ink.opacity(0.15)) |
| 115 | } |
116 | } |
| 116 | } |
117 | } |
App/Sources/SkyScreen.swift
+1
| @@ -59,6 +59,7 @@ struct SkyScreen: View { |
| 59 | .sheet(isPresented: $showSettings) { |
59 | .sheet(isPresented: $showSettings) { |
| 60 | SettingsScreen { model.applyLocationSettings() } |
60 | SettingsScreen { model.applyLocationSettings() } |
| 61 | .preferredColorScheme(.dark) |
61 | .preferredColorScheme(.dark) |
| |
62 | .tint(.blue) |
| 62 | } |
63 | } |
| 63 | .sheet(item: $libraryPage) { page in |
64 | .sheet(item: $libraryPage) { page in |
| 64 | NavigationStack { LibraryPageView(page: page) } |
65 | NavigationStack { LibraryPageView(page: page) } |
App/Sources/SystemScreen.swift
+6 −4
| @@ -38,9 +38,11 @@ struct SystemScreen: View { |
| 38 | .font(.subheadline.weight(.medium).monospacedDigit()) |
38 | .font(.subheadline.weight(.medium).monospacedDigit()) |
| 39 | .frame(width: 80, alignment: .leading) |
39 | .frame(width: 80, alignment: .leading) |
| 40 | Slider(value: $offsetDays, in: range).tint(ink) |
40 | Slider(value: $offsetDays, in: range).tint(ink) |
| 41 | Button("Today") { playing = false; offsetDays = 0 } |
41 | Button { playing = false; offsetDays = 0 } label: { |
| 42 | .font(.footnote.weight(.medium)) |
42 | Text("Today").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle()) |
| 43 | .opacity(abs(offsetDays) < 1 ? 0.4 : 1) |
43 | } |
| |
44 | .font(.footnote.weight(.medium)) |
| |
45 | .opacity(abs(offsetDays) < 1 ? 0.6 : 1) |
| 44 | } |
46 | } |
| 45 | .padding(.top, 12) |
47 | .padding(.top, 12) |
| 46 | |
48 | |
| @@ -58,7 +60,7 @@ struct SystemScreen: View { |
| 58 | ForEach(Spacecraft.all) { craft in |
60 | ForEach(Spacecraft.all) { craft in |
| 59 | GridRow { |
61 | GridRow { |
| 60 | if let page = Library.page(spacecraft: craft.name) { |
62 | if let page = Library.page(spacecraft: craft.name) { |
| 61 | Button { libraryPage = page } label: { Text(craft.name).underline() }.buttonStyle(.plain) |
63 | Button { libraryPage = page } label: { Text(craft.name).underline().frame(minHeight: 44, alignment: .leading).contentShape(Rectangle()) }.buttonStyle(.plain) |
| 62 | } else { |
64 | } else { |
| 63 | Text(craft.name) |
65 | Text(craft.name) |
| 64 | } |
66 | } |
App/Sources/TodaySheet.swift
+8 −4
| @@ -121,10 +121,12 @@ struct TodayBar: View { |
| 121 | scrubber |
121 | scrubber |
| 122 | Button(action: onExpand) { |
122 | Button(action: onExpand) { |
| 123 | HStack { |
123 | HStack { |
| 124 | Text(summary).font(.footnote).opacity(0.75).lineLimit(2) |
124 | Text(summary).font(.footnote).opacity(0.8) |
| 125 | Spacer() |
125 | Spacer() |
| 126 | Image(systemName: "chevron.up").font(.caption.weight(.medium)).opacity(0.6) |
126 | Image(systemName: "chevron.up").font(.caption.weight(.medium)).opacity(0.6) |
| 127 | } |
127 | } |
| |
128 | .frame(minHeight: 44) |
| |
129 | .contentShape(Rectangle()) |
| 128 | } |
130 | } |
| 129 | .buttonStyle(.plain) |
131 | .buttonStyle(.plain) |
| 130 | .accessibilityHint("Shows tonight's details") |
132 | .accessibilityHint("Shows tonight's details") |
| @@ -157,9 +159,11 @@ struct TodayBar: View { |
| 157 | .font(.footnote.monospacedDigit()) |
159 | .font(.footnote.monospacedDigit()) |
| 158 | .frame(width: 70, alignment: .leading) |
160 | .frame(width: 70, alignment: .leading) |
| 159 | Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink) |
161 | Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink) |
| 160 | Button("Now") { model.offsetHours = 0 } |
162 | Button { model.offsetHours = 0 } label: { |
| 161 | .font(.footnote.weight(.medium)) |
163 | Text("Now").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle()) |
| 162 | .opacity(model.offsetHours == 0 ? 0.4 : 1) |
164 | } |
| |
165 | .font(.footnote.weight(.medium)) |
| |
166 | .opacity(model.offsetHours == 0 ? 0.6 : 1) |
| 163 | } |
167 | } |
| 164 | } |
168 | } |
| 165 | |
169 | |
App/UITests/AccessibilityAuditTests.swift
added
+49
| @@ -0,0 +1,49 @@ |
| |
1 | import XCTest |
| |
2 | |
| |
3 | /// Runs Xcode's accessibility audit on each main screen and prints every issue. |
| |
4 | /// Issues are recorded, not failed, so one run reports everything. |
| |
5 | final class AccessibilityAuditTests: XCTestCase { |
| |
6 | var app: XCUIApplication! |
| |
7 | var issues: [String] = [] |
| |
8 | |
| |
9 | override func setUp() { |
| |
10 | continueAfterFailure = true |
| |
11 | app = XCUIApplication() |
| |
12 | app.launchArguments += ["-onboarded", "YES", "-locationMode", "city", "-nightMode", "off"] |
| |
13 | app.launch() |
| |
14 | } |
| |
15 | |
| |
16 | private func audit(_ screen: String, _ types: XCUIAccessibilityAuditType = .all) { |
| |
17 | try? app.performAccessibilityAudit(for: types) { issue in |
| |
18 | let label = issue.element?.label ?? "" |
| |
19 | let id = issue.element?.identifier ?? "" |
| |
20 | let line = "[\(screen)] \(issue.auditType): \(issue.compactDescription) — element: '\(label)' \(id)" |
| |
21 | self.issues.append(line) |
| |
22 | print("A11Y " + line) |
| |
23 | return true |
| |
24 | } |
| |
25 | } |
| |
26 | |
| |
27 | private func tab(_ name: String) { |
| |
28 | app.tabBars.buttons[name].firstMatch.tap() |
| |
29 | sleep(3) |
| |
30 | } |
| |
31 | |
| |
32 | func testAuditAllScreens() { |
| |
33 | sleep(3) |
| |
34 | audit("Sky") |
| |
35 | app.buttons["Settings"].tap(); sleep(1) |
| |
36 | audit("Settings") |
| |
37 | app.buttons["Done"].tap(); sleep(1) |
| |
38 | tab("System"); audit("System") |
| |
39 | tab("Sun"); audit("Sun") |
| |
40 | tab("Launches"); audit("Launches") |
| |
41 | tab("Library"); audit("Library") |
| |
42 | app.staticTexts["Planets"].firstMatch.tap(); sleep(1) |
| |
43 | app.staticTexts["Saturn"].firstMatch.tap(); sleep(2) |
| |
44 | audit("Library page: Saturn") |
| |
45 | print("ACCESSIBILITY-AUDIT-BEGIN") |
| |
46 | issues.forEach { print($0) } |
| |
47 | print("ACCESSIBILITY-AUDIT-END \(issues.count) issues") |
| |
48 | } |
| |
49 | } |
App/project.yml
+16
| @@ -10,6 +10,8 @@ targets: |
| 10 | Moonrise: |
10 | Moonrise: |
| 11 | type: application |
11 | type: application |
| 12 | platform: iOS |
12 | platform: iOS |
| |
13 | scheme: |
| |
14 | testTargets: [MoonriseUITests] |
| 13 | sources: [Sources, Shared] |
15 | sources: [Sources, Shared] |
| 14 | dependencies: |
16 | dependencies: |
| 15 | - package: MoonriseKit |
17 | - package: MoonriseKit |
| @@ -81,3 +83,17 @@ targets: |
| 81 | TARGETED_DEVICE_FAMILY: "1" |
83 | TARGETED_DEVICE_FAMILY: "1" |
| 82 | MARKETING_VERSION: "1.0" |
84 | MARKETING_VERSION: "1.0" |
| 83 | CURRENT_PROJECT_VERSION: "1" |
85 | CURRENT_PROJECT_VERSION: "1" |
| |
86 | MoonriseUITests: |
| |
87 | type: bundle.ui-testing |
| |
88 | platform: iOS |
| |
89 | sources: [UITests] |
| |
90 | dependencies: |
| |
91 | - target: Moonrise |
| |
92 | settings: |
| |
93 | base: |
| |
94 | PRODUCT_BUNDLE_IDENTIFIER: sh.krz.moonrise.uitests |
| |
95 | DEVELOPMENT_TEAM: ZCNAX3VL9D |
| |
96 | CODE_SIGN_STYLE: Automatic |
| |
97 | SWIFT_VERSION: "5.0" |
| |
98 | TARGETED_DEVICE_FAMILY: "1" |
| |
99 | GENERATE_INFOPLIST_FILE: YES |