diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj
index c850360b1..3c64d8f3d 100644
--- a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj
+++ b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj
@@ -18,11 +18,11 @@
580C37631AB0F62C0015E709 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C37551AB0F56E0015E709 /* libRCTImage.a */; };
580C37641AB0F6350015E709 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C375A1AB0F5970015E709 /* libRCTNetwork.a */; };
580C37651AB0F63E0015E709 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C375F1AB0F5D10015E709 /* libRCTText.a */; };
- 580C37921AB1090B0015E709 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C378F1AB104B00015E709 /* libRCTTest.a */; };
+ 58B80D5F1ABA4147004008FB /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C378F1AB104B00015E709 /* libRCTTest.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
- 004D28A41AAF61C70097A701 /* PBXContainerItemProxy */ = {
+ 58005BCB1ABA44F10062E044 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
@@ -117,8 +117,8 @@
580C37621AB0F6260015E709 /* libRCTGeolocation.a in Frameworks */,
580C37631AB0F62C0015E709 /* libRCTImage.a in Frameworks */,
580C37641AB0F6350015E709 /* libRCTNetwork.a in Frameworks */,
+ 58B80D5F1ABA4147004008FB /* libRCTTest.a in Frameworks */,
580C37651AB0F63E0015E709 /* libRCTText.a in Frameworks */,
- 580C37921AB1090B0015E709 /* libRCTTest.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -258,7 +258,7 @@
buildRules = (
);
dependencies = (
- 004D28A51AAF61C70097A701 /* PBXTargetDependency */,
+ 58005BCC1ABA44F10062E044 /* PBXTargetDependency */,
);
name = IntegrationTestsTests;
productName = IntegrationTestsTests;
@@ -438,10 +438,10 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- 004D28A51AAF61C70097A701 /* PBXTargetDependency */ = {
+ 58005BCC1ABA44F10062E044 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 13B07F861A680F5B00A75B9A /* IntegrationTests */;
- targetProxy = 004D28A41AAF61C70097A701 /* PBXContainerItemProxy */;
+ targetProxy = 58005BCB1ABA44F10062E044 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@@ -468,6 +468,7 @@
);
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
+ "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"",
"$(inherited)",
);
INFOPLIST_FILE = IntegrationTestsTests/Info.plist;
diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js
index ccd69460a..de1703528 100644
--- a/IntegrationTests/IntegrationTestsApp.js
+++ b/IntegrationTests/IntegrationTestsApp.js
@@ -25,6 +25,7 @@ var TESTS = [
require('./IntegrationTestHarnessTest'),
require('./TimersTest'),
require('./AsyncStorageTest'),
+ require('./SimpleSnapshotTest'),
];
TESTS.forEach(
diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
index e049b8e70..578d3915f 100644
--- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
+++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
@@ -24,34 +24,57 @@
- (void)setUp
{
- _runner = [[RCTTestRunner alloc] initWithApp:@"IntegrationTests/IntegrationTestsApp"];
+#ifdef __LP64__
+ RCTAssert(!__LP64__, @"Tests should be run on 32-bit device simulators (e.g. iPhone 5)");
+#endif
+ NSString *version = [[UIDevice currentDevice] systemVersion];
+ RCTAssert([version isEqualToString:@"8.1"], @"Tests should be run on iOS 8.1, found %@", version);
+ _runner = initRunnerForApp(@"IntegrationTests/IntegrationTestsApp");
+
+ // If tests have changes, set recordMode = YES below and run the affected tests on an iPhone5, iOS 8.1 simulator.
+ _runner.recordMode = NO;
}
+#pragma mark Logic Tests
+
- (void)testTheTester
{
- [_runner runTest:@"IntegrationTestHarnessTest"];
+ [_runner runTest:_cmd module:@"IntegrationTestHarnessTest"];
}
- (void)testTheTester_waitOneFrame
{
- [_runner runTest:@"IntegrationTestHarnessTest" initialProps:@{@"waitOneFrame": @YES} expectErrorBlock:nil];
+ [_runner runTest:_cmd module:@"IntegrationTestHarnessTest" initialProps:@{@"waitOneFrame": @YES} expectErrorBlock:nil];
}
- (void)testTheTester_ExpectError
{
- [_runner runTest:@"IntegrationTestHarnessTest"
+ [_runner runTest:_cmd
+ module:@"IntegrationTestHarnessTest"
initialProps:@{@"shouldThrow": @YES}
expectErrorRegex:[NSRegularExpression regularExpressionWithPattern:@"because shouldThrow" options:0 error:nil]];
}
- (void)testTimers
{
- [_runner runTest:@"TimersTest"];
+ [_runner runTest:_cmd module:@"TimersTest"];
}
- (void)testAsyncStorage
{
- [_runner runTest:@"AsyncStorageTest"];
+ [_runner runTest:_cmd module:@"AsyncStorageTest"];
+}
+
+#pragma mark Snapshot Tests
+
+- (void)testSimpleSnapshot
+{
+ [_runner runTest:_cmd module:@"SimpleSnapshotTest"];
+}
+
+- (void)testZZZ_NotInRecordMode
+{
+ RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit.");
}
@end
diff --git a/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSimpleSnapshot_1@2x.png b/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSimpleSnapshot_1@2x.png
new file mode 100644
index 000000000..fd91abf42
Binary files /dev/null and b/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSimpleSnapshot_1@2x.png differ
diff --git a/IntegrationTests/SimpleSnapshotTest.js b/IntegrationTests/SimpleSnapshotTest.js
new file mode 100644
index 000000000..1715f093f
--- /dev/null
+++ b/IntegrationTests/SimpleSnapshotTest.js
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015-present, 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.
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ View,
+} = React;
+
+var { TestModule } = React.addons;
+
+var SimpleSnapshotTest = React.createClass({
+ componentDidMount() {
+ if (!TestModule.verifySnapshot) {
+ throw new Error('TestModule.verifySnapshot not defined.');
+ }
+ requestAnimationFrame(() => TestModule.verifySnapshot(this.done));
+ },
+
+ done() {
+ TestModule.markTestCompleted();
+ },
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ box1: {
+ width: 80,
+ height: 50,
+ backgroundColor: 'red',
+ },
+ box2: {
+ top: -10,
+ left: 20,
+ width: 70,
+ height: 90,
+ backgroundColor: 'blue',
+ },
+});
+
+module.exports = SimpleSnapshotTest;
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h
new file mode 100644
index 000000000..063e32a48
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2013, 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
+
+#import
+
+#import
+
+#ifndef FB_REFERENCE_IMAGE_DIR
+#define FB_REFERENCE_IMAGE_DIR "\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\""
+#endif
+
+/**
+ Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though.
+ @param view The view to snapshot
+ @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
+ @param referenceImageDirectorySuffix An optional suffix, appended to the reference image directory path, such as "_iOS8"
+ */
+#define FBSnapshotVerifyViewWithReferenceDirectorySuffix(view__, identifier__, referenceImagesDirectorySuffix__) \
+{ \
+NSError *error__ = nil; \
+NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%s%@", FB_REFERENCE_IMAGE_DIR, referenceImagesDirectorySuffix__]; \
+BOOL comparisonSuccess__ = [self compareSnapshotOfView:(view__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \
+XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \
+}
+
+#define FBSnapshotVerifyView(view__, identifier__) \
+{ \
+FBSnapshotVerifyViewWithReferenceDirectorySuffix(view__, identifier__, @""); \
+}
+
+/**
+ Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though.
+ @param layer The layer to snapshot
+ @param identifier An optional identifier, used is there are multiple snapshot tests in a given -test method.
+ @param referenceImageDirectorySuffix An optional suffix, appended to the reference image directory path, such as "_iOS8"
+ */
+#define FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, referenceImagesDirectorySuffix__) \
+{ \
+NSError *error__ = nil; \
+NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%s%@", FB_REFERENCE_IMAGE_DIR, referenceImagesDirectorySuffix__]; \
+BOOL comparisonSuccess__ = [self compareSnapshotOfLayer:(layer__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \
+XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \
+}
+
+#define FBSnapshotVerifyLayer(layer__, identifier__) \
+{ \
+FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, @""); \
+}
+
+/**
+ The base class of view snapshotting tests. If you have small UI component, it's often easier to configure it in a test
+ and compare an image of the view to a reference image that write lots of complex layout-code tests.
+
+ In order to flip the tests in your subclass to record the reference images set `recordMode` to YES before calling
+ -[super setUp].
+ */
+@interface FBSnapshotTestCase : XCTestCase
+
+/**
+ When YES, the test macros will save reference images, rather than performing an actual test.
+ */
+@property (readwrite, nonatomic, assign) BOOL recordMode;
+
+/**
+ Performs the comparisong or records a snapshot of the layer if recordMode is YES.
+ @param layer The Layer to snapshot
+ @param referenceImagesDirectory The directory in which reference images are stored.
+ @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
+ @param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
+ @returns YES if the comparison (or saving of the reference image) succeeded.
+ */
+- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
+ referenceImagesDirectory:(NSString *)referenceImagesDirectory
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr;
+
+/**
+ Performs the comparisong 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 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
+ referenceImagesDirectory:(NSString *)referenceImagesDirectory
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr;
+
+@end
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m
new file mode 100644
index 000000000..ebd311c64
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2013, 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.h"
+
+#import "FBSnapshotTestController.h"
+
+@interface FBSnapshotTestCase ()
+
+@property (readwrite, nonatomic, retain) FBSnapshotTestController *snapshotController;
+
+@end
+
+@implementation FBSnapshotTestCase
+
+- (void)setUp
+{
+ [super setUp];
+ self.snapshotController = [[FBSnapshotTestController alloc] initWithTestName:NSStringFromClass([self class])];
+}
+
+- (void)tearDown
+{
+ self.snapshotController = nil;
+ [super tearDown];
+}
+
+- (BOOL)recordMode
+{
+ return self.snapshotController.recordMode;
+}
+
+- (void)setRecordMode:(BOOL)recordMode
+{
+ self.snapshotController.recordMode = recordMode;
+}
+
+- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
+ referenceImagesDirectory:(NSString *)referenceImagesDirectory
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ return [self _compareSnapshotOfViewOrLayer:layer
+ referenceImagesDirectory:referenceImagesDirectory
+ identifier:identifier
+ error:errorPtr];
+}
+
+- (BOOL)compareSnapshotOfView:(UIView *)view
+ referenceImagesDirectory:(NSString *)referenceImagesDirectory
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ return [self _compareSnapshotOfViewOrLayer:view
+ referenceImagesDirectory:referenceImagesDirectory
+ identifier:identifier
+ error:errorPtr];
+}
+
+#pragma mark -
+#pragma mark Private API
+
+- (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer
+ referenceImagesDirectory:(NSString *)referenceImagesDirectory
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ _snapshotController.referenceImagesDirectory = referenceImagesDirectory;
+ return [_snapshotController compareSnapshotOfViewOrLayer:viewOrLayer
+ selector:self.invocation.selector
+ identifier:identifier
+ error:errorPtr];
+}
+
+@end
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h
new file mode 100644
index 000000000..349384d8f
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2013, 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
+#import
+
+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;
+
+/**
+ @param testClass The subclass of FBSnapshotTestCase that is using this controller.
+ @param referenceImagesDirectory The directory where the reference images are stored.
+ @returns An instance of FBSnapshotTestController.
+ */
+- (id)initWithTestClass:(Class)testClass;
+
+/**
+ Designated initializer.
+ @param testName The name of the tests.
+ @param referenceImagesDirectory The directory where the reference images are stored.
+ @returns An instance of FBSnapshotTestController.
+ */
+- (id)initWithTestName:(NSString *)testName;
+
+
+/**
+ Performs the comparison of the layer.
+ @param layer The Layer to snapshot.
+ @param referenceImagesDirectory The directory in which reference images are stored.
+ @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 referenceImagesDirectory The directory in which reference images are stored.
+ @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 referenceImagesDirectory The directory in which reference images are stored.
+ @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)compareSnapshotOfViewOrLayer:(id)viewOrLayer
+ selector:(SEL)selector
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr;
+
+
+/**
+ The directory in which referfence images are stored.
+ */
+@property (readwrite, nonatomic, copy) NSString *referenceImagesDirectory;
+
+/**
+ 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 error 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 **)error;
+
+/**
+ Saves 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 error An error, if this methods returns NO, the error will be something useful.
+ @returns An image.
+ */
+- (BOOL)saveReferenceImage:(UIImage *)image
+ selector:(SEL)selector
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr;
+
+/**
+ Performs a pixel-by-pixel comparison of the two images.
+ @param referenceImage The reference (correct) image.
+ @param image The image to test against the reference.
+ @param error An error that indicates why the comparison failed if it does.
+ @param YES if the comparison succeeded and the images are the same.
+ */
+- (BOOL)compareReferenceImage:(UIImage *)referenceImage
+ toImage:(UIImage *)image
+ 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 error An error that indicates why the comparison failed if it does.
+ @param 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/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m
new file mode 100644
index 000000000..7d12736d1
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m
@@ -0,0 +1,392 @@
+/*
+ * Copyright (c) 2013, 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 "FBSnapshotTestController.h"
+
+#import "UIImage+Compare.h"
+#import "UIImage+Diff.h"
+
+#import
+
+#import
+
+NSString *const FBSnapshotTestControllerErrorDomain = @"FBSnapshotTestControllerErrorDomain";
+
+NSString *const FBReferenceImageFilePathKey = @"FBReferenceImageFilePathKey";
+
+typedef struct RGBAPixel {
+ char r;
+ char g;
+ char b;
+ char a;
+} RGBAPixel;
+
+@interface FBSnapshotTestController ()
+
+@property (readonly, nonatomic, copy) NSString *testName;
+
+@end
+
+@implementation FBSnapshotTestController
+{
+ NSFileManager *_fileManager;
+}
+
+#pragma mark -
+#pragma mark Lifecycle
+
+- (id)initWithTestClass:(Class)testClass;
+{
+ return [self initWithTestName:NSStringFromClass(testClass)];
+}
+
+- (id)initWithTestName:(NSString *)testName
+{
+ if ((self = [super init])) {
+ _testName = [testName copy];
+ _fileManager = [[NSFileManager alloc] init];
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Properties
+
+- (NSString *)description
+{
+ return [NSString stringWithFormat:@"%@ %@", [super description], _referenceImagesDirectory];
+}
+
+#pragma mark -
+#pragma mark Public API
+
+- (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)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;
+}
+
+- (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 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;
+}
+
+- (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image error:(NSError **)errorPtr
+{
+ if (CGSizeEqualToSize(referenceImage.size, image.size)) {
+
+ BOOL imagesEqual = [referenceImage compareWithImage:image];
+ 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;
+}
+
+#pragma mark -
+#pragma mark Private API
+
+typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) {
+ FBTestSnapshotFileNameTypeReference,
+ FBTestSnapshotFileNameTypeFailedReference,
+ FBTestSnapshotFileNameTypeFailedTest,
+ FBTestSnapshotFileNameTypeFailedTestDiff,
+};
+
+- (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 ([[UIScreen mainScreen] scale] > 1.0) {
+ 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)compareSnapshotOfLayer:(CALayer *)layer
+ selector:(SEL)selector
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ return [self compareSnapshotOfViewOrLayer:layer
+ selector:selector
+ identifier:identifier
+ error:errorPtr];
+}
+
+- (BOOL)compareSnapshotOfView:(UIView *)view
+ selector:(SEL)selector
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ return [self compareSnapshotOfViewOrLayer:view
+ selector:selector
+ identifier:identifier
+ error:errorPtr];
+}
+
+- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer
+ selector:(SEL)selector
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ if (self.recordMode) {
+ return [self _recordSnapshotOfViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr];
+ } else {
+ return [self _performPixelComparisonWithViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr];
+ }
+}
+
+#pragma mark -
+#pragma mark Private API
+
+- (BOOL)_performPixelComparisonWithViewOrLayer:(UIView *)viewOrLayer
+ selector:(SEL)selector
+ identifier:(NSString *)identifier
+ error:(NSError **)errorPtr
+{
+ UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr];
+ if (nil != referenceImage) {
+ UIImage *snapshot = [self _snapshotViewOrLayer:viewOrLayer];
+ BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot 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 _snapshotViewOrLayer:viewOrLayer];
+ return [self saveReferenceImage:snapshot selector:selector identifier:identifier error:errorPtr];
+}
+
+- (UIImage *)_snapshotViewOrLayer:(id)viewOrLayer
+{
+ CALayer *layer = nil;
+
+ if ([viewOrLayer isKindOfClass:[UIView class]]) {
+ return [self _renderView:viewOrLayer];
+ } else if ([viewOrLayer isKindOfClass:[CALayer class]]) {
+ layer = (CALayer *)viewOrLayer;
+ [layer layoutIfNeeded];
+ return [self _renderLayer:layer];
+ } else {
+ [NSException raise:@"Only UIView and CALayer classes can be snapshotted" format:@"%@", viewOrLayer];
+ }
+ return nil;
+}
+
+- (UIImage *)_renderLayer:(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 renderInContext:context];
+ }
+ CGContextRestoreGState(context);
+
+ UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ return snapshot;
+}
+
+- (UIImage *)_renderView:(UIView *)view
+{
+ [view layoutIfNeeded];
+ return [self _renderLayer:view.layer];
+}
+
+@end
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.h b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.h
new file mode 100644
index 000000000..11c6fa638
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/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
+
+@interface UIImage (Compare)
+
+- (BOOL)compareWithImage:(UIImage *)image;
+
+@end
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m
new file mode 100644
index 000000000..e38c6e4e8
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m
@@ -0,0 +1,91 @@
+//
+// 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 "UIImage+Compare.h"
+
+@implementation UIImage (Compare)
+
+- (BOOL)compareWithImage:(UIImage *)image
+{
+ NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size.");
+
+ // 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 = CGImageGetHeight(self.CGImage) * minBytesPerRow;
+ void *referenceImagePixels = calloc(1, referenceImageSizeBytes);
+ void *imagePixels = calloc(1, referenceImageSizeBytes);
+
+ if (!referenceImagePixels || !imagePixels) {
+ free(referenceImagePixels);
+ free(imagePixels);
+ return NO;
+ }
+
+ CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels,
+ CGImageGetWidth(self.CGImage),
+ CGImageGetHeight(self.CGImage),
+ CGImageGetBitsPerComponent(self.CGImage),
+ minBytesPerRow,
+ CGImageGetColorSpace(self.CGImage),
+ (CGBitmapInfo)kCGImageAlphaPremultipliedLast
+ );
+ CGContextRef imageContext = CGBitmapContextCreate(imagePixels,
+ CGImageGetWidth(image.CGImage),
+ CGImageGetHeight(image.CGImage),
+ CGImageGetBitsPerComponent(image.CGImage),
+ minBytesPerRow,
+ CGImageGetColorSpace(image.CGImage),
+ (CGBitmapInfo)kCGImageAlphaPremultipliedLast
+ );
+
+ CGFloat scaleFactor = [[UIScreen mainScreen] scale];
+ CGContextScaleCTM(referenceImageContext, scaleFactor, scaleFactor);
+ CGContextScaleCTM(imageContext, scaleFactor, scaleFactor);
+
+ if (!referenceImageContext || !imageContext) {
+ CGContextRelease(referenceImageContext);
+ CGContextRelease(imageContext);
+ free(referenceImagePixels);
+ free(imagePixels);
+ return NO;
+ }
+
+ CGContextDrawImage(referenceImageContext, CGRectMake(0.0f, 0.0f, self.size.width, self.size.height), self.CGImage);
+ CGContextDrawImage(imageContext, CGRectMake(0.0f, 0.0f, image.size.width, image.size.height), image.CGImage);
+ CGContextRelease(referenceImageContext);
+ CGContextRelease(imageContext);
+
+ BOOL imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0);
+ free(referenceImagePixels);
+ free(imagePixels);
+ return imageEqual;
+}
+
+@end
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.h b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.h
new file mode 100644
index 000000000..35595843f
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/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
+
+@interface UIImage (Diff)
+
+- (UIImage *)diffWithImage:(UIImage *)image;
+
+@end
diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.m b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.m
new file mode 100644
index 000000000..44ecb59ee
--- /dev/null
+++ b/Libraries/RCTTest/FBSnapshotTestCase/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 "UIImage+Diff.h"
+
+@implementation UIImage (Diff)
+
+- (UIImage *)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.0);
+ CGContextRef context = UIGraphicsGetCurrentContext();
+ [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
+ CGContextSetAlpha(context, 0.5f);
+ 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/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj
index f377b4c98..bb4457b1f 100644
--- a/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj
+++ b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj
@@ -10,6 +10,9 @@
585135371AB3C56F00882537 /* RCTTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 585135341AB3C56F00882537 /* RCTTestModule.m */; };
585135381AB3C57000882537 /* RCTTestRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 585135361AB3C56F00882537 /* RCTTestRunner.m */; };
585135391AB3C59A00882537 /* RCTTestRunner.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 585135351AB3C56F00882537 /* RCTTestRunner.h */; };
+ 58E64FED1AB964CD007446E2 /* FBSnapshotTestController.m in Sources */ = {isa = PBXBuildFile; fileRef = 58E64FE71AB964CD007446E2 /* FBSnapshotTestController.m */; };
+ 58E64FEE1AB964CD007446E2 /* UIImage+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = 58E64FE91AB964CD007446E2 /* UIImage+Compare.m */; };
+ 58E64FEF1AB964CD007446E2 /* UIImage+Diff.m in Sources */ = {isa = PBXBuildFile; fileRef = 58E64FEB1AB964CD007446E2 /* UIImage+Diff.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -31,6 +34,14 @@
585135341AB3C56F00882537 /* RCTTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTestModule.m; sourceTree = ""; };
585135351AB3C56F00882537 /* RCTTestRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTestRunner.h; sourceTree = ""; };
585135361AB3C56F00882537 /* RCTTestRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTestRunner.m; sourceTree = ""; };
+ 58E64FE41AB964CD007446E2 /* FBSnapshotTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSnapshotTestCase.h; sourceTree = ""; };
+ 58E64FE51AB964CD007446E2 /* FBSnapshotTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSnapshotTestCase.m; sourceTree = ""; };
+ 58E64FE61AB964CD007446E2 /* FBSnapshotTestController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSnapshotTestController.h; sourceTree = ""; };
+ 58E64FE71AB964CD007446E2 /* FBSnapshotTestController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSnapshotTestController.m; sourceTree = ""; };
+ 58E64FE81AB964CD007446E2 /* UIImage+Compare.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+Compare.h"; sourceTree = ""; };
+ 58E64FE91AB964CD007446E2 /* UIImage+Compare.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+Compare.m"; sourceTree = ""; };
+ 58E64FEA1AB964CD007446E2 /* UIImage+Diff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+Diff.h"; sourceTree = ""; };
+ 58E64FEB1AB964CD007446E2 /* UIImage+Diff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+Diff.m"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -51,6 +62,7 @@
585135341AB3C56F00882537 /* RCTTestModule.m */,
585135351AB3C56F00882537 /* RCTTestRunner.h */,
585135361AB3C56F00882537 /* RCTTestRunner.m */,
+ 58E64FE31AB964CD007446E2 /* FBSnapshotTestCase */,
580C37701AB104AF0015E709 /* Products */,
);
sourceTree = "";
@@ -63,6 +75,21 @@
name = Products;
sourceTree = "";
};
+ 58E64FE31AB964CD007446E2 /* FBSnapshotTestCase */ = {
+ isa = PBXGroup;
+ children = (
+ 58E64FE41AB964CD007446E2 /* FBSnapshotTestCase.h */,
+ 58E64FE51AB964CD007446E2 /* FBSnapshotTestCase.m */,
+ 58E64FE61AB964CD007446E2 /* FBSnapshotTestController.h */,
+ 58E64FE71AB964CD007446E2 /* FBSnapshotTestController.m */,
+ 58E64FE81AB964CD007446E2 /* UIImage+Compare.h */,
+ 58E64FE91AB964CD007446E2 /* UIImage+Compare.m */,
+ 58E64FEA1AB964CD007446E2 /* UIImage+Diff.h */,
+ 58E64FEB1AB964CD007446E2 /* UIImage+Diff.m */,
+ );
+ path = FBSnapshotTestCase;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -119,7 +146,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 58E64FEE1AB964CD007446E2 /* UIImage+Compare.m in Sources */,
585135371AB3C56F00882537 /* RCTTestModule.m in Sources */,
+ 58E64FEF1AB964CD007446E2 /* UIImage+Diff.m in Sources */,
+ 58E64FED1AB964CD007446E2 /* FBSnapshotTestController.m in Sources */,
585135381AB3C57000882537 /* RCTTestRunner.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h
index 1b50f5db2..0f5adcd2c 100644
--- a/Libraries/RCTTest/RCTTestModule.h
+++ b/Libraries/RCTTest/RCTTestModule.h
@@ -7,10 +7,20 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
+#import
+
#import "RCTBridgeModule.h"
+@class FBSnapshotTestController;
+
@interface RCTTestModule : NSObject
+// This is typically polled while running the runloop until true
@property (nonatomic, readonly, getter=isDone) BOOL done;
+// This is used to give meaningful names to snapshot image files.
+@property (nonatomic, assign) SEL testSelector;
+
+- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view;
+
@end
diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m
index a02d7759c..03f025f20 100644
--- a/Libraries/RCTTest/RCTTestModule.m
+++ b/Libraries/RCTTest/RCTTestModule.m
@@ -9,7 +9,46 @@
#import "RCTTestModule.h"
-@implementation RCTTestModule
+#import "FBSnapshotTestController.h"
+#import "RCTAssert.h"
+#import "RCTLog.h"
+
+@implementation RCTTestModule {
+ __weak FBSnapshotTestController *_snapshotController;
+ __weak UIView *_view;
+ NSMutableDictionary *_snapshotCounter;
+}
+
+- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view
+{
+ if ((self = [super init])) {
+ _snapshotController = controller;
+ _view = view;
+ _snapshotCounter = [NSMutableDictionary new];
+ }
+ return self;
+}
+
+- (void)verifySnapshot:(RCTResponseSenderBlock)callback
+{
+ RCT_EXPORT();
+
+ if (!_snapshotController) {
+ RCTLogWarn(@"No snapshot controller configured.");
+ callback(@[]);
+ return;
+ }
+
+ NSError *error = nil;
+ NSString *testName = NSStringFromSelector(_testSelector);
+ _snapshotCounter[testName] = @([_snapshotCounter[testName] integerValue] + 1);
+ BOOL success = [_snapshotController compareSnapshotOfView:_view
+ selector:_testSelector
+ identifier:[_snapshotCounter[testName] stringValue]
+ error:&error];
+ RCTAssert(success, @"Snapshot comparison failed: %@", error);
+ callback(@[]);
+}
- (void)markTestCompleted
{
diff --git a/Libraries/RCTTest/RCTTestRunner.h b/Libraries/RCTTest/RCTTestRunner.h
index 592afa479..9d56202ce 100644
--- a/Libraries/RCTTest/RCTTestRunner.h
+++ b/Libraries/RCTTest/RCTTestRunner.h
@@ -9,13 +9,63 @@
#import
+/**
+ * Use the initRunnerForApp macro for typical usage.
+ *
+ * Add this to your test target's gcc preprocessor macros:
+ *
+ * FB_REFERENCE_IMAGE_DIR="\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\""
+ */
+#define initRunnerForApp(app__) [[RCTTestRunner alloc] initWithApp:(app__) referenceDir:@FB_REFERENCE_IMAGE_DIR]
+
@interface RCTTestRunner : NSObject
+@property (nonatomic, assign) BOOL recordMode;
@property (nonatomic, copy) NSString *script;
-- (instancetype)initWithApp:(NSString *)app;
-- (void)runTest:(NSString *)moduleName;
-- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)expectErrorRegex;
-- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock;
+/**
+ * Initialize a runner. It's recommended that you use the initRunnerForApp macro instead of calling this directly.
+ *
+ * @param app The path to the app bundle without suffixes, e.g. IntegrationTests/IntegrationTestsApp
+ * @param referencesDir The path for snapshot references images. The initRunnerForApp macro uses
+ * FB_REFERENCE_IMAGE_DIR for this automatically.
+ */
+- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir;
+
+/**
+ * Simplest runTest function simply mounts the specified JS module with no initialProps and waits for it to call
+ *
+ * RCTTestModule.markTestCompleted()
+ *
+ * JS errors/exceptions and timeouts will fail the test. Snapshot tests call RCTTestModule.verifySnapshot whenever they
+ * want to verify what has been rendered (typically via requestAnimationFrame to make sure the latest state has been
+ * rendered in native.
+ *
+ * @param test Selector of the test, usually just `_cmd`.
+ * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS.
+ */
+- (void)runTest:(SEL)test module:(NSString *)moduleName;
+
+/**
+ * Same as runTest:, but allows for passing initialProps for providing mock data or requesting different behaviors, and
+ * expectErrorRegex verifies that the error you expected was thrown.
+ *
+ * @param test Selector of the test, usually just `_cmd`.
+ * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS.
+ * @param initialProps props that are passed into the component when rendered.
+ * @param expectErrorRegex A regex that must match the error thrown. If no error is thrown, the test fails.
+ */
+- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)expectErrorRegex;
+
+/**
+ * Same as runTest:, but allows for passing initialProps for providing mock data or requesting different behaviors, and
+ * expectErrorBlock provides arbitrary logic for processing errors (nil will cause any error to fail the test).
+ *
+ * @param test Selector of the test, usually just `_cmd`.
+ * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS.
+ * @param initialProps props that are passed into the component when rendered.
+ * @param expectErrorBlock A block that takes the error message and returns NO to fail the test.
+ */
+- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock;
@end
diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m
index 71459e51b..a6b710e3b 100644
--- a/Libraries/RCTTest/RCTTestRunner.m
+++ b/Libraries/RCTTest/RCTTestRunner.m
@@ -9,6 +9,7 @@
#import "RCTTestRunner.h"
+#import "FBSnapshotTestController.h"
#import "RCTRedBox.h"
#import "RCTRootView.h"
#import "RCTTestModule.h"
@@ -17,33 +18,55 @@
#define TIMEOUT_SECONDS 240
@implementation RCTTestRunner
-
-- (instancetype)initWithApp:(NSString *)app
{
- if (self = [super init]) {
+ FBSnapshotTestController *_snapshotController;
+}
+
+- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir
+{
+ if ((self = [super init])) {
+ NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
+ sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
+ _snapshotController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
+ _snapshotController.referenceImagesDirectory = referenceDir;
_script = [NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app];
}
return self;
}
-- (void)runTest:(NSString *)moduleName
+- (void)setRecordMode:(BOOL)recordMode
{
- [self runTest:moduleName initialProps:nil expectErrorBlock:nil];
+ _snapshotController.recordMode = recordMode;
}
-- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)errorRegex
+- (BOOL)recordMode
{
- [self runTest:moduleName initialProps:initialProps expectErrorBlock:^BOOL(NSString *error){
+ return _snapshotController.recordMode;
+}
+
+- (void)runTest:(SEL)test module:(NSString *)moduleName
+{
+ [self runTest:test module:moduleName initialProps:nil expectErrorBlock:nil];
+}
+
+- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)errorRegex
+{
+ [self runTest:test module:moduleName initialProps:initialProps expectErrorBlock:^BOOL(NSString *error){
return [errorRegex numberOfMatchesInString:error options:0 range:NSMakeRange(0, [error length])] > 0;
}];
}
-- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
+- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
{
- RCTTestModule *testModule = [[RCTTestModule alloc] init];
- RCTRootView *rootView = [[RCTRootView alloc] init];
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
- vc.view = rootView;
+ if ([vc.view isKindOfClass:[RCTRootView class]]) {
+ [(RCTRootView *)vc.view invalidate]; // Make sure the normal app view doesn't interfere
+ }
+ vc.view = [[UIView alloc] init];
+ RCTRootView *rootView = [[RCTRootView alloc] initWithFrame:CGRectMake(0, 0, 320, 2000)]; // Constant size for testing on multiple devices
+ RCTTestModule *testModule = [[RCTTestModule alloc] initWithSnapshotController:_snapshotController view:rootView];
+ testModule.testSelector = test;
+ [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized
rootView.moduleProvider = ^(void){
return @[testModule];
};
@@ -58,9 +81,13 @@
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date];
error = [[RCTRedBox sharedInstance] currentErrorMessage];
}
+ [rootView invalidate];
+ [rootView removeFromSuperview];
+ RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view);
+ vc.view = nil;
[[RCTRedBox sharedInstance] dismiss];
if (expectErrorBlock) {
- RCTAssert(expectErrorBlock(error), @"Expected an error but got none.");
+ RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched.");
} else if (error) {
RCTAssert(error == nil, @"RedBox error: %@", error);
} else {
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 2ad33b5d4..a6f25c9f1 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -62,11 +62,12 @@ var ReactNative = Object.assign(Object.create(require('React')), {
NativeModules: require('NativeModules'),
addons: {
- batchedUpdates: require('ReactUpdates').batchedUpdates,
LinkedStateMixin: require('LinkedStateMixin'),
Perf: undefined,
PureRenderMixin: require('ReactComponentWithPureRenderMixin'),
+ TestModule: require('NativeModules').TestModule,
TestUtils: undefined,
+ batchedUpdates: require('ReactUpdates').batchedUpdates,
cloneWithProps: require('cloneWithProps'),
update: require('update'),
},
diff --git a/ReactKit/Base/RCTRootView.h b/ReactKit/Base/RCTRootView.h
index 0ac716f3a..15f99fdee 100644
--- a/ReactKit/Base/RCTRootView.h
+++ b/ReactKit/Base/RCTRootView.h
@@ -11,7 +11,7 @@
#import "RCTBridge.h"
-@interface RCTRootView : UIView
+@interface RCTRootView : UIView
/**
* The URL of the bundled application script (required).
diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m
index e7aea0f43..cf3c9da49 100644
--- a/ReactKit/Base/RCTRootView.m
+++ b/ReactKit/Base/RCTRootView.m
@@ -106,12 +106,32 @@ static Class _globalExecutorClass;
[_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer"
args:@[self.reactTag]];
+ [self invalidate];
+}
+
+#pragma mark - RCTInvalidating
+
+- (BOOL)isValid
+{
+ return [_bridge isValid];
+}
+
+- (void)invalidate
+{
+ // Clear view
+ [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
+
+ [self removeGestureRecognizer:_touchHandler];
+ [_touchHandler invalidate];
+ [_executor invalidate];
// TODO: eventually we'll want to be able to share the bridge between
// multiple rootviews, in which case we'll need to move this elsewhere
[_bridge invalidate];
}
+#pragma mark Bundle loading
+
- (void)bundleFinishedLoading:(NSError *)error
{
if (error != nil) {
@@ -137,19 +157,12 @@ static Class _globalExecutorClass;
- (void)loadBundle
{
- // Clear view
- [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
+ [self invalidate];
if (!_scriptURL) {
return;
}
- // Clean up
- [self removeGestureRecognizer:_touchHandler];
- [_touchHandler invalidate];
- [_executor invalidate];
- [_bridge invalidate];
-
// Choose local executor if specified, followed by global, followed by default
_executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init];
_bridge = [[RCTBridge alloc] initWithExecutor:_executor moduleProvider:_moduleProvider];
@@ -209,6 +222,9 @@ static Class _globalExecutorClass;
[self bundleFinishedLoading:error];
return;
}
+ if (!_bridge.isValid) {
+ return; // Bridge was invalidated in the meanwhile
+ }
// Success!
RCTSourceCode *sourceCodeModule = _bridge.modules[NSStringFromClass([RCTSourceCode class])];
@@ -217,7 +233,9 @@ static Class _globalExecutorClass;
[_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
- [self bundleFinishedLoading:error];
+ if (_bridge.isValid) {
+ [self bundleFinishedLoading:error];
+ }
});
}];
diff --git a/runXcodeTests.sh b/runXcodeTests.sh
new file mode 100755
index 000000000..ae3255fb5
--- /dev/null
+++ b/runXcodeTests.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# Run from react-native root
+
+set -e
+
+xctool \
+ -project IntegrationTests/IntegrationTests.xcodeproj \
+ -scheme IntegrationTests \
+ -sdk iphonesimulator8.1 \
+ -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \
+ build test
+
+xctool \
+ -project Examples/UIExplorer/UIExplorer.xcodeproj \
+ -scheme UIExplorer \
+ -sdk iphonesimulator8.1 \
+ -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \
+ build test