Skip to content

Commit 1671432

Browse files
authored
Merge branch 'main' into feature/diff_tool_args
2 parents 9ad55a5 + 9e28725 commit 1671432

34 files changed

+326
-42
lines changed

README.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ If you want to use SnapshotTesting in any other project that uses [SwiftPM](http
143143

144144
```swift
145145
dependencies: [
146-
.package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.8.1"),
146+
.package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0"),
147147
]
148148
```
149149

@@ -161,7 +161,7 @@ targets: [
161161
If you use [Carthage](https://github.com/Carthage/Carthage), you can add the following dependency to your `Cartfile`:
162162

163163
``` ruby
164-
github "pointfreeco/swift-snapshot-testing" ~> 1.8.0
164+
github "pointfreeco/swift-snapshot-testing" ~> 1.9.0
165165
```
166166

167167
> ⚠️ Warning: Carthage instructs you to drag frameworks into your Xcode project. Xcode may automatically attempt to link these frameworks to your app target. `SnapshotTesting.framework` is only compatible with test targets, so when you first add it to your project:
@@ -179,7 +179,7 @@ If your project uses [CocoaPods](https://cocoapods.org), add the pod to any appl
179179

180180
```ruby
181181
target 'MyAppTests' do
182-
pod 'SnapshotTesting', '~> 1.8.1'
182+
pod 'SnapshotTesting', '~> 1.9.0'
183183
end
184184
```
185185

@@ -195,7 +195,10 @@ end
195195
- **Supports any platform that supports Swift.** Write snapshot tests for iOS, Linux, macOS, and tvOS.
196196
- **SceneKit, SpriteKit, and WebKit support.** Most snapshot testing libraries don't support these view subclasses.
197197
- **`Codable` support**. Snapshot encodable data structures into their [JSON](Documentation/Available-Snapshot-Strategies.md#json) and [property list](Documentation/Available-Snapshot-Strategies.md#plist) representations.
198-
- **Custom diff tool integration**.
198+
- **Custom diff tool integration**. Configure failure messages to print diff commands for [Kaleidoscope](https://kaleidoscope.app) (or your diff tool of choice).
199+
``` swift
200+
SnapshotTesting.diffTool = "ksdiff"
201+
```
199202

200203
## Plug-ins
201204

@@ -209,6 +212,8 @@ end
209212

210213
- [AccessibilitySnapshotColorBlindness](https://github.com/Sherlouk/AccessibilitySnapshotColorBlindness) adds snapshot strategies for color blindness simulation on iOS views, view controllers and images.
211214

215+
- [swift-snapshot-testing-stitch](https://github.com/Sherlouk/swift-snapshot-testing-stitch/) adds the ability to stitch multiple UIView's or UIViewController's together in a single test.
216+
212217
Have you written your own SnapshotTesting plug-in? [Add it here](https://github.com/pointfreeco/swift-snapshot-testing/edit/master/README.md) and submit a pull request!
213218

214219
## Related Tools

SnapshotTesting.podspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "SnapshotTesting"
3-
s.version = "1.8.1"
3+
s.version = "1.9.0"
44
s.summary = "Tests that save and assert against reference data"
55

66
s.description = <<-DESC

Sources/SnapshotTesting/Common/View.swift

+158-21
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,38 @@ public struct ViewImageConfig {
146146
return .init(safeArea: safeArea, size: size, traits: .iPhoneXr(orientation))
147147
}
148148

149+
public static let iPhone12 = ViewImageConfig.iPhone12(.portrait)
150+
151+
public static func iPhone12(_ orientation: Orientation) -> ViewImageConfig {
152+
let safeArea: UIEdgeInsets
153+
let size: CGSize
154+
switch orientation {
155+
case .landscape:
156+
safeArea = .init(top: 0, left: 47, bottom: 21, right: 47)
157+
size = .init(width: 844, height: 390)
158+
case .portrait:
159+
safeArea = .init(top: 47, left: 0, bottom: 34, right: 0)
160+
size = .init(width: 390, height: 844)
161+
}
162+
return .init(safeArea: safeArea, size: size, traits: .iPhone12(orientation))
163+
}
164+
165+
public static let iPhone12ProMax = ViewImageConfig.iPhone12ProMax(.portrait)
166+
167+
public static func iPhone12ProMax(_ orientation: Orientation) -> ViewImageConfig {
168+
let safeArea: UIEdgeInsets
169+
let size: CGSize
170+
switch orientation {
171+
case .landscape:
172+
safeArea = .init(top: 0, left: 47, bottom: 21, right: 47)
173+
size = .init(width: 926, height: 428)
174+
case .portrait:
175+
safeArea = .init(top: 47, left: 0, bottom: 34, right: 0)
176+
size = .init(width: 428, height: 926)
177+
}
178+
return .init(safeArea: safeArea, size: size, traits: .iPhone12ProMax(orientation))
179+
}
180+
149181
public static let iPadMini = ViewImageConfig.iPadMini(.landscape)
150182

151183
public static func iPadMini(_ orientation: Orientation) -> ViewImageConfig {
@@ -192,6 +224,63 @@ public struct ViewImageConfig {
192224
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
193225
}
194226

227+
public static let iPad9_7 = iPadMini
228+
229+
public static func iPad9_7(_ orientation: Orientation) -> ViewImageConfig {
230+
return iPadMini(orientation)
231+
}
232+
233+
public static func iPad9_7(_ orientation: TabletOrientation) -> ViewImageConfig {
234+
return iPadMini(orientation)
235+
}
236+
237+
public static let iPad10_2 = ViewImageConfig.iPad10_2(.landscape)
238+
239+
public static func iPad10_2(_ orientation: Orientation) -> ViewImageConfig {
240+
switch orientation {
241+
case .landscape:
242+
return ViewImageConfig.iPad10_2(.landscape(splitView: .full))
243+
case .portrait:
244+
return ViewImageConfig.iPad10_2(.portrait(splitView: .full))
245+
}
246+
}
247+
248+
public static func iPad10_2(_ orientation: TabletOrientation) -> ViewImageConfig {
249+
let size: CGSize
250+
let traits: UITraitCollection
251+
switch orientation {
252+
case .landscape(let splitView):
253+
switch splitView {
254+
case .oneThird:
255+
size = .init(width: 320, height: 810)
256+
traits = .iPad10_2_Compact_SplitView
257+
case .oneHalf:
258+
size = .init(width: 535, height: 810)
259+
traits = .iPad10_2_Compact_SplitView
260+
case .twoThirds:
261+
size = .init(width: 750, height: 810)
262+
traits = .iPad10_2
263+
case .full:
264+
size = .init(width: 1080, height: 810)
265+
traits = .iPad10_2
266+
}
267+
case .portrait(let splitView):
268+
switch splitView {
269+
case .oneThird:
270+
size = .init(width: 320, height: 1080)
271+
traits = .iPad10_2_Compact_SplitView
272+
case .twoThirds:
273+
size = .init(width: 480, height: 1080)
274+
traits = .iPad10_2_Compact_SplitView
275+
case .full:
276+
size = .init(width: 810, height: 1080)
277+
traits = .iPad10_2
278+
}
279+
}
280+
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
281+
}
282+
283+
195284
public static let iPadPro10_5 = ViewImageConfig.iPadPro10_5(.landscape)
196285

197286
public static func iPadPro10_5(_ orientation: Orientation) -> ViewImageConfig {
@@ -507,16 +596,76 @@ extension UITraitCollection {
507596
)
508597
case .portrait:
509598
return .init(
510-
traitsFrom: [
599+
traitsFrom: base + [
511600
.init(horizontalSizeClass: .compact),
512601
.init(verticalSizeClass: .regular)
513602
]
514603
)
515604
}
516605
}
517606

607+
public static func iPhone12(_ orientation: ViewImageConfig.Orientation)
608+
-> UITraitCollection {
609+
let base: [UITraitCollection] = [
610+
// .init(displayGamut: .P3),
611+
// .init(displayScale: 3),
612+
.init(forceTouchCapability: .available),
613+
.init(layoutDirection: .leftToRight),
614+
.init(preferredContentSizeCategory: .medium),
615+
.init(userInterfaceIdiom: .phone)
616+
]
617+
switch orientation {
618+
case .landscape:
619+
return .init(
620+
traitsFrom: base + [
621+
.init(horizontalSizeClass: .compact),
622+
.init(verticalSizeClass: .compact)
623+
]
624+
)
625+
case .portrait:
626+
return .init(
627+
traitsFrom: base + [
628+
.init(horizontalSizeClass: .compact),
629+
.init(verticalSizeClass: .regular)
630+
]
631+
)
632+
}
633+
}
634+
635+
public static func iPhone12ProMax(_ orientation: ViewImageConfig.Orientation)
636+
-> UITraitCollection {
637+
let base: [UITraitCollection] = [
638+
// .init(displayGamut: .P3),
639+
// .init(displayScale: 3),
640+
.init(forceTouchCapability: .available),
641+
.init(layoutDirection: .leftToRight),
642+
.init(preferredContentSizeCategory: .medium),
643+
.init(userInterfaceIdiom: .phone)
644+
]
645+
switch orientation {
646+
case .landscape:
647+
return .init(
648+
traitsFrom: base + [
649+
.init(horizontalSizeClass: .regular),
650+
.init(verticalSizeClass: .compact)
651+
]
652+
)
653+
case .portrait:
654+
return .init(
655+
traitsFrom: base + [
656+
.init(horizontalSizeClass: .compact),
657+
.init(verticalSizeClass: .regular)
658+
]
659+
)
660+
}
661+
}
662+
518663
public static let iPadMini = iPad
519664
public static let iPadMini_Compact_SplitView = iPadCompactSplitView
665+
public static let iPad9_7 = iPad
666+
public static let iPad9_7_Compact_SplitView = iPadCompactSplitView
667+
public static let iPad10_2 = iPad
668+
public static let iPad10_2_Compact_SplitView = iPadCompactSplitView
520669
public static let iPadPro10_5 = iPad
521670
public static let iPadPro10_5_Compact_SplitView = iPadCompactSplitView
522671
public static let iPadPro11 = iPad
@@ -604,7 +753,6 @@ extension View {
604753
#if os(iOS) || os(macOS)
605754
if let wkWebView = self as? WKWebView {
606755
return Async<Image> { callback in
607-
let delegate = NavigationDelegate()
608756
let work = {
609757
if #available(iOS 11.0, macOS 10.13, *) {
610758
inWindow {
@@ -613,7 +761,6 @@ extension View {
613761
return
614762
}
615763
wkWebView.takeSnapshot(with: nil) { image, _ in
616-
_ = delegate
617764
callback(image!)
618765
}
619766
}
@@ -627,8 +774,14 @@ extension View {
627774
}
628775

629776
if wkWebView.isLoading {
630-
delegate.didFinish = work
631-
wkWebView.navigationDelegate = delegate
777+
var subscription: NSKeyValueObservation?
778+
subscription = wkWebView.observe(\.isLoading, options: [.initial, .new]) { (webview, change) in
779+
subscription?.invalidate()
780+
subscription = nil
781+
if change.newValue == false {
782+
work()
783+
}
784+
}
632785
} else {
633786
work()
634787
}
@@ -647,22 +800,6 @@ extension View {
647800
#endif
648801
}
649802

650-
#if os(iOS) || os(macOS)
651-
private final class NavigationDelegate: NSObject, WKNavigationDelegate {
652-
var didFinish: () -> Void
653-
654-
init(didFinish: @escaping () -> Void = {}) {
655-
self.didFinish = didFinish
656-
}
657-
658-
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
659-
webView.evaluateJavaScript("document.readyState") { _, _ in
660-
self.didFinish()
661-
}
662-
}
663-
}
664-
#endif
665-
666803
#if os(iOS) || os(tvOS)
667804
extension UIApplication {
668805
static var sharedIfAvailable: UIApplication? {

Sources/SnapshotTesting/Snapshotting/UIImage.swift

+16-14
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ extension Snapshotting where Value == UIImage, Format == UIImage {
7171
}
7272
}
7373

74+
// remap snapshot & reference to same colorspace
75+
let imageContextColorSpace = CGColorSpace(name: CGColorSpace.sRGB)
76+
let imageContextBitsPerComponent = 8
77+
let imageContextBytesPerPixel = 4
78+
7479
private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool {
7580
guard let oldCgImage = old.cgImage else { return false }
7681
guard let newCgImage = new.cgImage else { return false }
@@ -80,23 +85,18 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool {
8085
guard oldCgImage.height != 0 else { return false }
8186
guard newCgImage.height != 0 else { return false }
8287
guard oldCgImage.height == newCgImage.height else { return false }
83-
// Values between images may differ due to padding to multiple of 64 bytes per row,
84-
// because of that a freshly taken view snapshot may differ from one stored as PNG.
85-
// At this point we're sure that size of both images is the same, so we can go with minimal `bytesPerRow` value
86-
// and use it to create contexts.
87-
let minBytesPerRow = min(oldCgImage.bytesPerRow, newCgImage.bytesPerRow)
88-
let byteCount = minBytesPerRow * oldCgImage.height
8988

89+
let byteCount = imageContextBytesPerPixel * oldCgImage.width * oldCgImage.height
9090
var oldBytes = [UInt8](repeating: 0, count: byteCount)
91-
guard let oldContext = context(for: oldCgImage, bytesPerRow: minBytesPerRow, data: &oldBytes) else { return false }
91+
guard let oldContext = context(for: oldCgImage, data: &oldBytes) else { return false }
9292
guard let oldData = oldContext.data else { return false }
93-
if let newContext = context(for: newCgImage, bytesPerRow: minBytesPerRow), let newData = newContext.data {
93+
if let newContext = context(for: newCgImage), let newData = newContext.data {
9494
if memcmp(oldData, newData, byteCount) == 0 { return true }
9595
}
9696
let newer = UIImage(data: new.pngData()!)!
9797
guard let newerCgImage = newer.cgImage else { return false }
9898
var newerBytes = [UInt8](repeating: 0, count: byteCount)
99-
guard let newerContext = context(for: newerCgImage, bytesPerRow: minBytesPerRow, data: &newerBytes) else { return false }
99+
guard let newerContext = context(for: newerCgImage, data: &newerBytes) else { return false }
100100
guard let newerData = newerContext.data else { return false }
101101
if memcmp(oldData, newerData, byteCount) == 0 { return true }
102102
if precision >= 1 { return false }
@@ -109,16 +109,17 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool {
109109
return true
110110
}
111111

112-
private func context(for cgImage: CGImage, bytesPerRow: Int, data: UnsafeMutableRawPointer? = nil) -> CGContext? {
112+
private func context(for cgImage: CGImage, data: UnsafeMutableRawPointer? = nil) -> CGContext? {
113+
let bytesPerRow = cgImage.width * imageContextBytesPerPixel
113114
guard
114-
let space = cgImage.colorSpace,
115+
let colorSpace = imageContextColorSpace,
115116
let context = CGContext(
116117
data: data,
117118
width: cgImage.width,
118119
height: cgImage.height,
119-
bitsPerComponent: cgImage.bitsPerComponent,
120+
bitsPerComponent: imageContextBitsPerComponent,
120121
bytesPerRow: bytesPerRow,
121-
space: space,
122+
space: colorSpace,
122123
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
123124
)
124125
else { return nil }
@@ -130,7 +131,8 @@ private func context(for cgImage: CGImage, bytesPerRow: Int, data: UnsafeMutable
130131
private func diff(_ old: UIImage, _ new: UIImage) -> UIImage {
131132
let width = max(old.size.width, new.size.width)
132133
let height = max(old.size.height, new.size.height)
133-
UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), true, 0)
134+
let scale = max(old.scale, new.scale)
135+
UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), true, scale)
134136
new.draw(at: .zero)
135137
old.draw(at: .zero, blendMode: .difference, alpha: 1)
136138
let differenceImage = UIGraphicsGetImageFromCurrentImageContext()!

Sources/SnapshotTesting/Snapshotting/URLRequest.swift

+12-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ extension Snapshotting where Value == URLRequest, Format == String {
1212
/// - Parameter pretty: Attempts to pretty print the body of the request (supports JSON).
1313
public static func raw(pretty: Bool) -> Snapshotting {
1414
return SimplySnapshotting.lines.pullback { (request: URLRequest) in
15-
let method = "\(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "(null)")"
15+
let method = "\(request.httpMethod ?? "GET") \(request.url?.sortingQueryItems()?.absoluteString ?? "(null)")"
1616

1717
let headers = (request.allHTTPHeaderFields ?? [:])
1818
.map { key, value in "\(key): \(value)" }
@@ -76,8 +76,18 @@ extension Snapshotting where Value == URLRequest, Format == String {
7676
}
7777

7878
// URL
79-
components.append("\"\(request.url!.absoluteString)\"")
79+
components.append("\"\(request.url!.sortingQueryItems()!.absoluteString)\"")
8080

8181
return components.joined(separator: " \\\n\t")
8282
}
8383
}
84+
85+
private extension URL {
86+
func sortingQueryItems() -> URL? {
87+
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
88+
let sortedQueryItems = components?.queryItems?.sorted { $0.name < $1.name }
89+
components?.queryItems = sortedQueryItems
90+
91+
return components?.url
92+
}
93+
}

0 commit comments

Comments
 (0)