208 lines
9.8 KiB
Swift
208 lines
9.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("Pro, unverifiable: the sheet has nothing to show — structurally like repo-nested")
|
|
func unverifiableHoldsNothing() {
|
|
// "Denial is not absence" (06 ▸ Rules ▸ Detection, ruled 2026-07-31): a denied ancestor check
|
|
// can never be told apart from a repository actually being there, so add-git stays as
|
|
// unreachable here as it is on a genuinely nested board.
|
|
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .unverifiable) == [])
|
|
}
|
|
|
|
@Test("Pro, git mode with an unreadable repository: nothing setup-shaped applies either")
|
|
func anUnreadableRepositoryHoldsNothing() {
|
|
// **The corrupt-`.git` loud failure** (06 ▸ Rules, ruled 2026-07-31): both sections here are
|
|
// *writes* to a repository — a branch created in it, an identity written into its config —
|
|
// and there is no repository the app can open to write either into. Repo-nested's emptiness,
|
|
// reached one step further along.
|
|
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true) == [])
|
|
|
|
// …and the mode is still `git` throughout: this is emptiness *within* git mode, never the
|
|
// fall to mode none that would let add-git be offered against an existing `.git`.
|
|
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
|
|
}
|
|
|
|
@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))
|
|
|
|
// **Pro, unverifiable**: the same unreachability, for the denial-not-absence reason — a
|
|
// denied ancestor check is never distinguishable from a repository actually being there.
|
|
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .unverifiable))
|
|
|
|
// **Pro, git mode over a repository that will not open**: no door either, so neither the
|
|
// popover's Board Settings… row nor the menu command offers a surface with nothing on it
|
|
// (06 ▸ Rules, the corrupt-`.git` loud failure).
|
|
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true))
|
|
|
|
// **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)
|
|
}
|
|
}
|