import CoreGraphics import Testing @testable import Kanban /// `DragPreviewAnchor` — given a preview's length and the point in it the pointer grabbed, how much /// transparent room goes on either side so the system's centred placement lands that point under the /// cursor? The visual result is a real drag session's and cannot be asserted here; this is the one /// decision it is made of. /// The centre of the padded box, and the anchor's position in it — the two figures the padding /// exists to make equal. private func centreAndAnchor( length: CGFloat, anchor: CGFloat ) -> (centre: CGFloat, anchor: CGFloat) { let padding = DragPreviewAnchor.padding(length: length, anchor: anchor) let padded = padding.before + length + padding.after return (centre: padded / 2, anchor: padding.before + anchor) } private func isClose(_ value: CGFloat, _ expected: CGFloat, _ tolerance: CGFloat = 0.0001) -> Bool { abs(value - expected) <= tolerance } @Suite("DragPreviewAnchor") struct DragPreviewAnchorTests { // MARK: The promise @Test("The padded preview's centre is the anchor, wherever the anchor is") func anchorBecomesTheCentre() { let length: CGFloat = 640 for anchor in stride(from: CGFloat(0), through: length, by: 16) { let result = centreAndAnchor(length: length, anchor: anchor) #expect( isClose(result.centre, result.anchor), "anchor \(anchor) landed at \(result.anchor), centre is \(result.centre)" ) } } @Test("A lane grabbed by its title bar hangs from the bar, not from its middle") func laneHeaderAnchor() { // A tall lane whose bar sits near the top: the compensation is most of a second lane's // height, all of it transparent and all of it above. let padding = DragPreviewAnchor.padding(length: 700, anchor: 20) #expect(padding.before == 660) #expect(padding.after == 0) // The bar's centre and the image's centre are then the same point — 20 down from the // replica's top edge, which is 680 down from the padded image's. let result = centreAndAnchor(length: 700, anchor: 20) #expect(isClose(result.centre, result.anchor)) #expect(isClose(result.centre, 680)) } // MARK: Which side @Test("An anchor above the middle pads before it, one below pads after it") func padsTheOppositeSide() { let high = DragPreviewAnchor.padding(length: 100, anchor: 30) #expect(high.before == 40) #expect(high.after == 0) let low = DragPreviewAnchor.padding(length: 100, anchor: 70) #expect(low.before == 0) #expect(low.after == 40) } @Test("An anchor already at the centre pads nothing") func centredAnchorPadsNothing() { #expect(DragPreviewAnchor.padding(length: 100, anchor: 50) == .none) } @Test("Either extreme pads a whole length — the preview hangs entirely from one edge") func extremesPadAWholeLength() { #expect(DragPreviewAnchor.padding(length: 100, anchor: 0) == .init(before: 100, after: 0)) #expect(DragPreviewAnchor.padding(length: 100, anchor: 100) == .init(before: 0, after: 100)) } // MARK: Symmetric padding does not disturb it @Test("The figure is invariant under the replica's own transparent margin") func invariantUnderSymmetricPadding() { // `BoardMetrics.replicaPadding` wraps every replica so its shadow is not clipped. Adding it // to both ends adds to the length and to the anchor alike, so the compensation is the same // number and the two paddings compose in either order. let margin: CGFloat = 12 let bare = DragPreviewAnchor.padding(length: 700, anchor: 20) let wrapped = DragPreviewAnchor.padding(length: 700 + 2 * margin, anchor: 20 + margin) #expect(bare == wrapped) } // MARK: Degenerate input @Test("An unmeasured preview pads nothing") func unmeasuredPadsNothing() { #expect(DragPreviewAnchor.padding(length: 0, anchor: 0) == .none) #expect(DragPreviewAnchor.padding(length: -100, anchor: 20) == .none) } @Test("Non-finite input pads nothing") func nonFinitePadsNothing() { #expect(DragPreviewAnchor.padding(length: .infinity, anchor: 20) == .none) #expect(DragPreviewAnchor.padding(length: .nan, anchor: 20) == .none) #expect(DragPreviewAnchor.padding(length: 700, anchor: .infinity) == .none) #expect(DragPreviewAnchor.padding(length: 700, anchor: .nan) == .none) } @Test("An anchor outside the preview clamps to its edges") func anchorClampsIntoThePreview() { #expect(DragPreviewAnchor.padding(length: 100, anchor: -50) == .init(before: 100, after: 0)) #expect(DragPreviewAnchor.padding(length: 100, anchor: 500) == .init(before: 0, after: 100)) } }