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`.