Run the accessibility verification pass
The automated half: AccessibilityAuditTests runs performAccessibilityAudit over all eight surfaces DESIGN/10 names — board with trash hidden and shown, card window in Preview/Edit/raw source, welcome, template chooser, board popover. One audit per test, .all audit types, no issue handler — nothing waived; a future false-positive excusal must match one element on one surface with its reason beside it. Navigation is menu-bar titles and the arrow grammar; no accessibility identifiers added to production code. The suite launches with --ui-test-fixture-board: the sandbox forbids handing the app a temp-folder path (no bookmark behind it), so the flag carries no payload and the app builds a known board inside its own container through the ordinary BoardWriter door — three lanes, six cards, a rich Markdown body with attachment, one card already in .trash/ — with the registry redirected to the same scratch directory so audit runs never pollute real recents. LaunchPlan replaces the restore Bool (welcome / restoreBoards / uiTestFixture, fixture wins outright), decided once in KanbanApp.init and dispatched by RestoreBootstrapView; pure and pinned by UITestLaunchTests, and the fixture itself is materialized and read back through BoardLoader in units — the only proof available headlessly. The manual half: KanbanUITests/AccessibilityVerification.md is the one document — the audit suite at the top (it needs a real display and Accessibility permission), the per-release VoiceOver smoke script with expected utterances quoted from AccessibilityPhrases, and the consolidated m11 checklist from all four implementation cards. 1588 unit tests green, UI target compiles, both schemes build. The audit run and smoke script await a real display — the manual pass is the user's. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -12,8 +12,8 @@ import os
|
||||
/// Window menu — whose only job is to run the pass and then dismiss itself. It exists for a few
|
||||
/// hundred milliseconds and never draws.
|
||||
///
|
||||
/// It is presented **only** when there is something to restore (`KanbanApp` decides), so the ordinary
|
||||
/// launch-to-welcome path never creates it.
|
||||
/// It is presented **only** when there is something to open (`KanbanApp` decides, via
|
||||
/// `LaunchPlan.presentsBootstrap`), so the ordinary launch-to-welcome path never creates it.
|
||||
///
|
||||
/// ### What the pass does
|
||||
///
|
||||
@@ -23,8 +23,18 @@ import os
|
||||
/// silent drop." Welcome comes up only if nothing was even attempted; a board that *was* attempted
|
||||
/// and then failed to load opens welcome from its own host, which is the same rule applied one layer
|
||||
/// down and keeps this pass from having to wait on loads it did not perform.
|
||||
///
|
||||
/// ### And one other pass, for the same reason
|
||||
///
|
||||
/// The accessibility audit suite's fixture board (`UITestLaunch`) is built and opened here too. It is
|
||||
/// the same job with a different source — filesystem work that must happen before the first real
|
||||
/// window, needing `openWindow` to finish — and giving it a second throwaway window would be a second
|
||||
/// copy of everything this file explains. Which pass runs is `plan`'s to say and nothing else's.
|
||||
struct RestoreBootstrapView: View {
|
||||
|
||||
/// Decided in `KanbanApp.init()`; this view only dispatches on it.
|
||||
let plan: LaunchPlan
|
||||
|
||||
@Environment(AppModel.self) private var appModel
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.dismissWindow) private var dismissWindow
|
||||
@@ -56,6 +66,20 @@ struct RestoreBootstrapView: View {
|
||||
// the app's first act, and `openBoard` needs the action now.
|
||||
appModel.captureWindowActions(open: openWindow, dismiss: dismissWindow)
|
||||
|
||||
switch plan {
|
||||
case .uiTestFixture:
|
||||
openFixtureBoard()
|
||||
case .restoreBoards, .welcome:
|
||||
// `.welcome` never presents this window, so it cannot arrive here — and if a future
|
||||
// launch path let it, the restoration pass is the harmless answer: it finds nothing
|
||||
// flagged and shows welcome, which is what `.welcome` asked for anyway.
|
||||
restoreFlaggedBoards()
|
||||
}
|
||||
|
||||
dismissWindow(id: WindowID.restoreBootstrap)
|
||||
}
|
||||
|
||||
private func restoreFlaggedBoards() {
|
||||
var attempted = 0
|
||||
for board in appModel.boardRegistry.restorables() {
|
||||
switch board {
|
||||
@@ -74,6 +98,25 @@ struct RestoreBootstrapView: View {
|
||||
if attempted == 0 {
|
||||
appModel.showWelcome()
|
||||
}
|
||||
dismissWindow(id: WindowID.restoreBootstrap)
|
||||
}
|
||||
|
||||
/// The audit suite's board: built here, opened through the same `openBoard` every other path
|
||||
/// uses, so it registers, bookmarks and titles itself exactly like a board the user opened.
|
||||
///
|
||||
/// **A failure lands on welcome as an ordinary launch failure**, with the fixture's own path on
|
||||
/// it. That is deliberate: a suite whose fixture failed to build would otherwise audit an empty
|
||||
/// screen and pass, which is the one outcome an accessibility gate must never produce.
|
||||
private func openFixtureBoard() {
|
||||
do {
|
||||
let url = try UITestLaunch.materializeFixtureBoard()
|
||||
appModel.openBoard(at: url)
|
||||
} catch {
|
||||
Self.logger.error("the UI-test fixture board could not be built: \(error.localizedDescription, privacy: .public)")
|
||||
appModel.recordLaunchFailure(
|
||||
path: UITestLaunch.fixtureBoardURL.path,
|
||||
message: "The UI-test fixture board could not be built: \(error.localizedDescription)"
|
||||
)
|
||||
appModel.showWelcome()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user