From 6eb98b0bd80d204bd25b84305b11720bba3f2250 Mon Sep 17 00:00:00 2001 From: Duncan Babbage Date: Sat, 7 Apr 2018 20:02:17 +1200 Subject: [PATCH] Implement AutoRecord for new snapshots. Provides an autoRecord parameter. If set, new tests that do not have a reference image will still execute, will return a failure, and will provide a fail image which is a snapshot of the view. This provides the opportunity for the user to review the image and decide whether to set it as a reference image. If recordMode == true, autoRecord is ignored and behavior is as previously. --- FBSnapshotTestCase/FBSnapshotTestCase.h | 6 ++++ FBSnapshotTestCase/FBSnapshotTestCase.m | 20 +++++++++-- FBSnapshotTestCase/FBSnapshotTestController.h | 5 +++ FBSnapshotTestCase/FBSnapshotTestController.m | 25 ++++++++++--- FBSnapshotTestCase/SwiftSupport.swift | 36 +++++++++++++++++-- 5 files changed, 81 insertions(+), 11 deletions(-) diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.h b/FBSnapshotTestCase/FBSnapshotTestCase.h index d0bc1b6..3dbd558 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCase.h +++ b/FBSnapshotTestCase/FBSnapshotTestCase.h @@ -93,6 +93,12 @@ */ @property (readwrite, nonatomic, assign) BOOL recordMode; +/** + When YES, a test will run and fail when no reference image exists, without needing to run + recordMode first. The fail image is stored and can be reviewed and accepted as a reference. + */ +@property (readwrite, nonatomic, assign) BOOL autoRecord; + /** When @c YES appends the name of the device model and OS to the snapshot file name. The default value is @c NO. diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.m b/FBSnapshotTestCase/FBSnapshotTestCase.m index 65fd2a2..92df7cc 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCase.m +++ b/FBSnapshotTestCase/FBSnapshotTestCase.m @@ -40,6 +40,17 @@ - (void)setRecordMode:(BOOL)recordMode _snapshotController.recordMode = recordMode; } +- (BOOL)autoRecord +{ + return _snapshotController.autoRecord; +} + +- (void)setAutoRecord:(BOOL)autoRecord +{ + NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__); + _snapshotController.autoRecord = autoRecord; +} + - (BOOL)isDeviceAgnostic { return _snapshotController.deviceAgnostic; @@ -122,12 +133,15 @@ - (NSString *)snapshotVerifyViewOrLayer:(id)viewOrLayer } } - if (!testSuccess) { - return [NSString stringWithFormat:@"Snapshot comparison failed: %@", errors.firstObject]; - } if (self.recordMode) { return @"Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!"; } + else if (!testSuccess && !self.autoRecord) { + return [NSString stringWithFormat:@"Snapshot comparison failed: %@", errors.firstObject]; + } + else if (!testSuccess && self.autoRecord) { + return [NSString stringWithFormat:@"No previous reference image. New image has been stored for approval."]; + } return nil; } diff --git a/FBSnapshotTestCase/FBSnapshotTestController.h b/FBSnapshotTestCase/FBSnapshotTestController.h index 67650dd..b968bd2 100644 --- a/FBSnapshotTestCase/FBSnapshotTestController.h +++ b/FBSnapshotTestCase/FBSnapshotTestController.h @@ -52,6 +52,11 @@ extern NSString *const FBDiffedImageKey; */ @interface FBSnapshotTestController : NSObject +/** + Auto record snapshots on first run of new tests. + */ +@property (readwrite, nonatomic, assign) BOOL autoRecord; + /** Record snapshots. */ diff --git a/FBSnapshotTestCase/FBSnapshotTestController.m b/FBSnapshotTestCase/FBSnapshotTestController.m index d64cf38..ef7bc3d 100644 --- a/FBSnapshotTestCase/FBSnapshotTestController.m +++ b/FBSnapshotTestCase/FBSnapshotTestController.m @@ -107,7 +107,7 @@ - (UIImage *)referenceImageForSelector:(SEL)selector UIImage *image = [UIImage imageWithContentsOfFile:filePath]; if (nil == image && NULL != errorPtr) { BOOL exists = [_fileManager fileExistsAtPath:filePath]; - if (!exists) { + if (!exists && !self.autoRecord) { *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain code:FBSnapshotTestControllerErrorCodeNeedsRecord userInfo:@{ @@ -115,6 +115,14 @@ - (UIImage *)referenceImageForSelector:(SEL)selector NSLocalizedDescriptionKey: @"Unable to load reference image.", NSLocalizedFailureReasonErrorKey: @"Reference image not found. You need to run the test in record mode", }]; + } else if (!exists && self.autoRecord) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeNeedsRecord + userInfo:@{ + FBReferenceImageFilePathKey: filePath, + NSLocalizedDescriptionKey: @"Unable to load reference image.", + NSLocalizedFailureReasonErrorKey: @"Reference image not found. Auto-recorded image saved for review", + }]; } else { *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain code:FBSnapshotTestControllerErrorCodeUnknown @@ -178,7 +186,7 @@ - (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage return NO; } - if (![referencePNGData writeToFile:referencePath options:NSDataWritingAtomic error:errorPtr]) { + if (![referencePNGData writeToFile:referencePath options:NSDataWritingAtomic error:errorPtr] && !self.autoRecord) { return NO; } @@ -197,7 +205,7 @@ - (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage UIImage *diffImage = [referenceImage fb_diffWithImage:testImage]; NSData *diffImageData = UIImagePNGRepresentation(diffImage); - if (![diffImageData writeToFile:diffPath options:NSDataWritingAtomic error:errorPtr]) { + if (![diffImageData writeToFile:diffPath options:NSDataWritingAtomic error:errorPtr] && !self.autoRecord) { return NO; } @@ -281,9 +289,16 @@ - (BOOL)_performPixelComparisonWithViewOrLayer:(id)viewOrLayer error:(NSError **)errorPtr { UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr]; - if (nil != referenceImage) { + if (nil != referenceImage || self.autoRecord == true) { UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer]; - BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot tolerance:tolerance error:errorPtr]; + BOOL imagesSame; + + if (referenceImage == nil && self.autoRecord) { + imagesSame = NO; + } else { + imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot 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 c2eec8d..3e07e27 100644 --- a/FBSnapshotTestCase/SwiftSupport.swift +++ b/FBSnapshotTestCase/SwiftSupport.swift @@ -24,13 +24,18 @@ if let envReferenceImageDirectory = envReferenceImageDirectory { for suffix in suffixes { let referenceImagesDirectory = "\(envReferenceImageDirectory)\(suffix)" + var referenceImageExists = true if viewOrLayer.isKind(of: UIView.self) { do { try compareSnapshot(of: viewOrLayer as! UIView, referenceImagesDirectory: referenceImagesDirectory, identifier: identifier, tolerance: tolerance) comparisonSuccess = true } catch let error1 as NSError { - error = error1 comparisonSuccess = false + if error1.code == 1 { + referenceImageExists = false + } else { + error = error1 + } } } else if viewOrLayer.isKind(of: CALayer.self) { do { @@ -39,6 +44,11 @@ } catch let error1 as NSError { error = error1 comparisonSuccess = false + if error1.code == 1 { + referenceImageExists = false + } else { + error = error1 + } } } else { assertionFailure("Only UIView and CALayer classes can be snapshotted") @@ -49,7 +59,13 @@ if comparisonSuccess || recordMode { break } + + assert(self.autoRecord == false || referenceImageExists, message: "No previous reference image. New image has been stored for approval.", file: file, line: line) + if self.autoRecord && !referenceImageExists { + break + } + assert(comparisonSuccess, message: "Snapshot comparison failed: \(String(describing: error))", file: file, line: line) } } else { @@ -86,16 +102,24 @@ public extension FBSnapshotTestCase { try compareSnapshotOfView(viewOrLayer as! UIView, referenceImagesDirectory: referenceImagesDirectory, identifier: identifier, tolerance: tolerance) comparisonSuccess = true } catch let error1 as NSError { - error = error1 comparisonSuccess = false + if error1.code == 1 { + referenceImageExists = false + } else { + error = error1 + } } } else if viewOrLayer.isKindOfClass(CALayer) { do { try compareSnapshotOfLayer(viewOrLayer as! CALayer, referenceImagesDirectory: referenceImagesDirectory, identifier: identifier, tolerance: tolerance) comparisonSuccess = true } catch let error1 as NSError { - error = error1 comparisonSuccess = false + if error1.code == 1 { + referenceImageExists = false + } else { + error = error1 + } } } else { assertionFailure("Only UIView and CALayer classes can be snapshotted") @@ -107,6 +131,12 @@ public extension FBSnapshotTestCase { break } + assert(self.autoRecord == false || referenceImageExists, message: "No previous reference image. New image has been stored for approval.", file: file, line: line) + + if self.autoRecord && !referenceImageExists { + break + } + assert(comparisonSuccess, message: "Snapshot comparison failed: \(error)", file: file, line: line) } } else {