import AppKit import Testing @testable import Kanban /// **`ColorSwatchPicker`'s pure pieces** (`ColorSwatchPicker.swift`), the 2026-08-10 rework that /// retired `ColorComboView`'s `NSMenu` dropdown and its pure model — `ColorComboItem`, `ColorComboMatch`, /// `ColorComboMenu`, `ColorComboModel.match`/`.menu`/`.displayName`/`.normalizedHex` — wholesale. This /// file used to hold that model's tests; nothing in it survives the dropdown's removal, so it now /// tests what replaced it: which palette a role offers, and the popover's own grid geometry. // MARK: - Role @Suite("ColorSwatchRole ▸ which palette") struct ColorSwatchRoleTests { @Test("Background lists the backgrounds table, foreground the foregrounds table") func rolesListTheirOwnTable() { #expect(ColorSwatchRole.background.palette.map(\.name) == Palette.backgrounds.map(\.name)) #expect(ColorSwatchRole.foreground.palette.map(\.name) == Palette.foregrounds.map(\.name)) } } // MARK: - Popover geometry /// **`SymbolPickerLayout`'s pattern, restated for a plain colour grid.** The layout is pure and /// font-derived (10-accessibility.md's full-relative-scaling rule), so every claim below is /// assertable without a popover on screen. @Suite("ColorSwatchPopoverLayout ▸ geometry") struct ColorSwatchPopoverLayoutTests { /// How wide `columns` wells and the gaps between them actually draw, at a given text size — /// `StyleEditorLayoutTests.gridWidth`'s own helper, restated for this layout's fixed seven. private func gridWidth(bodyPointSize: CGFloat) -> CGFloat { let columns = ColorSwatchPopoverLayout.columns let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: bodyPointSize) return CGFloat(columns) * layout.wellSide + CGFloat(columns - 1) * layout.wellSpacing } @Test("Seven columns — None + sixteen wells fall as 7 + 7 + 3, StyleEditorLayout.popover's own settled fit") func columnsAreSeven() { #expect(ColorSwatchPopoverLayout.columns == 7) // None + Palette.backgrounds (16) is 17 wells; seven columns is the count that keeps the // last row from overflowing to four rows or wasting a nearly-empty one. let wellCount = 1 + Palette.backgrounds.count let rows = (wellCount + ColorSwatchPopoverLayout.columns - 1) / ColorSwatchPopoverLayout.columns #expect(rows == 3) } @Test("The grid's well side matches the symbol popover's own scale-up over the shared base") func wellSideMatchesTheSymbolPopoversScale() { for size in [11.0, 13.0, 17.0, 24.0] as [CGFloat] { let color = ColorSwatchPopoverLayout.metrics(bodyPointSize: size) let symbol = SymbolPickerLayout.metrics(bodyPointSize: size) // Both scale `StyleEditorLayout.wellSide` by the identical `gridScale` — the two // popovers introduced together (2026-08-10) read at one scale. #expect(color.wellSide == symbol.wellSide, "the two popovers' wells drifted apart at \(size)pt") #expect(ColorSwatchPopoverLayout.gridScale == SymbolPickerLayout.gridScale) } } @Test("The popover's frame grows with the text, and its wells keep fitting inside it") func popoverScalesAndFits() { var previousWidth: CGFloat = 0 for size in [11.0, 13.0, 16.0, 18.0, 24.0, 36.0] as [CGFloat] { let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: size) #expect(layout.popoverWidth > previousWidth, "the popover must widen with the text at \(size)pt") previousWidth = layout.popoverWidth let content = layout.popoverWidth - 2 * layout.contentPadding #expect(gridWidth(bodyPointSize: size) <= content, "the grid overflows its popover at \(size)pt") } } @Test("The popover's settled geometry at the standard text size") func settledGeometryAtTheStandardBody() { let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: 13) // `StyleEditorLayout`'s own statics at 13pt: wellSpacing 6, sectionSpacing 14. Well side is // the style editor's own 20pt scaled by `gridScale`, computed the identical way the // implementation does rather than restated as a literal — `20 * 1.3` is not exactly // representable in binary floating point, and comparing two independently-rounded literals // is exactly the kind of thing that can drift a bit apart from the real computation. #expect(layout.wellSpacing == 6) #expect(layout.contentPadding == 14) let expectedSide = (StyleEditorLayout.wellSide(bodyPointSize: 13) * ColorSwatchPopoverLayout.gridScale).rounded() #expect(layout.wellSide == expectedSide) #expect(layout.wellSide == 26) #expect(layout.gridWidth == layout.wellSide * 7 + layout.wellSpacing * 6) #expect(layout.popoverWidth == layout.gridWidth + layout.contentPadding * 2) } } // MARK: - The collapsed face's None treatment /// **The owner's 2026-08-10 follow-up, verbatim**: "when 'none' selected the strike-through line /// should show on the selected color too." `ColorSwatchControl.showsNoneStrike(for:)` is the pure /// predicate behind that draw — the same lenient "nil or unresolvable both read as no colour" rule /// `StyleWellFace.Face.color`'s own doc already states for the popover's grid, restated here for the /// collapsed face so the two surfaces cannot drift apart about what counts as "None." @Suite("ColorSwatchControl ▸ the None face") struct ColorSwatchControlNoneFaceTests { @Test("A nil value shows the None strike") func nilShowsTheStrike() { #expect(ColorSwatchControl.showsNoneStrike(for: nil)) } @Test("A resolvable palette name does not show the strike") func resolvableNameHidesTheStrike() { let name = Palette.backgrounds[0].name #expect(!ColorSwatchControl.showsNoneStrike(for: name)) } @Test("A resolvable hand-written hex does not show the strike") func resolvableHexHidesTheStrike() { #expect(!ColorSwatchControl.showsNoneStrike(for: "#336699")) } @Test("An unresolvable stored value shows the strike, the same lenient rule the popover's grid uses") func unresolvableValueShowsTheStrike() { #expect(ColorSwatchControl.showsNoneStrike(for: "not-a-real-palette-name")) } }