Skip to content

feat(ios): handle private views #524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 31, 2024
206 changes: 206 additions & 0 deletions example/ios/InstabugTests/PrivateViewApiTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import <PrivateViewApi.h>
#import <Flutter/Flutter.h>
#import "FlutterPluginRegistrar+FlutterEngine.h"


@interface MockFlutterPluginRegistrar : NSObject <FlutterPluginRegistrar>
@end

@implementation MockFlutterPluginRegistrar

@end


@interface PrivateViewApiTests : XCTestCase
@property (nonatomic, strong) PrivateViewApi *api;
@property (nonatomic, strong) id mockFlutterApi;
@property (nonatomic, strong) id mockRegistrar;
@property (nonatomic, strong) id mockFlutterViewController;
@property (nonatomic, strong) id mockEngine;

@end

@implementation PrivateViewApiTests

#pragma mark - Setup / Teardown

- (void)setUp {
[super setUp];


self.mockFlutterApi = OCMClassMock([InstabugPrivateViewApi class]);


MockFlutterPluginRegistrar *mockRegistrar = [[MockFlutterPluginRegistrar alloc] init];

self.mockRegistrar = OCMPartialMock(mockRegistrar);

self.mockEngine = OCMClassMock([FlutterEngine class]);
OCMStub([self.mockRegistrar flutterEngine]).andReturn(self.mockEngine);

self.mockFlutterViewController = OCMClassMock([UIViewController class]);

OCMStub([self.mockEngine viewController]).andReturn(_mockFlutterViewController);

self.api = OCMPartialMock([[PrivateViewApi alloc] initWithFlutterApi:self.mockFlutterApi registrar: self.mockRegistrar]);
}

- (void)tearDown {
[self.mockFlutterApi stopMocking];
[self.mockRegistrar stopMocking];
[self.mockFlutterViewController stopMocking];
[self.mockEngine stopMocking];

self.api = nil;

[super tearDown];
}

#pragma mark - Tests

- (void)testMask_Success {
XCTestExpectation *expectation = [self expectationWithDescription:@"Mask method success"];

CGSize imageSize = CGSizeMake(100, 100); // 100x100 pixels

// Step 2: Create the image using UIGraphicsImageRenderer
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageSize];

UIImage *screenshot = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
// Draw a red rectangle as an example
[[UIColor redColor] setFill];
CGRect rect = CGRectMake(0, 0, imageSize.width, imageSize.height);
UIRectFill(rect);
}];

NSArray<NSNumber *> *rectangles = @[@10, @20, @30, @40];
UIView *mockView = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 30, 40)];

OCMStub([self.mockFlutterApi getPrivateViewsWithCompletion:([OCMArg invokeBlockWithArgs:rectangles, [NSNull null], nil])]);



OCMStub([self.mockFlutterViewController view]).andReturn(mockView);


[self.api mask:screenshot completion:^(UIImage *result) {
XCTAssertNotNil(result, @"Masked image should be returned.");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testMask_Error {
XCTestExpectation *expectation = [self expectationWithDescription:@"Mask method with error"];

UIImage *screenshot = [UIImage new];
FlutterError *error = [FlutterError errorWithCode:@"ERROR" message:@"Test error" details:nil];

OCMStub([self.mockFlutterApi getPrivateViewsWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], error, nil])]);

[self.api mask:screenshot completion:^(UIImage *result) {
XCTAssertEqual(result, screenshot, @"Original screenshot should be returned on error.");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testGetFlutterViewOrigin_ValidView {
UIView *mockView = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 100, 100)];

OCMStub([self.mockFlutterViewController view]).andReturn(mockView);

UIWindow* testWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[testWindow addSubview:mockView];

CGPoint origin = [self.api getFlutterViewOrigin];

XCTAssertEqual(origin.x, 10);
XCTAssertEqual(origin.y, 20);
}

- (void)testGetFlutterViewOrigin_NilView {

OCMStub([self.mockFlutterViewController view]).andReturn(nil);
//
CGPoint origin = [self.api getFlutterViewOrigin];

XCTAssertEqual(origin.x, 0);
XCTAssertEqual(origin.y, 0);
}

