Skip to content

Commit

Permalink
Configurable recursive description (#141)
Browse files Browse the repository at this point in the history
* Configurable recursive description

* Make private again

* Fix comment

* Remove stale snap

* Change tests

* Update Available-Snapshot-Strategies.md
  • Loading branch information
stephencelis authored Dec 16, 2018
1 parent 6fb30db commit deed033
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 48 deletions.
65 changes: 65 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)
- [`.image`](#image-8)
- [`.recursiveDescription`](#recursivedescription-3)
- [`URLRequest`](#urlrequest)
Expand Down Expand Up @@ -466,10 +467,27 @@ 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: 22, height: 22))

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

Records:
Expand All @@ -483,6 +501,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 +598,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
66 changes: 41 additions & 25 deletions Sources/SnapshotTesting/Common/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,39 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate {
#endif

#if os(iOS) || os(tvOS)
func prepareView(
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)")
}
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 +513,16 @@ 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
prepareView(
config: config,
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
traits: traits,
view: view,
viewController: viewController
)
// NB: Avoid safe area influence.
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 @@ -554,7 +570,7 @@ private func add(traits: UITraitCollection, viewController: UIViewController, to
rootViewController.view.layoutIfNeeded()
}

private class Window: UIWindow {
private 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
33 changes: 31 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,7 +195,21 @@ 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))
#endif
}
Expand Down Expand Up @@ -265,6 +284,16 @@ class SnapshotTestingTests: TestCase {
assertSnapshot(matching: viewController, as: .image(on: .iPadPro10_5), named: "ipad-pro-10-5")
assertSnapshot(matching: viewController, as: .image(on: .iPadPro12_9), named: "ipad-pro-12-9")

assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPhoneSe), named: "iphone-se")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPhone8), named: "iphone-8")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPhone8Plus), named: "iphone-8-plus")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPhoneX), named: "iphone-x")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPhoneXr), named: "iphone-xr")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPhoneXsMax), named: "iphone-xs-max")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPadMini), named: "ipad-mini")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPadPro10_5), named: "ipad-pro-10-5")
assertSnapshot(matching: viewController, as: .recursiveDescription(on: .iPadPro12_9), named: "ipad-pro-12-9")

assertSnapshot(matching: viewController, as: .image(on: .iPhoneSe(.portrait)), named: "iphone-se")
assertSnapshot(matching: viewController, as: .image(on: .iPhone8(.portrait)), named: "iphone-8")
assertSnapshot(matching: viewController, as: .image(on: .iPhone8Plus(.portrait)), named: "iphone-8-plus")
Expand Down Expand Up @@ -436,7 +465,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,5 @@
<UIView; frame = (0 0; 1024 768); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (484.5 20; 55.5 20.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 384; 25 20.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (985 384; 39 20.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (508.5 750; 7.5 18); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<UIView; frame = (0 0; 1112 834); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (528.5 20; 55.5 20.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 417; 25 20.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (1073 417; 39 20.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (552.5 816; 7.5 18); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<UIView; frame = (0 0; 1366 1024); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (655.5 20; 55.5 20.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 512; 25 20.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (1327 512; 39 20.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (679.5 1006; 7.5 18); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<UIView; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (180.5 20; 53 19.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 368.5; 24 19.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (377 368.5; 37 19.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (203.5 719; 7.5 17); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<UIView; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (161 20; 53 19.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 334; 24 19.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (338 334; 37 19.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (184 650; 7.5 17); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<UIView; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (133.5 20; 53 19.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 284.5; 24 19.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (283 284.5; 37 19.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (156.5 551; 7.5 17); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<UIView; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer>>
| <UILabel; frame = (161 44; 53 19.5); text = 'What's'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (0 401.5; 24 19.5); text = 'the'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (338 401.5; 37 19.5); text = 'point'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
| <UILabel; frame = (184 761; 7.5 17); text = '?'; userInteractionEnabled = NO; layer = <_UILabelLayer>>
Loading

0 comments on commit deed033

Please sign in to comment.