Skip to content

Commit

Permalink
Using drawViewHierarchyInRect instead of renderInContext for snap…
Browse files Browse the repository at this point in the history
…shot tests

Summary:
We found that `-[CALayer renderInContext:]` produces bad results in some cases (which is actually documented thing!),
so we decided to replace it with `-[UIView drawViewHierarchyInRect:]` which is more reliable (I hope).
As part of this change I completly removed support of `CALayer` from local fork of `RNTesterIntegrationTests`.

See #14011 (comment) for more details.

janicduplessis

Reviewed By: javache

Differential Revision: D5129492

fbshipit-source-id: 6a9227037c85bb8f862d55267f5301e177985ad9
  • Loading branch information
shergin authored and facebook-github-bot committed May 26, 2017
1 parent cc1a4b0 commit 3df537a
Show file tree
Hide file tree
Showing 32 changed files with 36 additions and 185 deletions.
51 changes: 0 additions & 51 deletions Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h
Expand Up @@ -18,44 +18,6 @@
#define FB_REFERENCE_IMAGE_DIR "\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\""
#endif

/**
Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though.
@param view The view to snapshot
@param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
@param referenceImageDirectorySuffix An optional suffix, appended to the reference image directory path, such as "_iOS8"
*/
#define FBSnapshotVerifyViewWithReferenceDirectorySuffix(view__, identifier__, referenceImagesDirectorySuffix__) \
{ \
NSError *error__ = nil; \
NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%s%@", FB_REFERENCE_IMAGE_DIR, referenceImagesDirectorySuffix__]; \
BOOL comparisonSuccess__ = [self compareSnapshotOfView:(view__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \
XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \
}

#define FBSnapshotVerifyView(view__, identifier__) \
{ \
FBSnapshotVerifyViewWithReferenceDirectorySuffix(view__, identifier__, @""); \
}

/**
Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though.
@param layer The layer to snapshot
@param identifier An optional identifier, used is there are multiple snapshot tests in a given -test method.
@param referenceImageDirectorySuffix An optional suffix, appended to the reference image directory path, such as "_iOS8"
*/
#define FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, referenceImagesDirectorySuffix__) \
{ \
NSError *error__ = nil; \
NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%s%@", FB_REFERENCE_IMAGE_DIR, referenceImagesDirectorySuffix__]; \
BOOL comparisonSuccess__ = [self compareSnapshotOfLayer:(layer__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \
XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \
}

#define FBSnapshotVerifyLayer(layer__, identifier__) \
{ \
FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, @""); \
}

/**
The base class of view snapshotting tests. If you have small UI component, it's often easier to configure it in a test
and compare an image of the view to a reference image that write lots of complex layout-code tests.
Expand All @@ -70,19 +32,6 @@ FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, @""); \
*/
@property (readwrite, nonatomic, assign) BOOL recordMode;

/**
Performs the comparisons or records a snapshot of the layer if recordMode is YES.
@param layer The Layer to snapshot
@param referenceImagesDirectory The directory in which reference images are stored.
@param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
@param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
error:(NSError **)errorPtr;

/**
Performs the comparisons or records a snapshot of the view if recordMode is YES.
@param view The view to snapshot
Expand Down
33 changes: 4 additions & 29 deletions Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m
Expand Up @@ -42,41 +42,16 @@ - (void)setRecordMode:(BOOL)recordMode
self.snapshotController.recordMode = recordMode;
}

- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
return [self _compareSnapshotOfViewOrLayer:layer
referenceImagesDirectory:referenceImagesDirectory
identifier:identifier
error:errorPtr];
}

- (BOOL)compareSnapshotOfView:(UIView *)view
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
return [self _compareSnapshotOfViewOrLayer:view
referenceImagesDirectory:referenceImagesDirectory
identifier:identifier
error:errorPtr];
}

#pragma mark -
#pragma mark Private API

- (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
_snapshotController.referenceImagesDirectory = referenceImagesDirectory;
return [_snapshotController compareSnapshotOfViewOrLayer:viewOrLayer
selector:self.invocation.selector
identifier:identifier
error:errorPtr];
return [_snapshotController compareSnapshotOfView:view
selector:self.invocation.selector
identifier:identifier
error:errorPtr];
}

@end
29 changes: 1 addition & 28 deletions Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h
Expand Up @@ -41,7 +41,7 @@ extern NSString *const FBReferenceImageFilePathKey;
@property(readwrite, nonatomic, assign) BOOL recordMode;

/**
@param testClass The subclass of FBSnapshotTestCase that is using this controller.d.
@param testClass The subclass of FBSnapshotTestCase that is using this controller.
@returns An instance of FBSnapshotTestController.
*/
- (id)initWithTestClass:(Class)testClass;
Expand All @@ -53,19 +53,6 @@ extern NSString *const FBReferenceImageFilePathKey;
*/
- (id)initWithTestName:(NSString *)testName;


/**
Performs the comparison of the layer.
@param layer The Layer to snapshot.
@param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method.
@param errorPtr An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;

/**
Performs the comparison of the view.
@param view The view to snapshot.
Expand All @@ -79,20 +66,6 @@ extern NSString *const FBReferenceImageFilePathKey;
identifier:(NSString *)identifier
error:(NSError **)errorPtr;

/**
Performs the comparison of a view or layer.
@param viewOrLayer The view or layer to snapshot.
@param selector selector
@param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method.
@param errorPtr An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;


/**
The directory in which reference images are stored.
*/
Expand Down
108 changes: 31 additions & 77 deletions Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m
Expand Up @@ -10,13 +10,13 @@

#import "FBSnapshotTestController.h"

#import "UIImage+Compare.h"
#import "UIImage+Diff.h"

#import <objc/runtime.h>

#import <UIKit/UIKit.h>

#import "UIImage+Compare.h"
#import "UIImage+Diff.h"

NSString *const FBSnapshotTestControllerErrorDomain = @"FBSnapshotTestControllerErrorDomain";

NSString *const FBReferenceImageFilePathKey = @"FBReferenceImageFilePathKey";
Expand All @@ -39,15 +39,14 @@ @implementation FBSnapshotTestController
NSFileManager *_fileManager;
}

