Files
lanework/KanbanTests/BoardInfoPopoverTests.swift
T
rzen c67c1037f1 The strip grows a Sync tab — a standing placeholder, nothing in it yet
BoardInfoTab gains .sync last in the strip, rendering BoardSyncTabView's one
honest caption. The tab claims the position where 07's remote half will
live without ruling where its setup surfaces land — that stays the open
Redesign card's question.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-07 22:00:19 -04:00

171 lines
8.5 KiB
Swift

import Foundation
import Testing
@testable import Kanban
/// **The popover's git slot, posture by posture** (03-board-ui.md ▸ Board popover ▸ Git tab;
/// 06-history-undo.md ▸ Rules).
///
/// `BoardGitSection.resolve` is the whole decision, pulled out as a pure function of the board's
/// **mode** precisely so the matrix is assertable — the views it selects are SwiftUI and stay
/// untested.
///
/// **The tier axis came out at 12-editions.md ▸ PIVOT 2026-08-07** (03's Git-tab note records the
/// consequence): git left the paywall, so the two free-tier postures — `.absent` on an ordinary
/// board and the `.proPointer` on one carrying an inert `.git`, both settled 2026-07-27 — described
/// a gate that no longer exists and retired with it. With them went the `.git`-on-disk probe that
/// fed them (`BoardGitNote.hasGitDirectory(at:)`) and the tab-strip membership filter that read
/// `.absent` (`BoardInfoTab.available`, whose suite retired here the same day): detection now runs at
/// every board open on every tier, and the mode is the one input left.
@Suite("Board popover ▸ the git section's posture")
struct BoardGitSectionTests {
@Test("Mode none has no repository to describe, git mode shows the branch")
func thePostureFollowsTheMode() {
#expect(BoardGitSection.resolve(mode: .none) == .noRepository)
#expect(BoardGitSection.resolve(mode: .git) == .branch)
}
@Test("A repo-nested board explains itself — setup is absent, not disabled")
func repoNestedExplainsRatherThanDisables() {
let section = BoardGitSection.resolve(mode: .repoNested)
// The design is insistent here: "not a hidden 'add git' but a short explanation … the option
// is absent because it *can't* apply, and the UI should teach that rather than look broken"
// (06 ▸ Rules). A posture that rendered a disabled setup control would satisfy neither half —
// and since the 2026-07-31 split the same sentence is what keeps the settings sheet itself
// out of reach on such a board (`BoardSettingsAvailabilityTests`).
#expect(section == .repoNested)
#expect(section != .noRepository)
}
@Test("An unverifiable board gets its own posture — structurally like nested, never the same case")
func unverifiableIsItsOwnPostureNotRepoNested() {
// "Denial is not absence" (06 ▸ Rules ▸ Detection, ruled 2026-07-31): a denied ancestor check
// is not a found repository, so the two must resolve to different cases even though both are
// action-less, prose-only sections.
let section = BoardGitSection.resolve(mode: .unverifiable)
#expect(section == .unverifiable)
#expect(section != .repoNested, "a denial is not a nesting")
#expect(section != .noRepository)
}
/// **A board whose repository will not open keeps the git section it has** (06 ▸ Rules, the
/// corrupt-`.git` loud failure, ruled 2026-07-31) — the *branch* section, held and explaining
/// itself, never the mode-none posture that would imply a board with no history to have.
///
/// The section case is deliberately blind to readability: what changes on such a board is what
/// the branch surface inside it says (`BoardGitBranchSurface.resolve`, whose broken presentation
/// and own sentence `BoardGitBranchSurfaceTests` pins), not which section the popover shows. The
/// posture matrix stays a function of the mode alone.
@Test("An unreadable repository is still the branch section — never the no-repository posture")
func anUnreadableRepositoryKeepsTheBranchSection() {
let section = BoardGitSection.resolve(mode: .git)
#expect(section == .branch)
#expect(section != .noRepository, "the board has a repository; it is unreadable, not absent")
}
@Test("Every posture is reachable, and none of them is two postures")
func theMatrixIsTotal() {
let resolved = Set(BoardGitMode.allCases.map { BoardGitSection.resolve(mode: $0) })
// One posture per mode, and every posture spoken for — which is also what makes the tab
// strip's membership rule trivially true since the pivot: no mode resolves to nothing, so
// the Git tab is never dropped (03-board-ui.md ▸ Board popover, the pivot note).
#expect(resolved == Set(BoardGitSection.allCases))
#expect(resolved.count == BoardGitMode.allCases.count, "no two modes share a posture")
}
}
/// **The tab strip carries every tab, always** (03-board-ui.md ▸ Board popover, the tabbed-
/// popover paragraph as the 2026-08-07 pivot note leaves it): "the absent posture is unreachable and
/// every board carries all three tabs. The membership rule stands structurally — the strip still asks
/// the posture — it just never hears 'absent' anymore." (Three became four the same night: **Sync**
/// joined as a standing placeholder, membership still `allCases`.)
///
/// So there is no `available(…)` seam left to pin; what is worth keeping is the strip's *order*,
/// which the popover's `Picker` takes from `allCases` and which the Git session fixed: Info first —
/// it is the board's face and the tab selection resets to it on every open — then Theme, then Git,
/// then the Sync placeholder at the end of the line, where a surface with nothing in it belongs.
@Suite("Board popover ▸ the tab strip")
struct BoardInfoTabStripTests {
@Test("Info, Theme, Git, Sync — in that order, on every board")
func theStripIsTheWholeSet() {
#expect(BoardInfoTab.allCases == [.info, .theme, .git, .sync])
}
@Test("The raw values are the segmented control's own labels")
func labelsAreTheRawValues() {
// The strip needs no separate label function, which is the only reason this enum is
// `String`-backed at all.
#expect(BoardInfoTab.allCases.map(\.rawValue) == ["Info", "Theme", "Git", "Sync"])
}
}
/// **The window-title widget's two strings** (03-board-ui.md ▸ Board popover, the card that widened
/// the widget from a chevron to the whole board-name area): `BoardInfoTitlebarSummary` is the pure
/// function this pins, exactly as `BoardGitSectionTests` above pins the section it shares its mode
/// input with. No disk I/O — the title half of this seam takes a raw `URL`, not a `BoardStore`, so a
/// plain `/tmp/...` path is enough.
///
/// **The tier axis came out at 12-editions.md ▸ PIVOT 2026-08-07.** The branch rule read `tier ==
/// .pro && mode == .git` until that day; git is tier-independent now, so a git-mode board shows its
/// branch and nothing else does.
@Suite("Board popover ▸ the widget's strings")
struct BoardInfoTitlebarSummaryTests {
private let root = URL(fileURLWithPath: "/tmp/My Board.board")
@Test("The title is the on-disk title, falling back to the folder name sans extension")
func titleFallsBackToTheFolderName() {
let named = BoardInfoTitlebarSummary(
snapshotTitle: "Sprint 12", rootURL: root, mode: .none, branch: nil
)
#expect(named.title == "Sprint 12")
let untitled = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: .none, branch: nil
)
#expect(untitled.title == "My Board")
let emptyTitled = BoardInfoTitlebarSummary(
snapshotTitle: "", rootURL: root, mode: .none, branch: nil
)
#expect(
emptyTitled.title == "My Board",
"an empty title reads the same as no title at all — the window-title rule this seam restates"
)
}
@Test("The branch shows in git mode, once it has been read")
func branchShowsInGitMode() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: .git, branch: "main"
)
#expect(summary.branch == "main")
}
@Test("No other mode shows a branch, whatever the git state hands it")
func onlyGitModeShowsABranch() {
for mode in BoardGitMode.allCases where mode != .git {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: mode, branch: "main"
)
#expect(
summary.branch == nil,
"mode \(mode): a board the app manages no repository for must never surface a stray branch value"
)
}
}
@Test("A git-mode board whose branch has not been read yet shows none, honestly")
func unreadBranchShowsNone() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: .git, branch: nil
)
#expect(summary.branch == nil)
}
}