The golden-path XCUITest suite, adapted to current rulings where the card body had gone stale: trash flows follow the materialized-trash grammar (no Put Back, restore is drag or cut/paste out), git flows are pro-m1 scope and skipped, and fixtures extend m11's in-container --ui-test-fixture-board mechanism (the sandbox forbids the card's --open-board path handoff) with exact-match variant flags: standard (the three-lane audit board), large (8 lanes x 40 cards for masonry/reflow), malformed (BoardWriter-built board with one card's index.md overwritten to unterminated YAML, opened through the ORDINARY path so the failure is the loader's own). EndToEndFlowTests: create card/lane, inline rename, coordinate drag across lanes, cut/paste, undo/redo of a move, delete-to-trash / show-trash / restore-by-cut-paste / Empty Trash confirm - all asserting on lane accessibility labels. FailFastLaunchTests: welcome appears, no board window ever, a welcome row carries the loader's sentence naming the file; byte-fidelity of the malformed board pinned unconditionally in KanbanTests plus an identically-refused relaunch. Performance: launch metric plus explicit wall-clock gates (30s launch / 5s Show Trash on 320 cards) since XCTest baselines don't travel. Powerbox panels (template save panel, Duplicate fallback, Open) are documented as manual in EndToEndVerification.md, not faked. The smoke test now launches on the standard fixture (it launched bare before, opening the developer's real boards); README's everyday test command scopes to -only-testing:KanbanTests. Suite compiles on both schemes (build-for-testing verified); flows await a real display + automation permission to execute - run instructions in KanbanUITests/EndToEndVerification.md. +8 unit tests; 1669 green both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
170 lines
8.6 KiB
Swift
170 lines
8.6 KiB
Swift
import XCTest
|
|
|
|
/// **The automated half of 10-accessibility.md ▸ Verification**, whose first clause is the reason
|
|
/// this file is a set of tests and not a checklist:
|
|
///
|
|
/// > **Automated audits are test failures**: Xcode's accessibility audit (`performAccessibilityAudit`)
|
|
/// > runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit,
|
|
/// > raw source), welcome, template chooser, board popover.
|
|
///
|
|
/// Eight surfaces, eight tests, one audit call each. `performAccessibilityAudit` audits **the app's
|
|
/// currently displayed UI** rather than a subtree, so each test's job is entirely navigation: get the
|
|
/// surface on screen, then let the audit look at whatever is there.
|
|
///
|
|
/// ### No waiving
|
|
///
|
|
/// The audit's issue handler is where a false positive would be excused, and every test here passes
|
|
/// `nil` — no handler, nothing excused. That is the design's own posture ("violations are test
|
|
/// failures, not warnings"), and it is worth keeping literal: a handler that swallowed one issue type
|
|
/// app-wide would also swallow the next real one of that type, on a surface nobody was thinking about
|
|
/// when the waiver was written. Should a genuine platform false positive ever need excusing, it goes
|
|
/// in as a closure that matches **that one element on that one surface** and carries the reason in a
|
|
/// comment beside it — never a bare `return true`.
|
|
///
|
|
/// ### `.all`, not a narrowed set
|
|
///
|
|
/// `XCUIAccessibilityAuditType.all` is the default and stays the default. The narrower types
|
|
/// (`.contrast`, `.elementDetection`, `.hitRegion`, `.sufficientElementDescription`, `.textClipped`,
|
|
/// `.trait`) each map onto a rule 10-accessibility.md states — contrast is its ≥ 4.5:1 clause,
|
|
/// sufficient-description is its labels, trait is its selection and heading traits — so scoping any
|
|
/// of them out would be scoping out a design rule. They are named here only so a future narrowing has
|
|
/// to argue with this paragraph first.
|
|
///
|
|
/// ### The fixture board
|
|
///
|
|
/// Every test launches with `UITestLaunch.fixtureFlag`, which makes the app build a known board
|
|
/// inside its own container and open it (see `UITestLaunch` for why the board cannot simply be
|
|
/// handed over on the command line — the sandbox). The board is three lanes, six cards, one card
|
|
/// with a rich Markdown body and an attachment, and one card already in `.trash/`. That is the
|
|
/// `standard` fixture variant; the launch helpers and the other two shapes live in
|
|
/// `UITestSupport.swift`.
|
|
///
|
|
/// ### Running these
|
|
///
|
|
/// They drive the real app through the real menu bar, so the machine running them must have granted
|
|
/// the test runner Accessibility control (System Settings ▸ Privacy & Security ▸ Accessibility) and
|
|
/// must not be locked or headless. A run that cannot get automation permission fails on the first
|
|
/// `click()`, not on an accessibility defect — see `KanbanUITests/AccessibilityVerification.md`,
|
|
/// which puts this suite at the top of the manual pass for exactly that reason.
|
|
///
|
|
/// ### Reading a failure
|
|
///
|
|
/// The navigation waits are deliberately loud, and they are also the part most likely to need
|
|
/// adjusting: a window is identified by its **title** (`app.windows["…"]`, which is what
|
|
/// `navigationTitle` produces) and an on-board element by its **label**. Both are specified —
|
|
/// 11-command-nexus.md fixes the menu titles, `AccessibilityPhrases` fixes the labels — but neither
|
|
/// says which `XCUIElement.ElementType` SwiftUI will choose, and a hidden-title-bar window (welcome)
|
|
/// is the one place a title might not surface at all. So: a failure on `performAccessibilityAudit`
|
|
/// is an accessibility defect and is what this suite is for; a failure on a `waitForExistence` above
|
|
/// it is a navigation problem in *this file*, and the audit never ran.
|
|
final class AccessibilityAuditTests: XCTestCase {
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
// A failed navigation step makes every later step in that test meaningless — and an audit
|
|
// that ran against the wrong surface would report a *pass*, which is worse than a failure.
|
|
continueAfterFailure = false
|
|
}
|
|
|
|
// MARK: - The board window
|
|
|
|
/// The board as it opens: lanes, cards, the toolbar, the search field — trash hidden, which is
|
|
/// the board's default state (03-board-ui.md § Trash).
|
|
@MainActor
|
|
func testBoardWindowWithTrashHidden() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
/// The same board with View ▸ Show Trash on — "when shown, it is the last container, labeled as
|
|
/// Trash with its count" (10-accessibility.md ▸ Trash lane), holding the fixture's one trashed
|
|
/// card so the column's own card elements are audited and not just its header.
|
|
@MainActor
|
|
func testBoardWindowWithTrashShown() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
app.clickMenuItem("Show Trash", in: "View")
|
|
XCTAssertTrue(
|
|
app.element(labeled: "Trash").waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the trash column did not appear"
|
|
)
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
// MARK: - The card window
|
|
|
|
/// Preview mode — the card window's default. The fixture's rich card carries the structures 10
|
|
/// makes claims about (headings, lists, a task list, a table, a code block, a link, an image with
|
|
/// alt text), so this is the audit of "Preview renders to the accessibility tree as structured
|
|
/// text" rather than of an empty body.
|
|
@MainActor
|
|
func testCardWindowPreviewMode() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
try app.openRichCardWindow()
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
/// Edit mode — View ▸ Edit Body (⌘E), "an ordinary accessible text editor" (10 ▸ Card window).
|
|
@MainActor
|
|
func testCardWindowEditMode() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
try app.openRichCardWindow()
|
|
app.clickMenuItem("Edit Body", in: "View")
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
/// The raw-source outlet — View ▸ Raw Source (⌥⌘E), the whole `index.md` as text.
|
|
///
|
|
/// Entered from Preview rather than from Edit, because the two are mutually exclusive by design
|
|
/// ("Edit Body disables while Raw Source is active" — 05-card-window.md ▸ Raw source outlet) and
|
|
/// stacking them would be auditing a state the app does not have.
|
|
@MainActor
|
|
func testCardWindowRawSourceMode() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
try app.openRichCardWindow()
|
|
app.clickMenuItem("Raw Source", in: "View")
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
// MARK: - Welcome, the template chooser, the board popover
|
|
|
|
/// The welcome window, reached by its own Window-menu row — and reached *after* the fixture board
|
|
/// has opened, so its recents list has a row in it. An empty welcome would audit the empty state
|
|
/// and miss the rows 10 specifies ("⟨name⟩, ⟨location⟩, N lanes, M cards").
|
|
@MainActor
|
|
func testWelcomeWindow() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
app.clickMenuItem("Welcome to Lanework", in: "Window")
|
|
XCTAssertTrue(
|
|
app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the welcome window did not appear"
|
|
)
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
/// The template chooser — File ▸ New Board… (⌥⌘N), the ten bundled templates as elements
|
|
/// "labeled by title", their mini previews hidden (10 ▸ Template chooser).
|
|
@MainActor
|
|
func testTemplateChooser() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
app.clickMenuItem("New Board…", in: "File")
|
|
XCTAssertTrue(
|
|
app.windows["New Board"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the template chooser did not appear"
|
|
)
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
|
|
/// The board popover — File ▸ Board Info (⌘I): "labeled controls throughout", with the git slot's
|
|
/// information readable as text and never by colour or shape alone (10 ▸ Board popover).
|
|
@MainActor
|
|
func testBoardInfoPopover() throws {
|
|
let app = XCUIApplication.launchedWithFixtureBoard()
|
|
app.clickMenuItem("Board Info", in: "File")
|
|
XCTAssertTrue(
|
|
app.popovers.firstMatch.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the board popover did not appear"
|
|
)
|
|
try app.performAccessibilityAudit()
|
|
}
|
|
}
|