Four scenes (welcome, restore bootstrap, board group, card group) with system restoration disabled in favor of the registry's open-now flags: set when a window actually opens, cleared only on user close, so quit — and crash — leave exactly the restoration set behind. AppModel joins windows to sessions (shared store, registry record, card refs, held security scope); CloseFlushCoordinator pins 02's strict close order as a seam-injected machine (card sessions end, windows drain, store flushes, record stamps, teardown) with named slots where m6/m7 flushes land. HostedWindowController proxies — never replaces — SwiftUI's window delegate to intercept windowShouldClose for the flush, report frames, and place saved frames onto live screens. Card windows are (board path, case-folded card id) values: reopen focuses, and a snapshot-pure fate function dismisses on delete, tombstone, tombstoned lane, or cross-board move. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
83 lines
3.7 KiB
Swift
83 lines
3.7 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Kanban
|
|
|
|
/// A window ref is two things at once — the value `WindowGroup(for:)` keys its windows on, and the
|
|
/// argument the host is built from — so it has exactly two promises: it survives the round trip
|
|
/// through a scene's archive, and two refs naming one thing are *equal*. Both are pinned here,
|
|
/// because a failure of either is invisible until the app opens a second window for something that
|
|
/// already has one.
|
|
|
|
@Suite("Window refs")
|
|
struct WindowRefsTests {
|
|
|
|
// MARK: Codable
|
|
|
|
@Test("Both refs round-trip through JSON unchanged")
|
|
func refsRoundTripThroughCoding() throws {
|
|
let board = BoardWindowRef(path: "/Users/x/Boards/Work.kanban")
|
|
let card = CardWindowRef(boardPath: board.path, cardID: "55555555-5555-4555-8555-555555555555")
|
|
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
#expect(try decoder.decode(BoardWindowRef.self, from: encoder.encode(board)) == board)
|
|
|
|
let decodedCard = try decoder.decode(CardWindowRef.self, from: encoder.encode(card))
|
|
#expect(decodedCard == card)
|
|
#expect(decodedCard.cardID == card.cardID, "the folder's exact spelling survives, not just its value")
|
|
#expect(decodedCard.boardPath == board.path)
|
|
}
|
|
|
|
@Test("A board ref and a URL agree in both directions")
|
|
func boardRefAndURLAgree() {
|
|
let url = URL(fileURLWithPath: "/Users/x/Boards/Work.kanban", isDirectory: true)
|
|
let ref = BoardWindowRef(url: url)
|
|
#expect(ref.path == url.path)
|
|
#expect(ref.url.path == url.path)
|
|
}
|
|
|
|
// MARK: Identity
|
|
|
|
@Test("A card ref's id compares by UUID value, not by spelling")
|
|
func cardRefFoldsTheCaseOfItsCardID() {
|
|
let board = "/Users/x/Boards/Work.kanban"
|
|
let lower = CardWindowRef(boardPath: board, cardID: "55555555-5555-4555-8555-555555555555")
|
|
let upper = CardWindowRef(boardPath: board, cardID: "55555555-5555-4555-8555-555555555555".uppercased())
|
|
|
|
// The consequence that matters: `WindowGroup(for:)` keys on this equality, so a card whose
|
|
// folder is spelled in caps focuses the window the lowercase spelling already opened instead
|
|
// of opening a second one for the same card.
|
|
#expect(lower == upper)
|
|
#expect(lower.hashValue == upper.hashValue)
|
|
#expect(Set([lower, upper]).count == 1)
|
|
|
|
// And `rawValue` is untouched by any of that — it is what builds URLs.
|
|
#expect(upper.cardID == "55555555-5555-4555-8555-555555555555".uppercased())
|
|
#expect(upper.cardIdentity == lower.cardIdentity)
|
|
}
|
|
|
|
@Test("The board half of a card ref is compared verbatim")
|
|
func cardRefsOnDifferentBoardsAreDifferent() {
|
|
let card = "55555555-5555-4555-8555-555555555555"
|
|
let here = CardWindowRef(boardPath: "/Users/x/Boards/Work.kanban", cardID: card)
|
|
let there = CardWindowRef(boardPath: "/Users/x/Boards/Home.kanban", cardID: card)
|
|
|
|
// The cross-board move rule in one assertion: the card's UUID travels with it, but the key
|
|
// that named its window does not — so the window dismisses exactly like a delete.
|
|
#expect(here != there)
|
|
#expect(here.board == BoardWindowRef(path: "/Users/x/Boards/Work.kanban"))
|
|
#expect(here.boardURL.path == "/Users/x/Boards/Work.kanban")
|
|
}
|
|
|
|
@Test("A card ref built from an ItemID keeps the id's exact spelling")
|
|
func cardRefFromItemIDKeepsRawValue() {
|
|
let board = BoardWindowRef(path: "/Users/x/Boards/Work.kanban")
|
|
let id = ItemID(rawValue: "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA")
|
|
let ref = CardWindowRef(board: board, cardID: id)
|
|
|
|
#expect(ref.cardID == id.rawValue)
|
|
#expect(ref == CardWindowRef(boardPath: board.path, cardID: id.rawValue.lowercased()))
|
|
}
|
|
}
|