1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
@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")
}
}