FIRIAMDisplayExecutorTests.m 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  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 = FIRIAMRenderAsCardView;
  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. appData:nil
  442. experimentPayload:nil];
  443. FIRInAppMessagingAction *testAction = [[FIRInAppMessagingAction alloc]
  444. initWithActionText:@"test"
  445. actionURL:testMessage.renderData.contentData.actionURL];
  446. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  447. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  448. action:testAction];
  449. self.displayExecutor.messageDisplayComponent = display;
  450. [self.clientMessageCache setMessageData:@[ testMessage ]];
  451. // not expecting triggering analytics recording
  452. OCMExpect([self.mockActionURLFollower
  453. followActionURL:[OCMArg isEqual:testMessage.renderData.contentData.actionURL]
  454. withCompletionBlock:[OCMArg any]]);
  455. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  456. OCMVerifyAll((id)self.mockActionURLFollower);
  457. }
  458. - (void)testClientTestMessageAvailableCase {
  459. // When test message is present in cache, even if the display time interval has not been
  460. // reached, we still render.
  461. // 10 seconds is less than DISPLAY_MIN_INTERVALS minutes, so we have not reached
  462. // minimal display time interval yet.
  463. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10);
  464. FIRIAMMessageDefinition *testMessage =
  465. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData
  466. appData:nil
  467. experimentPayload:nil];
  468. [self.clientMessageCache setMessageData:@[ self.m2, testMessage, self.m4 ]];
  469. // We have test message in the cache now.
  470. XCTAssertTrue([self.clientMessageCache hasTestMessage]);
  471. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  472. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  473. self.displayExecutor.messageDisplayComponent = display;
  474. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  475. // No more test message in the cache now.
  476. XCTAssertFalse([self.clientMessageCache hasTestMessage]);
  477. }
  478. // If a message is still being displayed, we won't try to display a second one on top of it
  479. - (void)testNoDualDisplay {
  480. // This setup allows next message to be displayed from display interval perspective.
  481. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  482. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  483. // This display component only detects a valid impression, but does not end the rendering
  484. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  485. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  486. self.displayExecutor.messageDisplayComponent = display;
  487. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  488. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  489. // m2 is being rendered
  490. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  491. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  492. XCTAssertEqual(1, remainingMsgCount);
  493. // try to display again when the in-display flag is already turned on (and not turned off yet)
  494. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  495. // Verify that the message in display component is still m2
  496. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  497. // message in cache remain unchanged for the second checkAndDisplayNext call
  498. remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  499. XCTAssertEqual(1, remainingMsgCount);
  500. }
  501. // this test case contracts testNoAnalyticsTrackingOnTestMessage to cover both positive
  502. // and negative cases
  503. - (void)testDoesAnalyticsTrackingOnNonTestMessage {
  504. // This setup allows next message to be displayed from display interval perspective.
  505. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  506. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  507. // not expecting triggering analytics recording
  508. OCMExpect([self.mockAnalyticsEventLogger
  509. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  510. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  511. withCampaignName:[OCMArg any]
  512. eventTimeInMs:[OCMArg any]
  513. completion:[OCMArg any]]);
  514. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  515. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  516. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  517. self.displayExecutor.messageDisplayComponent = display;
  518. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  519. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  520. }
  521. - (void)testDoesAnalyticsTrackingOnDisplayError {
  522. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  523. // last display time is set to 0 by default
  524. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  525. // not expecting triggering analytics recording
  526. OCMExpect([self.mockAnalyticsEventLogger
  527. logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError
  528. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  529. withCampaignName:[OCMArg any]
  530. eventTimeInMs:[OCMArg any]
  531. completion:[OCMArg any]]);
  532. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  533. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  534. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError];
  535. self.displayExecutor.messageDisplayComponent = display;
  536. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  537. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  538. }
  539. - (void)testAnalyticsTrackingOnMessageDismissCase {
  540. // This setup allows next message to be displayed from display interval perspective.
  541. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  542. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  543. // not expecting triggering analytics recording
  544. OCMExpect([self.mockAnalyticsEventLogger
  545. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageDismissAuto
  546. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  547. withCampaignName:[OCMArg any]
  548. eventTimeInMs:[OCMArg any]
  549. completion:[OCMArg any]]);
  550. // Make sure we don't log the url follow event.
  551. OCMReject([self.mockAnalyticsEventLogger
  552. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  553. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  554. withCampaignName:[OCMArg any]
  555. eventTimeInMs:[OCMArg any]
  556. completion:[OCMArg any]]);
  557. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  558. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  559. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  560. self.displayExecutor.messageDisplayComponent = display;
  561. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  562. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  563. }
  564. - (void)testAnalyticsTrackingOnMessageClickCase {
  565. // This setup allows next message to be displayed from display interval perspective.
  566. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  567. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  568. // We expect two analytics events for a click action:
  569. // An impression event and an action URL follow event
  570. OCMExpect([self.mockAnalyticsEventLogger
  571. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  572. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  573. withCampaignName:[OCMArg any]
  574. eventTimeInMs:[OCMArg any]
  575. completion:[OCMArg any]]);
  576. OCMExpect([self.mockAnalyticsEventLogger
  577. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  578. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  579. withCampaignName:[OCMArg any]
  580. eventTimeInMs:[OCMArg any]
  581. completion:[OCMArg any]]);
  582. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  583. FIRInAppMessagingAction *m2Action = [[FIRInAppMessagingAction alloc]
  584. initWithActionText:self.m2.renderData.contentData.actionButtonText
  585. actionURL:self.m2.renderData.contentData.actionURL];
  586. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  587. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  588. action:m2Action];
  589. self.displayExecutor.messageDisplayComponent = display;
  590. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  591. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  592. }
  593. - (void)testAnalyticsTrackingOnMessageClickCaseWithNoActionURL {
  594. // This setup allows next message to be displayed from display interval perspective.
  595. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  596. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  597. // We expect two analytics events for a click action:
  598. // An impression event and an action URL follow event
  599. OCMExpect([self.mockAnalyticsEventLogger
  600. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  601. forCampaignID:[OCMArg isEqual:self.m5.renderData.messageID]
  602. withCampaignName:[OCMArg any]
  603. eventTimeInMs:[OCMArg any]
  604. completion:[OCMArg any]]);
  605. [self.clientMessageCache setMessageData:@[ self.m5 ]];
  606. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  607. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  608. action:nil];
  609. self.displayExecutor.messageDisplayComponent = display;
  610. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  611. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  612. }
  613. - (void)testAnalyticsTrackingOnTestMessageClickCase {
  614. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  615. // last display time is set to 0 by default
  616. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  617. // We expect two analytics events for a click action:
  618. // An test message impression event and a test message click event
  619. OCMExpect([self.mockAnalyticsEventLogger
  620. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  621. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  622. withCampaignName:[OCMArg any]
  623. eventTimeInMs:[OCMArg any]
  624. completion:[OCMArg any]]);
  625. OCMExpect([self.mockAnalyticsEventLogger
  626. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  627. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  628. withCampaignName:[OCMArg any]
  629. eventTimeInMs:[OCMArg any]
  630. completion:[OCMArg any]]);
  631. FIRIAMMessageDefinition *testMessage =
  632. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData
  633. appData:nil
  634. experimentPayload:nil];
  635. [self.clientMessageCache setMessageData:@[ testMessage ]];
  636. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  637. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  638. self.displayExecutor.messageDisplayComponent = display;
  639. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  640. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  641. }
  642. - (void)testAnalyticsTrackingOnTestMessageDismissCase {
  643. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  644. // last display time is set to 0 by default
  645. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  646. // We expect a test message impression
  647. OCMExpect([self.mockAnalyticsEventLogger
  648. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  649. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  650. withCampaignName:[OCMArg any]
  651. eventTimeInMs:[OCMArg any]
  652. completion:[OCMArg any]]);
  653. // No click event
  654. OCMReject([self.mockAnalyticsEventLogger
  655. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  656. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  657. withCampaignName:[OCMArg any]
  658. eventTimeInMs:[OCMArg any]
  659. completion:[OCMArg any]]);
  660. FIRIAMMessageDefinition *testMessage =
  661. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData
  662. appData:nil
  663. experimentPayload:nil];
  664. [self.clientMessageCache setMessageData:@[ testMessage ]];
  665. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  666. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  667. self.displayExecutor.messageDisplayComponent = display;
  668. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  669. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  670. }
  671. - (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCaseWithActionURL {
  672. // This setup allows next message to be displayed from display interval perspective.
  673. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  674. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  675. // not expecting triggering analytics recording
  676. OCMExpect([self.mockAnalyticsEventLogger
  677. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  678. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  679. withCampaignName:[OCMArg any]
  680. eventTimeInMs:[OCMArg any]
  681. completion:[OCMArg any]]);
  682. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  683. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  684. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  685. self.displayExecutor.messageDisplayComponent = display;
  686. // M2 has an action URL. Conversion shouldn't be tracked yet.
  687. XCTAssertFalse(
  688. [self.displayExecutor shouldTrackConversionsOnImpressionForCurrentInAppMessage:self.m2]);
  689. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  690. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  691. }
  692. - (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCaseWithoutActionURL {
  693. // This setup allows next message to be displayed from display interval perspective.
  694. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  695. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  696. // not expecting triggering analytics recording
  697. OCMExpect([self.mockAnalyticsEventLogger
  698. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  699. forCampaignID:[OCMArg isEqual:self.m5.renderData.messageID]
  700. withCampaignName:[OCMArg any]
  701. eventTimeInMs:[OCMArg any]
  702. completion:[OCMArg any]]);
  703. [self.clientMessageCache setMessageData:@[ self.m5 ]];
  704. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  705. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  706. self.displayExecutor.messageDisplayComponent = display;
  707. // M5 has no action URL. Conversion should be tracked after impression.
  708. OCMExpect([self.mockAnalyticsEventLogger
  709. logConversionTrackingEventForCampaignID:[OCMArg isEqual:self.m5.renderData.messageID]]);
  710. XCTAssertTrue(
  711. [self.displayExecutor shouldTrackConversionsOnImpressionForCurrentInAppMessage:self.m5]);
  712. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  713. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  714. }
  715. - (void)testNoAnalyticsTrackingOnTestMessage {
  716. // This setup allows next message to be displayed from display interval perspective.
  717. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  718. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  719. FIRIAMMessageDefinition *testMessage =
  720. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData
  721. appData:nil
  722. experimentPayload:nil];
  723. // not expecting triggering analytics recording
  724. OCMReject([self.mockAnalyticsEventLogger
  725. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  726. forCampaignID:[OCMArg isEqual:self.m1.renderData.messageID]
  727. withCampaignName:[OCMArg any]
  728. eventTimeInMs:[OCMArg any]
  729. completion:[OCMArg any]]);
  730. [self.clientMessageCache setMessageData:@[ testMessage ]];
  731. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  732. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  733. self.displayExecutor.messageDisplayComponent = display;
  734. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  735. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  736. }
  737. - (void)testNoMessageAvailableCase {
  738. // This setup allows next message to be displayed from display interval perspective.
  739. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  740. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  741. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  742. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  743. self.displayExecutor.messageDisplayComponent = display;
  744. [self.clientMessageCache setMessageData:@[]];
  745. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  746. // No display has happened so the message stored in the display component should be nil
  747. XCTAssertNil(display.message);
  748. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  749. XCTAssertEqual(0, remainingMsgCount);
  750. }
  751. - (void)testIntervalBetweenOnAppOpenDisplays {
  752. self.displaySetting.displayMinIntervalInMinutes = 10;
  753. // last display time is set to 0 by default
  754. // 10 seconds is not long enough for satisfying the 10-min internal requirement
  755. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10);
  756. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  757. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  758. self.displayExecutor.messageDisplayComponent = display;
  759. [self.clientMessageCache setMessageData:@[ self.m1 ]];
  760. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  761. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  762. // No display has happened so the message stored in the display component should be nil
  763. XCTAssertNil(display.message);
  764. // still got one in the queue
  765. XCTAssertEqual(1, remainingMsgCount);
  766. }
  767. // making sure that we match on the event names for analytics based events
  768. - (void)testOnFirebaseAnalyticsEventDisplayMessages {
  769. // This setup allows next message to be displayed from display interval perspective.
  770. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  771. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  772. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  773. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  774. self.displayExecutor.messageDisplayComponent = display;
  775. // m1 and m3 are messages triggered by 'test_event' analytics events
  776. [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]];
  777. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"different event"];
  778. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  779. // No message matching event "different event", so no message is nil
  780. XCTAssertNil(display.message);
  781. // still got 2 in the queue
  782. XCTAssertEqual(2, remainingMsgCount);
  783. // now trigger it with 'test_event' and we would expect one message to be displayed and removed
  784. // from cache
  785. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  786. // Expecting the m1 being used for display
  787. XCTAssertEqualObjects(self.m1.renderData.messageID, display.message.campaignInfo.messageID);
  788. remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  789. // Now only one message remaining in the queue
  790. XCTAssertEqual(1, remainingMsgCount);
  791. }
  792. // no regular message rendering if suppress message display flag is turned on
  793. - (void)testNoRenderingIfMessageDisplayIsSuppressed {
  794. // This setup allows next message to be displayed from display interval perspective.
  795. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  796. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  797. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  798. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  799. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  800. self.displayExecutor.messageDisplayComponent = display;
  801. self.displayExecutor.suppressMessageDisplay = YES;
  802. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  803. // no message display has happened
  804. XCTAssertNil(display.message);
  805. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  806. // no message is removed from the cache
  807. XCTAssertEqual(2, remainingMsgCount);
  808. // now allow message rendering again
  809. self.displayExecutor.suppressMessageDisplay = NO;
  810. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  811. NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count;
  812. // one message was rendered and removed from the cache
  813. XCTAssertEqual(1, remainingMsgCount2);
  814. }
  815. - (void)testNoRenderingIfMessageDisplayIsSuppressedDuringImageLoading {
  816. // This setup allows next message to be displayed from display interval perspective.
  817. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  818. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  819. [self.clientMessageCache setMessageData:@[ self.m6 ]];
  820. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  821. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  822. self.displayExecutor.messageDisplayComponent = display;
  823. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  824. self.displayExecutor.suppressMessageDisplay = YES;
  825. XCTestExpectation *expectation = [[XCTestExpectation alloc] init];
  826. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(),
  827. ^{
  828. // no message display has happened
  829. XCTAssertNil(display.message);
  830. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  831. // no message is removed from the cache
  832. XCTAssertEqual(1, remainingMsgCount);
  833. [expectation fulfill];
  834. });
  835. [self waitForExpectations:@[ expectation ] timeout:0.1];
  836. }
  837. // No contextual message rendering if suppress message display flag is turned on
  838. - (void)testNoContextualMsgRenderingIfMessageDisplayIsSuppressed {
  839. // This setup allows next message to be displayed from display interval perspective.
  840. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  841. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  842. [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]];
  843. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  844. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  845. self.displayExecutor.messageDisplayComponent = display;
  846. self.displayExecutor.suppressMessageDisplay = YES;
  847. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  848. // no message display has happened
  849. XCTAssertNil(display.message);
  850. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  851. // No message is removed from the cache.
  852. XCTAssertEqual(2, remainingMsgCount);
  853. // now re-enable message rendering again
  854. self.displayExecutor.suppressMessageDisplay = NO;
  855. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  856. NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count;
  857. // one message was rendered and removed from the cache
  858. XCTAssertEqual(1, remainingMsgCount2);
  859. }
  860. - (void)testMessageClickedCallback {
  861. FIRInAppMessagingDisplayTestDelegate *delegate =
  862. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  863. self.mockInAppMessaging.delegate = delegate;
  864. // This setup allows next message to be displayed from display interval perspective.
  865. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  866. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  867. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  868. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  869. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  870. self.displayExecutor.messageDisplayComponent = display;
  871. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  872. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  873. XCTAssertTrue(delegate.receivedMessageClickedCallback);
  874. }
  875. - (void)testMessageImpressionCallback {
  876. FIRInAppMessagingDisplayTestDelegate *delegate =
  877. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  878. self.mockInAppMessaging.delegate = delegate;
  879. // This setup allows next message to be displayed from display interval perspective.
  880. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  881. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  882. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  883. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  884. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  885. self.displayExecutor.messageDisplayComponent = display;
  886. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  887. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  888. // Verify that the message content handed to display component is expected
  889. XCTAssertTrue(delegate.receivedMessageImpressionCallback);
  890. }
  891. - (void)testMessageErrorCallback {
  892. FIRInAppMessagingDisplayTestDelegate *delegate =
  893. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  894. self.mockInAppMessaging.delegate = delegate;
  895. // This setup allows next message to be displayed from display interval perspective.
  896. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  897. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  898. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  899. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  900. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError];
  901. self.displayExecutor.messageDisplayComponent = display;
  902. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  903. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  904. // Verify that the message content handed to display component is expected
  905. XCTAssertTrue(delegate.receivedMessageErrorCallback);
  906. }
  907. - (void)testMessageDismissedCallback {
  908. FIRInAppMessagingDisplayTestDelegate *delegate =
  909. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  910. self.mockInAppMessaging.delegate = delegate;
  911. // This setup allows next message to be displayed from display interval perspective.
  912. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  913. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  914. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  915. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  916. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  917. self.displayExecutor.messageDisplayComponent = display;
  918. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  919. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  920. // Verify that the message content handed to display component is expected
  921. XCTAssertTrue(delegate.receivedMessageDismissedCallback);
  922. }
  923. - (void)testMessageWithDataBundle {
  924. FIRInAppMessagingDisplayMessage *displayMessage = [self.displayExecutor
  925. displayMessageWithMessageDefinition:self.m4
  926. imageData:nil
  927. landscapeImageData:nil
  928. triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground];
  929. XCTAssertEqual(displayMessage.appData.count, 2);
  930. XCTAssertEqualObjects(displayMessage.appData[@"a"], @"b");
  931. XCTAssertEqualObjects(displayMessage.appData[@"up"], @"dog");
  932. }
  933. - (void)testMessageWithoutDataBundle {
  934. FIRInAppMessagingDisplayMessage *displayMessage = [self.displayExecutor
  935. displayMessageWithMessageDefinition:self.m3
  936. imageData:nil
  937. landscapeImageData:nil
  938. triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground];
  939. XCTAssertNil(displayMessage.appData);
  940. }
  941. - (void)testMessageWithExperimentPayload {
  942. FIRInAppMessagingDisplayMessage *displayMessage = [self.displayExecutor
  943. displayMessageWithMessageDefinition:self.m4
  944. imageData:nil
  945. landscapeImageData:nil
  946. triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground];
  947. XCTAssertNotNil(displayMessage.campaignInfo.experimentPayload);
  948. }
  949. - (void)testMessageDisplayTypes {
  950. FIRInAppMessagingImageData *imageData =
  951. [[FIRInAppMessagingImageData alloc] initWithImageURL:@"https://www.google.com"
  952. imageData:[NSData data]];
  953. FIRInAppMessagingDisplayTriggerType analyticsTriggerType =
  954. FIRInAppMessagingDisplayTriggerTypeOnAnalyticsEvent;
  955. FIRInAppMessagingDisplayMessage *bannerMessage =
  956. [self.displayExecutor displayMessageWithMessageDefinition:self.m1
  957. imageData:imageData
  958. landscapeImageData:nil
  959. triggerType:analyticsTriggerType];
  960. FIRInAppMessagingDisplayMessage *imageOnlyMessage =
  961. [self.displayExecutor displayMessageWithMessageDefinition:self.m3
  962. imageData:imageData
  963. landscapeImageData:nil
  964. triggerType:analyticsTriggerType];
  965. FIRInAppMessagingDisplayMessage *modalMessage =
  966. [self.displayExecutor displayMessageWithMessageDefinition:self.m2
  967. imageData:imageData
  968. landscapeImageData:nil
  969. triggerType:analyticsTriggerType];
  970. FIRInAppMessagingDisplayMessage *cardMessage =
  971. [self.displayExecutor displayMessageWithMessageDefinition:self.m6
  972. imageData:imageData
  973. landscapeImageData:nil
  974. triggerType:analyticsTriggerType];
  975. XCTAssertEqual(bannerMessage.type, FIRInAppMessagingDisplayMessageTypeBanner);
  976. XCTAssertEqual(imageOnlyMessage.type, FIRInAppMessagingDisplayMessageTypeImageOnly);
  977. XCTAssertEqual(modalMessage.type, FIRInAppMessagingDisplayMessageTypeModal);
  978. XCTAssertEqual(cardMessage.type, FIRInAppMessagingDisplayMessageTypeCard);
  979. }
  980. @end