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

Configurable recursive description #141

Merged
merged 6 commits into from
Dec 16, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
71 changes: 71 additions & 0 deletions Documentation/Available-Snapshot-Strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ If you'd like to submit your own custom strategy, see [Contributing](../CONTRIBU
- [`.image`](#image-7)
- [`.recursiveDescription`](#recursivedescription-2)
- [`UIViewController`](#uiviewcontroller)
- [`.hierarchy`](#hierarchy)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding your .hierarchy strategy to the documentation.

- [`.image`](#image-8)
- [`.recursiveDescription`](#recursivedescription-3)
- [`URLRequest`](#urlrequest)
Expand Down Expand Up @@ -466,6 +467,29 @@ A snapshot strategy for comparing views based on a recursive description of thei

**Format:** `String`

#### Parameters:

- `size: CGSize = nil`

A view size override.

- `traits: UITraitCollection = .init()`

A trait collection override.

#### Example

``` swift
// Layout on the current device.
assertSnapshot(matching: view, as: .recursiveDescription)

// Layout with a certain size.
assertSnapshot(matching: view, as: .recursiveDescription(size: .init(width: 44, height: 44))

// Layout with a certain trait collection.
assertSnapshot(matching: view, as: .recursiveDescription(traits: .init(horizontalSizeClass: .regular))
```

#### Example

``` swift
Expand All @@ -483,6 +507,35 @@ Records:

**Platforms:** iOS, tvOS

### `.hierarchy`

A snapshot strategy for comparing view controllers based on their embedded controller hierarchy.

**Format:** `String`

#### Example

``` swift
assertSnapshot(matching: vc, as: .hierarchy)
```

Records:

```
<UITabBarController>, state: appeared, view: <UILayoutContainerView>
| <UINavigationController>, state: appeared, view: <UILayoutContainerView>
| | <UIPageViewController>, state: appeared, view: <_UIPageViewControllerContentView>
| | | <UIViewController>, state: appeared, view: <UIView>
| <UINavigationController>, state: disappeared, view: <UILayoutContainerView> not in the window
| | <UIViewController>, state: disappeared, view: (view not loaded)
| <UINavigationController>, state: disappeared, view: <UILayoutContainerView> not in the window
| | <UIViewController>, state: disappeared, view: (view not loaded)
| <UINavigationController>, state: disappeared, view: <UILayoutContainerView> not in the window
| | <UIViewController>, state: disappeared, view: (view not loaded)
| <UINavigationController>, state: disappeared, view: <UILayoutContainerView> not in the window
| | <UIViewController>, state: disappeared, view: (view not loaded)
```

### `.image`

A snapshot strategy for comparing layers based on pixel equality.
Expand Down Expand Up @@ -551,10 +604,28 @@ A snapshot strategy for comparing view controller views based on a recursive des

**Format:** `String`

#### Parameters:

- `on: ViewImageConfig`

A set of device configuration settings.

- `size: CGSize = nil`

A view size override.

- `traits: UITraitCollection = .init()`

A trait collection override.

#### Example

``` swift
// Layout on the current device.
assertSnapshot(matching: vc, as: .recursiveDescription)

// Layout as if on a certain device.
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneSe(.portrait))
```

Records:
Expand Down
70 changes: 43 additions & 27 deletions Sources/SnapshotTesting/Common/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,40 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate {
#endif

#if os(iOS) || os(tvOS)
func prepareView(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we can prepare views for text-based snapshots, as well.

config: ViewImageConfig,
drawHierarchyInKeyWindow: Bool,
traits: UITraitCollection,
view: UIView,
viewController: UIViewController
) {
let size = config.size ?? viewController.view.frame.size
guard size.width > 0, size.height > 0 else {
fatalError("View not renderable to image at size \(size)")
}
// NB: Avoid safe area influence.
view.frame.size = size
if view != viewController.view {
viewController.view.bounds = view.bounds
viewController.view.addSubview(view)
}
let traits = UITraitCollection(traitsFrom: [config.traits, traits])
let window: UIWindow
if drawHierarchyInKeyWindow {
guard let keyWindow = UIApplication.shared.keyWindow else {
fatalError("'drawHierarchyInKeyWindow' requires tests to be run in a host application")
}
window = keyWindow
window.frame.size = size
} else {
window = Window(
config: .init(safeArea: config.safeArea, size: config.size ?? size, traits: traits),
viewController: viewController
)
}
add(traits: traits, viewController: viewController, to: window)
}

func snapshotView(
config: ViewImageConfig,
drawHierarchyInKeyWindow: Bool,
Expand All @@ -480,33 +514,15 @@ func snapshotView(
viewController: UIViewController
)
-> Async<UIImage> {
let size = config.size ?? viewController.view.frame.size
guard size.width > 0, size.height > 0 else {
fatalError("View not renderable to image at size \(size)")
}
let initialFrame = view.frame
// NB: Avoid safe area influence.
prepareView(
config: config,
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
traits: traits,
view: view,
viewController: viewController
)
if config.safeArea == .zero { view.frame.origin = .init(x: offscreen, y: offscreen) }
view.frame.size = size
if view != viewController.view {
viewController.view.bounds = view.bounds
viewController.view.addSubview(view)
}
let traits = UITraitCollection(traitsFrom: [config.traits, traits])
let window: UIWindow
if drawHierarchyInKeyWindow {
guard let keyWindow = UIApplication.shared.keyWindow else {
fatalError("'drawHierarchyInKeyWindow' requires tests to be run in a host application")
}
window = keyWindow
window.frame.size = size
} else {
window = Window(
config: .init(safeArea: config.safeArea, size: config.size ?? size, traits: traits),
viewController: viewController
)
}
add(traits: traits, viewController: viewController, to: window)
return view.snapshot ?? Async { callback in
addImagesForRenderedViews(view).sequence().run { views in
callback(
Expand Down Expand Up @@ -536,7 +552,7 @@ func renderer(bounds: CGRect, for traits: UITraitCollection) -> UIGraphicsImageR
return renderer
}

private func add(traits: UITraitCollection, viewController: UIViewController, to window: UIWindow) {
func add(traits: UITraitCollection, viewController: UIViewController, to window: UIWindow) {
let rootViewController = UIViewController()
rootViewController.view.backgroundColor = .clear
rootViewController.view.frame = window.frame
Expand All @@ -554,7 +570,7 @@ private func add(traits: UITraitCollection, viewController: UIViewController, to
rootViewController.view.layoutIfNeeded()
}

private class Window: UIWindow {
final class Window: UIWindow {
var config: ViewImageConfig

init(config: ViewImageConfig, viewController: UIViewController) {
Expand Down
30 changes: 21 additions & 9 deletions Sources/SnapshotTesting/Snapshotting/UIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,27 @@ extension Snapshotting where Value == UIView, Format == UIImage {

extension Snapshotting where Value == UIView, Format == String {
/// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies.
public static var recursiveDescription: Snapshotting<UIView, String> {
return SimplySnapshotting.lines.pullback { view in
view.setNeedsLayout()
view.layoutIfNeeded()
return purgePointers(
view.perform(Selector(("recursiveDescription"))).retain().takeUnretainedValue()
as! String
)
}
public static let recursiveDescription = Snapshotting<UIView, String>.recursiveDescription()

/// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies.
public static func recursiveDescription(
size: CGSize? = nil,
traits: UITraitCollection = .init()
)
-> Snapshotting<UIView, String> {
return SimplySnapshotting.lines.pullback { view in
prepareView(
config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: traits),
drawHierarchyInKeyWindow: false,
traits: .init(),
view: view,
viewController: .init()
)
return purgePointers(
view.perform(Selector(("recursiveDescription"))).retain().takeUnretainedValue()
as! String
)
}
}
}
#endif
48 changes: 40 additions & 8 deletions Sources/SnapshotTesting/Snapshotting/UIViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,50 @@ extension Snapshotting where Value == UIViewController, Format == UIImage {
}

extension Snapshotting where Value == UIViewController, Format == String {
/// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies.
public static var recursiveDescription: Snapshotting {
return Snapshotting<UIView, String>.recursiveDescription.pullback { $0.view }
}

/// A snapshot strategy for comparing view controllers based on their embedded controller hierarchy.
public static var hierarchy: Snapshotting {
return Snapshotting<String, String>.lines.pullback { vc in
purgePointers(
vc.perform(Selector(("_printHierarchy"))).retain().takeUnretainedValue() as! String
return Snapshotting<String, String>.lines.pullback { viewController in
prepareView(
config: .init(),
drawHierarchyInKeyWindow: false,
traits: .init(),
view: viewController.view,
viewController: viewController
)
return purgePointers(
viewController.perform(Selector(("_printHierarchy"))).retain().takeUnretainedValue() as! String
)
}
}

/// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies.
public static let recursiveDescription = Snapshotting.recursiveDescription()

/// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies.
///
/// - Parameters:
/// - config: A set of device configuration settings.
/// - size: A view size override.
/// - traits: A trait collection override.
public static func recursiveDescription(
on config: ViewImageConfig = .init(),
size: CGSize? = nil,
traits: UITraitCollection = .init()
)
-> Snapshotting<UIViewController, String> {
return SimplySnapshotting.lines.pullback { viewController in
prepareView(
config: .init(safeArea: config.safeArea, size: size ?? config.size, traits: config.traits),
drawHierarchyInKeyWindow: false,
traits: .init(),
view: viewController.view,
viewController: viewController
)
return purgePointers(
viewController.view.perform(Selector(("recursiveDescription"))).retain().takeUnretainedValue()
as! String
)
}
}
}
#endif
24 changes: 22 additions & 2 deletions Tests/SnapshotTestingTests/SnapshotTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class SnapshotTestingTests: TestCase {
// record = true
}

override func tearDown() {
record = false
super.tearDown()
}

func testAny() {
struct User { let id: Int, name: String, bio: String }
let user = User(id: 1, name: "Blobby", bio: "Blobbed around the world.")
Expand Down Expand Up @@ -190,8 +195,23 @@ class SnapshotTestingTests: TestCase {

func testTableViewController() {
#if os(iOS)
let tableViewController = UITableViewController()
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
let tableViewController = TableViewController()
assertSnapshot(matching: tableViewController, as: .image(on: .iPhoneSe))
assertSnapshot(matching: tableViewController, as: .recursiveDescription(on: .iPhoneSe))
#endif
}

Expand Down Expand Up @@ -436,7 +456,7 @@ class SnapshotTestingTests: TestCase {

func testViewControllerHierarchy() {
#if os(iOS)
let page = UIPageViewController.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
let page = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
page.setViewControllers([UIViewController()], direction: .forward, animated: false)
let tab = UITabBarController()
tab.viewControllers = [
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<UITableView; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray>; layer = <CALayer>; contentOffset: {0, -20}; contentSize: {320, 440}; adjustedContentInset: {20, 0, 0, 0}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot is now on a specific device override.

| <UITableViewCell; frame = (0 396; 320 44); text = '9'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '9'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 352; 320 44); text = '8'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '8'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 308; 320 44); text = '7'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '7'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 264; 320 44); text = '6'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '6'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 220; 320 44); text = '5'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '5'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 176; 320 44); text = '4'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '4'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 132; 320 44); text = '3'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '3'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 88; 320 44); text = '2'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '2'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 44; 320 44); text = '1'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '1'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <UITableViewCell; frame = (0 0; 320 44); text = '0'; autoresize = W; layer = <CALayer>>
| | <UITableViewCellContentView; frame = (0 0; 320 43.5); gestureRecognizers = <NSArray>; layer = <CALayer>>
| | | <UITableViewLabel; frame = (16 0; 288 43.5); text = '0'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| | <_UITableViewCellSeparatorView; frame = (16 43.5; 304 0.5); layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 483.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 527.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 571.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 615.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 659.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 703.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 747.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 791.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 835.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <_UITableViewCellSeparatorView; frame = (16 879.5; 304 0.5); autoresize = W; layer = <CALayer>>
| <UIImageView; frame = (314.5 119.5; 2.5 422.5); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer>>
| <UIImageView; frame = (6 542.5; 308 2.5); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer>>
Loading