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
@testable import Grain
import SwiftUI
import XCTest
/// The animation a double-tap plays over a photo — a heart, three ripples and a
/// scatter of particles, each on its own curve and delay.
@MainActor
final class LikeBurstRenderTests: 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 burst that plays when a card is double-tapped: a heart plus three
/// ripples, each on its own curve and delay.
func testRendersTheDoubleTapHeartThroughItsBurst() {
let state = HeartAnimationState(position: CGPoint(x: 120, y: 200))
ViewRender.render(DoubleTapHeartView(state: state), settle: 0)
state.start()
ViewRender.render(DoubleTapHeartView(state: state), settle: 0.2)
// The burst runs on transforms and opacity, which don't move any
// frames — so wait on the state rather than on the rendered tree.
let deadline = Date().addingTimeInterval(5)
while !state.isComplete, Date() < deadline {
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
XCTAssertTrue(state.isComplete, "The heart never finished, so it would stay on the card")
// Finished, and the view stops drawing rather than lingering.
ViewRender.render(DoubleTapHeartView(state: state), settle: 0)
}
func testTheHeartIsGivenARandomTilt() {
let tilts = (0 ..< 12).map { _ in HeartAnimationState(position: .zero).rotation }
XCTAssertTrue(tilts.allSatisfy { $0 >= -20 && $0 <= 20 })
XCTAssertGreaterThan(Set(tilts).count, 1, "Every heart landing at the same angle would read as a glitch")
}
/// Each particle in the burst sits at its own offset, so the slots have to
/// be distinct — and the view indexes a fixed table, so every slot the card
/// can ask for has to exist.
func testRendersEveryLikeParticleSlot() {
for index in 0 ..< 5 {
ViewRender.render(LikeParticleView(index: index), settle: 0)
}
}
}