FIRIAMDisplayExecutorTests.m 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <OCMock/OCMock.h>
  17. #import <XCTest/XCTest.h>
  18. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentData.h"
  19. #import "FirebaseInAppMessaging/Sources/Private/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h"
  20. #import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMDisplayExecutor.h"
  21. #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMActionURLFollower.h"
  22. #import "FirebaseInAppMessaging/Sources/Public/FirebaseInAppMessaging/FIRInAppMessaging.h"
  23. #import "FirebaseInAppMessaging/Sources/RenderingObjects/FIRInAppMessagingRenderingPrivate.h"
  24. #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
  25. // A class implementing protocol FIRIAMMessageContentData to be used for unit testing
  26. @interface FIRIAMMessageContentDataForTesting : NSObject <FIRIAMMessageContentData>
  27. @property(nonatomic, readwrite, nonnull) NSString *titleText;
  28. @property(nonatomic, readwrite, nonnull) NSString *bodyText;
  29. @property(nonatomic, nullable) NSString *actionButtonText;
  30. @property(nonatomic, nullable) NSString *secondaryActionButtonText;
  31. @property(nonatomic, nullable) NSURL *actionURL;
  32. @property(nonatomic, nullable) NSURL *secondaryActionURL;
  33. @property(nonatomic, nullable) NSURL *imageURL;
  34. @property(nonatomic, nullable) NSURL *landscapeImageURL;
  35. @property BOOL errorEncountered;
  36. @property BOOL loadImagesAsynchronously;
  37. - (instancetype)initWithMessageTitle:(NSString *)title
  38. messageBody:(NSString *)body
  39. actionButtonText:(nullable NSString *)actionButtonText
  40. secondaryActionButtonText:(nullable NSString *)secondaryActionButtonText
  41. actionURL:(nullable NSURL *)actionURL
  42. secondaryActionURL:(nullable NSURL *)secondaryActionURL
  43. imageURL:(nullable NSURL *)imageURL
  44. landscapeImageURL:(nullable NSURL *)landscapeImageURL
  45. hasImageError:(BOOL)hasImageError
  46. loadImagesAsynchronously:(BOOL)loadImagesAsynchronously;
  47. @end
  48. @implementation FIRIAMMessageContentDataForTesting
  49. - (instancetype)initWithMessageTitle:(NSString *)title
  50. messageBody:(NSString *)body
  51. actionButtonText:(nullable NSString *)actionButtonText
  52. secondaryActionButtonText:(nullable NSString *)secondaryActionButtonText
  53. actionURL:(nullable NSURL *)actionURL
  54. secondaryActionURL:(nullable NSURL *)secondaryActionURL
  55. imageURL:(nullable NSURL *)imageURL
  56. landscapeImageURL:(nullable NSURL *)landscapeImageURL
  57. hasImageError:(BOOL)hasImageError
  58. loadImagesAsynchronously:(BOOL)loadImagesAsynchronously {
  59. if (self = [super init]) {
  60. _titleText = title;
  61. _bodyText = body;
  62. _imageURL = imageURL;
  63. _landscapeImageURL = landscapeImageURL;
  64. _actionButtonText = actionButtonText;
  65. _secondaryActionButtonText = secondaryActionButtonText;
  66. _actionURL = actionURL;
  67. _secondaryActionURL = secondaryActionURL;
  68. _errorEncountered = hasImageError;
  69. _loadImagesAsynchronously = loadImagesAsynchronously;
  70. }
  71. return self;
  72. }
  73. - (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData,
  74. NSData *_Nullable landscapeImageData,
  75. NSError *_Nullable error))block {
  76. if (self.errorEncountered) {
  77. NSError *error = [NSError errorWithDomain:@"image error" code:0 userInfo:nil];
  78. if (_loadImagesAsynchronously) {
  79. [self performOnMainQueueAfterDelay:0.01
  80. block:^{
  81. block(nil, nil, error);
  82. }];
  83. } else {
  84. block(nil, nil, error);
  85. }
  86. } else {
  87. NSData *imageData = [@"image data" dataUsingEncoding:NSUTF8StringEncoding];
  88. NSData *landscapeImageData = [@"landscape image data" dataUsingEncoding:NSUTF8StringEncoding];
  89. if (_loadImagesAsynchronously) {
  90. [self performOnMainQueueAfterDelay:0.01
  91. block:^{
  92. block(imageData, landscapeImageData, nil);
  93. }];
  94. } else {
  95. block(imageData, landscapeImageData, nil);
  96. }
  97. }
  98. }
  99. - (void)performOnMainQueueAfterDelay:(NSTimeInterval)delay block:(void (^)(void))block {
  100. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), dispatch_get_main_queue(),
  101. ^{
  102. block();
  103. });
  104. }
  105. @end
  106. // Defines how the message display component triggers the delegate in unit testing.
  107. typedef NS_ENUM(NSInteger, FIRInAppMessagingDelegateInteraction) {
  108. // Message display component triggers messageDismissedWithType:.
  109. FIRInAppMessagingDelegateInteractionDismiss,
  110. // Message display component triggers messageClicked:.
  111. FIRInAppMessagingDelegateInteractionClick,
  112. // Message display component triggers displayErrorEncountered:.
  113. FIRInAppMessagingDelegateInteractionError,
  114. // Message has finished a valid impression, but it's not getting closed by the user.
  115. FIRInAppMessagingDelegateInteractionImpressionDetected,
  116. };
  117. // A class implementing protocol FIRInAppMessagingDisplay to be used for unit testing
  118. @interface FIRIAMMessageDisplayForTesting : NSObject <FIRInAppMessagingDisplay>
  119. @property FIRInAppMessagingDelegateInteraction delegateInteraction;
  120. @property(nonatomic, nullable, copy) FIRInAppMessagingAction *action;
  121. // used for interaction verification
  122. @property FIRInAppMessagingDisplayMessage *message;
  123. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction
  124. action:(nullable FIRInAppMessagingAction *)actionURL;
  125. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction;
  126. @end
  127. @implementation FIRIAMMessageDisplayForTesting
  128. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction
  129. action:(nullable FIRInAppMessagingAction *)action {
  130. if (self = [super init]) {
  131. _delegateInteraction = interaction;
  132. _action = action;
  133. }
  134. return self;
  135. }
  136. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction {
  137. return [self initWithDelegateInteraction:interaction action:nil];
  138. }
  139. - (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay
  140. displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {
  141. self.message = messageForDisplay;
  142. switch (self.delegateInteraction) {
  143. case FIRInAppMessagingDelegateInteractionClick:
  144. [displayDelegate messageClicked:messageForDisplay withAction:self.action];
  145. break;
  146. case FIRInAppMessagingDelegateInteractionDismiss:
  147. [displayDelegate messageDismissed:messageForDisplay
  148. dismissType:FIRInAppMessagingDismissTypeAuto];
  149. break;
  150. case FIRInAppMessagingDelegateInteractionError:
  151. [displayDelegate displayErrorForMessage:messageForDisplay
  152. error:[NSError errorWithDomain:NSURLErrorDomain
  153. code:0
  154. userInfo:nil]];
  155. break;
  156. case FIRInAppMessagingDelegateInteractionImpressionDetected:
  157. [displayDelegate impressionDetectedForMessage:messageForDisplay];
  158. break;
  159. }
  160. }
  161. @end
  162. @interface FIRInAppMessagingDisplayTestDelegate : NSObject <FIRInAppMessagingDisplayDelegate>
  163. @property(nonatomic) BOOL receivedMessageErrorCallback;
  164. @property(nonatomic) BOOL receivedMessageImpressionCallback;
  165. @property(nonatomic) BOOL receivedMessageClickedCallback;
  166. @property(nonatomic) BOOL receivedMessageDismissedCallback;
  167. @end
  168. @implementation FIRInAppMessagingDisplayTestDelegate
  169. - (void)displayErrorForMessage:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage
  170. error:(nonnull NSError *)error {
  171. self.receivedMessageErrorCallback = YES;
  172. }
  173. - (void)impressionDetectedForMessage:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage {
  174. self.receivedMessageImpressionCallback = YES;
  175. }
  176. - (void)messageClicked:(FIRInAppMessagingDisplayMessage *)inAppMessage
  177. withAction:(FIRInAppMessagingAction *)action {
  178. self.receivedMessageClickedCallback = YES;
  179. }
  180. - (void)messageDismissed:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage
  181. dismissType:(FIRInAppMessagingDismissType)dismissType {
  182. self.receivedMessageDismissedCallback = YES;
  183. }
  184. @end
  185. @interface FIRIAMDisplayExecutor (Testing)
  186. - (FIRInAppMessagingDisplayMessage *)
  187. displayMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
  188. imageData:(FIRInAppMessagingImageData *)imageData
  189. landscapeImageData:(nullable FIRInAppMessagingImageData *)landscapeImageData
  190. triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType;
  191. - (BOOL)shouldTrackConversionsOnImpressionForCurrentInAppMessage:
  192. (FIRIAMMessageDefinition *)inAppMessage;
  193. @end
  194. @interface FIRIAMDisplayExecutorTests : XCTestCase
  195. @property(nonatomic) FIRIAMDisplaySetting *displaySetting;
  196. @property FIRIAMMessageClientCache *clientMessageCache;
  197. @property id<FIRIAMBookKeeper> mockBookkeeper;
  198. @property id<FIRIAMTimeFetcher> mockTimeFetcher;
  199. @property FIRIAMDisplayExecutor *displayExecutor;
  200. @property FIRIAMActivityLogger *mockActivityLogger;
  201. @property FIRInAppMessaging *mockInAppMessaging;
  202. @property id<FIRIAMAnalyticsEventLogger> mockAnalyticsEventLogger;
  203. @property FIRIAMActionURLFollower *mockActionURLFollower;
  204. @property id<FIRInAppMessagingDisplay> mockMessageDisplayComponent;
  205. // Pre-defined messages
  206. @property FIRIAMMessageDefinition *m1, *m2, *m3, *m4, *m5, *m6;
  207. @end
  208. @implementation FIRIAMDisplayExecutorTests
  209. - (void)setupMessageTexture {
  210. // startTime, endTime here ensures messages with them are active
  211. NSTimeInterval activeStartTime = 0;
  212. NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000;
  213. // m1 & m3 will be of contextual trigger
  214. FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition =
  215. [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"];
  216. // m2, m4, m5, and m6 will be of app open trigger
  217. FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition =
  218. [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger];
  219. FIRIAMMessageContentDataForTesting *m1ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  220. initWithMessageTitle:@"m1 title"
  221. messageBody:@"message body"
  222. actionButtonText:nil
  223. secondaryActionButtonText:nil
  224. actionURL:[NSURL URLWithString:@"http://google.com"]
  225. secondaryActionURL:nil
  226. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  227. landscapeImageURL:nil
  228. hasImageError:NO
  229. loadImagesAsynchronously:NO];
  230. FIRIAMRenderingEffectSetting *renderSetting1 =
  231. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  232. renderSetting1.viewMode = FIRIAMRenderAsBannerView;
  233. FIRIAMMessageRenderData *renderData1 =
  234. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1"
  235. messageName:@"name"
  236. contentData:m1ContentData
  237. renderingEffect:renderSetting1];
  238. self.m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1
  239. startTime:activeStartTime
  240. endTime:activeEndTime
  241. triggerDefinition:@[ contextualTriggerDefinition ]];
  242. FIRIAMMessageContentDataForTesting *m2ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  243. initWithMessageTitle:@"m2 title"
  244. messageBody:@"message body"
  245. actionButtonText:nil
  246. secondaryActionButtonText:nil
  247. actionURL:[NSURL URLWithString:@"http://google.com"]
  248. secondaryActionURL:nil
  249. imageURL:[NSURL URLWithString:@"https://unsplash.it/300/400"]
  250. landscapeImageURL:nil
  251. hasImageError:NO
  252. loadImagesAsynchronously:NO];
  253. FIRIAMRenderingEffectSetting *renderSetting2 =
  254. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  255. renderSetting2.viewMode = FIRIAMRenderAsModalView;
  256. FIRIAMMessageRenderData *renderData2 =
  257. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2"
  258. messageName:@"name"
  259. contentData:m2ContentData
  260. renderingEffect:renderSetting2];
  261. self.m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2
  262. startTime:activeStartTime
  263. endTime:activeEndTime
  264. triggerDefinition:@[ appOpentriggerDefinition ]];
  265. FIRIAMMessageContentDataForTesting *m3ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  266. initWithMessageTitle:@"m3 title"
  267. messageBody:@"message body"
  268. actionButtonText:nil
  269. secondaryActionButtonText:nil
  270. actionURL:[NSURL URLWithString:@"http://google.com"]
  271. secondaryActionURL:nil
  272. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  273. landscapeImageURL:nil
  274. hasImageError:NO
  275. loadImagesAsynchronously:NO];
  276. FIRIAMRenderingEffectSetting *renderSetting3 =
  277. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  278. renderSetting3.viewMode = FIRIAMRenderAsImageOnlyView;
  279. FIRIAMMessageRenderData *renderData3 =
  280. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3"
  281. messageName:@"name"
  282. contentData:m3ContentData
  283. renderingEffect:renderSetting3];
  284. self.m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3
  285. startTime:activeStartTime
  286. endTime:activeEndTime
  287. triggerDefinition:@[ contextualTriggerDefinition ]];
  288. FIRIAMMessageContentDataForTesting *m4ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  289. initWithMessageTitle:@"m4 title"
  290. messageBody:@"message body"
  291. actionButtonText:nil
  292. secondaryActionButtonText:nil
  293. actionURL:[NSURL URLWithString:@"http://google.com"]
  294. secondaryActionURL:nil
  295. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  296. landscapeImageURL:nil
  297. hasImageError:NO
  298. loadImagesAsynchronously:NO];
  299. FIRIAMRenderingEffectSetting *renderSetting4 =
  300. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  301. renderSetting4.viewMode = FIRIAMRenderAsImageOnlyView;
  302. FIRIAMMessageRenderData *renderData4 =
  303. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m4"
  304. messageName:@"name"
  305. contentData:m4ContentData
  306. renderingEffect:renderSetting4];
  307. NSDictionary *experimentPayloadDictionary = @{
  308. @"experimentId" : @"_exp_1",
  309. @"experimentStartTimeMillis" : @1582143484729,
  310. @"overflowPolicy" : @"DISCARD_OLDEST",
  311. @"timeToLiveMillis" : @15552000000,
  312. @"triggerTimeoutMillis" : @15552000000,
  313. @"variantId" : @"1"
  314. };
  315. ABTExperimentPayload *experimentPayload =
  316. [[ABTExperimentPayload alloc] initWithDictionary:experimentPayloadDictionary];
  317. self.m4 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData4
  318. startTime:activeStartTime
  319. endTime:activeEndTime
  320. triggerDefinition:@[ appOpentriggerDefinition ]
  321. appData:@{@"a" : @"b", @"up" : @"dog"}
  322. experimentPayload:experimentPayload
  323. isTestMessage:NO];
  324. FIRIAMMessageContentDataForTesting *m5ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  325. initWithMessageTitle:nil
  326. messageBody:nil
  327. actionButtonText:nil
  328. secondaryActionButtonText:nil
  329. actionURL:nil
  330. secondaryActionURL:nil
  331. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  332. landscapeImageURL:nil
  333. hasImageError:NO
  334. loadImagesAsynchronously:NO];
  335. FIRIAMRenderingEffectSetting *renderSetting5 =
  336. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  337. renderSetting5.viewMode = FIRIAMRenderAsImageOnlyView;
  338. FIRIAMMessageRenderData *renderData5 =
  339. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m5"
  340. messageName:@"name"
  341. contentData:m5ContentData
  342. renderingEffect:renderSetting5];
  343. self.m5 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData5
  344. startTime:activeStartTime
  345. endTime:activeEndTime
  346. triggerDefinition:@[ appOpentriggerDefinition ]
  347. appData:nil
  348. experimentPayload:nil
  349. isTestMessage:NO];
  350. FIRIAMMessageContentDataForTesting *m6ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  351. initWithMessageTitle:@"m6 title"
  352. messageBody:@"message body"
  353. actionButtonText:nil
  354. secondaryActionButtonText:nil
  355. actionURL:[NSURL URLWithString:@"http://google.com"]
  356. secondaryActionURL:nil
  357. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  358. landscapeImageURL:nil
  359. hasImageError:NO
  360. loadImagesAsynchronously:YES];
  361. FIRIAMRenderingEffectSetting *renderSetting6 =
  362. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  363. renderSetting6.viewMode = FIRIAMRenderAsBannerView;
  364. FIRIAMMessageRenderData *renderData6 =
  365. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m6"
  366. messageName:@"name"
  367. contentData:m6ContentData
  368. renderingEffect:renderSetting6];
  369. self.m6 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData6
  370. startTime:activeStartTime
  371. endTime:activeEndTime
  372. triggerDefinition:@[ appOpentriggerDefinition ]];
  373. }
  374. NSTimeInterval DISPLAY_MIN_INTERVALS = 1;
  375. - (void)setUp {
  376. [super setUp];
  377. [self setupMessageTexture];
  378. self.displaySetting = [[FIRIAMDisplaySetting alloc] init];
  379. self.displaySetting.displayMinIntervalInMinutes = DISPLAY_MIN_INTERVALS;
  380. self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper));
  381. FIRIAMFetchResponseParser *parser =
  382. [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]];
  383. self.clientMessageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper
  384. usingResponseParser:parser];
  385. self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher));
  386. self.mockActivityLogger = OCMClassMock([FIRIAMActivityLogger class]);
  387. self.mockAnalyticsEventLogger = OCMProtocolMock(@protocol(FIRIAMAnalyticsEventLogger));
  388. self.mockInAppMessaging = OCMClassMock([FIRInAppMessaging class]);
  389. self.mockActionURLFollower = OCMClassMock([FIRIAMActionURLFollower class]);
  390. self.displayExecutor =
  391. [[FIRIAMDisplayExecutor alloc] initWithInAppMessaging:self.mockInAppMessaging
  392. setting:self.displaySetting
  393. messageCache:self.clientMessageCache
  394. timeFetcher:self.mockTimeFetcher
  395. bookKeeper:self.mockBookkeeper
  396. actionURLFollower:self.mockActionURLFollower
  397. activityLogger:self.mockActivityLogger
  398. analyticsEventLogger:self.mockAnalyticsEventLogger];
  399. OCMStub([self.mockBookkeeper recordNewImpressionForMessage:[OCMArg any]
  400. withStartTimestampInSeconds:1000]);
  401. }
  402. - (void)testRegularMessageAvailableCase {
  403. // This setup allows next message to be displayed from display interval perspective.
  404. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  405. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  406. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  407. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  408. self.displayExecutor.messageDisplayComponent = display;
  409. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  410. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  411. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  412. XCTAssertEqual(1, remainingMsgCount);
  413. // Verify that the message content handed to display component is expected
  414. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  415. }
  416. - (void)testFollowingActionURL {
  417. // This setup allows next message to be displayed from display interval perspective.
  418. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  419. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  420. FIRInAppMessagingAction *testAction =
  421. [[FIRInAppMessagingAction alloc] initWithActionText:@"test"
  422. actionURL:self.m2.renderData.contentData.actionURL];
  423. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  424. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  425. action:testAction];
  426. self.displayExecutor.messageDisplayComponent = display;
  427. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  428. // not expecting triggering analytics recording
  429. OCMExpect([self.mockActionURLFollower
  430. followActionURL:[OCMArg isEqual:self.m2.renderData.contentData.actionURL]
  431. withCompletionBlock:[OCMArg any]]);
  432. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  433. OCMVerifyAll((id)self.mockActionURLFollower);
  434. }
  435. - (void)testFollowingActionURLForTestMessage {
  436. // This setup allows next message to be displayed from display interval perspective.
  437. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  438. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  439. FIRIAMMessageDefinition *testMessage =
  440. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData
  441. experimentPayload:nil];
  442. FIRInAppMessagingAction *testAction = [[FIRInAppMessagingAction alloc]
  443. initWithActionText:@"test"
  444. actionURL:testMessage.renderData.contentData.actionURL];
  445. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  446. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  447. action:testAction];
  448. self.displayExecutor.messageDisplayComponent = display;
  449. [self.clientMessageCache setMessageData:@[ testMessage ]];
  450. // not expecting triggering analytics recording
  451. OCMExpect([self.mockActionURLFollower
  452. followActionURL:[OCMArg isEqual:testMessage.renderData.contentData.actionURL]
  453. withCompletionBlock:[OCMArg any]]);
  454. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  455. OCMVerifyAll((id)self.mockActionURLFollower);
  456. }
  457. - (void)testClientTestMessageAvailableCase {
  458. // When test message is present in cache, even if the display time interval has not been
  459. // reached, we still render.
  460. // 10 seconds is less than DISPLAY_MIN_INTERVALS minutes, so we have not reached
  461. // minimal display time interval yet.
  462. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10);
  463. FIRIAMMessageDefinition *testMessage =
  464. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData
  465. experimentPayload:nil];
  466. [self.clientMessageCache setMessageData:@[ self.m2, testMessage, self.m4 ]];
  467. // We have test message in the cache now.
  468. XCTAssertTrue([self.clientMessageCache hasTestMessage]);
  469. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  470. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  471. self.displayExecutor.messageDisplayComponent = display;
  472. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  473. // No more test message in the cache now.
  474. XCTAssertFalse([self.clientMessageCache hasTestMessage]);
  475. }
  476. // If a message is still being displayed, we won't try to display a second one on top of it
  477. - (void)testNoDualDisplay {
  478. // This setup allows next message to be displayed from display interval perspective.
  479. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  480. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  481. // This display component only detects a valid impression, but does not end the rendering
  482. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  483. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  484. self.displayExecutor.messageDisplayComponent = display;
  485. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  486. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  487. // m2 is being rendered
  488. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  489. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  490. XCTAssertEqual(1, remainingMsgCount);
  491. // try to display again when the in-display flag is already turned on (and not turned off yet)
  492. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  493. // Verify that the message in display component is still m2
  494. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  495. // message in cache remain unchanged for the second checkAndDisplayNext call
  496. remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  497. XCTAssertEqual(1, remainingMsgCount);
  498. }
  499. // this test case contracts testNoAnalyticsTrackingOnTestMessage to cover both positive
  500. // and negative cases
  501. - (void)testDoesAnalyticsTrackingOnNonTestMessage {
  502. // This setup allows next message to be displayed from display interval perspective.
  503. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  504. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  505. // not expecting triggering analytics recording
  506. OCMExpect([self.mockAnalyticsEventLogger
  507. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  508. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  509. withCampaignName:[OCMArg any]
  510. eventTimeInMs:[OCMArg any]
  511. completion:[OCMArg any]]);
  512. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  513. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  514. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  515. self.displayExecutor.messageDisplayComponent = display;
  516. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  517. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  518. }
  519. - (void)testDoesAnalyticsTrackingOnDisplayError {
  520. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  521. // last display time is set to 0 by default
  522. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  523. // not expecting triggering analytics recording
  524. OCMExpect([self.mockAnalyticsEventLogger
  525. logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError
  526. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  527. withCampaignName:[OCMArg any]
  528. eventTimeInMs:[OCMArg any]
  529. completion:[OCMArg any]]);
  530. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  531. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  532. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError];
  533. self.displayExecutor.messageDisplayComponent = display;
  534. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  535. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  536. }
  537. - (void)testAnalyticsTrackingOnMessageDismissCase {
  538. // This setup allows next message to be displayed from display interval perspective.
  539. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  540. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  541. // not expecting triggering analytics recording
  542. OCMExpect([self.mockAnalyticsEventLogger
  543. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageDismissAuto
  544. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  545. withCampaignName:[OCMArg any]
  546. eventTimeInMs:[OCMArg any]
  547. completion:[OCMArg any]]);
  548. // Make sure we don't log the url follow event.
  549. OCMReject([self.mockAnalyticsEventLogger
  550. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  551. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  552. withCampaignName:[OCMArg any]
  553. eventTimeInMs:[OCMArg any]
  554. completion:[OCMArg any]]);
  555. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  556. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  557. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  558. self.displayExecutor.messageDisplayComponent = display;
  559. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  560. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  561. }
  562. - (void)testAnalyticsTrackingOnMessageClickCase {
  563. // This setup allows next message to be displayed from display interval perspective.
  564. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  565. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  566. // We expect two analytics events for a click action:
  567. // An impression event and an action URL follow event
  568. OCMExpect([self.mockAnalyticsEventLogger
  569. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  570. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  571. withCampaignName:[OCMArg any]
  572. eventTimeInMs:[OCMArg any]
  573. completion:[OCMArg any]]);
  574. OCMExpect([self.mockAnalyticsEventLogger
  575. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  576. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  577. withCampaignName:[OCMArg any]
  578. eventTimeInMs:[OCMArg any]
  579. completion:[OCMArg any]]);
  580. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  581. FIRInAppMessagingAction *m2Action = [[FIRInAppMessagingAction alloc]
  582. initWithActionText:self.m2.renderData.contentData.actionButtonText
  583. actionURL:self.m2.renderData.contentData.actionURL];
  584. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  585. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  586. action:m2Action];
  587. self.displayExecutor.messageDisplayComponent = display;
  588. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  589. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  590. }
  591. - (void)testAnalyticsTrackingOnMessageClickCaseWithNoActionURL {
  592. // This setup allows next message to be displayed from display interval perspective.
  593. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  594. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  595. // We expect two analytics events for a click action:
  596. // An impression event and an action URL follow event
  597. OCMExpect([self.mockAnalyticsEventLogger
  598. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  599. forCampaignID:[OCMArg isEqual:self.m5.renderData.messageID]
  600. withCampaignName:[OCMArg any]
  601. eventTimeInMs:[OCMArg any]
  602. completion:[OCMArg any]]);
  603. [self.clientMessageCache setMessageData:@[ self.m5 ]];
  604. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  605. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  606. action:nil];
  607. self.displayExecutor.messageDisplayComponent = display;
  608. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  609. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  610. }
  611. - (void)testAnalyticsTrackingOnTestMessageClickCase {
  612. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  613. // last display time is set to 0 by default
  614. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  615. // We expect two analytics events for a click action:
  616. // An test message impression event and a test message click event
  617. OCMExpect([self.mockAnalyticsEventLogger
  618. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  619. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  620. withCampaignName:[OCMArg any]
  621. eventTimeInMs:[OCMArg any]
  622. completion:[OCMArg any]]);
  623. OCMExpect([self.mockAnalyticsEventLogger
  624. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  625. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  626. withCampaignName:[OCMArg any]
  627. eventTimeInMs:[OCMArg any]
  628. completion:[OCMArg any]]);
  629. FIRIAMMessageDefinition *testMessage =
  630. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData
  631. experimentPayload:nil];
  632. [self.clientMessageCache setMessageData:@[ testMessage ]];
  633. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  634. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  635. self.displayExecutor.messageDisplayComponent = display;
  636. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  637. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  638. }
  639. - (void)testAnalyticsTrackingOnTestMessageDismissCase {
  640. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  641. // last display time is set to 0 by default
  642. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  643. // We expect a test message impression
  644. OCMExpect([self.mockAnalyticsEventLogger
  645. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  646. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  647. withCampaignName:[OCMArg any]
  648. eventTimeInMs:[OCMArg any]
  649. completion:[OCMArg any]]);
  650. // No click event
  651. OCMReject([self.mockAnalyticsEventLogger
  652. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  653. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  654. withCampaignName:[OCMArg any]
  655. eventTimeInMs:[OCMArg any]
  656. completion:[OCMArg any]]);
  657. FIRIAMMessageDefinition *testMessage =
  658. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData
  659. experimentPayload:nil];
  660. [self.clientMessageCache setMessageData:@[ testMessage ]];
  661. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  662. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  663. self.displayExecutor.messageDisplayComponent = display;
  664. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  665. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  666. }
  667. - (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCaseWithActionURL {
  668. // This setup allows next message to be displayed from display interval perspective.
  669. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  670. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  671. // not expecting triggering analytics recording
  672. OCMExpect([self.mockAnalyticsEventLogger
  673. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  674. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  675. withCampaignName:[OCMArg any]
  676. eventTimeInMs:[OCMArg any]
  677. completion:[OCMArg any]]);
  678. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  679. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  680. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  681. self.displayExecutor.messageDisplayComponent = display;
  682. // M2 has an action URL. Conversion shouldn't be tracked yet.
  683. XCTAssertFalse(
  684. [self.displayExecutor shouldTrackConversionsOnImpressionForCurrentInAppMessage:self.m2]);
  685. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  686. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  687. }
  688. - (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCaseWithoutActionURL {
  689. // This setup allows next message to be displayed from display interval perspective.
  690. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  691. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  692. // not expecting triggering analytics recording
  693. OCMExpect([self.mockAnalyticsEventLogger
  694. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  695. forCampaignID:[OCMArg isEqual:self.m5.renderData.messageID]
  696. withCampaignName:[OCMArg any]
  697. eventTimeInMs:[OCMArg any]
  698. completion:[OCMArg any]]);
  699. [self.clientMessageCache setMessageData:@[ self.m5 ]];
  700. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  701. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  702. self.displayExecutor.messageDisplayComponent = display;
  703. // M5 has no action URL. Conversion should be tracked after impression.
  704. OCMExpect([self.mockAnalyticsEventLogger
  705. logConversionTrackingEventForCampaignID:[OCMArg isEqual:self.m5.renderData.messageID]]);
  706. XCTAssertTrue(
  707. [self.displayExecutor shouldTrackConversionsOnImpressionForCurrentInAppMessage:self.m5]);
  708. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  709. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  710. }
  711. - (void)testNoAnalyticsTrackingOnTestMessage {
  712. // This setup allows next message to be displayed from display interval perspective.
  713. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  714. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  715. FIRIAMMessageDefinition *testMessage =
  716. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData
  717. experimentPayload:nil];
  718. // not expecting triggering analytics recording
  719. OCMReject([self.mockAnalyticsEventLogger
  720. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  721. forCampaignID:[OCMArg isEqual:self.m1.renderData.messageID]
  722. withCampaignName:[OCMArg any]
  723. eventTimeInMs:[OCMArg any]
  724. completion:[OCMArg any]]);
  725. [self.clientMessageCache setMessageData:@[ testMessage ]];
  726. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  727. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  728. self.displayExecutor.messageDisplayComponent = display;
  729. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  730. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  731. }
  732. - (void)testNoMessageAvailableCase {
  733. // This setup allows next message to be displayed from display interval perspective.
  734. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  735. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  736. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  737. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  738. self.displayExecutor.messageDisplayComponent = display;
  739. [self.clientMessageCache setMessageData:@[]];
  740. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  741. // No display has happened so the message stored in the display component should be nil
  742. XCTAssertNil(display.message);
  743. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  744. XCTAssertEqual(0, remainingMsgCount);
  745. }
  746. - (void)testIntervalBetweenOnAppOpenDisplays {
  747. self.displaySetting.displayMinIntervalInMinutes = 10;
  748. // last display time is set to 0 by default
  749. // 10 seconds is not long enough for satisfying the 10-min internal requirement
  750. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10);
  751. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  752. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  753. self.displayExecutor.messageDisplayComponent = display;
  754. [self.clientMessageCache setMessageData:@[ self.m1 ]];
  755. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  756. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  757. // No display has happened so the message stored in the display component should be nil
  758. XCTAssertNil(display.message);
  759. // still got one in the queue
  760. XCTAssertEqual(1, remainingMsgCount);
  761. }
  762. // making sure that we match on the event names for analytics based events
  763. - (void)testOnFirebaseAnalyticsEventDisplayMessages {
  764. // This setup allows next message to be displayed from display interval perspective.
  765. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  766. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  767. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  768. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  769. self.displayExecutor.messageDisplayComponent = display;
  770. // m1 and m3 are messages triggered by 'test_event' analytics events
  771. [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]];
  772. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"different event"];
  773. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  774. // No message matching event "different event", so no message is nil
  775. XCTAssertNil(display.message);
  776. // still got 2 in the queue
  777. XCTAssertEqual(2, remainingMsgCount);
  778. // now trigger it with 'test_event' and we would expect one message to be displayed and removed
  779. // from cache
  780. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  781. // Expecting the m1 being used for display
  782. XCTAssertEqualObjects(self.m1.renderData.messageID, display.message.campaignInfo.messageID);
  783. remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  784. // Now only one message remaining in the queue
  785. XCTAssertEqual(1, remainingMsgCount);
  786. }
  787. // no regular message rendering if suppress message display flag is turned on
  788. - (void)testNoRenderingIfMessageDisplayIsSuppressed {
  789. // This setup allows next message to be displayed from display interval perspective.
  790. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  791. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  792. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  793. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  794. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  795. self.displayExecutor.messageDisplayComponent = display;
  796. self.displayExecutor.suppressMessageDisplay = YES;
  797. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  798. // no message display has happened
  799. XCTAssertNil(display.message);
  800. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  801. // no message is removed from the cache
  802. XCTAssertEqual(2, remainingMsgCount);
  803. // now allow message rendering again
  804. self.displayExecutor.suppressMessageDisplay = NO;
  805. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  806. NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count;
  807. // one message was rendered and removed from the cache
  808. XCTAssertEqual(1, remainingMsgCount2);
  809. }
  810. - (void)testNoRenderingIfMessageDisplayIsSuppressedDuringImageLoading {
  811. // This setup allows next message to be displayed from display interval perspective.
  812. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  813. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  814. [self.clientMessageCache setMessageData:@[ self.m6 ]];
  815. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  816. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  817. self.displayExecutor.messageDisplayComponent = display;
  818. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  819. self.displayExecutor.suppressMessageDisplay = YES;
  820. XCTestExpectation *expectation = [[XCTestExpectation alloc] init];
  821. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(),
  822. ^{
  823. // no message display has happened
  824. XCTAssertNil(display.message);
  825. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  826. // no message is removed from the cache
  827. XCTAssertEqual(1, remainingMsgCount);
  828. [expectation fulfill];
  829. });
  830. [self waitForExpectations:@[ expectation ] timeout:0.1];
  831. }
  832. // No contextual message rendering if suppress message display flag is turned on
  833. - (void)testNoContextualMsgRenderingIfMessageDisplayIsSuppressed {
  834. // This setup allows next message to be displayed from display interval perspective.
  835. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  836. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  837. [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]];
  838. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  839. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  840. self.displayExecutor.messageDisplayComponent = display;
  841. self.displayExecutor.suppressMessageDisplay = YES;
  842. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  843. // no message display has happened
  844. XCTAssertNil(display.message);
  845. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  846. // No message is removed from the cache.
  847. XCTAssertEqual(2, remainingMsgCount);
  848. // now re-enable message rendering again
  849. self.displayExecutor.suppressMessageDisplay = NO;
  850. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  851. NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count;
  852. // one message was rendered and removed from the cache
  853. XCTAssertEqual(1, remainingMsgCount2);
  854. }
  855. - (void)testMessageClickedCallback {
  856. FIRInAppMessagingDisplayTestDelegate *delegate =
  857. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  858. self.mockInAppMessaging.delegate = delegate;
  859. // This setup allows next message to be displayed from display interval perspective.
  860. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  861. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  862. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  863. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  864. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  865. self.displayExecutor.messageDisplayComponent = display;
  866. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  867. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  868. XCTAssertTrue(delegate.receivedMessageClickedCallback);
  869. }
  870. - (void)testMessageImpressionCallback {
  871. FIRInAppMessagingDisplayTestDelegate *delegate =
  872. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  873. self.mockInAppMessaging.delegate = delegate;
  874. // This setup allows next message to be displayed from display interval perspective.
  875. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  876. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  877. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  878. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  879. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  880. self.displayExecutor.messageDisplayComponent = display;
  881. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  882. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  883. // Verify that the message content handed to display component is expected
  884. XCTAssertTrue(delegate.receivedMessageImpressionCallback);
  885. }
  886. - (void)testMessageErrorCallback {
  887. FIRInAppMessagingDisplayTestDelegate *delegate =
  888. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  889. self.mockInAppMessaging.delegate = delegate;
  890. // This setup allows next message to be displayed from display interval perspective.
  891. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  892. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  893. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  894. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  895. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError];
  896. self.displayExecutor.messageDisplayComponent = display;
  897. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  898. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  899. // Verify that the message content handed to display component is expected
  900. XCTAssertTrue(delegate.receivedMessageErrorCallback);
  901. }
  902. - (void)testMessageDismissedCallback {
  903. FIRInAppMessagingDisplayTestDelegate *delegate =
  904. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  905. self.mockInAppMessaging.delegate = delegate;
  906. // This setup allows next message to be displayed from display interval perspective.
  907. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  908. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  909. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  910. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  911. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  912. self.displayExecutor.messageDisplayComponent = display;
  913. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  914. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  915. // Verify that the message content handed to display component is expected
  916. XCTAssertTrue(delegate.receivedMessageDismissedCallback);
  917. }
  918. - (void)testMessageWithDataBundle {
  919. FIRInAppMessagingDisplayMessage *displayMessage = [self.displayExecutor
  920. displayMessageWithMessageDefinition:self.m4
  921. imageData:nil
  922. landscapeImageData:nil
  923. triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground];
  924. XCTAssertEqual(displayMessage.appData.count, 2);
  925. XCTAssertEqualObjects(displayMessage.appData[@"a"], @"b");
  926. XCTAssertEqualObjects(displayMessage.appData[@"up"], @"dog");
  927. }
  928. - (void)testMessageWithoutDataBundle {
  929. FIRInAppMessagingDisplayMessage *displayMessage = [self.displayExecutor
  930. displayMessageWithMessageDefinition:self.m3
  931. imageData:nil
  932. landscapeImageData:nil
  933. triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground];
  934. XCTAssertNil(displayMessage.appData);
  935. }
  936. - (void)testMessageWithExperimentPayload {
  937. FIRInAppMessagingDisplayMessage *displayMessage = [self.displayExecutor
  938. displayMessageWithMessageDefinition:self.m4
  939. imageData:nil
  940. landscapeImageData:nil
  941. triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground];
  942. XCTAssertNotNil(displayMessage.campaignInfo.experimentPayload);
  943. }
  944. @end