Skip to content

Commit c3f2b91

Browse files
committed
feat(nsview): add option to set appearance, backingScaleFactor and colorSpace
Setting all three values allows for device independent snapshotting for macOS
1 parent c639e36 commit c3f2b91

File tree

1 file changed

+76
-4
lines changed

1 file changed

+76
-4
lines changed

Sources/SnapshotTesting/Snapshotting/NSView.swift

+76-4
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,50 @@ extension Snapshotting where Value == NSView, Format == NSImage {
99

1010
/// A snapshot strategy for comparing views based on pixel equality.
1111
///
12+
/// >This function calls to `NSView.cacheDisplay()` which has side-effects that cannot be undone. Under some circumstances
13+
/// >subviews will be added (e.g. for `NSButton`-views) and `NSView.needsLayout` will be set to `false`. Keep that in mind
14+
/// >when asserting with `.image` and `.recursiveDescription` within the same test.
15+
///
1216
/// - Parameters:
1317
/// - precision: The percentage of pixels that must match.
1418
/// - size: A view size override.
19+
/// - appearance: The appearance to use when drawing the view. Pass `nil` to use the view’s existing appearance.
20+
/// - windowForDrawing: The choice of window to use when drawing the view. Pass `.nil` to ignore.
21+
/// __Precondition__: If set to a none nil value, the view must not already
22+
/// be attached to an existing window. (We wouldn’t be able to easily restore the view and all its
23+
/// associated constraints to the original window after moving it to the new window.)
1524
public static func image(
1625
precision: Float = 1,
1726
size: CGSize? = nil,
18-
appearance: NSAppearance? = NSAppearance(named: .aqua)
27+
appearance: NSAppearance? = NSAppearance(named: .aqua),
28+
windowForDrawing: GenericWindow? = nil
1929
) -> Snapshotting {
2030
return SimplySnapshotting.image(precision: precision).asyncPullback { view in
21-
view.appearance = appearance ?? view.appearance
22-
let initialSize = view.frame.size
31+
32+
let initialFrame = view.frame
2333
if let size = size { view.frame.size = size }
2434
guard view.frame.width > 0, view.frame.height > 0 else {
2535
fatalError("View not renderable to image at size \(view.frame.size)")
2636
}
37+
38+
let initialAppearance = view.appearance
39+
if let appearance = appearance {
40+
view.appearance = appearance
41+
}
42+
43+
if let windowForDrawing = windowForDrawing {
44+
precondition(
45+
view.window == nil,
46+
"""
47+
If choosing to draw the view using a new window, the view must not already be attached to an existing window. \
48+
(We wouldn’t be able to easily restore the view and all its associated constraints to the original window \
49+
after moving it to the new window.)
50+
"""
51+
)
52+
windowForDrawing.contentView = NSView()
53+
windowForDrawing.contentView?.addSubview(view)
54+
}
55+
2756
return view.snapshot ?? Async { callback in
2857
addImagesForRenderedViews(view).sequence().run { views in
2958
let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
@@ -32,7 +61,23 @@ extension Snapshotting where Value == NSView, Format == NSImage {
3261
image.addRepresentation(bitmapRep)
3362
callback(image)
3463
views.forEach { $0.removeFromSuperview() }
35-
view.frame.size = initialSize
64+
if windowForDrawing != nil {
65+
view.removeFromSuperview()
66+
view.layer = nil
67+
view.subviews.forEach { subview in
68+
subview.layer = nil
69+
70+
}
71+
72+
// This is to maintain compatibility with `recursiveDescription` because the current
73+
// test snapshots expect `.needsLayout = false` and for some apple magic reason
74+
// `view.needsLayout = false` does not do anything, but this does.
75+
let bitmapRep2 = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
76+
view.cacheDisplay(in: view.bounds, to: bitmapRep2)
77+
78+
}
79+
view.appearance = initialAppearance
80+
view.frame = initialFrame
3681
}
3782
}
3883
}
@@ -50,4 +95,31 @@ extension Snapshotting where Value == NSView, Format == String {
5095
}
5196
}
5297
}
98+
99+
/// A NSWindow which can be configured in a deterministic way.
100+
public final class GenericWindow: NSWindow {
101+
public init(backingScaleFactor: CGFloat = 2.0, colorSpace: NSColorSpace? = nil) {
102+
self._backingScaleFactor = backingScaleFactor
103+
self._explicitlySpecifiedColorSpace = colorSpace
104+
105+
super.init(contentRect: NSRect.zero, styleMask: [], backing: .buffered, defer: true)
106+
}
107+
108+
private let _explicitlySpecifiedColorSpace: NSColorSpace?
109+
private var _systemSpecifiedColorspace: NSColorSpace?
110+
111+
private let _backingScaleFactor: CGFloat
112+
public override var backingScaleFactor: CGFloat {
113+
return _backingScaleFactor
114+
}
115+
116+
public override var colorSpace: NSColorSpace? {
117+
get {
118+
_explicitlySpecifiedColorSpace ?? self._systemSpecifiedColorspace
119+
}
120+
set {
121+
self._systemSpecifiedColorspace = newValue
122+
}
123+
}
124+
}
53125
#endif

0 commit comments

Comments
 (0)