Skip to content

Commit abf5610

Browse files
authored
Add swift-snapshot-testing + test (#229)
1 parent e88f066 commit abf5610

File tree

32 files changed

+322
-16
lines changed

32 files changed

+322
-16
lines changed

ios/HackerNews.xcodeproj/project.pbxproj

Lines changed: 148 additions & 0 deletions
Large diffs are not rendered by default.

ios/HackerNews.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/HackerNews/Utils/Previews.swift

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
//
77

88
import Foundation
9-
import SwiftUI
109
import SnapshotPreferences
10+
import SwiftUI
1111

1212
struct PreviewVariants<Content: View>: View {
1313
let content: Content
@@ -31,7 +31,7 @@ struct PreviewVariants<Content: View>: View {
3131
.navigationBarHidden(true)
3232
.previewDevice("iPhone 11 Pro Max")
3333
.previewDisplayName("iPhone 11 Pro Max, dark mode")
34-
34+
3535
self.content
3636
.environment(\.colorScheme, .light)
3737
.preferredColorScheme(.light)
@@ -45,7 +45,7 @@ struct PreviewVariants<Content: View>: View {
4545
.navigationBarHidden(true)
4646
.previewDevice("iPhone 8")
4747
.previewDisplayName("iPhone 8, dark mode")
48-
48+
4949
self.content
5050
.environment(\.colorScheme, .light)
5151
.preferredColorScheme(.light)
@@ -59,7 +59,7 @@ struct PreviewVariants<Content: View>: View {
5959
.navigationBarHidden(true)
6060
.previewDevice("iPad Air (5th generation)")
6161
.previewDisplayName("iPad Air, dark mode")
62-
62+
6363
self.content
6464
.environment(\.colorScheme, .light)
6565
.preferredColorScheme(.light)
@@ -72,12 +72,20 @@ struct PreviewVariants<Content: View>: View {
7272
}
7373

7474
struct PreviewHelpers {
75-
static func withNavigationView(@ViewBuilder content: () -> some View) -> some View {
75+
private static var referenceTimestamp: Int64 {
76+
let calendar = Calendar.current
77+
let oneYearAgo = calendar.date(byAdding: .hour, value: -1, to: Date())!
78+
return Int64(oneYearAgo.timeIntervalSince1970)
79+
}
80+
81+
static func withNavigationView(@ViewBuilder content: () -> some View)
82+
-> some View
83+
{
7684
NavigationStack {
7785
ZStack {
7886
HNColors.background
7987
.ignoresSafeArea()
80-
88+
8189
content()
8290
}
8391
.toolbarColorScheme(.dark, for: .navigationBar)
@@ -92,11 +100,13 @@ struct PreviewHelpers {
92100
}
93101
}
94102

95-
static func makeFakeStory(index: Int64 = 0, descendants: Int = 0, kids: [Int64]? = nil) -> Story {
103+
static func makeFakeStory(
104+
index: Int64 = 0, descendants: Int = 0, kids: [Int64]? = nil
105+
) -> Story {
96106
return Story(
97107
id: index,
98108
by: "dang",
99-
time: Int64(Date().timeIntervalSince1970) - Int64(index),
109+
time: referenceTimestamp - (index * 3600), // Each story 1 hour apart
100110
type: .story,
101111
title: "Test story \(index)",
102112
text: "Test story body \(index)",
@@ -111,22 +121,24 @@ struct PreviewHelpers {
111121
Comment(
112122
id: 1,
113123
by: "dang",
114-
time: Int64(Date.now.timeIntervalSince1970),
124+
time: referenceTimestamp,
115125
type: .comment,
116126
text: """
117-
Totally useless commentary:
118-
It makes me deeply happy to hear success stories like this for a project that's moving in the correctly opposite direction to that of the rest of the world.
127+
Totally useless commentary:
128+
It makes me deeply happy to hear success stories like this for a project that's moving in the correctly opposite direction to that of the rest of the world.
119129
120-
Engildification. Of which there should be more!
130+
Engildification. Of which there should be more!
121131
122-
My soul was also satisfied by the Sleeping At Night post which, along with the recent "Lie Still in Bed" article, makes for very simple options to attempt to fix sleep (discipline) issues
123-
""",
132+
My soul was also satisfied by the Sleeping At Night post which, along with the recent "Lie Still in Bed" article, makes for very simple options to attempt to fix sleep (discipline) issues
133+
""",
124134
parent: nil,
125135
kids: nil
126136
)
127137
}
128138

129-
static func makeFakeFlattenedComment(comment: Comment = makeFakeComment(), depth: Int = 0) -> FlattenedComment {
139+
static func makeFakeFlattenedComment(
140+
comment: Comment = makeFakeComment(), depth: Int = 0
141+
) -> FlattenedComment {
130142
FlattenedComment(comment: comment, depth: depth)
131143
}
132144
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// SwiftSnapshotTest.swift
3+
// HackerNews
4+
//
5+
// Created by Trevor Elkins on 11/19/24.
6+
//
7+
8+
import SnapshotTesting
9+
import SwiftUI
10+
import XCTest
11+
12+
@testable import HackerNews
13+
14+
final class SwiftSnapshotTest: XCTestCase {
15+
16+
var appViewModel: AppViewModel!
17+
18+
@MainActor override func setUp() {
19+
super.setUp()
20+
appViewModel = AppViewModel()
21+
}
22+
23+
override func tearDown() {
24+
appViewModel = nil
25+
super.tearDown()
26+
}
27+
28+
override func invokeTest() {
29+
// Change the record mode when updating tests
30+
withSnapshotTesting(record: .never) {
31+
super.invokeTest()
32+
}
33+
}
34+
35+
func testLoginScreen() {
36+
let view = LoginScreen(appState: appViewModel)
37+
38+
let devices = [
39+
("iPhone SE", ViewImageConfig.iPhoneSe),
40+
("iPhone 12", ViewImageConfig.iPhone12),
41+
("iPhone 13 Pro", ViewImageConfig.iPhone13Pro),
42+
]
43+
44+
for (name, config) in devices {
45+
assertSnapshot(
46+
of: view.toVC(),
47+
as: .image(on: config),
48+
named: "LoginScreen-\(name)"
49+
)
50+
assertSnapshot(
51+
of: view.toVC(),
52+
as: .image(on: config, traits: .init(userInterfaceStyle: .dark)),
53+
named: "LoginScreen-\(name)-DarkMode"
54+
)
55+
}
56+
}
57+
58+
@MainActor func testPostListScreen() {
59+
// Test default state
60+
let defaultView = PostListScreen(appState: appViewModel)
61+
62+
// Test loading state
63+
let loadingViewModel = AppViewModel()
64+
loadingViewModel.authState = .loggedIn
65+
loadingViewModel.storiesState = .loading
66+
let loadingView = PostListScreen(appState: loadingViewModel)
67+
68+
// Test loaded state with posts
69+
let loadedViewModel = AppViewModel()
70+
loadedViewModel.authState = .loggedIn
71+
loadedViewModel.storiesState = .loaded(
72+
stories: PreviewHelpers.makeFakeStories())
73+
let loadedView = PostListScreen(appState: loadedViewModel)
74+
75+
let devices = [
76+
("iPhone SE", ViewImageConfig.iPhoneSe),
77+
("iPhone 12", ViewImageConfig.iPhone12),
78+
("iPhone 13 Pro", ViewImageConfig.iPhone13Pro),
79+
]
80+
81+
// Test each state on different devices
82+
for (name, config) in devices {
83+
// Default state
84+
assertSnapshot(
85+
of: defaultView.toVC(),
86+
as: .image(on: config),
87+
named: "PostListScreen-Default-\(name)"
88+
)
89+
assertSnapshot(
90+
of: defaultView.toVC(),
91+
as: .image(on: config, traits: .init(userInterfaceStyle: .dark)),
92+
named: "PostListScreen-Default-\(name)-DarkMode"
93+
)
94+
95+
// Loading state
96+
assertSnapshot(
97+
of: loadingView.toVC(),
98+
as: .image(on: config),
99+
named: "PostListScreen-Loading-\(name)"
100+
)
101+
assertSnapshot(
102+
of: loadingView.toVC(),
103+
as: .image(on: config, traits: .init(userInterfaceStyle: .dark)),
104+
named: "PostListScreen-Loading-\(name)-DarkMode"
105+
)
106+
107+
// Loaded state with posts
108+
assertSnapshot(
109+
of: loadedView.toVC(),
110+
as: .image(on: config),
111+
named: "PostListScreen-LoadedPosts-\(name)"
112+
)
113+
assertSnapshot(
114+
of: loadedView.toVC(),
115+
as: .image(on: config, traits: .init(userInterfaceStyle: .dark)),
116+
named: "PostListScreen-LoadedPosts-\(name)-DarkMode"
117+
)
118+
}
119+
}
120+
}
121+
122+
extension View {
123+
fileprivate func toVC() -> UIViewController {
124+
let hostingController = UIHostingController(rootView: self)
125+
hostingController.view.frame = UIScreen.main.bounds
126+
return hostingController
127+
}
128+
}
Loading
Loading
Loading

0 commit comments

Comments
 (0)