- (void)testDrawMaskedImage {
CGSize size = CGSizeMake(100, 100);
UIGraphicsBeginImageContext(size);
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

NSArray<NSValue *> *privateViews = @[
[NSValue valueWithCGRect:CGRectMake(10, 10, 20, 20)],
[NSValue valueWithCGRect:CGRectMake(30, 30, 10, 10)]
];

UIImage *result = [self.api drawMaskedImage:screenshot withPrivateViews:privateViews];

XCTAssertNotNil(result);
XCTAssertEqual(result.size.width, 100);
XCTAssertEqual(result.size.height, 100);
}

- (void)testConvertToRectangles_ValidInput {
NSArray<NSNumber *> *rectangles = @[@10, @20, @30, @40];
UIView *mockView = [[UIView alloc] initWithFrame:CGRectMake(5, 5, 100, 100)];
OCMStub([self.mockFlutterViewController view]).andReturn(mockView);


NSArray<NSValue *> *converted = [self.api convertToRectangles:rectangles];

XCTAssertEqual(converted.count, 1);
CGRect rect = [converted[0] CGRectValue];
XCTAssertTrue(CGRectEqualToRect(rect, CGRectMake(10, 20, 21, 21)));
}

- (void)testConcurrentMaskCalls {
XCTestExpectation *expectation = [self expectationWithDescription:@"Handle concurrent calls"];

CGSize imageSize = CGSizeMake(100, 100); // 100x100 pixels

// Step 2: Create the image using UIGraphicsImageRenderer
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageSize];

UIImage *screenshot = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
// Draw a red rectangle as an example
[[UIColor redColor] setFill];
CGRect rect = CGRectMake(0, 0, imageSize.width, imageSize.height);
UIRectFill(rect);
}];

NSArray<NSNumber *> *rectangles = @[@10, @20, @30, @40];


OCMStub([self.mockFlutterApi getPrivateViewsWithCompletion:([OCMArg invokeBlockWithArgs:rectangles, [NSNull null], nil])]);


dispatch_group_t group = dispatch_group_create();

for (int i = 0; i < 5; i++) {
dispatch_group_enter(group);

[self.api mask:screenshot completion:^(UIImage *result) {
XCTAssertNotNil(result, @"Each call should return a valid image.");
dispatch_group_leave(group);
}];
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[expectation fulfill];
});

[self waitForExpectationsWithTimeout:2 handler:nil];
}

@end
2 changes: 2 additions & 0 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe

flutter_ios_podfile_setup
target 'Runner' do
pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/13.4.2/Instabug.podspec'

use_frameworks!
use_modular_headers!

Expand Down
8 changes: 5 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,28 @@ PODS:

DEPENDENCIES:
- Flutter (from `Flutter`)
- Instabug (from `https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/13.4.2/Instabug.podspec`)
- instabug_flutter (from `.symlinks/plugins/instabug_flutter/ios`)
- OCMock (= 3.6)

SPEC REPOS:
trunk:
- Instabug
- OCMock

EXTERNAL SOURCES:
Flutter:
:path: Flutter
Instabug:
:podspec: https://ios-releases.instabug.com/custom/feature-flutter-private-views-base/13.4.2/Instabug.podspec
instabug_flutter:
:path: ".symlinks/plugins/instabug_flutter/ios"

SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
Instabug: 7a71890217b97b1e32dbca96661845396b66da2f
Instabug: 7aacd5099c11ce96bc49dda40eba0963c06acccc
instabug_flutter: a2df87e3d4d9e410785e0b1ffef4bc64d1f4b787
OCMock: 5ea90566be239f179ba766fd9fbae5885040b992

PODFILE CHECKSUM: 8f7552fd115ace1988c3db54a69e4a123c448f84
PODFILE CHECKSUM: f2e19aef9f983becf80950af8e2d9c1b8f57e7a2

