Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tillhainbach's fix for macOS #1

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Run tests
run: make test-swift
env:
SNAPSHOT_ARTIFACTS: "./.temp/artifacts"

- name: Archive failing snapshots
if: always()
uses: actions/upload-artifact@v2
with:
name: failing-snapshots
path: .temp/artifacts

ubuntu:
strategy:
Expand Down
1 change: 1 addition & 0 deletions Sources/SnapshotTesting/Snapshotting/CGPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@

private let defaultNumberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.decimalSeparator = "."
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 3
return numberFormatter
Expand Down
13 changes: 7 additions & 6 deletions Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@
}
}

private let defaultNumberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 3
return numberFormatter
}()
private let defaultNumberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.decimalSeparator = "."
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 3
return numberFormatter
}()
#endif
121 changes: 99 additions & 22 deletions Sources/SnapshotTesting/Snapshotting/NSView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,73 @@
return .image()
}

/// A snapshot strategy for comparing views based on pixel equality.
///
/// > Note: Snapshots must be compared on the same OS as the device that originally took the
/// > reference to avoid discrepancies between images.
///
/// - Parameters:
/// - precision: The percentage of pixels that must match.
/// - perceptualPrecision: The percentage a pixel must match the source pixel to be considered a
/// match. 98-99% mimics
/// [the precision](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e) of the
/// human eye.
/// - size: A view size override.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, size: CGSize? = nil
) -> Snapshotting {
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision
).asyncPullback { view in
let initialSize = view.frame.size
if let size = size { view.frame.size = size }
guard view.frame.width > 0, view.frame.height > 0 else {
fatalError("View not renderable to image at size \(view.frame.size)")
/// A snapshot strategy for comparing views based on pixel equality.
///
/// >This function calls to `NSView.cacheDisplay()` which has side-effects that cannot be undone. Under some circumstances
/// >subviews will be added (e.g. for `NSButton`-views) and `NSView.needsLayout` will be set to `false`. Keep that in mind
/// >when asserting with `.image` and `.recursiveDescription` within the same test.
///
/// - Parameters:
/// - precision: The percentage of pixels that must match.
/// - perceptualPrecision: The percentage a pixel must match the source pixel to be considered a match. [98-99% mimics the precision of the human eye.](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e)
/// - size: A view size override.
/// - appearance: The appearance to use when drawing the view. Pass `nil` to use the view’s existing appearance.
/// - windowForDrawing: The choice of window to use when drawing the view. Pass `nil` to ignore.
public static func image(
precision: Float = 1,
perceptualPrecision: Float = 1,
size: CGSize? = nil,
appearance: NSAppearance? = NSAppearance(named: .aqua),
windowForDrawing: GenericWindow? = nil
) -> Snapshotting {
return SimplySnapshotting.image(precision: precision, perceptualPrecision: perceptualPrecision).asyncPullback { view in
let initialFrame = view.frame
if let size = size { view.frame.size = size }
guard view.frame.width > 0, view.frame.height > 0 else {
fatalError("View not renderable to image at size \(view.frame.size)")
}

let initialAppearance = view.appearance
if let appearance = appearance {
view.appearance = appearance
}

if let windowForDrawing = windowForDrawing {
precondition(
view.window == nil,
"""
If choosing to draw the view using a new window, the view must not already be attached to an existing window. \
(We wouldn’t be able to easily restore the view and all its associated constraints to the original window \
after moving it to the new window.)
"""
)
windowForDrawing.contentView = NSView()
windowForDrawing.contentView?.addSubview(view)
}

return view.snapshot ?? Async { callback in
addImagesForRenderedViews(view).sequence().run { views in
let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
view.cacheDisplay(in: view.bounds, to: bitmapRep)
let image = NSImage(size: view.bounds.size)
image.addRepresentation(bitmapRep)
callback(image)
views.forEach { $0.removeFromSuperview() }
view.appearance = initialAppearance
view.frame = initialFrame

if windowForDrawing != nil {
view.removeFromSuperview()
view.layer = nil
view.subviews.forEach { subview in
subview.layer = nil
}
// This is to maintain compatibility with `recursiveDescription` because the current
// test snapshots expect `.needsLayout = false` and for some apple magic reason
// `view.needsLayout = false` does not do anything, but this does.
let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
view.cacheDisplay(in: view.bounds, to: bitmapRep)
}
}
return view.snapshot
?? Async { callback in
Expand Down Expand Up @@ -71,4 +116,36 @@
}
}
}
}

/// A NSWindow which can be configured in a deterministic way.
public final class GenericWindow: NSWindow {
public init(backingScaleFactor: CGFloat = 2.0, colorSpace: NSColorSpace? = nil) {
self._backingScaleFactor = backingScaleFactor
self._explicitlySpecifiedColorSpace = colorSpace

super.init(contentRect: NSRect.zero, styleMask: [], backing: .buffered, defer: true)
}

private let _explicitlySpecifiedColorSpace: NSColorSpace?
private var _systemSpecifiedColorspace: NSColorSpace?

private let _backingScaleFactor: CGFloat
public override var backingScaleFactor: CGFloat {
return _backingScaleFactor
}

public override var colorSpace: NSColorSpace? {
get {
_explicitlySpecifiedColorSpace ?? self._systemSpecifiedColorspace
}
set {
self._systemSpecifiedColorspace = newValue
}
}
}

extension GenericWindow {
static let ci = GenericWindow(backingScaleFactor: 1.0, colorSpace: .genericRGB)
}
#endif
17 changes: 8 additions & 9 deletions Tests/SnapshotTestingTests/SnapshotTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ final class SnapshotTestingTests: XCTestCase {

func testNSView() {
#if os(macOS)
let button = NSButton()
button.bezelStyle = .rounded
button.title = "Push Me"
button.sizeToFit()
if !ProcessInfo.processInfo.environment.keys.contains("GITHUB_WORKFLOW") {
assertSnapshot(of: button, as: .image)
assertSnapshot(of: button, as: .recursiveDescription)
}
let button = NSButton()
button.bezelStyle = .rounded
button.title = "Push Me"
button.sizeToFit()
button.appearance = NSAppearance(named: .aqua)
if !ProcessInfo.processInfo.environment.keys.contains("GITHUB_WORKFLOW") {
assertSnapshot(matching: button, as: .image)
}
#endif
}

Expand All @@ -213,7 +213,6 @@ final class SnapshotTestingTests: XCTestCase {
view.layer?.cornerRadius = 5
if !ProcessInfo.processInfo.environment.keys.contains("GITHUB_WORKFLOW") {
assertSnapshot(of: view, as: .image)
assertSnapshot(of: view, as: .recursiveDescription)
}
#endif
}
Expand Down
Loading