import Foundation import Testing @testable import Kanban /// Template instantiation — the on-disk half of File ▸ New Board… (09-templates.md ▸ Instantiation). /// /// The window flow around it (the chooser, `NSSavePanel`, the open that follows) is untestable /// without a screen and deliberately holds no rules of its own; everything that *is* a rule — what /// gets written, what the board is called, and the order the lanes land in — lives in /// `BoardTemplate.instantiate(at:)` and is checked here against a real temp folder, through the /// app's own loader. // MARK: - Fixtures /// An empty temp folder to create *into* — the save panel's answer, minus the panel. private func temporaryLocation(named name: String) throws -> (url: URL, tearDown: () -> Void) { let container = FileManager.default.temporaryDirectory .appendingPathComponent("BoardTemplateTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: container, withIntermediateDirectories: true) return (container.appendingPathComponent(name, isDirectory: true), { try? FileManager.default.removeItem(at: container) }) } // MARK: - Tests @Suite("BoardTemplate") struct BoardTemplateTests { @Test("Basic writes a board and its three lanes, in order") func basicInstantiatesInOrder() throws { let location = try temporaryLocation(named: "Roadmap.kanban") defer { location.tearDown() } try BoardTemplate.basic.instantiate(at: location.url) let result = try BoardLoader.load(boardRoot: location.url) #expect(result.warnings.isEmpty, "a board this app just wrote must load clean") #expect(result.model.lanes.map { $0.title.value } == ["To Do", "Doing", "Done"]) #expect(result.model.lanes.allSatisfy { !$0.isDeleted }) #expect(result.model.lanes.allSatisfy { $0.cards.isEmpty }, "the Basic scaffold seeds no cards") } @Test("The lanes are ranked by the board convention — 1024 apart, from 1024") func lanesAreRankedByTheAppendConvention() throws { let location = try temporaryLocation(named: "Ranked.kanban") defer { location.tearDown() } try BoardTemplate.basic.instantiate(at: location.url) let orders = try BoardLoader.load(boardRoot: location.url).model.lanes.map(\.order) #expect(orders == [1024, 2048, 3072], "each lane appends after the one before it") } @Test("The board's title is the document name the user chose, not the template's") func titleComesFromTheChosenName() throws { let location = try temporaryLocation(named: "Q3 Planning.kanban") defer { location.tearDown() } try BoardTemplate.basic.instantiate(at: location.url) // 01-storage-format.md § Board naming: display name and folder name start out matching. #expect(try BoardLoader.load(boardRoot: location.url).model.title.value == "Q3 Planning") } @Test("An extension-less location is as legal a board, and keeps its whole name as the title") func extensionlessLocationWorks() throws { let location = try temporaryLocation(named: "Plain") defer { location.tearDown() } try BoardTemplate.basic.instantiate(at: location.url) #expect(try BoardLoader.load(boardRoot: location.url).model.title.value == "Plain") } @Test("Every lane folder is a fresh lowercase UUID") func laneFoldersAreMintedIdentities() throws { let location = try temporaryLocation(named: "Minted.kanban") defer { location.tearDown() } try BoardTemplate.basic.instantiate(at: location.url) let ids = try BoardLoader.load(boardRoot: location.url).model.lanes.map(\.id.rawValue) #expect(ids.count == 3) #expect(Set(ids).count == 3, "three lanes, three identities") #expect(ids.allSatisfy { $0 == $0.lowercased() }, "the app emits lowercase UUIDs") #expect(ids.allSatisfy { UUID(uuidString: $0) != nil }) } @Test("Instantiating over an existing board refuses rather than clobbering it") func refusesToOverwriteAnExistingBoard() throws { let location = try temporaryLocation(named: "Taken.kanban") defer { location.tearDown() } try BoardTemplate.basic.instantiate(at: location.url) let before = try Data(contentsOf: location.url.appendingPathComponent("index.md")) let error = writeFailure { try BoardTemplate.basic.instantiate(at: location.url) } #expect(error?.operation == .createBoard) #expect( try Data(contentsOf: location.url.appendingPathComponent("index.md")) == before, "the existing board is untouched — a create never replaces one" ) } @Test("The document name behind a chosen URL is the folder name without its extension") func documentNameStripsTheExtension() { #expect(BoardTemplate.documentName(of: URL(fileURLWithPath: "/Boards/Roadmap.kanban")) == "Roadmap") #expect(BoardTemplate.documentName(of: URL(fileURLWithPath: "/Boards/Roadmap")) == "Roadmap") } @Test("The chooser offers Basic, and Basic is first") func inventoryHoldsBasicFirst() { // m9-templates: this becomes the bundled inventory's ten, with Basic still first // (09-templates.md ▸ Inventory). #expect(BoardTemplate.all.first == BoardTemplate.basic) #expect(BoardTemplate.basic.slug == "basic") } }