AsyncDisplayKit + iOSSnapshotTestCase 사용하기

Texture(AsyncDisplayKit)에서 UI 스냅샷 테스트를 하고 싶어졌다. 하지만 iOSSnapshotTestCase의 기본 예제로는 사용이 불가능하다.

혹시나 싶어 Texture 깃허브를 찾아보았더니 이미 iOSSnapshotTestCase를 사용하고 있었다.

https://github.com/TextureGroup/Texture/blob/master/Tests/ASSnapshotTestCase.mm

위는 Objective-C로 작성되어있어서 Swift 프로젝트에 적용하기에 번거롭다는 단점이 있어서 조금 포팅을 했다. (일부 함수들은 매크로로 정의되어 있는 것 같아서 함수로 따로 빼두었다)

import AsyncDisplayKit
import FBSnapshotTestCase

class ASSnapshotTestCase: FBSnapshotTestCase {
  
  let helper = ASDisplayNodeTestsHelper()
  
  func ASSnapshotTestCaseDefaultSuffixes() -> NSOrderedSet {
    let suffixesSet: NSMutableOrderedSet = []
    suffixesSet.add("_64")
    return suffixesSet
  }
  
  /// Hack for testing.  ASDisplayNode lacks an explicit -render method, so we manually hit its layout & display codepaths.
  func hackilySynchronouslyRecursivelyRenderNode(_ node: ASDisplayNode) {
    // Disable asynchronous display for rendering snapshots since things like UITraitCollection are thread-local
    // so changes to them (`-[UITraitCollection performAsCurrentTraitCollection]`) aren't preserved across threads.
    // Since the goal of this method is to just to ensure a node is rendered before snapshotting, this should be reasonable default for all callers.
    #if AS_AT_LEAST_IOS13
    if #available(iOS 13.0, *) {
      ASTraitCollectionPropagateDown(node, ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection.current))
    }
    #endif
    node.displaysAsynchronously = false
    ASDisplayNodePerformBlockOnEveryNode(nil, node, true, { node in
      node.layer.setNeedsDisplay()
    })
    node.recursivelyEnsureDisplaySynchronously(true)
  }
  
  
  func layoutSpecUnderTest(
    _ layoutSpec: ASLayoutSpec,
    sizeRange: ASSizeRange,
    subnodes: [ASDisplayNode]
  ) {
    let node = ASTestNode()
    
    subnodes.forEach { node.addSubnode($0) }
    
    node.layoutSpecUnderTest = layoutSpec
    
    ASDisplayNodeSizeToRange(node: node, range: sizeRange)
    ASSnapshotVerifyLayer(node: node)
  }
  
  func ASSnapshotVerifyNode(node: ASDisplayNode) {
    hackilySynchronouslyRecursivelyRenderNode(node)
    FBSnapshotVerifyView(node.view)
  }
  
  func ASSnapshotVerifyLayer(node: ASDisplayNode) {
    hackilySynchronouslyRecursivelyRenderNode(node)
    FBSnapshotVerifyLayer(node.layer)
  }
  
  func ASDisplayNodeSizeToFitSize(node: ASDisplayNode, size: CGSize) {
    helper.ASDisplayNodeSizeToFitSize(node, size)
  }
  
  func ASDisplayNodeSizeToRange(node: ASDisplayNode, range: ASSizeRange) {
    helper.ASDisplayNodeSizeToFitSizeRange(node, range)
  }
  
}

사용은 아래처럼 하면 된다.

class IWCartEstimateDetailModifyHeaderNodeTests: ASSnapshotTestCase {
  
  override func setUp() {
    super.setUp()
    recordMode = false
  }
  
  func testNode() {
    let node = IWCartEstimateDetailModifyHeaderNode()
    ASDisplayNodeSizeToFitSize(node: node, size: .init(width: 375, height: 50))
    ASSnapshotVerifyNode(node: node)
  }
}

끝!