diff --git a/FBSnapshotTestCase.xcodeproj/project.pbxproj b/FBSnapshotTestCase.xcodeproj/project.pbxproj index a114d55..2eaaf7a 100644 --- a/FBSnapshotTestCase.xcodeproj/project.pbxproj +++ b/FBSnapshotTestCase.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 1335641B1B59C3F500A4E4BF /* UIImage+Snapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564151B59C3F500A4E4BF /* UIImage+Snapshot.m */; }; 13CBB39D1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h in Headers */ = {isa = PBXBuildFile; fileRef = 13CBB39B1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13CBB39E1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.m in Sources */ = {isa = PBXBuildFile; fileRef = 13CBB39C1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.m */; }; + 42F2B74420C0D7A400ABED24 /* rect_shade.png in Resources */ = {isa = PBXBuildFile; fileRef = 42F2B74320C0D7A400ABED24 /* rect_shade.png */; }; + 42F2B74520C0D7A400ABED24 /* rect_shade.png in Resources */ = {isa = PBXBuildFile; fileRef = 42F2B74320C0D7A400ABED24 /* rect_shade.png */; }; 827137841C63AB7000354E42 /* FBSnapshotTestCase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8271377A1C63AB6F00354E42 /* FBSnapshotTestCase.framework */; }; 827137911C63ABE900354E42 /* UIImage+Compare.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564101B59C3F500A4E4BF /* UIImage+Compare.h */; }; 827137921C63ABF000354E42 /* UIImage+Diff.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564121B59C3F500A4E4BF /* UIImage+Diff.h */; }; @@ -75,6 +77,7 @@ 133564151B59C3F500A4E4BF /* UIImage+Snapshot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+Snapshot.m"; sourceTree = ""; }; 13CBB39B1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSnapshotTestCasePlatform.h; sourceTree = ""; }; 13CBB39C1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSnapshotTestCasePlatform.m; sourceTree = ""; }; + 42F2B74320C0D7A400ABED24 /* rect_shade.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = rect_shade.png; sourceTree = ""; }; 8271377A1C63AB6F00354E42 /* FBSnapshotTestCase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FBSnapshotTestCase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 827137831C63AB7000354E42 /* FBSnapshotTestCase tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FBSnapshotTestCase tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; B31987F01AB782D000B0A900 /* FBSnapshotTestCase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FBSnapshotTestCase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -191,6 +194,7 @@ B31987FF1AB782D100B0A900 /* Tests */ = { isa = PBXGroup; children = ( + 42F2B74320C0D7A400ABED24 /* rect_shade.png */, B76C68271C6BD68100586E5B /* rect.png */, E5C2CD611B1F399A00669887 /* square_with_pixel.png */, B32447D91AB78B5E00B1D6FF /* square_with_text.png */, @@ -379,6 +383,7 @@ 827137A21C63AC0D00354E42 /* square-copy.png in Resources */, 827137A31C63AC0D00354E42 /* square.png in Resources */, 827137A11C63AC0900354E42 /* square_with_text.png in Resources */, + 42F2B74520C0D7A400ABED24 /* rect_shade.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -398,6 +403,7 @@ E5C2CD621B1F399A00669887 /* square_with_pixel.png in Resources */, B32447DE1AB78B5E00B1D6FF /* square.png in Resources */, B32447DD1AB78B5E00B1D6FF /* square-copy.png in Resources */, + 42F2B74420C0D7A400ABED24 /* rect_shade.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.h b/FBSnapshotTestCase/Categories/UIImage+Compare.h index ecb79a3..b2e6fd6 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Compare.h +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.h @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN @interface UIImage (Compare) -- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance; +- (BOOL)fb_compareWithImage:(UIImage *)image pixelTolerance:(CGFloat)pixelTolerance tolerance:(CGFloat)tolerance; @end diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.m b/FBSnapshotTestCase/Categories/UIImage+Compare.m index 5d50b5e..0c35227 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Compare.m +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.m @@ -44,7 +44,7 @@ @implementation UIImage (Compare) -- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance +- (BOOL)fb_compareWithImage:(UIImage *)image pixelTolerance:(CGFloat)pixelTolerance tolerance:(CGFloat)tolerance { NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size."); @@ -93,34 +93,20 @@ - (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance CGContextRelease(imageContext); BOOL imageEqual = YES; + FBComparePixel *p1 = referenceImagePixels; + FBComparePixel *p2 = imagePixels; // Do a fast compare if we can - if (tolerance == 0) { + if (tolerance == 0 && pixelTolerance == 0) { imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0); } else { + const NSUInteger pixelCount = referenceImageSize.width * referenceImageSize.height; // 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++; - } + imageEqual = [self _compareAllPixelsWithPixelTolerance:pixelTolerance + tolerance:tolerance + pixelCount:pixelCount + referencePixels:p1 + imagePixels:p2]; } free(referenceImagePixels); @@ -129,4 +115,66 @@ - (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance return imageEqual; } +- (BOOL)_comparePixelWithPixelTolerance:(CGFloat)pixelTolerance + referencePixel:(FBComparePixel *)referencePixel + imagePixel:(FBComparePixel *)imagePixel +{ + if (referencePixel->raw == imagePixel->raw) { + return YES; + } else if (pixelTolerance == 0) { + return NO; + } + + CGFloat redPercentDiff = [self _calculatePercentDifferenceForReferencePixelComponent:referencePixel->pixels.red + imagePixelComponent:imagePixel->pixels.red]; + CGFloat greenPercentDiff = [self _calculatePercentDifferenceForReferencePixelComponent:referencePixel->pixels.green + imagePixelComponent:imagePixel->pixels.green]; + CGFloat bluePercentDiff = [self _calculatePercentDifferenceForReferencePixelComponent:referencePixel->pixels.blue + imagePixelComponent:imagePixel->pixels.blue]; + CGFloat alphaPercentDiff = [self _calculatePercentDifferenceForReferencePixelComponent:referencePixel->pixels.alpha + imagePixelComponent:imagePixel->pixels.alpha]; + + BOOL anyDifferencesFound = (redPercentDiff > pixelTolerance || + greenPercentDiff > pixelTolerance || + bluePercentDiff > pixelTolerance || + alphaPercentDiff > pixelTolerance); + + return !anyDifferencesFound; +} + +- (CGFloat)_calculatePercentDifferenceForReferencePixelComponent:(char)p1 + imagePixelComponent:(char)p2 +{ + NSInteger referencePixelComponent = (unsigned char)p1; + NSInteger imagePixelComponent = (unsigned char)p2; + NSUInteger componentDifference = ABS(referencePixelComponent - imagePixelComponent); + return (CGFloat)componentDifference / 256; +} + +- (BOOL)_compareAllPixelsWithPixelTolerance:(CGFloat)pixelTolerance + tolerance:(CGFloat)tolerance + pixelCount:(NSUInteger)pixelCount + referencePixels:(FBComparePixel *)referencePixel + imagePixels:(FBComparePixel *)imagePixel +{ + NSUInteger numDiffPixels = 0; + for (NSUInteger n = 0; n < pixelCount; ++n) { + // If this pixel is different, increment the pixel diff count and see + // if we have hit our limit. + BOOL isIdenticalPixel = [self _comparePixelWithPixelTolerance:pixelTolerance referencePixel:referencePixel imagePixel:imagePixel]; + if (!isIdenticalPixel) { + numDiffPixels++; + + CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount; + if (percent > tolerance) { + return NO; + } + } + + referencePixel++; + imagePixel++; + } + return YES; +} + @end diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.h b/FBSnapshotTestCase/FBSnapshotTestCase.h index 5e802ec..c2b6bde 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCase.h +++ b/FBSnapshotTestCase/FBSnapshotTestCase.h @@ -47,24 +47,46 @@ /** 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 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 + @param suffixes An NSOrderedSet of strings for the different suffixes. + @param tolerance The overall 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__) +/** + 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 pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. + @param tolerance The overall percentage of pixels that can differ and still count as an 'identical' layer. + */ +#define FBSnapshotVerifyViewWithPixelOptions(view__, identifier__, suffixes__, pixelTolerance__, tolerance__) \ + FBSnapshotVerifyViewOrLayerWithPixelOptions(View, view__, identifier__, suffixes__, pixelTolerance__, 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 layer The layer 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' layer + @param suffixes An NSOrderedSet of strings for the different suffixes. + @param pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. + @param tolerance The overall percentage of pixels that can differ and still count as an 'identical' layer. + */ +#define FBSnapshotVerifyLayerWithPixelOptions(layer__, identifier__, suffixes__, pixelTolerance__, tolerance__) \ + FBSnapshotVerifyViewOrLayerWithPixelOptions(Layer, layer__, identifier__, suffixes__, pixelTolerance__, tolerance__) + +/** + 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 if there are multiple snapshot tests in a given -test method. + @param suffixes An NSOrderedSet of strings for the different suffixes. + @param tolerance The overall 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__) @@ -72,7 +94,6 @@ #define FBSnapshotVerifyLayer(layer__, identifier__) \ FBSnapshotVerifyLayerWithOptions(layer__, identifier__, FBSnapshotTestCaseDefaultSuffixes(), 0) - #define FBSnapshotVerifyViewOrLayerWithOptions(what__, viewOrLayer__, identifier__, suffixes__, tolerance__) \ { \ NSString *errorDescription = [self snapshotVerifyViewOrLayer:viewOrLayer__ identifier:identifier__ suffixes:suffixes__ tolerance:tolerance__ defaultReferenceDirectory:(@FB_REFERENCE_IMAGE_DIR) defaultImageDiffDirectory:(@IMAGE_DIFF_DIR)]; \ @@ -80,6 +101,13 @@ XCTAssertTrue(noErrors, @"%@", errorDescription); \ } +#define FBSnapshotVerifyViewOrLayerWithPixelOptions(what__, viewOrLayer__, identifier__, suffixes__, pixelTolerance__, tolerance__) \ + { \ + NSString *errorDescription = [self snapshotVerifyViewOrLayer:viewOrLayer__ identifier:identifier__ suffixes:suffixes__ pixelTolerance:pixelTolerance__ tolerance:tolerance__ defaultReferenceDirectory:(@FB_REFERENCE_IMAGE_DIR) defaultImageDiffDirectory:(@IMAGE_DIFF_DIR)]; \ + BOOL noErrors = (errorDescription == nil); \ + XCTAssertTrue(noErrors, @"%@", errorDescription); \ + } + NS_ASSUME_NONNULL_BEGIN /** @@ -148,10 +176,28 @@ NS_ASSUME_NONNULL_BEGIN /** Performs the comparison or records a snapshot of the layer if recordMode is YES. - @param viewOrLayer The UIView or CALayer to snapshot + @param viewOrLayer The UIView or CALayer 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 difference to still count as identical - 0 mean pixel perfect, 1 means I don't care + @param suffixes An NSOrderedSet of strings for the different suffixes. + @param tolerance The percentage difference to still count as identical - 0 mean pixel perfect, 1 means I don't care. + @param defaultReferenceDirectory The directory to default to for reference images. + @param defaultImageDiffDirectory The directory to default to for failed image diffs. + @returns nil if the comparison (or saving of the reference image) succeeded. Otherwise it contains an error description. + */ +- (NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer + identifier:(nullable NSString *)identifier + suffixes:(NSOrderedSet *)suffixes + tolerance:(CGFloat)tolerance + defaultReferenceDirectory:(nullable NSString *)defaultReferenceDirectory + defaultImageDiffDirectory:(nullable NSString *)defaultImageDiffDirectory; + +/** + Performs the comparison or records a snapshot of the layer if recordMode is YES. + @param viewOrLayer The UIView or CALayer 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 pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. Each color shade difference represents a 0.390625% change. + @param tolerance The percentage difference to still count as identical - 0 mean pixel perfect, 1 means I don't care. @param defaultReferenceDirectory The directory to default to for reference images. @param defaultImageDiffDirectory The directory to default to for failed image diffs. @returns nil if the comparison (or saving of the reference image) succeeded. Otherwise it contains an error description. @@ -159,17 +205,18 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer identifier:(nullable NSString *)identifier suffixes:(NSOrderedSet *)suffixes + pixelTolerance:(CGFloat)pixelTolerance tolerance:(CGFloat)tolerance defaultReferenceDirectory:(nullable NSString *)defaultReferenceDirectory defaultImageDiffDirectory:(nullable NSString *)defaultImageDiffDirectory; /** Performs the comparison or records a snapshot of the layer if recordMode is YES. - @param layer The Layer to snapshot + @param layer The Layer to snapshot. @param referenceImagesDirectory The directory in which reference images are stored. @param imageDiffDirectory The directory in which failed image diffs 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 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. */ @@ -180,13 +227,50 @@ NS_ASSUME_NONNULL_BEGIN tolerance:(CGFloat)tolerance error:(NSError **)errorPtr; +/** + 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 imageDiffDirectory The directory in which failed image diffs are stored. + @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method. + @param pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. Each color shade difference represents a 0.390625% change. + @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 + imageDiffDirectory:(NSString *)imageDiffDirectory + identifier:(nullable NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance + 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 imageDiffDirectory The directory in which failed image diffs 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 + imageDiffDirectory:(NSString *)imageDiffDirectory + identifier:(nullable 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 view The view to snapshot. @param referenceImagesDirectory The directory in which reference images are stored. @param imageDiffDirectory The directory in which failed image diffs 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 pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. Each color shade difference represents a 0.390625% change. + @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. */ @@ -194,6 +278,7 @@ NS_ASSUME_NONNULL_BEGIN referenceImagesDirectory:(NSString *)referenceImagesDirectory imageDiffDirectory:(NSString *)imageDiffDirectory identifier:(nullable NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance tolerance:(CGFloat)tolerance error:(NSError **)errorPtr; diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.m b/FBSnapshotTestCase/FBSnapshotTestCase.m index f6050e7..f7af94d 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCase.m +++ b/FBSnapshotTestCase/FBSnapshotTestCase.m @@ -90,6 +90,23 @@ - (NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer tolerance:(CGFloat)tolerance defaultReferenceDirectory:(NSString *)defaultReferenceDirectory defaultImageDiffDirectory:(NSString *)defaultImageDiffDirectory +{ + return [self snapshotVerifyViewOrLayer:viewOrLayer + identifier:identifier + suffixes:suffixes + pixelTolerance:0 + tolerance:tolerance + defaultReferenceDirectory:defaultReferenceDirectory + defaultImageDiffDirectory:defaultImageDiffDirectory]; +} + +- (NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer + identifier:(NSString *)identifier + suffixes:(NSOrderedSet *)suffixes + pixelTolerance:(CGFloat)pixelTolerance + tolerance:(CGFloat)tolerance + defaultReferenceDirectory:(NSString *)defaultReferenceDirectory + defaultImageDiffDirectory:(NSString *)defaultImageDiffDirectory { if (nil == viewOrLayer) { return @"Object to be snapshotted must not be nil"; @@ -115,7 +132,7 @@ - (NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer if (self.recordMode) { NSString *referenceImagesDirectory = [NSString stringWithFormat:@"%@%@", referenceImageDirectory, suffixes.firstObject]; - BOOL referenceImageSaved = [self _compareSnapshotOfViewOrLayer:viewOrLayer referenceImagesDirectory:referenceImagesDirectory imageDiffDirectory:imageDiffDirectory identifier:(identifier) tolerance:tolerance error:&error]; + BOOL referenceImageSaved = [self _compareSnapshotOfViewOrLayer:viewOrLayer referenceImagesDirectory:referenceImagesDirectory imageDiffDirectory:imageDiffDirectory identifier:(identifier) pixelTolerance:pixelTolerance tolerance:tolerance error:&error]; if (!referenceImageSaved) { [errors addObject:error]; } @@ -125,7 +142,7 @@ - (NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer BOOL referenceImageAvailable = [self referenceImageRecordedInDirectory:referenceImagesDirectory identifier:(identifier) error:&error]; if (referenceImageAvailable) { - BOOL comparisonSuccess = [self _compareSnapshotOfViewOrLayer:viewOrLayer referenceImagesDirectory:referenceImagesDirectory imageDiffDirectory:imageDiffDirectory identifier:identifier tolerance:tolerance error:&error]; + BOOL comparisonSuccess = [self _compareSnapshotOfViewOrLayer:viewOrLayer referenceImagesDirectory:referenceImagesDirectory imageDiffDirectory:imageDiffDirectory identifier:identifier pixelTolerance:pixelTolerance tolerance:tolerance error:&error]; [errors removeAllObjects]; if (comparisonSuccess) { testSuccess = YES; @@ -160,6 +177,24 @@ - (BOOL)compareSnapshotOfLayer:(CALayer *)layer referenceImagesDirectory:referenceImagesDirectory imageDiffDirectory:imageDiffDirectory identifier:identifier + pixelTolerance:0 + tolerance:tolerance + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + referenceImagesDirectory:(NSString *)referenceImagesDirectory + imageDiffDirectory:(NSString *)imageDiffDirectory + identifier:(NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + return [self _compareSnapshotOfViewOrLayer:layer + referenceImagesDirectory:referenceImagesDirectory + imageDiffDirectory:(NSString *)imageDiffDirectory + identifier:identifier + pixelTolerance:pixelTolerance tolerance:tolerance error:errorPtr]; } @@ -175,6 +210,24 @@ - (BOOL)compareSnapshotOfView:(UIView *)view referenceImagesDirectory:referenceImagesDirectory imageDiffDirectory:imageDiffDirectory identifier:identifier + pixelTolerance:0 + tolerance:tolerance + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfView:(UIView *)view + referenceImagesDirectory:(NSString *)referenceImagesDirectory + imageDiffDirectory:(NSString *)imageDiffDirectory + identifier:(NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr +{ + return [self _compareSnapshotOfViewOrLayer:view + referenceImagesDirectory:referenceImagesDirectory + imageDiffDirectory:(NSString *)imageDiffDirectory + identifier:identifier + pixelTolerance:pixelTolerance tolerance:tolerance error:errorPtr]; } @@ -222,6 +275,7 @@ - (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer referenceImagesDirectory:(NSString *)referenceImagesDirectory imageDiffDirectory:(NSString *)imageDiffDirectory identifier:(NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance tolerance:(CGFloat)tolerance error:(NSError **)errorPtr { @@ -230,6 +284,7 @@ - (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer return [_snapshotController compareSnapshotOfViewOrLayer:viewOrLayer selector:self.invocation.selector identifier:identifier + pixelTolerance:pixelTolerance tolerance:tolerance error:errorPtr]; } diff --git a/FBSnapshotTestCase/FBSnapshotTestController.h b/FBSnapshotTestCase/FBSnapshotTestController.h index 5161b05..6064caa 100644 --- a/FBSnapshotTestCase/FBSnapshotTestController.h +++ b/FBSnapshotTestCase/FBSnapshotTestController.h @@ -133,7 +133,7 @@ extern NSString *const FBDiffedImageKey; @param viewOrLayer 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 tolerance The percentage of pixels that can differ and still be considered 'identical'. @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. */ @@ -143,6 +143,23 @@ extern NSString *const FBDiffedImageKey; tolerance:(CGFloat)tolerance error:(NSError **)errorPtr; +/** + Performs the comparison of a view or layer. + @param viewOrLayer 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 pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. + @param tolerance The percentage of pixels that can differ and still be considered 'identical'. + @param errorPtr An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if the comparison (or saving of the reference image) succeeded. + */ +- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(nullable NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr; + /** Loads a reference image. @param selector The test method being run. @@ -158,12 +175,27 @@ extern NSString *const FBDiffedImageKey; 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 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; + +/** + 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 pixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. + @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 + pixelTolerance:(CGFloat)pixelTolerance tolerance:(CGFloat)tolerance error:(NSError **)errorPtr; diff --git a/FBSnapshotTestCase/FBSnapshotTestController.m b/FBSnapshotTestCase/FBSnapshotTestController.m index 0029344..ec3bc8b 100644 --- a/FBSnapshotTestCase/FBSnapshotTestController.m +++ b/FBSnapshotTestCase/FBSnapshotTestController.m @@ -63,6 +63,7 @@ - (BOOL)compareSnapshotOfLayer:(CALayer *)layer return [self compareSnapshotOfViewOrLayer:layer selector:selector identifier:identifier + pixelTolerance:0 tolerance:0 error:errorPtr]; } @@ -75,6 +76,7 @@ - (BOOL)compareSnapshotOfView:(UIView *)view return [self compareSnapshotOfViewOrLayer:view selector:selector identifier:identifier + pixelTolerance:0 tolerance:0 error:errorPtr]; } @@ -84,11 +86,27 @@ - (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer identifier:(NSString *)identifier tolerance:(CGFloat)tolerance error:(NSError **)errorPtr +{ + return [self compareSnapshotOfViewOrLayer:viewOrLayer + selector:selector + identifier:identifier + pixelTolerance:0 + tolerance:tolerance + error:errorPtr]; +} + + +- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance + 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]; + return [self _performPixelComparisonWithViewOrLayer:viewOrLayer selector:selector identifier:identifier pixelTolerance:pixelTolerance tolerance:tolerance error:errorPtr]; } } @@ -121,9 +139,22 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image tolerance:(CGFloat)tolerance error:(NSError **)errorPtr +{ + return [self compareReferenceImage:referenceImage + toImage:image + pixelTolerance:0 + tolerance:tolerance + error:errorPtr]; +} + +- (BOOL)compareReferenceImage:(UIImage *)referenceImage + toImage:(UIImage *)image + pixelTolerance:(CGFloat)pixelTolerance + tolerance:(CGFloat)tolerance + error:(NSError **)errorPtr { BOOL sameImageDimensions = CGSizeEqualToSize(referenceImage.size, image.size); - if (sameImageDimensions && [referenceImage fb_compareWithImage:image tolerance:tolerance]) { + if (sameImageDimensions && [referenceImage fb_compareWithImage:image pixelTolerance:pixelTolerance tolerance:tolerance]) { return YES; } @@ -267,13 +298,14 @@ - (NSString *)_failedFilePathForSelector:(SEL)selector - (BOOL)_performPixelComparisonWithViewOrLayer:(id)viewOrLayer selector:(SEL)selector identifier:(NSString *)identifier + pixelTolerance:(CGFloat)pixelTolerance 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]; + BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot pixelTolerance:pixelTolerance tolerance:tolerance error:errorPtr]; if (!imagesSame) { NSError *saveError = nil; if ([self saveFailedReferenceImage:referenceImage testImage:snapshot selector:selector identifier:identifier error:&saveError] == NO) { diff --git a/FBSnapshotTestCase/SwiftSupport.swift b/FBSnapshotTestCase/SwiftSupport.swift index 1e4594e..7fe87ca 100644 --- a/FBSnapshotTestCase/SwiftSupport.swift +++ b/FBSnapshotTestCase/SwiftSupport.swift @@ -8,15 +8,15 @@ */ public extension FBSnapshotTestCase { - public func FBSnapshotVerifyView(_ view: UIView, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), tolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { - FBSnapshotVerifyViewOrLayer(view, identifier: identifier, suffixes: suffixes, tolerance: tolerance, file: file, line: line) + public func FBSnapshotVerifyView(_ view: UIView, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), pixelTolerance: CGFloat = 0, tolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { + FBSnapshotVerifyViewOrLayer(view, identifier: identifier, suffixes: suffixes, pixelTolerance: pixelTolerance, tolerance: tolerance, file: file, line: line) } - public func FBSnapshotVerifyLayer(_ layer: CALayer, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), tolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { - FBSnapshotVerifyViewOrLayer(layer, identifier: identifier, suffixes: suffixes, tolerance: tolerance, file: file, line: line) + public func FBSnapshotVerifyLayer(_ layer: CALayer, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), pixelTolerance: CGFloat = 0, tolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { + FBSnapshotVerifyViewOrLayer(layer, identifier: identifier, suffixes: suffixes, pixelTolerance: pixelTolerance, tolerance: tolerance, file: file, line: line) } - private func FBSnapshotVerifyViewOrLayer(_ viewOrLayer: AnyObject, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), tolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { + private func FBSnapshotVerifyViewOrLayer(_ viewOrLayer: AnyObject, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), pixelTolerance: CGFloat = 0, tolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { let envReferenceImageDirectory = self.getReferenceImageDirectory(withDefault: FB_REFERENCE_IMAGE_DIR) let envImageDiffDirectory = self.getImageDiffDirectory(withDefault: IMAGE_DIFF_DIR) var error: NSError? @@ -27,7 +27,7 @@ public extension FBSnapshotTestCase { let imageDiffDirectory = envImageDiffDirectory if viewOrLayer.isKind(of: UIView.self) { do { - try compareSnapshot(of: viewOrLayer as! UIView, referenceImagesDirectory: referenceImagesDirectory, imageDiffDirectory: imageDiffDirectory, identifier: identifier, tolerance: tolerance) + try compareSnapshot(of: viewOrLayer as! UIView, referenceImagesDirectory: referenceImagesDirectory, imageDiffDirectory: imageDiffDirectory, identifier: identifier, pixelTolerance: pixelTolerance, tolerance: tolerance) comparisonSuccess = true } catch let error1 as NSError { error = error1 @@ -35,7 +35,7 @@ public extension FBSnapshotTestCase { } } else if viewOrLayer.isKind(of: CALayer.self) { do { - try compareSnapshot(of: viewOrLayer as! CALayer, referenceImagesDirectory: referenceImagesDirectory, imageDiffDirectory: imageDiffDirectory, identifier: identifier, tolerance: tolerance) + try compareSnapshot(of: viewOrLayer as! CALayer, referenceImagesDirectory: referenceImagesDirectory, imageDiffDirectory: imageDiffDirectory, identifier: identifier, pixelTolerance: pixelTolerance, tolerance: tolerance) comparisonSuccess = true } catch let error1 as NSError { error = error1 diff --git a/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m b/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m index e6d0d26..a37efc8 100644 --- a/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m +++ b/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m @@ -59,7 +59,7 @@ - (void)testCompareReferenceImageWithVeryLowToleranceShouldNotMatch FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; // With virtually no margin for error, this should fail to be equal NSError *error = nil; - XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage tolerance:0.0001 error:&error]); + XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage tolerance:.0001 error:&error]); XCTAssertNotNil(error); XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent); } @@ -114,6 +114,37 @@ - (void)testFailedImageWithDeviceAgnosticShouldHaveModelOnName XCTAssertTrue([(NSString *)[error.userInfo objectForKey:FBReferenceImageFilePathKey] containsString:deviceAgnosticReferencePath]); } +- (void)testCompareReferenceImageWithLowPixelToleranceShouldNotMatch +{ + UIImage *referenceImage = [self _bundledImageNamed:@"square" type:@"png"]; + XCTAssertNotNil(referenceImage); + UIImage *testImage = [self _bundledImageNamed:@"square_with_pixel" type:@"png"]; + XCTAssertNotNil(testImage); + + id testClass = nil; + FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; + // With virtually no margin for error, this should fail to be equal + NSError *error = nil; + XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage pixelTolerance:.06 tolerance:0 error:&error]); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent); +} + +- (void)testCompareReferenceImageWithLowPixelToleranceShouldMatch +{ + UIImage *referenceImage = [self _bundledImageNamed:@"rect" type:@"png"]; + XCTAssertNotNil(referenceImage); + UIImage *testImage = [self _bundledImageNamed:@"rect_shade" type:@"png"]; + XCTAssertNotNil(testImage); + + id testClass = nil; + FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; + // With some tolerance these should be considered the same + NSError *error = nil; + XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage pixelTolerance:.06 tolerance:0 error:&error]); + XCTAssertNil(error); +} + #pragma mark - Private helper methods - (UIImage *)_bundledImageNamed:(NSString *)name type:(NSString *)type diff --git a/FBSnapshotTestCaseTests/rect_shade.png b/FBSnapshotTestCaseTests/rect_shade.png new file mode 100644 index 0000000..bb9a0c5 Binary files /dev/null and b/FBSnapshotTestCaseTests/rect_shade.png differ