Files
lanework/KanbanTests/BoardInfoPopoverTests.swift
T

167 lines
7.5 KiB
Swift

import Foundation
import Testing
@testable import Kanban
/// **The board popover's git-note detection** (12-editions.md ▸ The free tier and `.git`, ruled
/// 2026-07-27): the free tier's whole git story is one line, shown only on a board that carries an
/// inert `.git`. The seam
/// that decides *whether* to show it — `BoardGitNote.hasGitDirectory(at:)` — is a pure, read-only
/// `FileManager` check, pinned here against real bytes on disk. Everything else about the note (its
/// wording, its placement in `BoardInfoView`) is SwiftUI rendering and deliberately untested.
struct BoardInfoPopoverTests {
@Test("A board with a .git at its root reports true")
func trueForABoardWithGit() 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))
#expect(BoardGitNote.hasGitDirectory(at: fixture.root))
}
@Test("An ordinary board with no .git reports false")
func falseForAnOrdinaryBoard() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
#expect(!BoardGitNote.hasGitDirectory(at: fixture.root))
}
}
/// **The popover's git slot, posture by posture** (03-board-ui.md ▸ Board popover; 06-history-undo.md
/// ▸ Rules; 12-editions.md ▸ The free tier and `.git`).
///
/// `BoardGitSection.resolve` is the whole decision, pulled out as a pure function of the tier and the
/// board's mode precisely so the matrix is assertable — the views it selects are SwiftUI and stay
/// untested, exactly as the note's wording and placement do above.
@Suite("Board popover ▸ the git section's posture")
struct BoardGitSectionTests {
@Test("The free tier: absent on an ordinary board, a Pro pointer on a board carrying an inert .git")
func theFreeTierIsContextual() {
#expect(BoardGitSection.resolve(tier: .free, mode: .none, hasGitDirectory: false) == .absent)
#expect(BoardGitSection.resolve(tier: .free, mode: .none, hasGitDirectory: true) == .proPointer)
}
@Test("Pro: mode none has no repository to describe, git mode shows the branch")
func proFollowsTheMode() {
#expect(BoardGitSection.resolve(tier: .pro, mode: .none, hasGitDirectory: false) == .noRepository)
#expect(BoardGitSection.resolve(tier: .pro, mode: .git, hasGitDirectory: true) == .branch)
}
@Test("A repo-nested board explains itself — setup is absent, not disabled")
func repoNestedExplainsRatherThanDisables() {
let section = BoardGitSection.resolve(tier: .pro, mode: .repoNested, hasGitDirectory: false)
// 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(tier: .pro, mode: .unverifiable, hasGitDirectory: false)
#expect(section == .unverifiable)
#expect(section != .repoNested, "a denial is not a nesting")
#expect(section != .noRepository)
}
@Test("Every posture is reachable, and none of them is two postures")
func theMatrixIsTotal() {
let resolved = Set(
Tier.allCases.flatMap { tier in
BoardGitMode.allCases.flatMap { mode in
[true, false].map { BoardGitSection.resolve(tier: tier, mode: mode, hasGitDirectory: $0) }
}
}
)
#expect(resolved == Set(BoardGitSection.allCases))
}
}
/// **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
/// tier/mode inputs 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.
@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, tier: .free, mode: .none, branch: nil
)
#expect(named.title == "Sprint 12")
let untitled = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .free, mode: .none, branch: nil
)
#expect(untitled.title == "My Board")
let emptyTitled = BoardInfoTitlebarSummary(
snapshotTitle: "", rootURL: root, tier: .free, 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 under Pro, in git mode, once it has been read")
func branchShowsForProGitMode() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: "main"
)
#expect(summary.branch == "main")
}
@Test("The free tier never shows a branch, whatever the mode or the git state hands it")
func theFreeTierNeverShowsABranch() {
for mode in BoardGitMode.allCases {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .free, mode: mode, branch: "main"
)
#expect(summary.branch == nil, "detection never runs under the free tier, so a stray branch value must never surface")
}
}
@Test("An inert .git under Pro — mode none, repo-nested, or unverifiable — never shows a branch")
func inertGitNeverShowsABranch() {
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .none, branch: "main").branch == nil
)
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .repoNested, branch: "main").branch
== nil
)
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .unverifiable, branch: "main").branch
== nil
)
}
@Test("A git-mode board whose branch has not been read yet shows none, honestly")
func unreadBranchShowsNone() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: nil
)
#expect(summary.branch == nil)
}
}