FIRIAMDisplayExecutorTests.m 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  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 "FIRIAMActionURLFollower.h"
  19. #import "FIRIAMDisplayExecutor.h"
  20. #import "FIRIAMDisplayTriggerDefinition.h"
  21. #import "FIRIAMMessageContentData.h"
  22. #import "FIRInAppMessaging.h"
  23. // A class implementing protocol FIRIAMMessageContentData to be used for unit testing
  24. @interface FIRIAMMessageContentDataForTesting : NSObject <FIRIAMMessageContentData>
  25. @property(nonatomic, readwrite, nonnull) NSString *titleText;
  26. @property(nonatomic, readwrite, nonnull) NSString *bodyText;
  27. @property(nonatomic, nullable) NSString *actionButtonText;
  28. @property(nonatomic, nullable) NSString *secondaryActionButtonText;
  29. @property(nonatomic, nullable) NSURL *actionURL;
  30. @property(nonatomic, nullable) NSURL *secondaryActionURL;
  31. @property(nonatomic, nullable) NSURL *imageURL;
  32. @property(nonatomic, nullable) NSURL *landscapeImageURL;
  33. @property BOOL errorEncountered;
  34. - (instancetype)initWithMessageTitle:(NSString *)title
  35. messageBody:(NSString *)body
  36. actionButtonText:(nullable NSString *)actionButtonText
  37. secondaryActionButtonText:(nullable NSString *)secondaryActionButtonText
  38. actionURL:(nullable NSURL *)actionURL
  39. secondaryActionURL:(nullable NSURL *)secondaryActionURL
  40. imageURL:(nullable NSURL *)imageURL
  41. landscapeImageURL:(nullable NSURL *)landscapeImageURL
  42. hasImageError:(BOOL)hasImageError;
  43. @end
  44. @implementation FIRIAMMessageContentDataForTesting
  45. - (instancetype)initWithMessageTitle:(NSString *)title
  46. messageBody:(NSString *)body
  47. actionButtonText:(nullable NSString *)actionButtonText
  48. secondaryActionButtonText:(nullable NSString *)secondaryActionButtonText
  49. actionURL:(nullable NSURL *)actionURL
  50. secondaryActionURL:(nullable NSURL *)secondaryActionURL
  51. imageURL:(nullable NSURL *)imageURL
  52. landscapeImageURL:(nullable NSURL *)landscapeImageURL
  53. hasImageError:(BOOL)hasImageError {
  54. if (self = [super init]) {
  55. _titleText = title;
  56. _bodyText = body;
  57. _imageURL = imageURL;
  58. _landscapeImageURL = landscapeImageURL;
  59. _actionButtonText = actionButtonText;
  60. _secondaryActionButtonText = secondaryActionButtonText;
  61. _actionURL = actionURL;
  62. _secondaryActionURL = secondaryActionURL;
  63. _errorEncountered = hasImageError;
  64. }
  65. return self;
  66. }
  67. - (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData,
  68. NSData *_Nullable landscapeImageData,
  69. NSError *_Nullable error))block {
  70. if (self.errorEncountered) {
  71. block(nil, nil, [NSError errorWithDomain:@"image error" code:0 userInfo:nil]);
  72. } else {
  73. NSData *imageData = [@"image data" dataUsingEncoding:NSUTF8StringEncoding];
  74. NSData *landscapeImageData = [@"landscape image data" dataUsingEncoding:NSUTF8StringEncoding];
  75. block(imageData, landscapeImageData, nil);
  76. }
  77. }
  78. @end
  79. // Defines how the message display component triggers the delegate in unit testing.
  80. typedef NS_ENUM(NSInteger, FIRInAppMessagingDelegateInteraction) {
  81. // Message display component triggers messageDismissedWithType:.
  82. FIRInAppMessagingDelegateInteractionDismiss,
  83. // Message display component triggers messageClicked:.
  84. FIRInAppMessagingDelegateInteractionClick,
  85. // Message display component triggers displayErrorEncountered:.
  86. FIRInAppMessagingDelegateInteractionError,
  87. // Message has finished a valid impression, but it's not getting closed by the user.
  88. FIRInAppMessagingDelegateInteractionImpressionDetected,
  89. };
  90. // A class implementing protocol FIRInAppMessagingDisplay to be used for unit testing
  91. @interface FIRIAMMessageDisplayForTesting : NSObject <FIRInAppMessagingDisplay>
  92. @property FIRInAppMessagingDelegateInteraction delegateInteraction;
  93. @property(nonatomic, nullable, copy) FIRInAppMessagingAction *action;
  94. // used for interaction verificatio
  95. @property FIRInAppMessagingDisplayMessage *message;
  96. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction
  97. action:(nullable FIRInAppMessagingAction *)actionURL;
  98. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction;
  99. @end
  100. @implementation FIRIAMMessageDisplayForTesting
  101. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction
  102. action:(nullable FIRInAppMessagingAction *)action {
  103. if (self = [super init]) {
  104. _delegateInteraction = interaction;
  105. _action = action;
  106. }
  107. return self;
  108. }
  109. - (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction {
  110. return [self initWithDelegateInteraction:interaction action:nil];
  111. }
  112. - (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay
  113. displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {
  114. self.message = messageForDisplay;
  115. switch (self.delegateInteraction) {
  116. case FIRInAppMessagingDelegateInteractionClick:
  117. [displayDelegate messageClicked:messageForDisplay withAction:self.action];
  118. break;
  119. case FIRInAppMessagingDelegateInteractionDismiss:
  120. [displayDelegate messageDismissed:messageForDisplay
  121. dismissType:FIRInAppMessagingDismissTypeAuto];
  122. break;
  123. case FIRInAppMessagingDelegateInteractionError:
  124. [displayDelegate displayErrorForMessage:messageForDisplay
  125. error:[NSError errorWithDomain:NSURLErrorDomain
  126. code:0
  127. userInfo:nil]];
  128. break;
  129. case FIRInAppMessagingDelegateInteractionImpressionDetected:
  130. [displayDelegate impressionDetectedForMessage:messageForDisplay];
  131. break;
  132. }
  133. }
  134. @end
  135. @interface FIRInAppMessagingDisplayTestDelegate : NSObject <FIRInAppMessagingDisplayDelegate>
  136. @property(nonatomic) BOOL receivedMessageErrorCallback;
  137. @property(nonatomic) BOOL receivedMessageImpressionCallback;
  138. @property(nonatomic) BOOL receivedMessageClickedCallback;
  139. @property(nonatomic) BOOL receivedMessageDismissedCallback;
  140. @end
  141. @implementation FIRInAppMessagingDisplayTestDelegate
  142. - (void)displayErrorForMessage:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage
  143. error:(nonnull NSError *)error {
  144. self.receivedMessageErrorCallback = YES;
  145. }
  146. - (void)impressionDetectedForMessage:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage {
  147. self.receivedMessageImpressionCallback = YES;
  148. }
  149. - (void)messageClicked:(FIRInAppMessagingDisplayMessage *)inAppMessage
  150. withAction:(FIRInAppMessagingAction *)action {
  151. self.receivedMessageClickedCallback = YES;
  152. }
  153. - (void)messageDismissed:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage
  154. dismissType:(FIRInAppMessagingDismissType)dismissType {
  155. self.receivedMessageDismissedCallback = YES;
  156. }
  157. @end
  158. @interface FIRIAMDisplayExecutorTests : XCTestCase
  159. @property(nonatomic) FIRIAMDisplaySetting *displaySetting;
  160. @property FIRIAMMessageClientCache *clientMessageCache;
  161. @property id<FIRIAMBookKeeper> mockBookkeeper;
  162. @property id<FIRIAMTimeFetcher> mockTimeFetcher;
  163. @property FIRIAMDisplayExecutor *displayExecutor;
  164. @property FIRIAMActivityLogger *mockActivityLogger;
  165. @property FIRInAppMessaging *mockInAppMessaging;
  166. @property id<FIRIAMAnalyticsEventLogger> mockAnalyticsEventLogger;
  167. @property FIRIAMActionURLFollower *mockActionURLFollower;
  168. @property id<FIRInAppMessagingDisplay> mockMessageDisplayComponent;
  169. // three pre-defined messages
  170. @property FIRIAMMessageDefinition *m1, *m2, *m3, *m4;
  171. @end
  172. @implementation FIRIAMDisplayExecutorTests
  173. - (void)setupMessageTexture {
  174. // startTime, endTime here ensures messages with them are active
  175. NSTimeInterval activeStartTime = 0;
  176. NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000;
  177. // m1 & m3 will be of contextual trigger
  178. FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition =
  179. [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"];
  180. // m2 and m4 will be of app open trigger
  181. FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition =
  182. [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger];
  183. FIRIAMMessageContentDataForTesting *m1ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  184. initWithMessageTitle:@"m1 title"
  185. messageBody:@"message body"
  186. actionButtonText:nil
  187. secondaryActionButtonText:nil
  188. actionURL:[NSURL URLWithString:@"http://google.com"]
  189. secondaryActionURL:nil
  190. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  191. landscapeImageURL:nil
  192. hasImageError:NO];
  193. FIRIAMRenderingEffectSetting *renderSetting1 =
  194. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  195. renderSetting1.viewMode = FIRIAMRenderAsBannerView;
  196. FIRIAMMessageRenderData *renderData1 =
  197. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1"
  198. messageName:@"name"
  199. contentData:m1ContentData
  200. renderingEffect:renderSetting1];
  201. self.m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1
  202. startTime:activeStartTime
  203. endTime:activeEndTime
  204. triggerDefinition:@[ contextualTriggerDefinition ]];
  205. FIRIAMMessageContentDataForTesting *m2ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  206. initWithMessageTitle:@"m2 title"
  207. messageBody:@"message body"
  208. actionButtonText:nil
  209. secondaryActionButtonText:nil
  210. actionURL:[NSURL URLWithString:@"http://google.com"]
  211. secondaryActionURL:nil
  212. imageURL:[NSURL URLWithString:@"https://unsplash.it/300/400"]
  213. landscapeImageURL:nil
  214. hasImageError:NO];
  215. FIRIAMRenderingEffectSetting *renderSetting2 =
  216. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  217. renderSetting2.viewMode = FIRIAMRenderAsModalView;
  218. FIRIAMMessageRenderData *renderData2 =
  219. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2"
  220. messageName:@"name"
  221. contentData:m2ContentData
  222. renderingEffect:renderSetting2];
  223. self.m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2
  224. startTime:activeStartTime
  225. endTime:activeEndTime
  226. triggerDefinition:@[ appOpentriggerDefinition ]];
  227. FIRIAMMessageContentDataForTesting *m3ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  228. initWithMessageTitle:@"m3 title"
  229. messageBody:@"message body"
  230. actionButtonText:nil
  231. secondaryActionButtonText:nil
  232. actionURL:[NSURL URLWithString:@"http://google.com"]
  233. secondaryActionURL:nil
  234. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  235. landscapeImageURL:nil
  236. hasImageError:NO];
  237. FIRIAMRenderingEffectSetting *renderSetting3 =
  238. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  239. renderSetting3.viewMode = FIRIAMRenderAsImageOnlyView;
  240. FIRIAMMessageRenderData *renderData3 =
  241. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3"
  242. messageName:@"name"
  243. contentData:m3ContentData
  244. renderingEffect:renderSetting3];
  245. self.m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3
  246. startTime:activeStartTime
  247. endTime:activeEndTime
  248. triggerDefinition:@[ contextualTriggerDefinition ]];
  249. FIRIAMMessageContentDataForTesting *m4ContentData = [[FIRIAMMessageContentDataForTesting alloc]
  250. initWithMessageTitle:@"m4 title"
  251. messageBody:@"message body"
  252. actionButtonText:nil
  253. secondaryActionButtonText:nil
  254. actionURL:[NSURL URLWithString:@"http://google.com"]
  255. secondaryActionURL:nil
  256. imageURL:[NSURL URLWithString:@"https://google.com/image"]
  257. landscapeImageURL:nil
  258. hasImageError:NO];
  259. FIRIAMRenderingEffectSetting *renderSetting4 =
  260. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  261. renderSetting4.viewMode = FIRIAMRenderAsImageOnlyView;
  262. FIRIAMMessageRenderData *renderData4 =
  263. [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m4"
  264. messageName:@"name"
  265. contentData:m4ContentData
  266. renderingEffect:renderSetting4];
  267. self.m4 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData4
  268. startTime:activeStartTime
  269. endTime:activeEndTime
  270. triggerDefinition:@[ appOpentriggerDefinition ]];
  271. }
  272. NSTimeInterval DISPLAY_MIN_INTERVALS = 1;
  273. - (void)setUp {
  274. [super setUp];
  275. [self setupMessageTexture];
  276. self.displaySetting = [[FIRIAMDisplaySetting alloc] init];
  277. self.displaySetting.displayMinIntervalInMinutes = DISPLAY_MIN_INTERVALS;
  278. self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper));
  279. FIRIAMFetchResponseParser *parser =
  280. [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]];
  281. self.clientMessageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper
  282. usingResponseParser:parser];
  283. self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher));
  284. self.mockActivityLogger = OCMClassMock([FIRIAMActivityLogger class]);
  285. self.mockAnalyticsEventLogger = OCMProtocolMock(@protocol(FIRIAMAnalyticsEventLogger));
  286. self.mockInAppMessaging = OCMClassMock([FIRInAppMessaging class]);
  287. self.mockActionURLFollower = OCMClassMock([FIRIAMActionURLFollower class]);
  288. self.displayExecutor =
  289. [[FIRIAMDisplayExecutor alloc] initWithInAppMessaging:self.mockInAppMessaging
  290. setting:self.displaySetting
  291. messageCache:self.clientMessageCache
  292. timeFetcher:self.mockTimeFetcher
  293. bookKeeper:self.mockBookkeeper
  294. actionURLFollower:self.mockActionURLFollower
  295. activityLogger:self.mockActivityLogger
  296. analyticsEventLogger:self.mockAnalyticsEventLogger];
  297. OCMStub([self.mockBookkeeper recordNewImpressionForMessage:[OCMArg any]
  298. withStartTimestampInSeconds:1000]);
  299. }
  300. - (void)testRegularMessageAvailableCase {
  301. // This setup allows next message to be displayed from display interval perspective.
  302. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  303. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  304. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  305. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  306. self.displayExecutor.messageDisplayComponent = display;
  307. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  308. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  309. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  310. XCTAssertEqual(1, remainingMsgCount);
  311. // Verify that the message content handed to display component is expected
  312. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  313. }
  314. - (void)testFollowingActionURL {
  315. // This setup allows next message to be displayed from display interval perspective.
  316. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  317. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  318. FIRInAppMessagingAction *testAction =
  319. [[FIRInAppMessagingAction alloc] initWithActionText:@"test"
  320. actionURL:self.m2.renderData.contentData.actionURL];
  321. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  322. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  323. action:testAction];
  324. self.displayExecutor.messageDisplayComponent = display;
  325. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  326. // not expecting triggering analytics recording
  327. OCMExpect([self.mockActionURLFollower
  328. followActionURL:[OCMArg isEqual:self.m2.renderData.contentData.actionURL]
  329. withCompletionBlock:[OCMArg any]]);
  330. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  331. OCMVerifyAll((id)self.mockActionURLFollower);
  332. }
  333. - (void)testFollowingActionURLForTestMessage {
  334. // This setup allows next message to be displayed from display interval perspective.
  335. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  336. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  337. FIRIAMMessageDefinition *testMessage =
  338. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData];
  339. FIRInAppMessagingAction *testAction = [[FIRInAppMessagingAction alloc]
  340. initWithActionText:@"test"
  341. actionURL:testMessage.renderData.contentData.actionURL];
  342. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  343. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick
  344. action:testAction];
  345. self.displayExecutor.messageDisplayComponent = display;
  346. [self.clientMessageCache setMessageData:@[ testMessage ]];
  347. // not expecting triggering analytics recording
  348. OCMExpect([self.mockActionURLFollower
  349. followActionURL:[OCMArg isEqual:testMessage.renderData.contentData.actionURL]
  350. withCompletionBlock:[OCMArg any]]);
  351. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  352. OCMVerifyAll((id)self.mockActionURLFollower);
  353. }
  354. - (void)testClientTestMessageAvailableCase {
  355. // When test message is present in cache, even if the display time interval has not been
  356. // reached, we still render.
  357. // 10 seconds is less than DISPLAY_MIN_INTERVALS minutes, so we have not reached
  358. // minimal display time interval yet.
  359. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10);
  360. FIRIAMMessageDefinition *testMessage =
  361. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData];
  362. [self.clientMessageCache setMessageData:@[ self.m2, testMessage, self.m4 ]];
  363. // We have test message in the cache now.
  364. XCTAssertTrue([self.clientMessageCache hasTestMessage]);
  365. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  366. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  367. self.displayExecutor.messageDisplayComponent = display;
  368. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  369. // No more test message in the cache now.
  370. XCTAssertFalse([self.clientMessageCache hasTestMessage]);
  371. }
  372. // If a message is still being displayed, we won't try to display a second one on top of it
  373. - (void)testNoDualDisplay {
  374. // This setup allows next message to be displayed from display interval perspective.
  375. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  376. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  377. // This display component only detects a valid impression, but does not end the renderig
  378. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  379. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  380. self.displayExecutor.messageDisplayComponent = display;
  381. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  382. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  383. // m2 is being rendered
  384. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  385. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  386. XCTAssertEqual(1, remainingMsgCount);
  387. // try to display again when the in-display flag is already turned on (and not turned off yet)
  388. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  389. // Verify that the message in display component is still m2
  390. XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID);
  391. // message in cache remain unchanged for the second checkAndDisplayNext call
  392. remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  393. XCTAssertEqual(1, remainingMsgCount);
  394. }
  395. // this test case contracts testNoAnalyticsTrackingOnTestMessage to cover both positive
  396. // and negative cases
  397. - (void)testDoesAnalyticsTrackingOnNonTestMessage {
  398. // This setup allows next message to be displayed from display interval perspective.
  399. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  400. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  401. // not expecting triggering analytics recording
  402. OCMExpect([self.mockAnalyticsEventLogger
  403. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  404. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  405. withCampaignName:[OCMArg any]
  406. eventTimeInMs:[OCMArg any]
  407. completion:[OCMArg any]]);
  408. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  409. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  410. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  411. self.displayExecutor.messageDisplayComponent = display;
  412. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  413. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  414. }
  415. - (void)testDoesAnalyticsTrackingOnDisplayError {
  416. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  417. // last display time is set to 0 by default
  418. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  419. // not expecting triggering analytics recording
  420. OCMExpect([self.mockAnalyticsEventLogger
  421. logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError
  422. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  423. withCampaignName:[OCMArg any]
  424. eventTimeInMs:[OCMArg any]
  425. completion:[OCMArg any]]);
  426. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  427. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  428. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError];
  429. self.displayExecutor.messageDisplayComponent = display;
  430. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  431. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  432. }
  433. - (void)testAnalyticsTrackingOnMessageDismissCase {
  434. // This setup allows next message to be displayed from display interval perspective.
  435. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  436. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  437. // not expecting triggering analytics recording
  438. OCMExpect([self.mockAnalyticsEventLogger
  439. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageDismissAuto
  440. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  441. withCampaignName:[OCMArg any]
  442. eventTimeInMs:[OCMArg any]
  443. completion:[OCMArg any]]);
  444. // Make sure we don't log the url follow event.
  445. OCMReject([self.mockAnalyticsEventLogger
  446. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  447. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  448. withCampaignName:[OCMArg any]
  449. eventTimeInMs:[OCMArg any]
  450. completion:[OCMArg any]]);
  451. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  452. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  453. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  454. self.displayExecutor.messageDisplayComponent = display;
  455. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  456. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  457. }
  458. - (void)testAnalyticsTrackingOnMessageClickCase {
  459. // This setup allows next message to be displayed from display interval perspective.
  460. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  461. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  462. // We expect two analytics events for a click action:
  463. // An impression event and an action URL follow event
  464. OCMExpect([self.mockAnalyticsEventLogger
  465. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  466. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  467. withCampaignName:[OCMArg any]
  468. eventTimeInMs:[OCMArg any]
  469. completion:[OCMArg any]]);
  470. OCMExpect([self.mockAnalyticsEventLogger
  471. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  472. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  473. withCampaignName:[OCMArg any]
  474. eventTimeInMs:[OCMArg any]
  475. completion:[OCMArg any]]);
  476. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  477. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  478. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  479. self.displayExecutor.messageDisplayComponent = display;
  480. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  481. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  482. }
  483. - (void)testAnalyticsTrackingOnTestMessageClickCase {
  484. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  485. // last display time is set to 0 by default
  486. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  487. // We expect two analytics events for a click action:
  488. // An test message impression event and a test message click event
  489. OCMExpect([self.mockAnalyticsEventLogger
  490. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  491. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  492. withCampaignName:[OCMArg any]
  493. eventTimeInMs:[OCMArg any]
  494. completion:[OCMArg any]]);
  495. OCMExpect([self.mockAnalyticsEventLogger
  496. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  497. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  498. withCampaignName:[OCMArg any]
  499. eventTimeInMs:[OCMArg any]
  500. completion:[OCMArg any]]);
  501. FIRIAMMessageDefinition *testMessage =
  502. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData];
  503. [self.clientMessageCache setMessageData:@[ testMessage ]];
  504. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  505. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  506. self.displayExecutor.messageDisplayComponent = display;
  507. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  508. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  509. }
  510. - (void)testAnalyticsTrackingOnTestMessageDismissCase {
  511. // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes
  512. // last display time is set to 0 by default
  513. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000);
  514. // We expect a test message impression
  515. OCMExpect([self.mockAnalyticsEventLogger
  516. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  517. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  518. withCampaignName:[OCMArg any]
  519. eventTimeInMs:[OCMArg any]
  520. completion:[OCMArg any]]);
  521. // No click event
  522. OCMReject([self.mockAnalyticsEventLogger
  523. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  524. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  525. withCampaignName:[OCMArg any]
  526. eventTimeInMs:[OCMArg any]
  527. completion:[OCMArg any]]);
  528. FIRIAMMessageDefinition *testMessage =
  529. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData];
  530. [self.clientMessageCache setMessageData:@[ testMessage ]];
  531. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  532. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  533. self.displayExecutor.messageDisplayComponent = display;
  534. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  535. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  536. }
  537. - (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCase {
  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:FIRIAMAnalyticsEventMessageImpression
  544. forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID]
  545. withCampaignName:[OCMArg any]
  546. eventTimeInMs:[OCMArg any]
  547. completion:[OCMArg any]]);
  548. [self.clientMessageCache setMessageData:@[ self.m2 ]];
  549. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  550. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  551. self.displayExecutor.messageDisplayComponent = display;
  552. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  553. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  554. }
  555. - (void)testNoAnalyticsTrackingOnTestMessage {
  556. // This setup allows next message to be displayed from display interval perspective.
  557. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  558. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  559. FIRIAMMessageDefinition *testMessage =
  560. [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData];
  561. // not expecting triggering analytics recording
  562. OCMReject([self.mockAnalyticsEventLogger
  563. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  564. forCampaignID:[OCMArg isEqual:self.m1.renderData.messageID]
  565. withCampaignName:[OCMArg any]
  566. eventTimeInMs:[OCMArg any]
  567. completion:[OCMArg any]]);
  568. [self.clientMessageCache setMessageData:@[ testMessage ]];
  569. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  570. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  571. self.displayExecutor.messageDisplayComponent = display;
  572. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  573. OCMVerifyAll((id)self.mockAnalyticsEventLogger);
  574. }
  575. - (void)testNoMessageAvailableCase {
  576. // This setup allows next message to be displayed from display interval perspective.
  577. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  578. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  579. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  580. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  581. self.displayExecutor.messageDisplayComponent = display;
  582. [self.clientMessageCache setMessageData:@[]];
  583. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  584. // No display has happened so the message stored in the display component should be nil
  585. XCTAssertNil(display.message);
  586. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  587. XCTAssertEqual(0, remainingMsgCount);
  588. }
  589. - (void)testIntervalBetweenOnAppOpenDisplays {
  590. self.displaySetting.displayMinIntervalInMinutes = 10;
  591. // last display time is set to 0 by default
  592. // 10 seconds is not long enough for satisfying the 10-min internal requirement
  593. OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10);
  594. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  595. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  596. self.displayExecutor.messageDisplayComponent = display;
  597. [self.clientMessageCache setMessageData:@[ self.m1 ]];
  598. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  599. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  600. // No display has happened so the message stored in the display component should be nil
  601. XCTAssertNil(display.message);
  602. // still got one in the queue
  603. XCTAssertEqual(1, remainingMsgCount);
  604. }
  605. // making sure that we match on the event names for analytics based events
  606. - (void)testOnFirebaseAnalyticsEventDisplayMessages {
  607. // This setup allows next message to be displayed from display interval perspective.
  608. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  609. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  610. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  611. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  612. self.displayExecutor.messageDisplayComponent = display;
  613. // m1 and m3 are messages triggered by 'test_event' analytics events
  614. [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]];
  615. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"different event"];
  616. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  617. // No message matching event "different event", so no message is nil
  618. XCTAssertNil(display.message);
  619. // still got 2 in the queue
  620. XCTAssertEqual(2, remainingMsgCount);
  621. // now trigger it with 'test_event' and we would expect one message to be displayed and removed
  622. // from cache
  623. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  624. // Expecting the m1 being used for display
  625. XCTAssertEqualObjects(self.m1.renderData.messageID, display.message.campaignInfo.messageID);
  626. remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  627. // Now only one message remaining in the queue
  628. XCTAssertEqual(1, remainingMsgCount);
  629. }
  630. // no regular message rendering if suppress message display flag is turned on
  631. - (void)testNoRenderingIfMessageDisplayIsSuppressed {
  632. // This setup allows next message to be displayed from display interval perspective.
  633. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  634. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  635. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  636. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  637. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  638. self.displayExecutor.messageDisplayComponent = display;
  639. self.displayExecutor.suppressMessageDisplay = YES;
  640. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  641. // no message display has happened
  642. XCTAssertNil(display.message);
  643. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  644. // no message is removed from the cache
  645. XCTAssertEqual(2, remainingMsgCount);
  646. // now allow message rendering again
  647. self.displayExecutor.suppressMessageDisplay = NO;
  648. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  649. NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count;
  650. // one message was rendered and removed from the cache
  651. XCTAssertEqual(1, remainingMsgCount2);
  652. }
  653. // No contextual message rendering if suppress message display flag is turned on
  654. - (void)testNoContextualMsgRenderingIfMessageDisplayIsSuppressed {
  655. // This setup allows next message to be displayed from display interval perspective.
  656. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  657. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  658. [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]];
  659. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  660. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  661. self.displayExecutor.messageDisplayComponent = display;
  662. self.displayExecutor.suppressMessageDisplay = YES;
  663. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  664. // no message display has happened
  665. XCTAssertNil(display.message);
  666. NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count;
  667. // No message is removed from the cache.
  668. XCTAssertEqual(2, remainingMsgCount);
  669. // now re-enable message rendering again
  670. self.displayExecutor.suppressMessageDisplay = NO;
  671. [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"];
  672. NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count;
  673. // one message was rendered and removed from the cache
  674. XCTAssertEqual(1, remainingMsgCount2);
  675. }
  676. - (void)testMessageClickedCallback {
  677. FIRInAppMessagingDisplayTestDelegate *delegate =
  678. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  679. self.mockInAppMessaging.delegate = delegate;
  680. // This setup allows next message to be displayed from display interval perspective.
  681. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  682. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  683. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  684. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  685. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick];
  686. self.displayExecutor.messageDisplayComponent = display;
  687. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  688. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  689. XCTAssertTrue(delegate.receivedMessageClickedCallback);
  690. }
  691. - (void)testMessageImpressionCallback {
  692. FIRInAppMessagingDisplayTestDelegate *delegate =
  693. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  694. self.mockInAppMessaging.delegate = delegate;
  695. // This setup allows next message to be displayed from display interval perspective.
  696. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  697. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  698. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  699. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  700. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected];
  701. self.displayExecutor.messageDisplayComponent = display;
  702. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  703. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  704. // Verify that the message content handed to display component is expected
  705. XCTAssertTrue(delegate.receivedMessageImpressionCallback);
  706. }
  707. - (void)testMessageErrorCallback {
  708. FIRInAppMessagingDisplayTestDelegate *delegate =
  709. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  710. self.mockInAppMessaging.delegate = delegate;
  711. // This setup allows next message to be displayed from display interval perspective.
  712. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  713. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  714. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  715. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  716. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError];
  717. self.displayExecutor.messageDisplayComponent = display;
  718. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  719. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  720. // Verify that the message content handed to display component is expected
  721. XCTAssertTrue(delegate.receivedMessageErrorCallback);
  722. }
  723. - (void)testMessageDismissedCallback {
  724. FIRInAppMessagingDisplayTestDelegate *delegate =
  725. [[FIRInAppMessagingDisplayTestDelegate alloc] init];
  726. self.mockInAppMessaging.delegate = delegate;
  727. // This setup allows next message to be displayed from display interval perspective.
  728. OCMStub([self.mockTimeFetcher currentTimestampInSeconds])
  729. .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100);
  730. OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate);
  731. FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc]
  732. initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss];
  733. self.displayExecutor.messageDisplayComponent = display;
  734. [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]];
  735. [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
  736. // Verify that the message content handed to display component is expected
  737. XCTAssertTrue(delegate.receivedMessageDismissedCallback);
  738. }
  739. @end