diff --git a/platform/darwin/test/MGLMapSnapshotterTests.m b/platform/darwin/test/MGLMapSnapshotterTests.m index a692b95c4..6e07cbad3 100644 --- a/platform/darwin/test/MGLMapSnapshotterTests.m +++ b/platform/darwin/test/MGLMapSnapshotterTests.m @@ -1,9 +1,58 @@ #import #import +MGLImage *MGLNormalizedImage(MGLImage *sourceImage) { +#if TARGET_OS_IPHONE + CGSize scaledSize = CGSizeMake(sourceImage.size.width * sourceImage.scale, sourceImage.size.height * sourceImage.scale); + UIGraphicsBeginImageContextWithOptions(scaledSize, NO, UIScreen.mainScreen.scale); + [sourceImage drawInRect:(CGRect){ .origin = CGPointZero, .size = scaledSize }]; + UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return normalizedImage; +#else + return [NSImage imageWithSize:sourceImage.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) { + [sourceImage drawInRect:dstRect]; + return YES; + }]; +#endif +} + +BOOL MGLEqualImages(MGLImage *leftImage, MGLImage *rightImage) { +#if TARGET_OS_IPHONE + NSData *leftData = UIImagePNGRepresentation(MGLNormalizedImage(leftImage)); + NSData *rightData = UIImagePNGRepresentation(MGLNormalizedImage(rightImage)); + return [leftData isEqualToData:rightData]; +#else + CGImageRef leftCGImage = [MGLNormalizedImage(leftImage) CGImageForProposedRect:nil context:nil hints:nil]; + NSBitmapImageRep *leftImageRep = [[NSBitmapImageRep alloc] initWithCGImage:leftCGImage]; + NSData *leftData = [leftImageRep representationUsingType:NSPNGFileType properties:@{}]; + + CGImageRef rightCGImage = [MGLNormalizedImage(rightImage) CGImageForProposedRect:nil context:nil hints:nil]; + NSBitmapImageRep *rightImageRep = [[NSBitmapImageRep alloc] initWithCGImage:rightCGImage]; + NSData *rightData = [rightImageRep representationUsingType:NSPNGFileType properties:@{}]; + + return [leftData isEqualToData:rightData]; +#endif +} + +MGLImage *MGLImageFromCurrentContext() { +#if TARGET_OS_IPHONE + return UIGraphicsGetImageFromCurrentImageContext(); +#else + CGContextRef context = NSGraphicsContext.currentContext.CGContext; + CGImageRef cgImage = CGBitmapContextCreateImage(context); + CGFloat scale = NSScreen.mainScreen.backingScaleFactor; + NSSize imageSize = NSMakeSize(CGImageGetWidth(cgImage) / scale, CGImageGetHeight(cgImage) / scale); + NSImage *image = [[NSImage alloc] initWithCGImage:cgImage size:imageSize]; + CGImageRelease(cgImage); + return image; +#endif +} + @interface MGLMapSnapshotterTests : XCTestCase @property (nonatomic) XCTestExpectation *styleLoadingExpectation; +@property (nonatomic, copy, nullable) void (^runtimeStylingActions)(MGLStyle *style); @end @@ -22,6 +71,7 @@ - (void)tearDown { [MGLAccountManager setAccessToken:nil]; self.styleLoadingExpectation = nil; + self.runtimeStylingActions = nil; [super tearDown]; } @@ -39,9 +89,6 @@ NSImage *blankImage = [NSImage imageWithSize:NSMakeSize(500, 500) flipped:NO drawingHandler:^BOOL(NSRect dstRect) { return YES; }]; - CGImageRef blankCGImage = [blankImage CGImageForProposedRect:nil context:nil hints:nil]; - NSBitmapImageRep *blankImageRep = [[NSBitmapImageRep alloc] initWithCGImage:blankCGImage]; - NSData *blankImageData = [blankImageRep representationUsingType:NSPNGFileType properties:@{}]; #endif NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"]; @@ -55,15 +102,8 @@ XCTAssertNotNil(snapshotOverlay); if (snapshotOverlay) { XCTAssertNotEqual(snapshotOverlay.context, NULL); -#if TARGET_OS_IPHONE - UIImage *imageFromContext = UIGraphicsGetImageFromCurrentImageContext(); - XCTAssertEqualObjects(UIImagePNGRepresentation(blankImage), UIImagePNGRepresentation(imageFromContext), @"Base map in snapshot should be blank."); -#else - CGImageRef imageFromContext = CGBitmapContextCreateImage(snapshotOverlay.context); - NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:imageFromContext]; - NSData *imageData = [imageRep representationUsingType:NSPNGFileType properties:@{}]; - XCTAssertEqualObjects(blankImageData, imageData, @"Base map in snapshot should be blank."); -#endif + MGLImage *imageFromContext = MGLImageFromCurrentContext(); + XCTAssertTrue(MGLEqualImages(blankImage, imageFromContext), @"Base map in snapshot should be blank."); } [overlayExpectation fulfill]; } completionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) { @@ -101,11 +141,71 @@ [self waitForExpectations:@[self.styleLoadingExpectation, completionExpectation] timeout:2 enforceOrder:YES]; } +- (void)testRuntimeStyling { + [self testRuntimeStylingActions:^(MGLStyle *style) { + MGLBackgroundStyleLayer *backgroundLayer = [[MGLBackgroundStyleLayer alloc] initWithIdentifier:@"background"]; + backgroundLayer.backgroundColor = [NSExpression expressionForConstantValue:[MGLColor orangeColor]]; + [style addLayer:backgroundLayer]; + } expectedImageName:@"Fixtures/MGLMapSnapshotterTests/background"]; +} + +/** + Tests that applying the given runtime styling actions on a blank style results in a snapshot image that matches the image with the given name in the asset catalog. + + @param actions Runtime styling actions to apply to the blank style. + @param expectedImageName Name of the test fixture image in Media.xcassets. + */ +- (void)testRuntimeStylingActions:(void (^)(MGLStyle *style))actions expectedImageName:(NSString *)expectedImageName { + self.styleLoadingExpectation = [self expectationWithDescription:@"Style should finish loading."]; + XCTestExpectation *overlayExpectation = [self expectationWithDescription:@"Overlay handler should get called."]; + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Completion handler should get called."]; + +#if TARGET_OS_IPHONE + UIImage *expectedImage = [UIImage imageNamed:expectedImageName inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil]; +#else + NSImage *expectedImage = [[NSBundle bundleForClass:[self class]] imageForResource:expectedImageName]; +#endif + XCTAssertNotNil(expectedImage, @"Image fixture “%@” missing from Media.xcassets.", expectedImageName); + + NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"]; + MGLMapCamera *camera = [MGLMapCamera camera]; + MGLMapSnapshotOptions *options = [[MGLMapSnapshotOptions alloc] initWithStyleURL:styleURL camera:camera size:CGSizeMake(500, 500)]; + + MGLMapSnapshotter *snapshotter = [[MGLMapSnapshotter alloc] initWithOptions:options]; + snapshotter.delegate = self; + self.runtimeStylingActions = actions; + + [snapshotter startWithOverlayHandler:^(MGLMapSnapshotOverlay * _Nonnull snapshotOverlay) { + XCTAssertNotNil(snapshotOverlay); + if (snapshotOverlay) { + XCTAssertNotEqual(snapshotOverlay.context, NULL); + MGLImage *actualImage = MGLImageFromCurrentContext(); + XCTAssertTrue(MGLEqualImages(expectedImage, actualImage), @"Bare snapshot before ornamentation differs from expected image."); + } + [overlayExpectation fulfill]; + } completionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertNotNil(snapshot); + if (snapshot) { + XCTAssertEqual(snapshot.image.size.width, 500); + XCTAssertEqual(snapshot.image.size.height, 500); + } + [completionExpectation fulfill]; + }]; + [self waitForExpectations:@[self.styleLoadingExpectation, overlayExpectation, completionExpectation] timeout:2 enforceOrder:YES]; + self.runtimeStylingActions = nil; +} + #pragma mark MGLMapSnapshotterDelegate methods - (void)mapSnapshotter:(MGLMapSnapshotter *)snapshotter didFinishLoadingStyle:(MGLStyle *)style { XCTAssertNotNil(snapshotter); XCTAssertNotNil(style); + + if (self.runtimeStylingActions) { + self.runtimeStylingActions(style); + } + [self.styleLoadingExpectation fulfill]; } diff --git a/platform/darwin/test/Media.xcassets/Fixtures/Contents.json b/platform/darwin/test/Media.xcassets/Fixtures/Contents.json new file mode 100644 index 000000000..38f0c81fc --- /dev/null +++ b/platform/darwin/test/Media.xcassets/Fixtures/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/Contents.json b/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/Contents.json new file mode 100644 index 000000000..38f0c81fc --- /dev/null +++ b/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/background.imageset/Contents.json b/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/background.imageset/Contents.json new file mode 100644 index 000000000..94b8e3263 --- /dev/null +++ b/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/background.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "background.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/background.imageset/background.png b/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/background.imageset/background.png new file mode 100644 index 000000000..4f5f393e8 Binary files /dev/null and b/platform/darwin/test/Media.xcassets/Fixtures/MGLMapSnapshotterTests/background.imageset/background.png differ