The popover/sheet split lands: a board-scoped, titled, sectioned sheet on the board window hosts everything setup-shaped, opened from the popover's Board Settings… row and the new Board ▸ Board Settings… menu row. The three existing setup controls relocate — add-git (whose noteFormVisible lines now make the sheet the form-anchored failure surface), branch creation (a standing field; create-and-switch runs the identical settle sequence), and the commit-identity fields (the 2s visibility-scoped poll rides with them). The popover keeps the daily face and its postures; its Pro/mode-none section becomes header + door (.addGit renamed .noRepository). Availability is derived from the section inventory (Pro + mode none or git), so pro-m2's sections can't drift from the doors; the sheet's fields join the caret-chord disable set. The audit suite pins what the free fixture can reach; the sheet's own audit is manual until a tier override is ruled. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
178 lines
7.8 KiB
Swift
178 lines
7.8 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Kanban
|
|
|
|
/// **The board settings sheet's two pure seams** (03-board-ui.md ▸ Board settings sheet, ruled
|
|
/// 2026-07-31 — the popover/sheet split; 04-interactions.md ▸ The map's configuration carve-out):
|
|
/// what the sheet holds for a board, and therefore whether the sheet exists for that board at all.
|
|
///
|
|
/// They are pinned here for `BoardGitSection.resolve`'s reason one surface over: the *inventory* is
|
|
/// the decision worth asserting and the SwiftUI that renders it is not. Every case below is a plain
|
|
/// value — no board on disk, no window, no session.
|
|
@Suite("Board settings sheet ▸ the sections")
|
|
struct BoardSettingsSectionTests {
|
|
|
|
@Test("Pro, mode none: the sheet is add-git and nothing else")
|
|
func modeNoneHoldsAddGit() {
|
|
// "add-git (mode none; opt-in init — 06)" — 03's own first entry for this surface, and the
|
|
// control that moved here out of the popover with the split.
|
|
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .none) == [.git])
|
|
}
|
|
|
|
@Test("Pro, git mode: branch creation and the commit identity, in that order")
|
|
func gitModeHoldsCreationAndIdentity() {
|
|
// "branch creation (switching stays in the popover…)" and "commit identity name/email (06 —
|
|
// the visibility-scoped 2 s config re-read rides with the fields)".
|
|
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
|
|
}
|
|
|
|
@Test("Every section is reachable from some posture, and no posture invents one")
|
|
func theInventoryIsTotal() {
|
|
let offered = Set(
|
|
Tier.allCases.flatMap { tier in
|
|
BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(tier: tier, mode: $0) }
|
|
}
|
|
)
|
|
|
|
#expect(offered == Set(BoardSettingsSection.allCases))
|
|
}
|
|
|
|
@Test("The sections carry the headers VoiceOver navigates by")
|
|
func headersAreNamed() {
|
|
// 10-accessibility.md ▸ Board settings sheet: "titled and sectioned with headers VoiceOver
|
|
// can navigate by". The strings are the surface's spoken structure, so they are stated once
|
|
// and pinned once.
|
|
#expect(BoardSettingsSection.git.title == "Git")
|
|
#expect(BoardSettingsSection.branch.title == "Branch")
|
|
#expect(BoardSettingsSection.commitIdentity.title == "Commit Identity")
|
|
}
|
|
}
|
|
|
|
/// **Where the sheet can be opened from, tier by mode** — the answer both doors validate on: Board ▸
|
|
/// Board Settings…'s `disabled` state, and whether the popover's git section renders its Board
|
|
/// Settings… row at all.
|
|
@Suite("Board settings sheet ▸ availability")
|
|
struct BoardSettingsAvailabilityTests {
|
|
|
|
@Test("The whole matrix: Pro on a none-or-git board, and nowhere else")
|
|
func theMatrix() {
|
|
// Pro, and a board whose mode leaves something to set up.
|
|
#expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .none))
|
|
#expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .git))
|
|
|
|
// **Pro, repo-nested**: "nothing setup-shaped can apply" — no add-git (06's prose, not a
|
|
// disabled button), no branch of ours to create, no repo-local config of ours to write. The
|
|
// popover's explanation stands and no door opens.
|
|
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .repoNested))
|
|
|
|
// **The free tier**: no setup exists there at all (12-editions.md ▸ The free tier and
|
|
// `.git`), whatever mode a stray value claims — detection never runs off Pro, so the mode is
|
|
// swept for completeness rather than because it can vary.
|
|
for mode in BoardGitMode.allCases {
|
|
#expect(
|
|
!BoardSettingsAvailability.resolve(tier: .free, mode: mode),
|
|
"the free tier has no board settings sheet in any mode"
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test("Reachable means exactly 'has something to show'")
|
|
func reachabilityIsTheInventory() {
|
|
// The derivation, not a coincidence: a surface whose whole job is hosting setup controls has
|
|
// no honest empty state, so the two answers are one answer. pro-m2's sections join the
|
|
// inventory and this identity keeps holding.
|
|
for tier in Tier.allCases {
|
|
for mode in BoardGitMode.allCases {
|
|
#expect(
|
|
BoardSettingsAvailability.resolve(tier: tier, mode: mode)
|
|
== !BoardSettingsSection.resolve(tier: tier, mode: mode).isEmpty
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// **The window's sheet flag** (`BoardSettingsPresentation`) — `BoardInfoPresentation`'s sibling,
|
|
/// with the one thing the popover flag does not carry: the session posture both doors validate on.
|
|
@MainActor
|
|
@Suite("Board settings sheet ▸ the window's presentation")
|
|
struct BoardSettingsPresentationTests {
|
|
|
|
@Test("It starts closed, unadopted, and unreachable")
|
|
func startsClosed() {
|
|
let presentation = BoardSettingsPresentation()
|
|
|
|
#expect(presentation.isPresented == false)
|
|
#expect(presentation.tier == .free)
|
|
#expect(presentation.git == nil)
|
|
// The free tier's posture is the safe default for a window whose session has not been adopted
|
|
// yet: an unreachable sheet, rather than a sheet with no sections in it.
|
|
#expect(presentation.isReachable == false)
|
|
#expect(presentation.sections.isEmpty)
|
|
}
|
|
|
|
@Test("An unreachable board's sheet refuses to present")
|
|
func presentRefusesWhereUnreachable() {
|
|
let presentation = BoardSettingsPresentation()
|
|
|
|
presentation.present()
|
|
#expect(presentation.isPresented == false, "the free tier has no sheet to open")
|
|
}
|
|
|
|
@Test("Two windows hold their own flags")
|
|
func perWindow() {
|
|
// `BoardInfoPresentation`'s rule, restated for the sheet: "two board windows each hold their
|
|
// own and can never toggle each other's".
|
|
let first = BoardSettingsPresentation()
|
|
let second = BoardSettingsPresentation()
|
|
|
|
first.isPresented = true
|
|
#expect(second.isPresented == false)
|
|
}
|
|
|
|
@Test("Adopting a Pro git session makes the sheet reachable, and dismissal is idempotent")
|
|
func adoptingASession() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
|
|
// Mode `none` — the add-git posture, which is the sheet's whole job on a board with no
|
|
// repository yet.
|
|
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
|
|
#expect(git.mode == .none)
|
|
|
|
let presentation = BoardSettingsPresentation()
|
|
presentation.adopt(tier: .pro, git: git)
|
|
|
|
#expect(presentation.isReachable)
|
|
#expect(presentation.sections == [.git])
|
|
|
|
presentation.present()
|
|
#expect(presentation.isPresented)
|
|
|
|
presentation.dismiss()
|
|
presentation.dismiss()
|
|
#expect(presentation.isPresented == false)
|
|
}
|
|
|
|
@Test("The free tier's session leaves both doors shut, .git on the folder or not")
|
|
func aFreeTierSessionIsUnreachable() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
|
|
|
|
// `compose` is the tier gate: the free tier gets no git state at all, so there is nothing for
|
|
// a settings sheet to be about even on a board carrying an inert `.git` — the popover's
|
|
// one-line Pro pointer is that board's whole story (12-editions.md).
|
|
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
|
|
|
|
let presentation = BoardSettingsPresentation()
|
|
presentation.adopt(tier: .free, git: nil)
|
|
|
|
#expect(presentation.isReachable == false)
|
|
presentation.present()
|
|
#expect(presentation.isPresented == false)
|
|
}
|
|
}
|