#pragma mark -
#pragma mark Lifecycle
#pragma mark - Lifecycle

- (id)initWithTestClass:(Class)testClass;
- (instancetype)initWithTestClass:(Class)testClass;
{
return [self initWithTestName:NSStringFromClass(testClass)];
}

- (id)initWithTestName:(NSString *)testName
- (instancetype)initWithTestName:(NSString *)testName
{
if ((self = [super init])) {
_testName = [testName copy];
Expand All @@ -56,16 +55,14 @@ - (id)initWithTestName:(NSString *)testName
return self;
}

#pragma mark -
#pragma mark Properties
#pragma mark - Properties

- (NSString *)description
{
return [NSString stringWithFormat:@"%@ %@", [super description], _referenceImagesDirectory];
}

#pragma mark -
#pragma mark Public API
#pragma mark - Public API

- (UIImage *)referenceImageForSelector:(SEL)selector
identifier:(NSString *)identifier
Expand Down Expand Up @@ -211,8 +208,7 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image
return NO;
}

#pragma mark -
#pragma mark Private API
#pragma mark - Private API

typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) {
FBTestSnapshotFileNameTypeReference,
Expand Down Expand Up @@ -280,51 +276,28 @@ - (NSString *)_failedFilePathForSelector:(SEL)selector
return filePath;
}

- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
return [self compareSnapshotOfViewOrLayer:layer
selector:selector
identifier:identifier
error:errorPtr];
}

- (BOOL)compareSnapshotOfView:(UIView *)view
- (BOOL)compareSnapshotOfView:(id)view
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
return [self compareSnapshotOfViewOrLayer:view
selector:selector
identifier:identifier
error:errorPtr];
}

- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
if (self.recordMode) {
return [self _recordSnapshotOfViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr];
return [self _recordSnapshotOfView:view selector:selector identifier:identifier error:errorPtr];
} else {
return [self _performPixelComparisonWithViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr];
return [self _performPixelComparisonWithView:view selector:selector identifier:identifier error:errorPtr];
}
}

#pragma mark -
#pragma mark Private API
#pragma mark - Private API

- (BOOL)_performPixelComparisonWithViewOrLayer:(UIView *)viewOrLayer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
- (BOOL)_performPixelComparisonWithView:(UIView *)view
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr];
if (nil != referenceImage) {
UIImage *snapshot = [self _snapshotViewOrLayer:viewOrLayer];
UIImage *snapshot = [self _snapshotView:view];
BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot error:errorPtr];
if (!imagesSame) {
[self saveFailedReferenceImage:referenceImage
Expand All @@ -338,46 +311,33 @@ - (BOOL)_performPixelComparisonWithViewOrLayer:(UIView *)viewOrLayer
return NO;
}

- (BOOL)_recordSnapshotOfViewOrLayer:(id)viewOrLayer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
- (BOOL)_recordSnapshotOfView:(UIView *)view
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
UIImage *snapshot = [self _snapshotViewOrLayer:viewOrLayer];
UIImage *snapshot = [self _snapshotView:view];
return [self saveReferenceImage:snapshot selector:selector identifier:identifier error:errorPtr];
}

- (UIImage *)_snapshotViewOrLayer:(id)viewOrLayer
- (UIImage *)_snapshotView:(UIView *)view
{
CALayer *layer = nil;

if ([viewOrLayer isKindOfClass:[UIView class]]) {
return [self _renderView:viewOrLayer];
} else if ([viewOrLayer isKindOfClass:[CALayer class]]) {
layer = (CALayer *)viewOrLayer;
[layer layoutIfNeeded];
return [self _renderLayer:layer];
} else {
[NSException raise:@"Only UIView and CALayer classes can be snapshotted" format:@"%@", viewOrLayer];
}
return nil;
}
[view layoutIfNeeded];

- (UIImage *)_renderLayer:(CALayer *)layer
{
CGRect bounds = layer.bounds;
CGRect bounds = view.bounds;

NSAssert1(CGRectGetWidth(bounds), @"Zero width for layer %@", layer);
NSAssert1(CGRectGetHeight(bounds), @"Zero height for layer %@", layer);
NSAssert1(CGRectGetWidth(bounds), @"Zero width for view %@", view);
NSAssert1(CGRectGetHeight(bounds), @"Zero height for view %@", view);

UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
NSAssert1(context, @"Could not generate context for layer %@", layer);
NSAssert1(context, @"Could not generate context for view %@", view);

UIGraphicsPushContext(context);
CGContextSaveGState(context);
{
[layer renderInContext:context];
BOOL success = [view drawViewHierarchyInRect:bounds afterScreenUpdates:YES];
NSAssert1(success, @"Could not create snapshot for view %@", view);
}
CGContextRestoreGState(context);
UIGraphicsPopContext();
Expand All @@ -388,10 +348,4 @@ - (UIImage *)_renderLayer:(CALayer *)layer
return snapshot;
}

- (UIImage *)_renderView:(UIView *)view
{
[view layoutIfNeeded];
return [self _renderLayer:view.layer];
}

@end
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3df537a

Please sign in to comment.