~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
112 lines · 4.5 KB Raw History
@testable import Grain
import SwiftUI
import XCTest

/// The facepile sits next to a caption that can wrap. When the caption wants
/// more room the row proposes less width to the facepile, so the facepile has
/// to report the same size regardless of what it is offered.
@MainActor
final class FacepileLayoutTests: GrainTestCase {
    private struct Member: FacepileMember {
        let did: String
        let avatar: String?
    }

    private static let people = (0 ..< 3).map { Member(did: "did:plc:\($0)", avatar: nil) }

    /// Size the facepile reports when offered `proposedWidth` of horizontal room.
    private func facepileSize(proposedWidth: CGFloat, size: CGFloat = 20) -> CGSize {
        let host = UIHostingController(rootView: FacepileView(people: Self.people, size: size))
        return host.sizeThatFits(in: CGSize(width: proposedWidth, height: .greatestFiniteMagnitude))
    }

    func testFacepileIgnoresACrampedWidthProposal() {
        let roomy = facepileSize(proposedWidth: 350)
        let cramped = facepileSize(proposedWidth: 24)

        XCTAssertEqual(roomy.width, cramped.width, accuracy: 0.5, "Facepile narrowed when squeezed")
        XCTAssertEqual(roomy.height, cramped.height, accuracy: 0.5, "Facepile shrank when squeezed")
    }

    func testThreeTwentyPointAvatarsOverlappingByEight() {
        let size = facepileSize(proposedWidth: 350)

        XCTAssertEqual(size.width, 20 * 3 - 8 * 2, accuracy: 0.5)
        XCTAssertEqual(size.height, 20, accuracy: 0.5)
    }

    func testFacepileSizeScalesWithTheSizeParameter() {
        XCTAssertEqual(facepileSize(proposedWidth: 350, size: 24).height, 24, accuracy: 0.5)
    }

    /// Measures the facepile as laid out inside the real caption row, where the
    /// caption competes for width. ImageRenderer runs a full layout pass, so the
    /// GeometryReader below reports the size the row actually granted.
    private func facepileSizeInRow(caption: String, width: CGFloat) -> CGSize {
        final class Box {
            var size = CGSize.zero
        }
        let box = Box()

        let row = HStack(spacing: 6) {
            FacepileView(people: Self.people, size: 20)
                .background(
                    GeometryReader { geo in
                        Color.clear.onAppear { box.size = geo.size }
                    }
                )
            Text(caption)
                .font(.caption)
                .lineLimit(2)
            Spacer(minLength: 0)
        }
        .frame(width: width)

        let renderer = ImageRenderer(content: row)
        _ = renderer.uiImage
        return box.size
    }

    /// Caption height vs facepile height as Dynamic Type grows.
    private func rowMetrics(_ type: DynamicTypeSize) -> (caption: CGFloat, facepile: CGFloat) {
        let caption = UIHostingController(
            rootView: Text("Favorited by Someone and Someone Else")
                .font(.caption)
                .lineLimit(2)
                .dynamicTypeSize(type)
        ).sizeThatFits(in: CGSize(width: 250, height: CGFloat.greatestFiniteMagnitude))

        let facepile = UIHostingController(
            rootView: FacepileView(people: Self.people, size: 20).dynamicTypeSize(type)
        ).sizeThatFits(in: CGSize(width: 350, height: CGFloat.greatestFiniteMagnitude))

        return (caption.height, facepile.height)
    }

    func testFacepileGrowsWithTheCaptionUnderDynamicType() {
        let small = rowMetrics(.large)
        let big = rowMetrics(.accessibility3)

        XCTAssertGreaterThan(big.caption, small.caption, "Caption should grow with Dynamic Type")
        XCTAssertGreaterThan(
            big.facepile, small.facepile,
            "Facepile should grow with the caption, not stay pinned while text around it doubles"
        )
    }

    func testFacepileIsUnchangedAtTheDefaultTextSize() {
        XCTAssertEqual(rowMetrics(.large).facepile, 20, accuracy: 0.5)
    }

    func testFacepileKeepsItsSizeWhenTheCaptionWraps() {
        let short = facepileSizeInRow(caption: "Favorited by A", width: 350)
        let wrapping = facepileSizeInRow(
            caption: "Favorited by Bartholomew Cubbins-Fitzgerald, Wilhelmina Featherstonehaugh and others you follow",
            width: 350
        )

        XCTAssertNotEqual(short, .zero, "Measurement harness failed to lay out")
        XCTAssertEqual(short.width, wrapping.width, accuracy: 0.5, "Facepile width changed when the caption wrapped")
        XCTAssertEqual(short.height, wrapping.height, accuracy: 0.5, "Facepile height changed when the caption wrapped")
    }
}