Project import
diff --git a/FBSnapshotTestCase.modulemap b/FBSnapshotTestCase.modulemap new file mode 100644 index 0000000..733e78b --- /dev/null +++ b/FBSnapshotTestCase.modulemap
@@ -0,0 +1,15 @@ +framework module FBSnapshotTestCase { + umbrella header "FBSnapshotTestCase.h" + + export * + module * { export * } + + header "FBSnapshotTestCase.h" + header "FBSnapshotTestCasePlatform.h" + header "FBSnapshotTestController.h" + + private header "UIImage+Compare.h" + private header "UIImage+Diff.h" + private header "UIImage+Snapshot.h" +} +
diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.h b/FBSnapshotTestCase/Categories/UIImage+Compare.h new file mode 100644 index 0000000..9091d62 --- /dev/null +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.h
@@ -0,0 +1,37 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#import <UIKit/UIKit.h> + +@interface UIImage (Compare) + +- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance; + +@end
diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.m b/FBSnapshotTestCase/Categories/UIImage+Compare.m new file mode 100644 index 0000000..c997f57 --- /dev/null +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.m
@@ -0,0 +1,134 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#import <FBSnapshotTestCase/UIImage+Compare.h> + +// This makes debugging much more fun +typedef union { + uint32_t raw; + unsigned char bytes[4]; + struct { + char red; + char green; + char blue; + char alpha; + } __attribute__ ((packed)) pixels; +} FBComparePixel; + +@implementation UIImage (Compare) + +- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance +{ + NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size."); + + CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(self.CGImage), CGImageGetHeight(self.CGImage)); + CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage)); + + // The images have the equal size, so we could use the smallest amount of bytes because of byte padding + size_t minBytesPerRow = MIN(CGImageGetBytesPerRow(self.CGImage), CGImageGetBytesPerRow(image.CGImage)); + size_t referenceImageSizeBytes = referenceImageSize.height * minBytesPerRow; + void *referenceImagePixels = calloc(1, referenceImageSizeBytes); + void *imagePixels = calloc(1, referenceImageSizeBytes); + + if (!referenceImagePixels || !imagePixels) { + free(referenceImagePixels); + free(imagePixels); + return NO; + } + + CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels, + referenceImageSize.width, + referenceImageSize.height, + CGImageGetBitsPerComponent(self.CGImage), + minBytesPerRow, + CGImageGetColorSpace(self.CGImage), + (CGBitmapInfo)kCGImageAlphaPremultipliedLast + ); + CGContextRef imageContext = CGBitmapContextCreate(imagePixels, + imageSize.width, + imageSize.height, + CGImageGetBitsPerComponent(image.CGImage), + minBytesPerRow, + CGImageGetColorSpace(image.CGImage), + (CGBitmapInfo)kCGImageAlphaPremultipliedLast + ); + + if (!referenceImageContext || !imageContext) { + CGContextRelease(referenceImageContext); + CGContextRelease(imageContext); + free(referenceImagePixels); + free(imagePixels); + return NO; + } + + CGContextDrawImage(referenceImageContext, CGRectMake(0, 0, referenceImageSize.width, referenceImageSize.height), self.CGImage); + CGContextDrawImage(imageContext, CGRectMake(0, 0, imageSize.width, imageSize.height), image.CGImage); + + CGContextRelease(referenceImageContext); + CGContextRelease(imageContext); + + BOOL imageEqual = YES; + + // Do a fast compare if we can + if (tolerance == 0) { + imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0); + } else { + // Go through each pixel in turn and see if it is different + const NSInteger pixelCount = referenceImageSize.width * referenceImageSize.height; + + FBComparePixel *p1 = referenceImagePixels; + FBComparePixel *p2 = imagePixels; + + NSInteger numDiffPixels = 0; + for (int n = 0; n < pixelCount; ++n) { + // If this pixel is different, increment the pixel diff count and see + // if we have hit our limit. + if (p1->raw != p2->raw) { + numDiffPixels ++; + + CGFloat percent = (CGFloat)numDiffPixels / pixelCount; + if (percent > tolerance) { + imageEqual = NO; + break; + } + } + + p1++; + p2++; + } + } + + free(referenceImagePixels); + free(imagePixels); + + return imageEqual; +} + +@end
diff --git a/FBSnapshotTestCase/Categories/UIImage+Diff.h b/FBSnapshotTestCase/Categories/UIImage+Diff.h new file mode 100644 index 0000000..a0863f3 --- /dev/null +++ b/FBSnapshotTestCase/Categories/UIImage+Diff.h
@@ -0,0 +1,37 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#import <UIKit/UIKit.h> + +@interface UIImage (Diff) + +- (UIImage *)fb_diffWithImage:(UIImage *)image; + +@end
diff --git a/FBSnapshotTestCase/Categories/UIImage+Diff.m b/FBSnapshotTestCase/Categories/UIImage+Diff.m new file mode 100644 index 0000000..ebb72fe --- /dev/null +++ b/FBSnapshotTestCase/Categories/UIImage+Diff.m
@@ -0,0 +1,56 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#import <FBSnapshotTestCase/UIImage+Diff.h> + +@implementation UIImage (Diff) + +- (UIImage *)fb_diffWithImage:(UIImage *)image +{ + if (!image) { + return nil; + } + CGSize imageSize = CGSizeMake(MAX(self.size.width, image.size.width), MAX(self.size.height, image.size.height)); + UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; + CGContextSetAlpha(context, 0.5); + CGContextBeginTransparencyLayer(context, NULL); + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + CGContextSetBlendMode(context, kCGBlendModeDifference); + CGContextSetFillColorWithColor(context,[UIColor whiteColor].CGColor); + CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height)); + CGContextEndTransparencyLayer(context); + UIImage *returnImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return returnImage; +} + +@end
diff --git a/FBSnapshotTestCase/Categories/UIImage+Snapshot.h b/FBSnapshotTestCase/Categories/UIImage+Snapshot.h new file mode 100644 index 0000000..b0d5b26 --- /dev/null +++ b/FBSnapshotTestCase/Categories/UIImage+Snapshot.h
@@ -0,0 +1,24 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <UIKit/UIKit.h> + +@interface UIImage (Snapshot) + +/// Uses renderInContext: to get a snapshot of the layer. ++ (UIImage *)fb_imageForLayer:(CALayer *)layer; + +/// Uses renderInContext: to get a snapshot of the view layer. ++ (UIImage *)fb_imageForViewLayer:(UIView *)view; + +/// Uses drawViewHierarchyInRect: to get a snapshot of the view and adds the view into a window if needed. ++ (UIImage *)fb_imageForView:(UIView *)view; + +@end
diff --git a/FBSnapshotTestCase/Categories/UIImage+Snapshot.m b/FBSnapshotTestCase/Categories/UIImage+Snapshot.m new file mode 100644 index 0000000..c792077 --- /dev/null +++ b/FBSnapshotTestCase/Categories/UIImage+Snapshot.m
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <FBSnapshotTestCase/UIImage+Snapshot.h> + +@implementation UIImage (Snapshot) + ++ (UIImage *)fb_imageForLayer:(CALayer *)layer +{ + CGRect bounds = layer.bounds; + NSAssert1(CGRectGetWidth(bounds), @"Zero width for layer %@", layer); + NSAssert1(CGRectGetHeight(bounds), @"Zero height for layer %@", layer); + + UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + NSAssert1(context, @"Could not generate context for layer %@", layer); + CGContextSaveGState(context); + [layer layoutIfNeeded]; + [layer renderInContext:context]; + CGContextRestoreGState(context); + + UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return snapshot; +} + ++ (UIImage *)fb_imageForViewLayer:(UIView *)view +{ + [view layoutIfNeeded]; + return [self fb_imageForLayer:view.layer]; +} + ++ (UIImage *)fb_imageForView:(UIView *)view +{ + CGRect bounds = view.bounds; + NSAssert1(CGRectGetWidth(bounds), @"Zero width for view %@", view); + NSAssert1(CGRectGetHeight(bounds), @"Zero height for view %@", view); + + UIWindow *window = view.window; + if (window == nil) { + window = [[UIWindow alloc] initWithFrame:bounds]; + [window addSubview:view]; + [window makeKeyAndVisible]; + } + + UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0); + [view layoutIfNeeded]; + [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; + + UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return snapshot; +} + +@end
diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.h b/FBSnapshotTestCase/FBSnapshotTestCase.h new file mode 100644 index 0000000..54e301e --- /dev/null +++ b/FBSnapshotTestCase/FBSnapshotTestCase.h
@@ -0,0 +1,200 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <FBSnapshotTestCase/FBSnapshotTestCasePlatform.h> + +#import <QuartzCore/QuartzCore.h> + +#import <UIKit/UIKit.h> + +#import <XCTest/XCTest.h> + +/* + There are three ways of setting reference image directories. + + 1. Set the preprocessor macro FB_REFERENCE_IMAGE_DIR to a double quoted + c-string with the path. + 2. Set an environment variable named FB_REFERENCE_IMAGE_DIR with the path. This + takes precedence over the preprocessor macro to allow for run-time override. + 3. Keep everything unset, which will cause the reference images to be looked up + inside the bundle holding the current test, in the + Resources/ReferenceImages_* directories. + */ +#ifndef FB_REFERENCE_IMAGE_DIR +#define FB_REFERENCE_IMAGE_DIR "" +#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 suffixes An NSOrderedSet of strings for the different suffixes + @param tolerance The percentage of pixels that can differ and still count as an 'identical' view + */ +#define FBSnapshotVerifyViewWithOptions(view__, identifier__, suffixes__, tolerance__) \ + FBSnapshotVerifyViewOrLayerWithOptions(View, view__, identifier__, suffixes__, tolerance__) + +#define FBSnapshotVerifyView(view__, identifier__) \ + FBSnapshotVerifyViewWithOptions(view__, identifier__, FBSnapshotTestCaseDefaultSuffixes(), 0) + + +/** + 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 suffixes An NSOrderedSet of strings for the different suffixes + @param tolerance The percentage of pixels that can differ and still count as an 'identical' layer + */ +#define FBSnapshotVerifyLayerWithOptions(layer__, identifier__, suffixes__, tolerance__) \ + FBSnapshotVerifyViewOrLayerWithOptions(Layer, layer__, identifier__, suffixes__, tolerance__) + +#define FBSnapshotVerifyLayer(layer__, identifier__) \ + FBSnapshotVerifyLayerWithOptions(layer__, identifier__, FBSnapshotTestCaseDefaultSuffixes(), 0) + + +#define FBSnapshotVerifyViewOrLayerWithOptions(what__, viewOrLayer__, identifier__, suffixes__, tolerance__) \ +{ \ + NSString *referenceImageDirectory = [self getReferenceImageDirectoryWithDefault:(@ FB_REFERENCE_IMAGE_DIR)]; \ + XCTAssertNotNil(referenceImageDirectory, @"Missing value for referenceImagesDirectory - Set FB_REFERENCE_IMAGE_DIR as Environment variable in your scheme.");\ + XCTAssertTrue((suffixes__.count > 0), @"Suffixes set cannot be empty %@", suffixes__); \ + \ + BOOL testSuccess__ = NO; \ + NSError *error__ = nil; \ + NSMutableArray *errors__ = [NSMutableArray array]; \ + \ + if (self.recordMode) { \ + \ + NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%@%@", referenceImageDirectory, suffixes__.firstObject]; \ + BOOL referenceImageSaved__ = [self compareSnapshotOf ## what__ :(viewOrLayer__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) tolerance:(tolerance__) error:&error__]; \ + if (!referenceImageSaved__) { \ + [errors__ addObject:error__]; \ + } \ + } else { \ + \ + for (NSString *suffix__ in suffixes__) { \ + NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%@%@", referenceImageDirectory, suffix__]; \ + BOOL referenceImageAvailable = [self referenceImageRecordedInDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \ + \ + if (referenceImageAvailable) { \ + BOOL comparisonSuccess__ = [self compareSnapshotOf ## what__ :(viewOrLayer__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) tolerance:(tolerance__) error:&error__]; \ + [errors__ removeAllObjects]; \ + if (comparisonSuccess__) { \ + testSuccess__ = YES; \ + break; \ + } else { \ + [errors__ addObject:error__]; \ + } \ + } else { \ + [errors__ addObject:error__]; \ + } \ + } \ + } \ + XCTAssertTrue(testSuccess__, @"Snapshot comparison failed: %@", errors__.firstObject); \ + XCTAssertFalse(self.recordMode, @"Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!"); \ +} + + +/** + 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. + + In order to flip the tests in your subclass to record the reference images set @c recordMode to @c YES. + + @attention When recording, the reference image directory should be explicitly + set, otherwise the images may be written to somewhere inside the + simulator directory. + + For example: + @code + - (void)setUp + { + [super setUp]; + self.recordMode = YES; + } + @endcode + */ +@interface FBSnapshotTestCase : XCTestCase + +/** + When YES, the test macros will save reference images, rather than performing an actual test. + */ +@property (readwrite, nonatomic, assign) BOOL recordMode; + +/** + When @c YES appends the name of the device model and OS to the snapshot file name. + The default value is @c NO. + */ +@property (readwrite, nonatomic, assign, getter=isDeviceAgnostic) BOOL deviceAgnostic; + +/** + When YES, renders a snapshot of the complete view hierarchy as visible onscreen. + There are several things that do not work if renderInContext: is used. + - UIVisualEffect #70 + - UIAppearance #91 + - Size Classes #92 + + @attention If the view does't belong to a UIWindow, it will create one and add the view as a subview. + */ +@property (readwrite, nonatomic, assign) BOOL usesDrawViewHierarchyInRect; + +- (void)setUp NS_REQUIRES_SUPER; +- (void)tearDown NS_REQUIRES_SUPER; + +/** + Performs the comparison 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 tolerance The percentage difference to still count as identical - 0 mean pixel perfect, 1 means I don't care + @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 + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr; + +/** + Performs the comparison or records a snapshot of the view if recordMode is YES. + @param view The view 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 tolerance The percentage difference to still count as identical - 0 mean pixel perfect, 1 means I don't care + @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)compareSnapshotOfView:(UIView *)view + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr; + +/** + Checks if reference image with identifier based name exists in the reference images directory. + @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 errorPtr An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if reference image exists. + */ +- (BOOL)referenceImageRecordedInDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Returns the reference image directory. + + Helper function used to implement the assert macros. + + @param dir directory to use if environment variable not specified. Ignored if null or empty. + */ +- (NSString *)getReferenceImageDirectoryWithDefault:(NSString *)dir; + +@end
diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.m b/FBSnapshotTestCase/FBSnapshotTestCase.m new file mode 100644 index 0000000..3ee351f --- /dev/null +++ b/FBSnapshotTestCase/FBSnapshotTestCase.m
@@ -0,0 +1,136 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <FBSnapshotTestCase/FBSnapshotTestCase.h> +#import <FBSnapshotTestCase/FBSnapshotTestController.h> + +@implementation FBSnapshotTestCase +{ + FBSnapshotTestController *_snapshotController; +} + +#pragma mark - Overrides + +- (void)setUp +{ + [super setUp]; + _snapshotController = [[FBSnapshotTestController alloc] initWithTestName:NSStringFromClass([self class])]; +} + +- (void)tearDown +{ + _snapshotController = nil; + [super tearDown]; +} + +- (BOOL)recordMode +{ + return _snapshotController.recordMode; +} + +- (void)setRecordMode:(BOOL)recordMode +{ + NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__); + _snapshotController.recordMode = recordMode; +} + +- (BOOL)isDeviceAgnostic +{ + return _snapshotController.deviceAgnostic; +} + +- (void)setDeviceAgnostic:(BOOL)deviceAgnostic +{ + NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__); + _snapshotController.deviceAgnostic = deviceAgnostic; +} + +- (BOOL)usesDrawViewHierarchyInRect +{ + return _snapshotController.usesDrawViewHierarchyInRect; +} + +- (void)setUsesDrawViewHierarchyInRect:(BOOL)usesDrawViewHierarchyInRect +{ + NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__); + _snapshotController.usesDrawViewHierarchyInRect = usesDrawViewHierarchyInRect; +} + +#pragma mark - Public API + +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + return [self _compareSnapshotOfViewOrLayer:layer + referenceImagesDirectory:referenceImagesDirectory + identifier:identifier + tolerance:tolerance + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfView:(UIView *)view + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + return [self _compareSnapshotOfViewOrLayer:view + referenceImagesDirectory:referenceImagesDirectory + identifier:identifier + tolerance:tolerance + error:errorPtr]; +} + +- (BOOL)referenceImageRecordedInDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__); + _snapshotController.referenceImagesDirectory = referenceImagesDirectory; + UIImage *referenceImage = [_snapshotController referenceImageForSelector:self.invocation.selector + identifier:identifier + error:errorPtr]; + + return (referenceImage != nil); +} + +- (NSString *)getReferenceImageDirectoryWithDefault:(NSString *)dir +{ + NSString *envReferenceImageDirectory = [NSProcessInfo processInfo].environment[@"FB_REFERENCE_IMAGE_DIR"]; + if (envReferenceImageDirectory) { + return envReferenceImageDirectory; + } + if (dir && dir.length > 0) { + return dir; + } + return [[NSBundle bundleForClass:self.class].resourcePath stringByAppendingPathComponent:@"ReferenceImages"]; +} + + +#pragma mark - Private API + +- (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + _snapshotController.referenceImagesDirectory = referenceImagesDirectory; + return [_snapshotController compareSnapshotOfViewOrLayer:viewOrLayer + selector:self.invocation.selector + identifier:identifier + tolerance:tolerance + error:errorPtr]; +} + +@end
diff --git a/FBSnapshotTestCase/FBSnapshotTestCasePlatform.h b/FBSnapshotTestCase/FBSnapshotTestCasePlatform.h new file mode 100644 index 0000000..e04acf2 --- /dev/null +++ b/FBSnapshotTestCase/FBSnapshotTestCasePlatform.h
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <Foundation/Foundation.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + Returns a Boolean value that indicates whether the snapshot test is running in 64Bit. + This method is a convenience for creating the suffixes set based on the architecture + that the test is running. + + @returns @c YES if the test is running in 64bit, otherwise @c NO. + */ +BOOL FBSnapshotTestCaseIs64Bit(void); + +/** + Returns a default set of strings that is used to append a suffix based on the architectures. + @warning Do not modify this function, you can create your own and use it with @c FBSnapshotVerifyViewWithOptions() + + @returns An @c NSOrderedSet object containing strings that are appended to the reference images directory. + */ +NSOrderedSet *FBSnapshotTestCaseDefaultSuffixes(void); + +/** + Returns a fully «normalized» file name. + Strips punctuation and spaces and replaces them with @c _. Also appends the device model, running OS and screen size to the file name. + + @returns An @c NSString object containing the passed @c fileName with the device model, OS and screen size appended at the end. + */ +NSString *FBDeviceAgnosticNormalizedFileName(NSString *fileName); + +#ifdef __cplusplus +} +#endif
diff --git a/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m b/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m new file mode 100644 index 0000000..4f6fb01 --- /dev/null +++ b/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <FBSnapshotTestCase/FBSnapshotTestCasePlatform.h> +#import <UIKit/UIKit.h> + +BOOL FBSnapshotTestCaseIs64Bit(void) +{ +#if __LP64__ + return YES; +#else + return NO; +#endif +} + +NSOrderedSet *FBSnapshotTestCaseDefaultSuffixes(void) +{ + NSMutableOrderedSet *suffixesSet = [[NSMutableOrderedSet alloc] init]; + [suffixesSet addObject:@"_32"]; + [suffixesSet addObject:@"_64"]; + if (FBSnapshotTestCaseIs64Bit()) { + return [suffixesSet reversedOrderedSet]; + } + return [suffixesSet copy]; +} + +NSString *FBDeviceAgnosticNormalizedFileName(NSString *fileName) +{ + UIDevice *device = [UIDevice currentDevice]; + CGSize screenSize = [[UIApplication sharedApplication] keyWindow].bounds.size; + NSString *os = device.systemVersion; + + fileName = [NSString stringWithFormat:@"%@_%@%@_%.0fx%.0f", fileName, device.model, os, screenSize.width, screenSize.height]; + + NSMutableCharacterSet *invalidCharacters = [NSMutableCharacterSet new]; + [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]]; + [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; + NSArray *validComponents = [fileName componentsSeparatedByCharactersInSet:invalidCharacters]; + fileName = [validComponents componentsJoinedByString:@"_"]; + + return fileName; +} \ No newline at end of file
diff --git a/FBSnapshotTestCase/FBSnapshotTestController.h b/FBSnapshotTestCase/FBSnapshotTestController.h new file mode 100644 index 0000000..5719aba --- /dev/null +++ b/FBSnapshotTestCase/FBSnapshotTestController.h
@@ -0,0 +1,151 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <Foundation/Foundation.h> +#import <UIKit/UIKit.h> + +typedef NS_ENUM(NSInteger, FBSnapshotTestControllerErrorCode) { + FBSnapshotTestControllerErrorCodeUnknown, + FBSnapshotTestControllerErrorCodeNeedsRecord, + FBSnapshotTestControllerErrorCodePNGCreationFailed, + FBSnapshotTestControllerErrorCodeImagesDifferentSizes, + FBSnapshotTestControllerErrorCodeImagesDifferent, +}; +/** + Errors returned by the methods of FBSnapshotTestController use this domain. + */ +extern NSString *const FBSnapshotTestControllerErrorDomain; + +/** + Errors returned by the methods of FBSnapshotTestController sometimes contain this key in the `userInfo` dictionary. + */ +extern NSString *const FBReferenceImageFilePathKey; + +/** + Provides the heavy-lifting for FBSnapshotTestCase. It loads and saves images, along with performing the actual pixel- + by-pixel comparison of images. + Instances are initialized with the test class, and directories to read and write to. + */ +@interface FBSnapshotTestController : NSObject + +/** + Record snapshots. + */ +@property (readwrite, nonatomic, assign) BOOL recordMode; + +/** + When @c YES appends the name of the device model and OS to the snapshot file name. + The default value is @c NO. + */ +@property (readwrite, nonatomic, assign, getter=isDeviceAgnostic) BOOL deviceAgnostic; + +/** + Uses drawViewHierarchyInRect:afterScreenUpdates: to draw the image instead of renderInContext: + */ +@property (readwrite, nonatomic, assign) BOOL usesDrawViewHierarchyInRect; + +/** + The directory in which referfence images are stored. + */ +@property (readwrite, nonatomic, copy) NSString *referenceImagesDirectory; + +/** + @param testClass The subclass of FBSnapshotTestCase that is using this controller. + @returns An instance of FBSnapshotTestController. + */ +- (instancetype)initWithTestClass:(Class)testClass; + +/** + Designated initializer. + @param testName The name of the tests. + @returns An instance of FBSnapshotTestController. + */ +- (instancetype)initWithTestName:(NSString *)testName; + +/** + Performs the comparison of the layer. + @param layer The Layer to snapshot. + @param selector The test method being run. + @param identifier An optional identifier, used is there are muliptle 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 + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs the comparison of the view. + @param view The view to snapshot. + @param selector The test method being run. + @param identifier An optional identifier, used is there are muliptle 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)compareSnapshotOfView:(UIView *)view + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs the comparison of a view or layer. + @param view The view or layer to snapshot. + @param selector The test method being run. + @param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method. + @param tolerance The percentage of pixels that can differ and still be considered 'identical' + @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)compareSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr; + +/** + Loads a reference image. + @param selector The test method being run. + @param identifier The optional identifier, used when multiple images are tested in a single -test method. + @param errorPtr An error, if this methods returns nil, the error will be something useful. + @returns An image. + */ +- (UIImage *)referenceImageForSelector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs a pixel-by-pixel comparison of the two images with an allowable margin of error. + @param referenceImage The reference (correct) image. + @param image The image to test against the reference. + @param tolerance The percentage of pixels that can differ and still be considered 'identical' + @param errorPtr An error that indicates why the comparison failed if it does. + @returns YES if the comparison succeeded and the images are the same(ish). + */ +- (BOOL)compareReferenceImage:(UIImage *)referenceImage + toImage:(UIImage *)image + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr; + +/** + Saves the reference image and the test image to `failedOutputDirectory`. + @param referenceImage The reference (correct) image. + @param testImage The image to test against the reference. + @param selector The test method being run. + @param identifier The optional identifier, used when multiple images are tested in a single -test method. + @param errorPtr An error that indicates why the comparison failed if it does. + @returns YES if the save succeeded. + */ +- (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage + testImage:(UIImage *)testImage + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; +@end
diff --git a/FBSnapshotTestCase/FBSnapshotTestController.m b/FBSnapshotTestCase/FBSnapshotTestController.m new file mode 100644 index 0000000..4cebe10 --- /dev/null +++ b/FBSnapshotTestCase/FBSnapshotTestController.m
@@ -0,0 +1,356 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import <FBSnapshotTestCase/FBSnapshotTestController.h> +#import <FBSnapshotTestCase/FBSnapshotTestCasePlatform.h> +#import <FBSnapshotTestCase/UIImage+Compare.h> +#import <FBSnapshotTestCase/UIImage+Diff.h> +#import <FBSnapshotTestCase/UIImage+Snapshot.h> + +#import <UIKit/UIKit.h> + +NSString *const FBSnapshotTestControllerErrorDomain = @"FBSnapshotTestControllerErrorDomain"; +NSString *const FBReferenceImageFilePathKey = @"FBReferenceImageFilePathKey"; + +typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) { + FBTestSnapshotFileNameTypeReference, + FBTestSnapshotFileNameTypeFailedReference, + FBTestSnapshotFileNameTypeFailedTest, + FBTestSnapshotFileNameTypeFailedTestDiff, +}; + +@implementation FBSnapshotTestController +{ + NSString *_testName; + NSFileManager *_fileManager; +} + +#pragma mark - Initializers + +- (instancetype)initWithTestClass:(Class)testClass; +{ + return [self initWithTestName:NSStringFromClass(testClass)]; +} + +- (instancetype)initWithTestName:(NSString *)testName +{ + if (self = [super init]) { + _testName = [testName copy]; + _deviceAgnostic = NO; + + _fileManager = [[NSFileManager alloc] init]; + } + return self; +} + +#pragma mark - Overrides + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ %@", [super description], _referenceImagesDirectory]; +} + +#pragma mark - Public API + +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + return [self compareSnapshotOfViewOrLayer:layer + selector:selector + identifier:identifier + tolerance:0 + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfView:(UIView *)view + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + return [self compareSnapshotOfViewOrLayer:view + selector:selector + identifier:identifier + tolerance:0 + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + if (self.recordMode) { + return [self _recordSnapshotOfViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr]; + } else { + return [self _performPixelComparisonWithViewOrLayer:viewOrLayer selector:selector identifier:identifier tolerance:tolerance error:errorPtr]; + } +} + +- (UIImage *)referenceImageForSelector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + NSString *filePath = [self _referenceFilePathForSelector:selector identifier:identifier]; + UIImage *image = [UIImage imageWithContentsOfFile:filePath]; + if (nil == image && NULL != errorPtr) { + BOOL exists = [_fileManager fileExistsAtPath:filePath]; + if (!exists) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeNeedsRecord + userInfo:@{ + FBReferenceImageFilePathKey: filePath, + NSLocalizedDescriptionKey: @"Unable to load reference image.", + NSLocalizedFailureReasonErrorKey: @"Reference image not found. You need to run the test in record mode", + }]; + } else { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeUnknown + userInfo:nil]; + } + } + return image; +} + +- (BOOL)compareReferenceImage:(UIImage *)referenceImage + toImage:(UIImage *)image + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + if (CGSizeEqualToSize(referenceImage.size, image.size)) { + BOOL imagesEqual = [referenceImage fb_compareWithImage:image tolerance:tolerance]; + if (NULL != errorPtr) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeImagesDifferent + userInfo:@{ + NSLocalizedDescriptionKey: @"Images different", + }]; + } + return imagesEqual; + } + if (NULL != errorPtr) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeImagesDifferentSizes + userInfo:@{ + NSLocalizedDescriptionKey: @"Images different sizes", + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"referenceImage:%@, image:%@", + NSStringFromCGSize(referenceImage.size), + NSStringFromCGSize(image.size)], + }]; + } + return NO; +} + +- (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage + testImage:(UIImage *)testImage + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + NSData *referencePNGData = UIImagePNGRepresentation(referenceImage); + NSData *testPNGData = UIImagePNGRepresentation(testImage); + + NSString *referencePath = [self _failedFilePathForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeFailedReference]; + + NSError *creationError = nil; + BOOL didCreateDir = [_fileManager createDirectoryAtPath:[referencePath stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&creationError]; + if (!didCreateDir) { + if (NULL != errorPtr) { + *errorPtr = creationError; + } + return NO; + } + + if (![referencePNGData writeToFile:referencePath options:NSDataWritingAtomic error:errorPtr]) { + return NO; + } + + NSString *testPath = [self _failedFilePathForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeFailedTest]; + + if (![testPNGData writeToFile:testPath options:NSDataWritingAtomic error:errorPtr]) { + return NO; + } + + NSString *diffPath = [self _failedFilePathForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeFailedTestDiff]; + + UIImage *diffImage = [referenceImage fb_diffWithImage:testImage]; + NSData *diffImageData = UIImagePNGRepresentation(diffImage); + + if (![diffImageData writeToFile:diffPath options:NSDataWritingAtomic error:errorPtr]) { + return NO; + } + + NSLog(@"If you have Kaleidoscope installed you can run this command to see an image diff:\n" + @"ksdiff \"%@\" \"%@\"", referencePath, testPath); + + return YES; +} + +#pragma mark - Private API + +- (NSString *)_fileNameForSelector:(SEL)selector + identifier:(NSString *)identifier + fileNameType:(FBTestSnapshotFileNameType)fileNameType +{ + NSString *fileName = nil; + switch (fileNameType) { + case FBTestSnapshotFileNameTypeFailedReference: + fileName = @"reference_"; + break; + case FBTestSnapshotFileNameTypeFailedTest: + fileName = @"failed_"; + break; + case FBTestSnapshotFileNameTypeFailedTestDiff: + fileName = @"diff_"; + break; + default: + fileName = @""; + break; + } + fileName = [fileName stringByAppendingString:NSStringFromSelector(selector)]; + if (0 < identifier.length) { + fileName = [fileName stringByAppendingFormat:@"_%@", identifier]; + } + + if (self.isDeviceAgnostic) { + fileName = FBDeviceAgnosticNormalizedFileName(fileName); + } + + if ([[UIScreen mainScreen] scale] > 1) { + fileName = [fileName stringByAppendingFormat:@"@%.fx", [[UIScreen mainScreen] scale]]; + } + fileName = [fileName stringByAppendingPathExtension:@"png"]; + return fileName; +} + +- (NSString *)_referenceFilePathForSelector:(SEL)selector + identifier:(NSString *)identifier +{ + NSString *fileName = [self _fileNameForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeReference]; + NSString *filePath = [_referenceImagesDirectory stringByAppendingPathComponent:_testName]; + filePath = [filePath stringByAppendingPathComponent:fileName]; + return filePath; +} + +- (NSString *)_failedFilePathForSelector:(SEL)selector + identifier:(NSString *)identifier + fileNameType:(FBTestSnapshotFileNameType)fileNameType +{ + NSString *fileName = [self _fileNameForSelector:selector + identifier:identifier + fileNameType:fileNameType]; + NSString *folderPath = NSTemporaryDirectory(); + if (getenv("IMAGE_DIFF_DIR")) { + folderPath = @(getenv("IMAGE_DIFF_DIR")); + } + NSString *filePath = [folderPath stringByAppendingPathComponent:_testName]; + filePath = [filePath stringByAppendingPathComponent:fileName]; + return filePath; +} + +- (BOOL)_performPixelComparisonWithViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr]; + if (nil != referenceImage) { + UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer]; + BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot tolerance:tolerance error:errorPtr]; + if (!imagesSame) { + [self saveFailedReferenceImage:referenceImage + testImage:snapshot + selector:selector + identifier:identifier + error:errorPtr]; + } + return imagesSame; + } + return NO; +} + +- (BOOL)_recordSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer]; + return [self _saveReferenceImage:snapshot selector:selector identifier:identifier error:errorPtr]; +} + +- (BOOL)_saveReferenceImage:(UIImage *)image + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + BOOL didWrite = NO; + if (nil != image) { + NSString *filePath = [self _referenceFilePathForSelector:selector identifier:identifier]; + NSData *pngData = UIImagePNGRepresentation(image); + if (nil != pngData) { + NSError *creationError = nil; + BOOL didCreateDir = [_fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&creationError]; + if (!didCreateDir) { + if (NULL != errorPtr) { + *errorPtr = creationError; + } + return NO; + } + didWrite = [pngData writeToFile:filePath options:NSDataWritingAtomic error:errorPtr]; + if (didWrite) { + NSLog(@"Reference image save at: %@", filePath); + } + } else { + if (nil != errorPtr) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodePNGCreationFailed + userInfo:@{ + FBReferenceImageFilePathKey: filePath, + }]; + } + } + } + return didWrite; +} + +- (UIImage *)_imageForViewOrLayer:(id)viewOrLayer +{ + if ([viewOrLayer isKindOfClass:[UIView class]]) { + if (_usesDrawViewHierarchyInRect) { + return [UIImage fb_imageForView:viewOrLayer]; + } else { + return [UIImage fb_imageForViewLayer:viewOrLayer]; + } + } else if ([viewOrLayer isKindOfClass:[CALayer class]]) { + return [UIImage fb_imageForLayer:viewOrLayer]; + } else { + [NSException raise:@"Only UIView and CALayer classes can be snapshotted" format:@"%@", viewOrLayer]; + } + return nil; +} + +@end
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2dd780c --- /dev/null +++ b/LICENSE
@@ -0,0 +1,29 @@ +BSD License + +For the FBSnapshotTestCase software + +Copyright (c) 2013, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc23b83 --- /dev/null +++ b/README.md
@@ -0,0 +1,97 @@ +FBSnapshotTestCase +====================== + +[](https://travis-ci.org/facebook/ios-snapshot-test-case) [](http://cocoadocs.org/docsets/FBSnapshotTestCase/) + +What it does +------------ + +A "snapshot test case" takes a configured `UIView` or `CALayer` and uses the +`renderInContext:` method to get an image snapshot of its contents. It +compares this snapshot to a "reference image" stored in your source code +repository and fails the test if the two images don't match. + +Why? +---- + +At Facebook we write a lot of UI code. As you might imagine, each type of +feed story is rendered using a subclass of `UIView`. There are a lot of edge +cases that we want to handle correctly: + +- What if there is more text than can fit in the space available? +- What if an image doesn't match the size of an image view? +- What should the highlighted state look like? + +It's straightforward to test logic code, but less obvious how you should test +views. You can do a lot of rectangle asserts, but these are hard to understand +or visualize. Looking at an image diff shows you exactly what changed and how +it will look to users. + +We developed `FBSnapshotTestCase` to make snapshot tests easy. + +Installation with CocoaPods +--------------------------- + +1. Add the following lines to your Podfile: + + ``` + target "Tests" do + pod 'FBSnapshotTestCase' + end + ``` + + If you support iOS 7 use `FBSnapshotTestCase/Core` instead, which doesn't contain Swift support. + + Replace "Tests" with the name of your test project. + +2. There are [three ways](https://github.com/facebook/ios-snapshot-test-case/blob/master/FBSnapshotTestCase/FBSnapshotTestCase.h#L19-L29) of setting reference image directories, the recommended one is to define `FB_REFERENCE_IMAGE_DIR` in your scheme. This should point to the directory where you want reference images to be stored. At Facebook, we normally use this: + +|Name|Value| +|:---|:----| +|`FB_REFERENCE_IMAGE_DIR`|`$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages`| + + + + +Creating a snapshot test +------------------------ + +1. Subclass `FBSnapshotTestCase` instead of `XCTestCase`. +2. From within your test, use `FBSnapshotVerifyView`. +3. Run the test once with `self.recordMode = YES;` in the test's `-setUp` + method. (This creates the reference images on disk.) +4. Remove the line enabling record mode and run the test. + +Features +-------- + +- Automatically names reference images on disk according to test class and + selector. +- Prints a descriptive error message to the console on failure. (Bonus: + failure message includes a one-line command to see an image diff if + you have [Kaleidoscope](http://www.kaleidoscopeapp.com) installed.) +- Supply an optional "identifier" if you want to perform multiple snapshots + in a single test method. +- Support for `CALayer` via `FBSnapshotVerifyLayer`. +- `usesDrawViewHierarchyInRect` to handle cases like `UIVisualEffect`, `UIAppearance` and Size Classes. +- `isDeviceAgnostic` to allow appending the device model (`iPhone`, `iPad`, `iPod Touch`, etc), OS version and screen size to the images (allowing to have multiple tests for the same «snapshot» for different `OS`s and devices). + +Notes +----- + +Your unit test must be an "application test", not a "logic test." (That is, it +must be run within the Simulator so that it has access to UIKit.) In Xcode 5 +and later new projects only offer application tests, but older projects will +have separate targets for the two types. + +Authors +------- + +`FBSnapshotTestCase` was written at Facebook by +[Jonathan Dann](https://facebook.com/j.p.dann) with significant contributions by +[Todd Krabach](https://facebook.com/toddkrabach). + +License +------- + +`FBSnapshotTestCase` is BSD-licensed. See `LICENSE`.