import Testing @testable import Kanban /// `caretChordsYield(boardInfo:boardSettings:search:)` — 04-interactions.md ▸ Grammar's caret-chords /// rule as one expression, and `BoardCommands.swift`'s single seam for it: Board ▸ Move Left/Move /// Right ⌘←/⌘→ and the lane-width pair ⌥⌘←/⌥⌘→ disable via menu validation whenever *any* text /// control has keyboard focus, because ⌘←/⌘→ are the standard line-start/end caret chords and an /// enabled key equivalent fires before a field ever sees the key. /// /// The function reads exactly three flags and nothing else, so every test here constructs /// `BoardInfoPresentation`, `BoardSettingsPresentation` and `BoardSearchPresentation` directly rather /// than through a `BoardStore` — a fixture that stood up a board would be exercising machinery this /// seam never touches. @MainActor @Suite("caretChordsYield ▸ the caret-chords rule") struct CaretChordTests { // MARK: Scopeless windows @Test("A card window publishes neither presentation, and the chords stay enabled") func scopelessWindowDoesNotYield() { // "Card-window fields need nothing: those windows never publish a boardStore, so both items // are already scopeless there" (caretChordsYield's doc comment) — nil/nil is that window's // steady state, not a corner case. #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: nil) == false) } // MARK: The board popover, alone @Test("The board popover open yields; closed, it does not") func popoverPresence() { let boardInfo = BoardInfoPresentation() #expect(caretChordsYield(boardInfo: boardInfo, boardSettings: nil, search: nil) == false, "closed by default") boardInfo.isPresented = true #expect(caretChordsYield(boardInfo: boardInfo, boardSettings: nil, search: nil) == true) boardInfo.isPresented = false #expect(caretChordsYield(boardInfo: boardInfo, boardSettings: nil, search: nil) == false, "closing re-enables the chords") } // MARK: The settings sheet, alone @Test("The settings sheet open yields; closed, it does not") func settingsSheetPresence() { // The sheet joined this seam with the 2026-07-31 popover/sheet split: it is where the branch // name field and the commit-identity fields live now, so the surface that used to be covered // by "the popover is open" has to be covered by "the sheet is up" as well. let settings = BoardSettingsPresentation() #expect(caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == false, "closed by default") settings.isPresented = true #expect(caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == true) settings.isPresented = false #expect( caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == false, "dismissing re-enables the chords" ) } @Test("The sheet's own reachability is not this seam's question — a presented sheet yields either way") func presentationRatherThanReachability() { // `isReachable` gates the two *doors* (`BoardSettingsAvailability`); this function reads the // flag that says a sheet is on screen. A presentation left unadopted reads free-tier and // therefore unreachable, and if something ever put its flag up anyway the chords must still // yield — what a caret chord competes with is a field that exists, not a tier. let settings = BoardSettingsPresentation() #expect(settings.isReachable == false, "unadopted reads as the free tier") settings.isPresented = true #expect(caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == true) } // MARK: The search field, alone @Test("The search field focused yields; unfocused, it does not") func searchFocus() { let search = BoardSearchPresentation() #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: search) == false, "unfocused by default") search.isFocused = true #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: search) == true) search.isFocused = false #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: search) == false, "losing focus re-enables the chords") } // MARK: Both surfaces together @Test("Either surface active is enough to yield; both absent-or-inert is the only false") func eitherSurfaceIsSufficient() { let closedInfo = BoardInfoPresentation() let openInfo = BoardInfoPresentation() openInfo.isPresented = true let unfocusedSearch = BoardSearchPresentation() let focusedSearch = BoardSearchPresentation() focusedSearch.isFocused = true // One side published and inert, the other absent (the still-loading-window shape): false. #expect(caretChordsYield(boardInfo: closedInfo, boardSettings: nil, search: nil) == false) #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: unfocusedSearch) == false) // One side published and active, the other absent: true. #expect(caretChordsYield(boardInfo: openInfo, boardSettings: nil, search: nil) == true) #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: focusedSearch) == true) // Both published, both inert: false — the popover being open at all and the field holding // focus are each read independently, so neither's mere presence counts on its own. #expect(caretChordsYield(boardInfo: closedInfo, boardSettings: nil, search: unfocusedSearch) == false) // Both published, one or both active: true. This is an `||`, not an `&&` — one live text // surface is enough to send the chords to it, whatever the other surface is doing. #expect(caretChordsYield(boardInfo: openInfo, boardSettings: nil, search: unfocusedSearch) == true) #expect(caretChordsYield(boardInfo: closedInfo, boardSettings: nil, search: focusedSearch) == true) #expect(caretChordsYield(boardInfo: openInfo, boardSettings: nil, search: focusedSearch) == true) // And the third surface composes the same way — inert beside two inert siblings, sufficient // beside them when it is up. let closedSheet = BoardSettingsPresentation() let openSheet = BoardSettingsPresentation() openSheet.isPresented = true #expect(caretChordsYield(boardInfo: closedInfo, boardSettings: closedSheet, search: unfocusedSearch) == false) #expect(caretChordsYield(boardInfo: closedInfo, boardSettings: openSheet, search: unfocusedSearch) == true) #expect(caretChordsYield(boardInfo: openInfo, boardSettings: openSheet, search: focusedSearch) == true) } // MARK: Inline editors are a different seam @Test("An inline title editor is not this seam's job — acceptsBoardMutations covers it") func inlineEditingIsAForeignConcern() { // Inline title editors are the caret-chords rule's *first* named surface (the doc comment's // list), but they are answered through `acceptsBoardMutations`, not here: `MoveLaneCommands` // and `LaneWidthCommands` each disable on `yieldsCaretChords || `, so a rename or a new-card placeholder already closes the items through that other // half of the `||` before this function is ever asked. Pinning `caretChordsYield`'s own // independence from `isEditingInline` is what keeps that split honest. let transient = TransientBoardState() transient.beginRename(of: ItemID(rawValue: "11111111-1111-4111-8111-111111111111"), currentTitle: "Card") #expect(transient.isEditingInline == true, "the rename editor is open") // No board popover, no search focus — caretChordsYield answers false regardless of the open // editor above, because it never reads isEditingInline at all. #expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: nil) == false) } }