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

Add additional options to list layout #173

Merged
merged 5 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- [Added additional layout configuration options](https://github.com/kyleve/Listable/pull/173/): `headerToFirstSectionSpacing` and `lastSectionToFooterSpacing` now let you control the spacing between the list header and content, and the content and the list footer.

- [Add support for snapshot testing `Item`s](https://github.com/kyleve/Listable/pull/171) via the `ItemPreviewView` class. This is a view type which takes in some configuration options, and an `Item`, which you can then use in snapshot tests to verify the appearance of your `Item` and `ItemContent` .

```
Expand Down Expand Up @@ -42,6 +44,8 @@

- Update `Item` callbacks to [allow for providing more info to the callback parameters](https://github.com/kyleve/Listable/pull/160).

- [`ListAppearance.Layout.padding` is now applied around all content in the list](https://github.com/kyleve/Listable/pull/173/), not only around sections. To regain the previous behavior, use `headerToFirstSectionSpacing` and `lastSectionToFooterSpacing`.

### Misc

# Past Releases
Expand Down
80 changes: 76 additions & 4 deletions Internal Pods/Snapshot/Sources/Snapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,23 @@ public struct Snapshot<Iteration:SnapshotIteration>
internal var onFail : OnFail = XCTFail

public init(
iterations : [Iteration],
for iteration: Iteration ,
settings : SnapshotSettings = .init(),
input : Iteration.RenderingFormat
) {
self.init(for: [iteration], settings : settings) { _ in input }
}

public init(
for iterations: [Iteration] ,
settings : SnapshotSettings = .init(),
input : Iteration.RenderingFormat
) {
self.init(for: iterations, settings : settings) { _ in input }
}

public init(
for iterations : [Iteration],
settings : SnapshotSettings = .init(),
test : @escaping Test
) {
Expand Down Expand Up @@ -56,10 +72,13 @@ public struct Snapshot<Iteration:SnapshotIteration>
functionName: functionName.description,
iteration: iteration.name
)

var onFailData : Data? = nil

do {
let rendering = iteration.prepare(render: try self.test(iteration))
let data = try OutputFormat.snapshotData(with: rendering)
onFailData = data

let existingData = try self.existingData(at: url)

Expand All @@ -74,7 +93,29 @@ public struct Snapshot<Iteration:SnapshotIteration>
try data.write(to: url)
}
} catch {
self.onFail("Snapshot test '\(iteration.name)' with format '\(OutputFormat.self)' failed with error: \(error).", testFilePath, line)
let data : String = {
if let onFailData = onFailData {
return onFailData.base64EncodedString()
} else {
return "Error generating snapshotData."
}
}()

self.onFail(
"""
Snapshot test '\(iteration.name)' with format '\(OutputFormat.self)' failed.

Error: \(error).

File extension: '.\(output.outputInfo.fileExtension)'.

Base64 Data (pass this to `Data.saveBase64(toPath: "~/Development/etc ...", content: "...")` to inspect locally):

'\(data)'.

""",
testFilePath, line
)
}
}
}
Expand Down Expand Up @@ -124,6 +165,29 @@ public struct Snapshot<Iteration:SnapshotIteration>
}


public extension Data
{
static func saveBase64(toPath path : String, content : String) -> Bool
{
let url = URL(fileURLWithPath: (path as NSString).expandingTildeInPath)

guard let data = Data(base64Encoded: content) else {
print("Could not create data from base64 string.")
return false
}

do {
try data.write(to: url)
} catch {
print("Could not write data to disk. Error: \(error)")
return false
}

return true
}
}


public enum SnapshotValidationError : Error
{
case notMatching
Expand All @@ -144,8 +208,16 @@ public protocol SnapshotOutputFormat

public struct SnapshotOutputInfo : Equatable
{
var directoryName : String
var fileExtension : String
public var directoryName : String
public var fileExtension : String

public init(
directoryName : String,
fileExtension : String
) {
self.directoryName = directoryName
self.fileExtension = fileExtension
}
}


Expand Down
8 changes: 4 additions & 4 deletions Internal Pods/Snapshot/Sources/ViewHierarchySnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import UIKit


public struct ViewHierarchySnapshot : SnapshotOutputFormat
public struct ViewHierarchySnapshot<ViewType:UIView> : SnapshotOutputFormat
{
// MARK: SnapshotOutputFormat

public typealias RenderingFormat = UIView
public typealias RenderingFormat = ViewType

public static func snapshotData(with renderingFormat : UIView) throws -> Data
public static func snapshotData(with renderingFormat : ViewType) throws -> Data
{
let hierarchy = renderingFormat.textHierarchy
let string = hierarchy.stringValue
Expand All @@ -29,7 +29,7 @@ public struct ViewHierarchySnapshot : SnapshotOutputFormat
)
}

public static func validate(render view: UIView, existingData : Data) throws
public static func validate(render view: ViewType, existingData : Data) throws
{
let textHierarchy = try ViewHierarchySnapshot.snapshotData(with: view)

Expand Down
67 changes: 63 additions & 4 deletions Internal Pods/Snapshot/Sources/ViewImageSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import UIKit


public struct ViewImageSnapshot : SnapshotOutputFormat
public struct ViewImageSnapshot<ViewType:UIView> : SnapshotOutputFormat
{
// MARK: SnapshotOutputFormat

public typealias RenderingFormat = UIView
public typealias RenderingFormat = ViewType

public static func snapshotData(with renderingFormat : UIView) throws -> Data
public static func snapshotData(with renderingFormat : ViewType) throws -> Data
{
return renderingFormat.toImage.pngData()!
}
Expand All @@ -26,7 +26,7 @@ public struct ViewImageSnapshot : SnapshotOutputFormat
)
}

public static func validate(render newView : UIView, existingData: Data) throws
public static func validate(render newView : ViewType, existingData: Data) throws
{
let existing = try ViewImageSnapshot.image(with: existingData)
let new = newView.toImage
Expand Down Expand Up @@ -79,10 +79,69 @@ extension UIView
}


/// Image diffing copied from Point Free (https://github.com/pointfreeco/swift-snapshot-testing/blob/master/Sources/SnapshotTesting/Snapshotting/UIImage.swift)
/// with some changes. Notably, we base the
extension UIImage
{
static func compareImages(lhs : UIImage, rhs : UIImage) -> Bool
{
return lhs.pngData() == rhs.pngData()
}

private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool {
guard let oldCgImage = old.cgImage else { return false }
guard let newCgImage = new.cgImage else { return false }
guard oldCgImage.width != 0 else { return false }
guard newCgImage.width != 0 else { return false }
guard oldCgImage.width == newCgImage.width else { return false }
guard oldCgImage.height != 0 else { return false }
guard newCgImage.height != 0 else { return false }
guard oldCgImage.height == newCgImage.height else { return false }

// Values between images may differ due to padding to multiple of 64 bytes per row,
// because of that a freshly taken view snapshot may differ from one stored as PNG.
// At this point we're sure that size of both images is the same, so we can go with minimal `bytesPerRow` value
// and use it to create contexts.
let minBytesPerRow = min(oldCgImage.bytesPerRow, newCgImage.bytesPerRow)
let byteCount = minBytesPerRow * oldCgImage.height

var oldBytes = [UInt8](repeating: 0, count: byteCount)
guard let oldContext = context(for: oldCgImage, bytesPerRow: minBytesPerRow, data: &oldBytes) else { return false }
guard let oldData = oldContext.data else { return false }
if let newContext = context(for: newCgImage, bytesPerRow: minBytesPerRow), let newData = newContext.data {
if memcmp(oldData, newData, byteCount) == 0 { return true }
}
let newer = UIImage(data: new.pngData()!)!
guard let newerCgImage = newer.cgImage else { return false }
var newerBytes = [UInt8](repeating: 0, count: byteCount)
guard let newerContext = context(for: newerCgImage, bytesPerRow: minBytesPerRow, data: &newerBytes) else { return false }
guard let newerData = newerContext.data else { return false }
if memcmp(oldData, newerData, byteCount) == 0 { return true }
if precision >= 1 { return false }
var differentPixelCount = 0
let threshold = 1 - precision
for byte in 0..<byteCount {
if oldBytes[byte] != newerBytes[byte] { differentPixelCount += 1 }
if Float(differentPixelCount) / Float(byteCount) > threshold { return false}
}
return true
}

private func context(for cgImage: CGImage, bytesPerRow: Int, data: UnsafeMutableRawPointer? = nil) -> CGContext? {
guard
let space = cgImage.colorSpace,
let context = CGContext(
data: data,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: bytesPerRow,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
else { return nil }

context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
return context
}
}
14 changes: 12 additions & 2 deletions Internal Pods/Snapshot/Sources/ViewIterations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,19 @@ public struct SizedViewIteration<ViewType:UIView> : SnapshotIteration

public func prepare(render : ViewType) -> ViewType
{
render.frame.origin = .zero
render.frame.size = self.size
render.frame = CGRect(origin: .zero, size: self.size)
render.layoutIfNeeded()

// Some views, like UICollectionView, do not lay out properly
// without spinning the runloop once, in order to update the onscreen cells.
self.waitForOneRunloop()

return render
}

private func waitForOneRunloop()
{
let runloop = RunLoop.main
runloop.run(mode: .default, before: Date(timeIntervalSinceNow: 0.001))
}
}
4 changes: 2 additions & 2 deletions Internal Pods/Snapshot/Tests/SnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class SnapshotTests : XCTestCase
{
fileprivate func newSnapshot(with test : @escaping Snapshot<TestIteration>.Test) -> Snapshot<TestIteration>
{
return Snapshot(iterations: [TestIteration(name: "Test")], test: test)
return Snapshot(for: [TestIteration(name: "Test")], test: test)
}

func test_no_asset_writes_and_passes()
Expand Down Expand Up @@ -106,7 +106,7 @@ class SnapshotTests : XCTestCase
SizedViewIteration(size: CGSize(width: 300.0, height: 300.0)),
]

let snapshot = Snapshot(iterations: iterations) { iteration in
let snapshot = Snapshot(for: iterations) { iteration in
let root = ViewType1(frame: .init(origin: .zero, size: .init(width: 150.0, height: 150.0)))
root.backgroundColor = .init(white: 0.8, alpha: 1.0)

Expand Down
Loading