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
@testable import Grain
import SwiftUI
import XCTest
/// The small pieces drawn around a gallery card: the page dots, the toasts a
/// share or a copy puts up, the long-press action sheet, and the thumbnails in
/// a profile grid.
@MainActor
final class CardChromeRenderTests: GrainTestCase {
private var account: TestAccount!
/// Async overrides: the synchronous `setUp`/`tearDown` are nonisolated, so
/// touching main-actor state from them warns.
override func setUp() async throws {
try await super.setUp()
account = TestAccount()
MockURLProtocol.respondByPath(Fixtures.routes)
}
override func tearDown() async throws {
MockURLProtocol.handler = nil
account.restore()
try await super.tearDown()
}
/// The dots collapse past five, and the current one is drawn larger, so the
/// indicator is rendered at each shape it takes.
func testRendersThePageIndicatorAtEachShape() {
func photos(_ count: Int, portrait: Bool = false) -> [GrainPhoto] {
(0 ..< count).map { index in
GrainPhoto(
uri: "at://did:plc:test/social.grain.photo/\(index)",
cid: "bafy\(index)",
thumb: "https://test.local/t.jpg",
fullsize: "https://test.local/f.jpg",
aspectRatio: portrait ? AspectRatio(width: 2, height: 3) : AspectRatio(width: 3, height: 2)
)
}
}
// One photo draws nothing; three fits; eight has to window.
ViewRender.render(PageIndicatorView(photos: photos(1), currentPage: 0, hasPortrait: false), settle: 0)
ViewRender.render(PageIndicatorView(photos: photos(3), currentPage: 1, hasPortrait: false), settle: 0)
for page in [0, 3, 7] {
ViewRender.render(PageIndicatorView(photos: photos(8), currentPage: page, hasPortrait: false), settle: 0)
}
// A portrait photo in the set flips the dots to a darker tint.
ViewRender.render(PageIndicatorView(photos: photos(4), currentPage: 0, hasPortrait: true), settle: 0)
ViewRender.render(
PageIndicatorView(photos: photos(4, portrait: true), currentPage: 0, hasPortrait: true), settle: 0
)
}
/// A single photo has nothing to page between, so the indicator has to take
/// no space rather than draw one lonely dot.
func testThePageIndicatorIsEmptyForASinglePhoto() {
let single = [GrainPhoto(
uri: "at://did:plc:test/social.grain.photo/1", cid: "c",
thumb: "https://test.local/t.jpg", fullsize: "https://test.local/f.jpg",
aspectRatio: AspectRatio(width: 3, height: 2)
)]
let one = UIHostingController(rootView: PageIndicatorView(photos: single, currentPage: 0, hasPortrait: false))
.sizeThatFits(in: CGSize(width: 402, height: 100))
let many = UIHostingController(
rootView: PageIndicatorView(photos: single + single, currentPage: 0, hasPortrait: false)
).sizeThatFits(in: CGSize(width: 402, height: 100))
XCTAssertLessThan(one.height, many.height)
}
func testRendersTheCopiedToast() {
ViewRender.render(CopiedToastView(), settle: 0)
}
func testRendersTheCopiedCheckmarkToast() {
ViewRender.render(CopiedCheckmarkToast(), settle: 0)
}
/// The long-press menu on a card — report on someone else's, delete on your
/// own, and both absent when neither applies.
func testRendersTheCardActionsSheetForEachOwner() {
ViewRender.render(GalleryActionsSheet(onReport: {}, onDelete: nil), settle: 0)
ViewRender.render(GalleryActionsSheet(onReport: nil, onDelete: {}), settle: 0)
ViewRender.render(GalleryActionsSheet(onReport: {}, onDelete: {}), settle: 0)
ViewRender.render(GalleryActionsSheet(), settle: 0)
}
/// Profile grid thumbnails are drawn from Nuke's cache when it has them and
/// loaded lazily when it doesn't.
func testRendersAProfileGridThumbnail() {
ViewRender.render(ProfileGridThumbnail(urlString: "https://test.local/thumb.jpg"), settle: 0.2)
ViewRender.render(ProfileGridThumbnail(urlString: ""), settle: 0.1)
ViewRender.render(ProfileGridThumbnail(urlString: "not a url"), settle: 0.1)
}
}