COCOAPODS: 1.14.3
16 changes: 14 additions & 2 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9D381ECFBB01BD0E978EBDF2 /* Pods_InstabugTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71679BEC094CFF3474195C2E /* Pods_InstabugTests.framework */; };
BEF638212CC82C7C004D29E9 /* PrivateViewApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BEF638202CC82C7C004D29E9 /* PrivateViewApiTests.m */; };
BEF638292CCA5E2B004D29E9 /* Pods_InstabugTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71679BEC094CFF3474195C2E /* Pods_InstabugTests.framework */; };
CC080E112937B7DB0041170A /* InstabugApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC080E102937B7DB0041170A /* InstabugApiTests.m */; };
CC198C61293E1A21007077C8 /* SurveysApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC198C60293E1A21007077C8 /* SurveysApiTests.m */; };
CC359DB92937720C0067A924 /* ApmApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC359DB82937720C0067A924 /* ApmApiTests.m */; };
Expand All @@ -26,6 +27,7 @@
CC9925D7293DFB03001FD3EE /* InstabugLogApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9925D6293DFB03001FD3EE /* InstabugLogApiTests.m */; };
CC9925D9293DFD7F001FD3EE /* RepliesApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9925D8293DFD7F001FD3EE /* RepliesApiTests.m */; };
CCADBDD8293CFED300AE5EB8 /* BugReportingApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCADBDD7293CFED300AE5EB8 /* BugReportingApiTests.m */; };
EDD1293B2F5742BC05EDD9F6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEF6382E2CCA6D7D004D29E9 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -78,6 +80,9 @@
B03C8370EEFE061BDDDA1DA1 /* Pods-InstabugUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugUITests.debug.xcconfig"; path = "Target Support Files/Pods-InstabugUITests/Pods-InstabugUITests.debug.xcconfig"; sourceTree = "<group>"; };
BA5633844585BB93FE7BCCE7 /* Pods-InstabugTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugTests.profile.xcconfig"; path = "Target Support Files/Pods-InstabugTests/Pods-InstabugTests.profile.xcconfig"; sourceTree = "<group>"; };
BE26C80C2BD55575009FECCF /* IBGCrashReporting+CP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IBGCrashReporting+CP.h"; sourceTree = "<group>"; };
BEF638202CC82C7C004D29E9 /* PrivateViewApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PrivateViewApiTests.m; sourceTree = "<group>"; };
BEF6382C2CCA6176004D29E9 /* instabug_flutter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = instabug_flutter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BEF6382E2CCA6D7D004D29E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF9025BBD0A6FD7B193E903A /* Pods-InstabugTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugTests.debug.xcconfig"; path = "Target Support Files/Pods-InstabugTests/Pods-InstabugTests.debug.xcconfig"; sourceTree = "<group>"; };
C090017925D9A030006F3DAE /* InstabugTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstabugTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C090017D25D9A031006F3DAE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -102,14 +107,15 @@
buildActionMask = 2147483647;
files = (
65C88E6E8EAE049E32FF2F52 /* Pods_Runner.framework in Frameworks */,
EDD1293B2F5742BC05EDD9F6 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C090017625D9A030006F3DAE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9D381ECFBB01BD0E978EBDF2 /* Pods_InstabugTests.framework in Frameworks */,
BEF638292CCA5E2B004D29E9 /* Pods_InstabugTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -135,6 +141,8 @@
54C1C903B090526284242B67 /* Frameworks */ = {
isa = PBXGroup;
children = (
BEF6382E2CCA6D7D004D29E9 /* Pods_Runner.framework */,
BEF6382C2CCA6176004D29E9 /* instabug_flutter.framework */,
853739F5879F6E4272829F47 /* Pods_Runner.framework */,
71679BEC094CFF3474195C2E /* Pods_InstabugTests.framework */,
F5446C0D3B2623D9BCC7CCE3 /* Pods_InstabugUITests.framework */,
Expand Down Expand Up @@ -194,6 +202,7 @@
C090017A25D9A031006F3DAE /* InstabugTests */ = {
isa = PBXGroup;
children = (
BEF638202CC82C7C004D29E9 /* PrivateViewApiTests.m */,
CC198C60293E1A21007077C8 /* SurveysApiTests.m */,
CC78720A2938D1C5008CB2A5 /* Util */,
CC080E102937B7DB0041170A /* InstabugApiTests.m */,
Expand Down Expand Up @@ -457,6 +466,7 @@
CC080E112937B7DB0041170A /* InstabugApiTests.m in Sources */,
CC198C61293E1A21007077C8 /* SurveysApiTests.m in Sources */,
CCADBDD8293CFED300AE5EB8 /* BugReportingApiTests.m in Sources */,
BEF638212CC82C7C004D29E9 /* PrivateViewApiTests.m in Sources */,
CC9925D9293DFD7F001FD3EE /* RepliesApiTests.m in Sources */,
206286ED2ABD0A1F00925509 /* SessionReplayApiTests.m in Sources */,
CC9925D2293DEB0B001FD3EE /* CrashReportingApiTests.m in Sources */,
Expand Down Expand Up @@ -802,6 +812,7 @@
"@loader_path/Frameworks",
);
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.instabug.InstabugTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "InstabugTests/InstabugTests-Bridging-Header.h";
Expand Down Expand Up @@ -835,6 +846,7 @@
"@loader_path/Frameworks",
);
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.instabug.InstabugTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "InstabugTests/InstabugTests-Bridging-Header.h";
Expand Down
6 changes: 5 additions & 1 deletion ios/Classes/InstabugFlutterPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "RepliesApi.h"
#import "SessionReplayApi.h"
#import "SurveysApi.h"
#import "PrivateViewApi.h"

@implementation InstabugFlutterPlugin

Expand All @@ -17,11 +18,14 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
InitBugReportingApi([registrar messenger]);
InitCrashReportingApi([registrar messenger]);
InitFeatureRequestsApi([registrar messenger]);
InitInstabugApi([registrar messenger]);
PrivateViewApi* privateViewApi = InitPrivateViewApi([registrar messenger],registrar);
InitInstabugApi([registrar messenger],privateViewApi);
InitInstabugLogApi([registrar messenger]);
InitRepliesApi([registrar messenger]);
InitSessionReplayApi([registrar messenger]);
InitSurveysApi([registrar messenger]);


}

@end
4 changes: 3 additions & 1 deletion ios/Classes/Modules/InstabugApi.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#import "InstabugPigeon.h"
#import "PrivateViewApi.h"

extern void InitInstabugApi(id<FlutterBinaryMessenger> messenger);
extern void InitInstabugApi(id<FlutterBinaryMessenger> _Nonnull messenger, PrivateViewApi * _Nonnull api);

@interface InstabugApi : NSObject <InstabugHostApi>
@property (nonatomic, strong) PrivateViewApi* _Nonnull privateViewApi;

- (UIImage *)getImageForAsset:(NSString *)assetName;
- (UIFont *)getFontForAsset:(NSString *)assetName error:(FlutterError *_Nullable *_Nonnull)error;
Expand Down
12 changes: 11 additions & 1 deletion ios/Classes/Modules/InstabugApi.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
#import "IBGNetworkLogger+CP.h"
#import "InstabugApi.h"
#import "ArgsRegistry.h"
#import "../Util/Instabug+CP.h"

#define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16)) / 255.0 green:((float)((rgbValue & 0xFF00) >> 8)) / 255.0 blue:((float)(rgbValue & 0xFF)) / 255.0 alpha:((float)((rgbValue & 0xFF000000) >> 24)) / 255.0];

extern void InitInstabugApi(id<FlutterBinaryMessenger> messenger) {
extern void InitInstabugApi(id<FlutterBinaryMessenger> _Nonnull messenger, PrivateViewApi * _Nonnull privateViewApi) {
InstabugApi *api = [[InstabugApi alloc] init];
api.privateViewApi = privateViewApi;
InstabugHostApiSetup(messenger, api);
}

Expand Down Expand Up @@ -53,6 +55,14 @@ - (void)initToken:(NSString *)token invocationEvents:(NSArray<NSString *> *)invo

[Instabug setSdkDebugLogsLevel:resolvedLogLevel];
[Instabug startWithToken:token invocationEvents:resolvedEvents];

[Instabug setScreenshotMaskingHandler:^(UIImage * _Nonnull screenshot, void (^ _Nonnull completion)(UIImage * _Nullable)) {
[self.privateViewApi mask:screenshot completion:^(UIImage * _Nonnull maskedImage) {
if (maskedImage != nil) {
completion(maskedImage);
}
}];
}];
}

- (void)showWithError:(FlutterError *_Nullable *_Nonnull)error {
Expand Down
Loading