From 3ccee78a82e9d205fc2b0b217e3beb7f5af724e5 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 14 Oct 2024 12:35:13 +0500 Subject: [PATCH 1/9] Some fixes for view duration and added tests --- Countly.m | 4 +- Countly.xcodeproj/project.pbxproj | 4 + CountlyTests/CountlyViewTests.swift | 199 ++++++++++++++++++++++++++++ CountlyViewData.h | 2 +- CountlyViewData.m | 7 +- CountlyViewTrackingInternal.m | 8 +- 6 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 CountlyTests/CountlyViewTests.swift diff --git a/Countly.m b/Countly.m index d6e46173..f97c7629 100644 --- a/Countly.m +++ b/Countly.m @@ -400,13 +400,13 @@ - (void)suspend isSuspended = YES; + [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; if (!CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance endSession]; - [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; - [CountlyPersistency.sharedInstance saveToFile]; } diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 1394aab3..ac965640 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 3961C6BA2C6633C000DD38BA /* CountlyWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */; }; 3964A3E72C2AF8E90091E677 /* CountlySegmentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */; }; 3966DBCF2C11EE270002ED97 /* CountlyDeviceIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */; }; + 3969D0232CB80848000F8A32 /* CountlyViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3969D0222CB80848000F8A32 /* CountlyViewTests.swift */; }; 3972EDDB2C08A38D00EB9D3E /* CountlyEventStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */; }; 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */; }; 399117D12C69F73D00DC4C66 /* CountlyContentBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */; }; @@ -131,6 +132,7 @@ 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyWebViewManager.m; sourceTree = ""; }; 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlySegmentationTests.swift; sourceTree = ""; }; 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyDeviceIDTests.swift; sourceTree = ""; }; + 3969D0222CB80848000F8A32 /* CountlyViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyViewTests.swift; sourceTree = ""; }; 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyEventStruct.swift; sourceTree = ""; }; 3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyUserProfileTests.swift; sourceTree = ""; }; 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyContentBuilder.m; sourceTree = ""; }; @@ -217,6 +219,7 @@ 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */, 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */, 399B464F2C52813700AD384E /* CountlyLocationTests.swift */, + 3969D0222CB80848000F8A32 /* CountlyViewTests.swift */, ); path = CountlyTests; sourceTree = ""; @@ -463,6 +466,7 @@ buildActionMask = 2147483647; files = ( 1A5C4C972B35B0850032EE1F /* CountlyTests.swift in Sources */, + 3969D0232CB80848000F8A32 /* CountlyViewTests.swift in Sources */, 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */, 1A50D7052B3C5AA3009C6938 /* CountlyBaseTestCase.swift in Sources */, 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */, diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift new file mode 100644 index 00000000..54f7c1de --- /dev/null +++ b/CountlyTests/CountlyViewTests.swift @@ -0,0 +1,199 @@ +// +// CountlyViewTrackingTests.swift +// CountlyTests +// +// Copyright © 2024 Countly. All rights reserved. +// + +import XCTest +@testable import Countly + +class CountlyViewTrackingTests: CountlyBaseTestCase { + + func checkPersistentValues() { + let countlyPersistency = CountlyPersistency.sharedInstance() + if(countlyPersistency != nil) { + if let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? NSMutableArray, + let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? NSMutableArray, + let startedEvents = CountlyPersistency.sharedInstance().value(forKey: "startedEvents") as? NSMutableDictionary, + let isQueueBeingModified = CountlyPersistency.sharedInstance().value(forKey: "isQueueBeingModified") as? Bool { + print("Successfully access private properties.") + + + } + else { + print("Failed to access private properties.") + } + } + + } + + func testViewForegroundBackground() { + let config = createBaseConfig() + // No Device ID provided during init + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + + let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseViewExpectation.fulfill() + } + + let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") + DispatchQueue.global().asyncAfter(deadline: .now() + 10) { // Delayed by 10 seconds + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeViewExpectation.fulfill() + } + + let bgExpectation = XCTestExpectation(description: "Wait for background notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 15) { // Delayed by 15 seconds + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + bgExpectation.fulfill() + } + + let fgExpectation = XCTestExpectation(description: "Wait for active notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 20) { // Delayed by 20 seconds + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + fgExpectation.fulfill() + } + + let bgExpectation1 = XCTestExpectation(description: "Wait for second background notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 25) { // Delayed by 25 seconds + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + bgExpectation1.fulfill() + } + + let fgExpectation1 = XCTestExpectation(description: "Wait for second active notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 30) { // Delayed by 30 seconds + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + fgExpectation1.fulfill() + } + + // Wait for all expectations or timeout + wait(for: [pauseViewExpectation, resumeViewExpectation, bgExpectation, fgExpectation, bgExpectation1, fgExpectation1], timeout: 35) + + let viewID1 = Countly.sharedInstance().views().startView("startView") + + checkPersistentValues() + Countly.sharedInstance().views().stopAllViews(nil); + + checkPersistentValues() + } + + + func testViewTrackingInit_CNR_AV() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + + let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseViewExpectation.fulfill() + } + + wait(for: [pauseViewExpectation], timeout: 10) + } + + func testViewTrackingInit_CR_CNG_AV() throws { + let config = createBaseConfig() + config.requiresConsent = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithoutConsent") + XCTAssertNil(viewID, "Auto-stopped view should not be started when consent is not given.") + } + + func testViewTrackingInit_CR_CGV_AV() throws { + let config = createBaseConfig() + config.requiresConsent = true + config.consents = [CLYConsent.viewTracking] + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithConsent") + XCTAssertNotNil(viewID, "Auto-stopped view should be started when view tracking consent is given.") + } + + func testManualViewTrackingInit_CNR_MV() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestManualView") + XCTAssertNotNil(viewID, "Manual view should be started successfully.") + + Countly.sharedInstance().views().stopView(withID: viewID) + } + + func testManualViewTrackingInit_CR_CNG_MV() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestManualViewWithoutConsent") + XCTAssertNil(viewID, "Manual view should not be started when consent is not given.") + } + + func testManualViewTrackingInit_CR_CGV_MV() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + config.consents = [CLYConsent.viewTracking] + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestManualViewWithConsent") + XCTAssertNotNil(viewID, "Manual view should be started when view tracking consent is given.") + + Countly.sharedInstance().views().stopView(withID: viewID) + } + + func testPauseAndResumeViewTracking() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestViewPauseResume") + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + + let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") + DispatchQueue.global().asyncAfter(deadline: .now() + 2) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseViewExpectation.fulfill() + } + + let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") + DispatchQueue.global().asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeViewExpectation.fulfill() + } + + wait(for: [pauseViewExpectation, resumeViewExpectation], timeout: 10) + } + + func testViewTrackingWithBackgroundAndForegroundNotifications() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestViewNotifications") + + let bgExpectation = XCTestExpectation(description: "Wait for background notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + bgExpectation.fulfill() + } + + let fgExpectation = XCTestExpectation(description: "Wait for foreground notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 10) { + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + fgExpectation.fulfill() + } + + wait(for: [bgExpectation, fgExpectation], timeout: 15) + XCTAssertNotNil(viewID, "View should handle background and foreground notifications correctly.") + } +} diff --git a/CountlyViewData.h b/CountlyViewData.h index d56dc687..e2f40f18 100644 --- a/CountlyViewData.h +++ b/CountlyViewData.h @@ -60,7 +60,7 @@ * Duration of the view * @discussion it returns the duration of view in foreground after view started. */ -- (NSTimeInterval)duration; +- (NSInteger)duration; /** diff --git a/CountlyViewData.m b/CountlyViewData.m index 28ddb6f3..ff8b58d0 100644 --- a/CountlyViewData.m +++ b/CountlyViewData.m @@ -23,17 +23,18 @@ - (instancetype)initWithID:(NSString *)viewID viewName:(NSString *)viewName return self; } -- (NSTimeInterval)duration +- (NSInteger)duration { NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - self.viewStartTime; - return duration; + return (NSInteger)round(duration); // Rounds to the nearest integer, to fix long value converted to 0 on server side. } - (void)pauseView { if (self.viewStartTime) { - self.viewStartTime = 0; + // For safe side we have set the value to current time stamp instead of 0 when pausing the view, as setting it to 0 could result in an invalid duration value. + self.viewStartTime = CountlyCommon.sharedInstance.uniqueTimestamp; } } diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 17c50dc2..89af7504 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -377,10 +377,10 @@ - (void)stopViewWithIDInternal:(NSString *) viewKey customSegmentation:(NSDictio segmentation[kCountlyVTKeyName] = viewData.viewName; segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; - NSTimeInterval duration = viewData.duration; + NSInteger duration = viewData.duration; [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventView segmentation:segmentation count:1 sum:0 duration:duration ID:viewData.viewID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; - CLY_LOG_D(@"%s View tracking ended: %@ duration: %.17g", __FUNCTION__, viewData.viewName, duration); + CLY_LOG_D(@"%s View tracking ended: %@ duration: %ld", __FUNCTION__, viewData.viewName, (long)duration); if (!autoPaused) { [self.viewDataDictionary removeObjectForKey:viewKey]; } @@ -511,7 +511,7 @@ -(CountlyViewData* ) currentView - (void)stopAutoStoppedView { CountlyViewData* currentView = self.currentView; - if (currentView && currentView.isAutoStoppedView) + if (currentView && currentView.isAutoStoppedView && !currentView.willStartAgain) { [self stopViewWithIDInternal:self.currentView.viewID customSegmentation:nil]; } @@ -537,8 +537,8 @@ - (void)stopRunningViewsInternal - (void)pauseViewInternal:(CountlyViewData*) viewData { - [viewData pauseView]; [self stopViewWithIDInternal:viewData.viewID customSegmentation:nil autoPaused:YES]; + [viewData pauseView]; } - (void)startStoppedViewsInternal From 520ab8e708be6a6cb44773b596558f710444f676 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 21 Oct 2024 21:32:22 +0500 Subject: [PATCH 2/9] WIP : View tests --- CountlyTests/CountlyBaseTestCase.swift | 2 +- CountlyTests/CountlyViewTests.swift | 524 ++++++++++++++++++++----- CountlyViewTrackingInternal.m | 2 +- 3 files changed, 422 insertions(+), 106 deletions(-) diff --git a/CountlyTests/CountlyBaseTestCase.swift b/CountlyTests/CountlyBaseTestCase.swift index 90a5f60e..d786fa86 100644 --- a/CountlyTests/CountlyBaseTestCase.swift +++ b/CountlyTests/CountlyBaseTestCase.swift @@ -13,7 +13,7 @@ class CountlyBaseTestCase: XCTestCase { var countly: Countly! var deviceID: String = "" let appKey: String = "appkey" - var host: String = "https://test.count.ly/" + var host: String = "https://testing.count.ly/" override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 54f7c1de..38dee23b 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -10,190 +10,506 @@ import XCTest class CountlyViewTrackingTests: CountlyBaseTestCase { - func checkPersistentValues() { - let countlyPersistency = CountlyPersistency.sharedInstance() - if(countlyPersistency != nil) { - if let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? NSMutableArray, - let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? NSMutableArray, - let startedEvents = CountlyPersistency.sharedInstance().value(forKey: "startedEvents") as? NSMutableDictionary, - let isQueueBeingModified = CountlyPersistency.sharedInstance().value(forKey: "isQueueBeingModified") as? Bool { - print("Successfully access private properties.") - - - } - else { - print("Failed to access private properties.") - } + func testStartAndStopView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + expectation.fulfill() } + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") } - func testViewForegroundBackground() { + func testStartAndStopViewWithSegmentation() throws { let config = createBaseConfig() - // No Device ID provided during init Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + let viewID = Countly.sharedInstance().views().startView("View1", segmentation: ["key": "value"]) + XCTAssertNotNil(viewID, "View should be started successfully with segmentation.") - let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { - Countly.sharedInstance().views().pauseView(withID: viewID) - pauseViewExpectation.fulfill() + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + expectation.fulfill() } - let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") - DispatchQueue.global().asyncAfter(deadline: .now() + 10) { // Delayed by 10 seconds - Countly.sharedInstance().views().resumeView(withID: viewID) - resumeViewExpectation.fulfill() - } + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) + } + + func testStartViewAndStopViewWithID() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) - let bgExpectation = XCTestExpectation(description: "Wait for background notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 15) { // Delayed by 15 seconds - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - bgExpectation.fulfill() + let viewID = Countly.sharedInstance().views().startView("View1") ?? "" + XCTAssertNotNil(viewID, "View should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withID: viewID) + expectation.fulfill() } - let fgExpectation = XCTestExpectation(description: "Wait for active notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 20) { // Delayed by 20 seconds - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - fgExpectation.fulfill() + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(withID: viewID) + } + + func testStartAndStopMultipleViewsIncludingAutoStoppedViews() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID1 = Countly.sharedInstance().views().startView("View1") + let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 5 seconds before stopping the views.") + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID2) + expectation.fulfill() } - let bgExpectation1 = XCTestExpectation(description: "Wait for second background notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 25) { // Delayed by 25 seconds - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - bgExpectation1.fulfill() + wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + checkRecordedEventsForView(withID: viewID2) + } + + func testPauseAndResumeViewsForMultipleViews() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID1 = Countly.sharedInstance().views().startView("View1") + let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().pauseView(withID: viewID1) + + // Now wait for 3 seconds before resuming the view + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().resumeView(withID: viewID1) + + // Wait for another 4 seconds before stopping the views + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID2) + expectation.fulfill() + } + } } - let fgExpectation1 = XCTestExpectation(description: "Wait for second active notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 30) { // Delayed by 30 seconds - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - fgExpectation1.fulfill() + wait(for: [expectation], timeout: 15.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + checkRecordedEventsForView(withID: viewID2) + } + + func testMultiplePauseAndResumeCyclesOnSameView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().pauseView(withID: viewID) + + let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().resumeView(withID: viewID) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + self.wait(for: [resumeExpectation], timeout: 13.0) } - // Wait for all expectations or timeout - wait(for: [pauseViewExpectation, resumeViewExpectation, bgExpectation, fgExpectation, bgExpectation1, fgExpectation1], timeout: 35) + wait(for: [expectation], timeout: 10.0) + checkRecordedEventsForView(viewName: "View1") + } + + func testStartViewWhileAutoViewTrackingEnabled() throws { + let config = createBaseConfig() + config.enableAutomaticViewTracking = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNil(viewID, "Manual view tracking should be ignored when auto view tracking is enabled.") + } + + func testStartAndStopAutoStoppedViewWithSegmentation() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) - let viewID1 = Countly.sharedInstance().views().startView("startView") + let viewID = Countly.sharedInstance().views().startAutoStoppedView("View1", segmentation: ["key": "value"]) + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully with segmentation.") - checkPersistentValues() - Countly.sharedInstance().views().stopAllViews(nil); + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withID: viewID) + expectation.fulfill() + } - checkPersistentValues() + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } - - func testViewTrackingInit_CNR_AV() throws { + func testStartAutoStoppedViewAndInitiateAnother() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + let viewID1 = Countly.sharedInstance().views().startAutoStoppedView("View1") + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + var viewID2 = "" + let expectation = XCTestExpectation(description: "Wait for 4 seconds before starting the second view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + let stopExpectation = XCTestExpectation(description: "Wait for 3 seconds before stopping both views.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withID: viewID1) + Countly.sharedInstance().views().stopView(withID: viewID2) + + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + + wait(for: [expectation], timeout: 6.0) + checkRecordedEventsForView(withID: viewID1) + checkRecordedEventsForView(withID: viewID2) + } + + func testStartRegularViewPauseAndResumeMultipleTimesThenStop() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) - XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") - let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + let expectation = XCTestExpectation(description: "Wait for 3 seconds before pausing the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().pauseView(withID: viewID) - pauseViewExpectation.fulfill() + + let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().resumeView(withID: viewID) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + self.wait(for: [resumeExpectation], timeout: 7.0) } - wait(for: [pauseViewExpectation], timeout: 10) + wait(for: [expectation], timeout: 10.0) + checkRecordedEventsForView(viewName: "View1") } + - func testViewTrackingInit_CR_CNG_AV() throws { + func testStopAllViewsWithSpecificSegmentation() throws { let config = createBaseConfig() - config.requiresConsent = true Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithoutConsent") - XCTAssertNil(viewID, "Auto-stopped view should not be started when consent is not given.") + Countly.sharedInstance().views().startView("View1") + Countly.sharedInstance().views().startView("View2") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping all views.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopAllViews(["key": "value"]) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkAllViewsStoppedWithSegmentation(["key": "value"]) } - func testViewTrackingInit_CR_CGV_AV() throws { + func testAddSegmentationToAlreadyStartedViewUsingViewName() throws { let config = createBaseConfig() - config.requiresConsent = true - config.consents = [CLYConsent.viewTracking] Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithConsent") - XCTAssertNotNil(viewID, "Auto-stopped view should be started when view tracking consent is given.") + Countly.sharedInstance().views().startView("View1") + + let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value"]) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + + wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) } - func testManualViewTrackingInit_CNR_MV() throws { + func testAddSegmentationToAlreadyStartedViewUsingViewID() throws { let config = createBaseConfig() - config.manualSessionHandling = true Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestManualView") - XCTAssertNotNil(viewID, "Manual view should be started successfully.") + let viewID = Countly.sharedInstance().views().startView("View1") - Countly.sharedInstance().views().stopView(withID: viewID) + let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().addSegmentationToView(withID: viewID, segmentation: ["key": "value"]) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withID: viewID) + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + + wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } - func testManualViewTrackingInit_CR_CNG_MV() throws { + func testStartViewWithConsentNotGiven() throws { let config = createBaseConfig() - config.manualSessionHandling = true config.requiresConsent = true Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestManualViewWithoutConsent") - XCTAssertNil(viewID, "Manual view should not be started when consent is not given.") + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNil(viewID, "Event should not be recorded when consent is not given.") + + Countly.sharedInstance().views().stopView(withName: "View1") // This should also not affect recorded events + checkNoRecordedEvents() } - func testManualViewTrackingInit_CR_CGV_MV() throws { + func testSetAndUpdateGlobalViewSegmentationWithViewInteractions() throws { let config = createBaseConfig() - config.manualSessionHandling = true - config.requiresConsent = true - config.consents = [CLYConsent.viewTracking] Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestManualViewWithConsent") - XCTAssertNotNil(viewID, "Manual view should be started when view tracking consent is given.") + Countly.sharedInstance().views().startView("View1") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View1.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + + Countly.sharedInstance().views().setGlobalViewSegmentation(["key": "value"]) + + let startExpectation = XCTestExpectation(description: "Wait for 3 seconds before starting View2.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().startView("View2") + Countly.sharedInstance().views().updateGlobalViewSegmentation(["key": "newValue"]) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View2.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View2") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + self.wait(for: [startExpectation], timeout: 7.0) + } - Countly.sharedInstance().views().stopView(withID: viewID) + wait(for: [expectation], timeout: 10.0) // Wait for the expectation to be fulfilled + checkGlobalSegmentationApplied(expected: ["key": "newValue"]) } - func testPauseAndResumeViewTracking() throws { + func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestViewPauseResume") - XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + Countly.sharedInstance().views().startView("View1") - let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { - Countly.sharedInstance().views().pauseView(withID: viewID) - pauseViewExpectation.fulfill() + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") - DispatchQueue.global().asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().resumeView(withID: viewID) - resumeViewExpectation.fulfill() + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + } + + func testStartMultipleViewsMoveAppToBackgroundAndReturnToForeground() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + Countly.sharedInstance().views().startView("View1") + Countly.sharedInstance().views().startAutoStoppedView("View2") + + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withName: "View2") + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [pauseViewExpectation, resumeViewExpectation], timeout: 10) + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + checkRecordedEventsForView(viewName: "View2") } - func testViewTrackingWithBackgroundAndForegroundNotifications() throws { + func testStartViewBackgroundAppResumeViewWhenReturningToForeground() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestViewNotifications") + Countly.sharedInstance().views().startView("View1") - let bgExpectation = XCTestExpectation(description: "Wait for background notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - bgExpectation.fulfill() + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - let fgExpectation = XCTestExpectation(description: "Wait for foreground notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 10) { - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - fgExpectation.fulfill() + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + } + + func testAttemptToStopANonStartedView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + // Attempt to stop a non-started view +// let beforeEventCount = getRecordedEventCount() + Countly.sharedInstance().views().stopView(withName: "ViewNotStarted") +// let afterEventCount = getRecordedEventCount() + +// XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") + } + + func testUpdateSegmentationMultipleTimesOnTheSameView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + let waitForStart = XCTestExpectation(description: "Wait for 4 seconds before adding segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) + + let waitForSecondSegmentation = XCTestExpectation(description: "Wait for 4 seconds before adding second segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) + + let waitForStop = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForStop.fulfill() + } + self.wait(for: [waitForStop], timeout: 5.0) + } + self.wait(for: [waitForSecondSegmentation], timeout: 5.0) + } + + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply + } + + func testBackgroundAndForegroundTriggers() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + Countly.sharedInstance().views().startView("View1") + + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [bgExpectation, fgExpectation], timeout: 15) - XCTAssertNotNil(viewID, "View should handle background and foreground notifications correctly.") + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + } + + + // Helper methods to validate results + private func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { + // Implement your logic to check recorded events for the specified view + } + + private func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { + // Implement your logic to check recorded events for the specified view ID + } + + private func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { + // Implement your logic to check that all views have been stopped with specific segmentation + } + + private func checkGlobalSegmentationApplied(expected: [String: String]) { + // Implement your logic to verify global segmentation applied correctly + } + + private func checkNoRecordedEvents() { + // Implement logic to verify no recorded events } } diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 89af7504..de91ad17 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -10,7 +10,7 @@ @interface CountlyViewTrackingInternal () #if (TARGET_OS_IOS || TARGET_OS_TV) @property (nonatomic) NSMutableSet* automaticViewTrackingExclusionList; #endif -@property (nonatomic) NSMutableDictionary * viewDataDictionary; +@property (nonatomic, strong) NSMutableDictionary * viewDataDictionary; @property (nonatomic) NSMutableDictionary* viewSegmentation; @property (nonatomic) BOOL isFirstView; @end From 45865223ff7fa70a801a992fea5aed9b02654feb Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 22:05:59 +0500 Subject: [PATCH 3/9] fixed views test to wait expectations by combining them --- CountlyTests/CountlyViewTests.swift | 499 +++++++++++++++++++--------- 1 file changed, 337 insertions(+), 162 deletions(-) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 38dee23b..1a0ac209 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -17,13 +17,17 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + let expectation = XCTestExpectation(description: "View should be stopped after 3 seconds.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withName: "View1") expectation.fulfill() } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the expectation to be fulfilled within 5 seconds + wait(for: [expectation], timeout: 5.0) + + // Check recorded events after view has been stopped checkRecordedEventsForView(viewName: "View1") } @@ -31,16 +35,22 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view with segmentation let viewID = Countly.sharedInstance().views().startView("View1", segmentation: ["key": "value"]) XCTAssertNotNil(viewID, "View should be started successfully with segmentation.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + let expectation = XCTestExpectation(description: "View should be stopped after 4 seconds.") + + // Stop the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") - expectation.fulfill() + expectation.fulfill() // Fulfill expectation once view is stopped } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the stop operation to complete within the timeout + wait(for: [expectation], timeout: 5.0) + + // Check recorded events for the view with segmentation checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) } @@ -48,16 +58,23 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("View1") ?? "" - XCTAssertNotNil(viewID, "View should be started successfully.") + guard let viewID = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View should be started successfully, but viewID is nil.") + return + } + + let expectation = XCTestExpectation(description: "View should be stopped after 3 seconds.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + // Stop the view after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withID: viewID) - expectation.fulfill() + expectation.fulfill() // Fulfill expectation once view is stopped } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the expectation to be fulfilled within 5 seconds + wait(for: [expectation], timeout: 5.0) + + // Check recorded events for the view using the viewID checkRecordedEventsForView(withID: viewID) } @@ -65,19 +82,30 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID1 = Countly.sharedInstance().views().startView("View1") - let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") - XCTAssertNotNil(viewID1, "View1 should be started successfully.") - XCTAssertNotNil(viewID2, "View2 should be started successfully.") + // Ensure views are started successfully + guard let viewID1 = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View1 should be started successfully.") + return + } + + guard let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") else { + XCTFail("View2 should be started successfully.") + return + } - let expectation = XCTestExpectation(description: "Wait for 5 seconds before stopping the views.") + let expectation = XCTestExpectation(description: "Views should be stopped after 5 seconds.") + + // Stop the views after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withID: viewID2) expectation.fulfill() } - wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for the stop operation to complete + wait(for: [expectation], timeout: 7.0) // Increased timeout to ensure sufficient time + + // Check recorded events for both views checkRecordedEventsForView(viewName: "View1") checkRecordedEventsForView(withID: viewID2) } @@ -86,29 +114,48 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID1 = Countly.sharedInstance().views().startView("View1") - let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + // Start views + guard let viewID1 = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View1 should be started successfully.") + return + } + + guard let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") else { + XCTFail("View2 should be started successfully.") + return + } + XCTAssertNotNil(viewID1, "View1 should be started successfully.") XCTAssertNotNil(viewID2, "View2 should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + // Create expectations + let pauseExpectation = XCTestExpectation(description: "Pause View1 after 4 seconds.") + let resumeExpectation = XCTestExpectation(description: "Resume View1 after pausing for 3 seconds.") + let stopExpectation = XCTestExpectation(description: "Stop both views after resuming View1 for 4 seconds.") + + // Pause View1 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().pauseView(withID: viewID1) - - // Now wait for 3 seconds before resuming the view - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().resumeView(withID: viewID1) - - // Wait for another 4 seconds before stopping the views - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - Countly.sharedInstance().views().stopView(withID: viewID2) - expectation.fulfill() - } - } + pauseExpectation.fulfill() } - wait(for: [expectation], timeout: 15.0) // Wait for the expectation to be fulfilled + // Resume View1 after an additional 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 7) { + Countly.sharedInstance().views().resumeView(withID: viewID1) + resumeExpectation.fulfill() + } + + // Stop both views after 4 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 11) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID2) + stopExpectation.fulfill() + } + + // Wait for expectations to be fulfilled + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 15.0) + + // Check recorded events for both views checkRecordedEventsForView(viewName: "View1") checkRecordedEventsForView(withID: viewID2) } @@ -117,37 +164,52 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("View1") + // Start view and assert it's started successfully + guard let viewID = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View1 should be started successfully.") + return + } XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + // Create expectations + let pauseExpectation = XCTestExpectation(description: "Pause View1 after 4 seconds.") + let resumeExpectation = XCTestExpectation(description: "Resume View1 after 3 seconds of pause.") + let stopExpectation = XCTestExpectation(description: "Stop View1 after another 4 seconds of resuming.") + + // Pause the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().pauseView(withID: viewID) - - let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().resumeView(withID: viewID) - - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - stopExpectation.fulfill() - } - self.wait(for: [stopExpectation], timeout: 5.0) - } - self.wait(for: [resumeExpectation], timeout: 13.0) + pauseExpectation.fulfill() + } + + // Resume the view after 3 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 7) { // 4 + 3 seconds + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeExpectation.fulfill() } - wait(for: [expectation], timeout: 10.0) + // Stop the view after another 4 seconds of resuming + DispatchQueue.main.asyncAfter(deadline: .now() + 11) { // 4 + 3 + 4 seconds + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + + // Wait for all expectations to be fulfilled + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + + // Check recorded events for the view checkRecordedEventsForView(viewName: "View1") } func testStartViewWhileAutoViewTrackingEnabled() throws { let config = createBaseConfig() - config.enableAutomaticViewTracking = true + config.enableAutomaticViewTracking = true // Enable auto view tracking Countly.sharedInstance().start(with: config) + // Start a manual view tracking call let viewID = Countly.sharedInstance().views().startView("View1") + + // Assert that manual view tracking returns nil when auto tracking is enabled XCTAssertNil(viewID, "Manual view tracking should be ignored when auto view tracking is enabled.") } @@ -155,42 +217,63 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("View1", segmentation: ["key": "value"]) + // Start the auto-stopped view with segmentation + guard let viewID = Countly.sharedInstance().views().startAutoStoppedView("View1", segmentation: ["key": "value"]) else { + XCTFail("Auto-stopped view should be started successfully with segmentation.") + return + } + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully with segmentation.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + // Create an expectation for stopping the view after 4 seconds + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the auto-stopped view.") + + // Stop the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withID: viewID) - expectation.fulfill() + stopExpectation.fulfill() } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the stop expectation + wait(for: [stopExpectation], timeout: 6.0) // Allow a small buffer beyond the 4-second delay + + // Check the recorded events for the view, including segmentation checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } - + func testStartAutoStoppedViewAndInitiateAnother() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) let viewID1 = Countly.sharedInstance().views().startAutoStoppedView("View1") XCTAssertNotNil(viewID1, "View1 should be started successfully.") + var viewID2 = "" - let expectation = XCTestExpectation(description: "Wait for 4 seconds before starting the second view.") + let startExpectation = XCTestExpectation(description: "Start second view after 4 seconds") + let stopExpectation = XCTestExpectation(description: "Stop both views after 3 seconds") + + // Start second view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") XCTAssertNotNil(viewID2, "View2 should be started successfully.") - let stopExpectation = XCTestExpectation(description: "Wait for 3 seconds before stopping both views.") + // Fulfill startExpectation after starting View2 + startExpectation.fulfill() + + // Stop both views after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withID: viewID1) Countly.sharedInstance().views().stopView(withID: viewID2) + // Fulfill stopExpectation after stopping both views stopExpectation.fulfill() } - self.wait(for: [stopExpectation], timeout: 5.0) } - wait(for: [expectation], timeout: 6.0) + // Wait for both expectations + wait(for: [startExpectation, stopExpectation], timeout: 10.0) + + // Check recorded events after views have been stopped checkRecordedEventsForView(withID: viewID1) checkRecordedEventsForView(withID: viewID2) } @@ -199,46 +282,63 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before pausing the view.") + // Create expectations + let pauseExpectation = XCTestExpectation(description: "Pause the view after 3 seconds") + let resumeExpectation = XCTestExpectation(description: "Resume the view after another 3 seconds") + let stopExpectation = XCTestExpectation(description: "Stop the view after 4 seconds") + + // Pause the view after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().pauseView(withID: viewID) - - let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().resumeView(withID: viewID) - - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - stopExpectation.fulfill() - } - self.wait(for: [stopExpectation], timeout: 5.0) - } - self.wait(for: [resumeExpectation], timeout: 7.0) + pauseExpectation.fulfill() + } + + // Resume the view after another 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 6) { + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeExpectation.fulfill() + } + + // Stop the view after 4 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() } - wait(for: [expectation], timeout: 10.0) + // Wait for all expectations to be fulfilled + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + + // Check recorded events after stopping the view checkRecordedEventsForView(viewName: "View1") } - func testStopAllViewsWithSpecificSegmentation() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - Countly.sharedInstance().views().startView("View1") - Countly.sharedInstance().views().startView("View2") + // Start multiple views + let viewID1 = Countly.sharedInstance().views().startView("View1") + let viewID2 = Countly.sharedInstance().views().startView("View2") + + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + // Create expectation for stopping all views + let stopAllViewsExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping all views.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping all views.") DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopAllViews(["key": "value"]) - expectation.fulfill() + stopAllViewsExpectation.fulfill() } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the expectation to be fulfilled + wait(for: [stopAllViewsExpectation], timeout: 5.0) + + // Verify that all views have been stopped with the specified segmentation checkAllViewsStoppedWithSegmentation(["key": "value"]) } @@ -246,21 +346,30 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - Countly.sharedInstance().views().startView("View1") + // Start the view and assert it starts successfully + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View1 should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + // Create expectations + let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + + // Add segmentation after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value"]) + addSegmentationExpectation.fulfill() - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + // Stop the view after another 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") - stopExpectation.fulfill() + stopViewExpectation.fulfill() } - self.wait(for: [stopExpectation], timeout: 5.0) } - wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) + + // Check if the recorded events include the added segmentation checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) } @@ -268,24 +377,72 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view and assert it starts successfully let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View1 should be started successfully.") + + // Create expectations + let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + // Add segmentation after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().addSegmentationToView(withID: viewID, segmentation: ["key": "value"]) + addSegmentationExpectation.fulfill() - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + // Stop the view after another 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withID: viewID) - stopExpectation.fulfill() + stopViewExpectation.fulfill() } - self.wait(for: [stopExpectation], timeout: 5.0) } - wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) + + // Check if the recorded events include the added segmentation checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } + // Refactor remaining tests similarly... + + func testUpdateSegmentationMultipleTimesOnTheSameView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + // Create expectations + let waitForStart = XCTestExpectation(description: "Wait for 4 seconds before adding segmentation.") + let waitForSecondSegmentation = XCTestExpectation(description: "Wait for 4 seconds before adding second segmentation.") + let waitForStop = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + + // Add first segmentation + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) + waitForStart.fulfill() + + // Add second segmentation + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) + waitForSecondSegmentation.fulfill() + + // Stop the view + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForStop.fulfill() + } + } + } + + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForSecondSegmentation, waitForStop], timeout: 12.0) + + // Check recorded events for the view with the last segmentation + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply + } + func testStartViewWithConsentNotGiven() throws { let config = createBaseConfig() config.requiresConsent = true @@ -302,60 +459,82 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the first view Countly.sharedInstance().views().startView("View1") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View1.") + // Create expectations for various events + let stopView1Expectation = XCTestExpectation(description: "Expect View1 to be stopped after 4 seconds.") + let startView2Expectation = XCTestExpectation(description: "Expect View2 to start after 3 seconds.") + let stopView2Expectation = XCTestExpectation(description: "Expect View2 to be stopped after 4 seconds.") + + // Stop View1 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") + stopView1Expectation.fulfill() // Fulfill View1 stop expectation + // Set global view segmentation Countly.sharedInstance().views().setGlobalViewSegmentation(["key": "value"]) - let startExpectation = XCTestExpectation(description: "Wait for 3 seconds before starting View2.") + // Start View2 after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().startView("View2") + startView2Expectation.fulfill() // Fulfill View2 start expectation + + // Update global view segmentation Countly.sharedInstance().views().updateGlobalViewSegmentation(["key": "newValue"]) - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View2.") + // Stop View2 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View2") - stopExpectation.fulfill() + stopView2Expectation.fulfill() // Fulfill View2 stop expectation } - self.wait(for: [stopExpectation], timeout: 5.0) } - self.wait(for: [startExpectation], timeout: 7.0) } - wait(for: [expectation], timeout: 10.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [stopView1Expectation, startView2Expectation, stopView2Expectation], timeout: 12.0) + + // Check if the global segmentation has been applied checkGlobalSegmentationApplied(expected: ["key": "newValue"]) } - + func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the first view Countly.sharedInstance().views().startView("View1") - let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + // Create expectations for various events + let backgroundAppExpectation = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitInBackgroundExpectation = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitAfterForegroundExpectation = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Background the app after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + backgroundAppExpectation.fulfill() // Fulfill background app expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitInBackgroundExpectation.fulfill() // Fulfill wait in background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Stop the view after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") - waitForForeground.fulfill() + waitAfterForegroundExpectation.fulfill() // Fulfill wait after foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [backgroundAppExpectation, waitInBackgroundExpectation, waitAfterForegroundExpectation], timeout: 12.0) + + // Check recorded events for the view after transitions checkRecordedEventsForView(viewName: "View1") } @@ -363,31 +542,41 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the views Countly.sharedInstance().views().startView("View1") Countly.sharedInstance().views().startAutoStoppedView("View2") + // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + waitForStart.fulfill() // Fulfill the start expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitForBackground.fulfill() // Fulfill the background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Stop the views after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withName: "View2") - waitForForeground.fulfill() + waitForForeground.fulfill() // Fulfill the foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + + // Check recorded events for the views after transitions checkRecordedEventsForView(viewName: "View1") checkRecordedEventsForView(viewName: "View2") } @@ -396,32 +585,42 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view Countly.sharedInstance().views().startView("View1") + // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + waitForStart.fulfill() // Fulfill the start expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitForBackground.fulfill() // Fulfill the background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Stop the view after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") - waitForForeground.fulfill() + waitForForeground.fulfill() // Fulfill the foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + + // Check recorded events for the view after transitions checkRecordedEventsForView(viewName: "View1") } - + func testAttemptToStopANonStartedView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) @@ -434,64 +633,40 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") } - func testUpdateSegmentationMultipleTimesOnTheSameView() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - let viewID = Countly.sharedInstance().views().startView("View1") - XCTAssertNotNil(viewID, "View should be started successfully.") - - let waitForStart = XCTestExpectation(description: "Wait for 4 seconds before adding segmentation.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) - - let waitForSecondSegmentation = XCTestExpectation(description: "Wait for 4 seconds before adding second segmentation.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) - - let waitForStop = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().stopView(withName: "View1") - waitForStop.fulfill() - } - self.wait(for: [waitForStop], timeout: 5.0) - } - self.wait(for: [waitForSecondSegmentation], timeout: 5.0) - } - - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply - } - func testBackgroundAndForegroundTriggers() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) Countly.sharedInstance().views().startView("View1") + // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + waitForStart.fulfill() // Fulfill the start expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitForBackground.fulfill() // Fulfill the background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - waitForForeground.fulfill() + waitForForeground.fulfill() // Fulfill the foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) } - // Helper methods to validate results private func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { // Implement your logic to check recorded events for the specified view From 38bdb224af10d8978bef234ecf1e487f6d5e3797 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 25 Oct 2024 21:45:55 +0500 Subject: [PATCH 4/9] Validating recorded views and queued request for views --- CountlyTests/CountlyEventStruct.swift | 8 +- CountlyTests/CountlyViewTests.swift | 357 +++++++++++++++++++++----- 2 files changed, 303 insertions(+), 62 deletions(-) diff --git a/CountlyTests/CountlyEventStruct.swift b/CountlyTests/CountlyEventStruct.swift index 15086096..921d89f6 100644 --- a/CountlyTests/CountlyEventStruct.swift +++ b/CountlyTests/CountlyEventStruct.swift @@ -61,9 +61,9 @@ struct AnyCodable: Codable { struct CountlyEventStruct: Codable { let key: String let ID: String - let CVID: String + let CVID: String? let PVID: String? - let PEID: String + let PEID: String? let segmentation: [String: Any]? let count: UInt let sum: Double @@ -81,9 +81,9 @@ struct CountlyEventStruct: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: .key) ID = try container.decode(String.self, forKey: .ID) - CVID = try container.decode(String.self, forKey: .CVID) + CVID = try container.decodeIfPresent(String.self, forKey: .CVID) PVID = try container.decodeIfPresent(String.self, forKey: .PVID) - PEID = try container.decode(String.self, forKey: .PEID) + PEID = try container.decodeIfPresent(String.self, forKey: .PEID) count = try container.decode(UInt.self, forKey: .count) sum = try container.decode(Double.self, forKey: .sum) timestamp = try container.decode(TimeInterval.self, forKey: .timestamp) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 1a0ac209..e4dccec8 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -8,28 +8,45 @@ import XCTest @testable import Countly -class CountlyViewTrackingTests: CountlyBaseTestCase { +class CountlyViewTrackingTests: CountlyViewBaseTest { func testStartAndStopView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the first view with "View1" and set an expectation to stop after 3 seconds let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "View should be stopped after 3 seconds.") + let expectation = XCTestExpectation(description: "First view should be stopped after 3 seconds.") DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withName: "View1") expectation.fulfill() } - // Wait for the expectation to be fulfilled within 5 seconds - wait(for: [expectation], timeout: 5.0) + // Start the second view with "View1" and set an expectation to stop after 5 seconds + let viewID1 = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID1, "View should be started successfully.") - // Check recorded events after view has been stopped - checkRecordedEventsForView(viewName: "View1") + let expectation1 = XCTestExpectation(description: "Second view should be stopped after 5 seconds.") + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().stopView(withName: "View1") + expectation1.fulfill() + } + + // Wait for both expectations to be fulfilled within 10 seconds + wait(for: [expectation, expectation1], timeout: 10.0) + + // Verify recorded events + let startedEventsCount = ["View1": 2] // Expecting 2 start events for "View1" + let endedEventsDurations = ["View1": [3, 5]] // Expecting 2 stop events with durations 3 and 5 seconds + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } + func testStartAndStopViewWithSegmentation() throws { let config = createBaseConfig() @@ -50,8 +67,12 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for the stop operation to complete within the timeout wait(for: [expectation], timeout: 5.0) - // Check recorded events for the view with segmentation - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) + // Verify recorded events + let startedEventsCount = ["View1": 1] // Expecting 1 start events for "View1" + let endedEventsDurations = ["View1": [4]] // Expecting 1 stop events with durations 4 seconds + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartViewAndStopViewWithID() throws { @@ -74,8 +95,12 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for the expectation to be fulfilled within 5 seconds wait(for: [expectation], timeout: 5.0) - // Check recorded events for the view using the viewID - checkRecordedEventsForView(withID: viewID) + // Verify recorded events + let startedEventsCount = ["View1": 1] // Expecting 1 start events for "View1" + let endedEventsDurations = ["View1": [3]] // Expecting 1 stop events with durations 3 seconds + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartAndStopMultipleViewsIncludingAutoStoppedViews() throws { @@ -88,17 +113,15 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { return } - guard let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") else { - XCTFail("View2 should be started successfully.") - return - } + Countly.sharedInstance().views().startAutoStoppedView("View2") let expectation = XCTestExpectation(description: "Views should be stopped after 5 seconds.") // Stop the views after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - Countly.sharedInstance().views().stopView(withName: "View1") - Countly.sharedInstance().views().stopView(withID: viewID2) + Countly.sharedInstance().views().startView("View3") + + Countly.sharedInstance().views().stopView(withID: viewID1) expectation.fulfill() } @@ -106,8 +129,16 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { wait(for: [expectation], timeout: 7.0) // Increased timeout to ensure sufficient time // Check recorded events for both views - checkRecordedEventsForView(viewName: "View1") - checkRecordedEventsForView(withID: viewID2) + // Verify recorded events + let startedEventsCount = ["View1": 1, + "View2" : 1, + "View3" : 1] + + let endedEventsDurations = ["View1": [5], + "View2": [5]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testPauseAndResumeViewsForMultipleViews() throws { @@ -145,8 +176,8 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { resumeExpectation.fulfill() } - // Stop both views after 4 more seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 11) { + // Stop both views after 5 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 12) { Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withID: viewID2) stopExpectation.fulfill() @@ -156,8 +187,15 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 15.0) // Check recorded events for both views - checkRecordedEventsForView(viewName: "View1") - checkRecordedEventsForView(withID: viewID2) + // Verify recorded events + let startedEventsCount = ["View1": 1, + "View2" : 1] + + let endedEventsDurations = ["View1": [4, 5], + "View2": [12]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testMultiplePauseAndResumeCyclesOnSameView() throws { @@ -174,7 +212,11 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Create expectations let pauseExpectation = XCTestExpectation(description: "Pause View1 after 4 seconds.") let resumeExpectation = XCTestExpectation(description: "Resume View1 after 3 seconds of pause.") - let stopExpectation = XCTestExpectation(description: "Stop View1 after another 4 seconds of resuming.") + + let pauseExpectation1 = XCTestExpectation(description: "Pause View1 after 3 seconds.") + let resumeExpectation1 = XCTestExpectation(description: "Resume View1 after 5 seconds of pause.") + + let stopExpectation = XCTestExpectation(description: "Stop View1 after another 5 seconds of resuming.") // Pause the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { @@ -188,17 +230,37 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { resumeExpectation.fulfill() } + // Pause the view after 4 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseExpectation1.fulfill() + } + + // Resume the view after 3 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 15) { // 4 + 3 seconds + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeExpectation1.fulfill() + } + // Stop the view after another 4 seconds of resuming - DispatchQueue.main.asyncAfter(deadline: .now() + 11) { // 4 + 3 + 4 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 20) { // 4 + 3 + 4 seconds Countly.sharedInstance().views().stopView(withName: "View1") stopExpectation.fulfill() } + Countly.sharedInstance().views().startView("View1") + Countly.sharedInstance().views().stopView(withName: "View1") + // Wait for all expectations to be fulfilled - wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + wait(for: [pauseExpectation, resumeExpectation,pauseExpectation1, resumeExpectation1, stopExpectation], timeout: 35.0) - // Check recorded events for the view - checkRecordedEventsForView(viewName: "View1") + // Verify recorded events + let startedEventsCount = ["View1": 2] + + let endedEventsDurations = ["View1": [4, 3, 5, 0]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartViewWhileAutoViewTrackingEnabled() throws { @@ -208,9 +270,17 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Start a manual view tracking call let viewID = Countly.sharedInstance().views().startView("View1") - + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID) // Assert that manual view tracking returns nil when auto tracking is enabled XCTAssertNil(viewID, "Manual view tracking should be ignored when auto view tracking is enabled.") + // Verify recorded events + let startedEventsCount: [String: Int] = [:] + + let endedEventsDurations : [String: [Int]] = [:] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartAndStopAutoStoppedViewWithSegmentation() throws { @@ -237,8 +307,12 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for the stop expectation wait(for: [stopExpectation], timeout: 6.0) // Allow a small buffer beyond the 4-second delay - // Check the recorded events for the view, including segmentation - checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) + let startedEventsCount = ["View1": 1] + + let endedEventsDurations = ["View1": [4]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartAutoStoppedViewAndInitiateAnother() throws { @@ -273,9 +347,14 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for both expectations wait(for: [startExpectation, stopExpectation], timeout: 10.0) - // Check recorded events after views have been stopped - checkRecordedEventsForView(withID: viewID1) - checkRecordedEventsForView(withID: viewID2) + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [4], + "View2": [3]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartRegularViewPauseAndResumeMultipleTimesThenStop() throws { @@ -285,35 +364,44 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Start the view let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - + var viewID2 = ""; // Create expectations let pauseExpectation = XCTestExpectation(description: "Pause the view after 3 seconds") - let resumeExpectation = XCTestExpectation(description: "Resume the view after another 3 seconds") - let stopExpectation = XCTestExpectation(description: "Stop the view after 4 seconds") + let resumeExpectation = XCTestExpectation(description: "Resume the view after another 4 seconds") + let stopExpectation = XCTestExpectation(description: "Stop the view after 5 seconds") // Pause the view after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().pauseView(withID: viewID) + viewID2 = Countly.sharedInstance().views().startView("View2") pauseExpectation.fulfill() } // Resume the view after another 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 6) { + DispatchQueue.main.asyncAfter(deadline: .now() + 7) { Countly.sharedInstance().views().resumeView(withID: viewID) + Countly.sharedInstance().views().pauseView(withID: viewID2) resumeExpectation.fulfill() } // Stop the view after 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + DispatchQueue.main.asyncAfter(deadline: .now() + 12) { Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().resumeView(withID: viewID2) stopExpectation.fulfill() } // Wait for all expectations to be fulfilled - wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 20) - // Check recorded events after stopping the view - checkRecordedEventsForView(viewName: "View1") + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [3, 5], + "View2": [4]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStopAllViewsWithSpecificSegmentation() throws { @@ -336,10 +424,16 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { } // Wait for the expectation to be fulfilled - wait(for: [stopAllViewsExpectation], timeout: 5.0) + wait(for: [stopAllViewsExpectation], timeout: 6.0) + + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [4], + "View2": [4]] - // Verify that all views have been stopped with the specified segmentation - checkAllViewsStoppedWithSegmentation(["key": "value"]) + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testAddSegmentationToAlreadyStartedViewUsingViewName() throws { @@ -498,6 +592,9 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { checkGlobalSegmentationApplied(expected: ["key": "newValue"]) } +} + +class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) @@ -564,7 +661,7 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { waitForBackground.fulfill() // Fulfill the background expectation // Wait after foreground for 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // Stop the views after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withName: "View2") @@ -574,11 +671,25 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { } // Wait for all expectations to be fulfilled - wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 20) - // Check recorded events for the views after transitions - checkRecordedEventsForView(viewName: "View1") - checkRecordedEventsForView(viewName: "View2") + let startedQueuedEventsCount = ["View1": 1, + "View2": 1] + + let endedQueuedEventsDurations = ["View1": [3], + "View2": [3]] + + // Call validateRecordedEvents to check if the events match expectations + validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) + + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [5], + "View2": [5]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartViewBackgroundAppResumeViewWhenReturningToForeground() throws { @@ -620,17 +731,17 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Check recorded events for the view after transitions checkRecordedEventsForView(viewName: "View1") } - + func testAttemptToStopANonStartedView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) // Attempt to stop a non-started view -// let beforeEventCount = getRecordedEventCount() + // let beforeEventCount = getRecordedEventCount() Countly.sharedInstance().views().stopView(withName: "ViewNotStarted") -// let afterEventCount = getRecordedEventCount() + // let afterEventCount = getRecordedEventCount() -// XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") + // XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") } func testBackgroundAndForegroundTriggers() throws { @@ -666,25 +777,155 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for all expectations to be fulfilled wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) } +} +class CountlyViewBaseTest: CountlyBaseTestCase { + // Helper methods to validate results - private func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { + func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { // Implement your logic to check recorded events for the specified view } - private func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { + func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { // Implement your logic to check recorded events for the specified view ID } - private func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { + func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { // Implement your logic to check that all views have been stopped with specific segmentation } - private func checkGlobalSegmentationApplied(expected: [String: String]) { + func checkGlobalSegmentationApplied(expected: [String: String]) { // Implement your logic to verify global segmentation applied correctly } - private func checkNoRecordedEvents() { + func checkNoRecordedEvents() { // Implement logic to verify no recorded events } + + func validateRecordedViews(startedEventsCount: [String: Int], endedEventsDurations: [String: [Int]]) { + // Access recorded events + guard let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? [CountlyEvent] else { + fatalError("Failed to get recordedEvents from CountlyPersistency") + } + + // XCTAssertNotEqual(recordedEvents.count, 0, "No recorded events found") + + // Track occurrences for started and ended events + var actualStartedEventsCount: [String: Int] = [:] + var actualEndedEventsDurations: [String: [Int]] = [:] + + // Iterate through recorded events to populate actual counts and durations + for event in recordedEvents { + // Check for start events with "visit": "1" + if event.key == kCountlyReservedEventView + { + if let eventKey = event.segmentation?["name"] as? String { + if let visit = event.segmentation?["visit"], visit as! Int == 1 { + actualStartedEventsCount[eventKey, default: 0] += 1 + } + else{ + actualEndedEventsDurations[eventKey, default: []].append(Int(event.duration)) + } + } + } + } + + // Validate started events count + for (key, expectedCount) in startedEventsCount { + let actualCount = actualStartedEventsCount[key] ?? 0 + XCTAssertEqual(actualCount, expectedCount, "Started events count for key \(key) does not match expected count \(expectedCount)") + } + + // Validate ended events durations + for (key, expectedDurations) in endedEventsDurations { + let actualDurations = actualEndedEventsDurations[key] ?? [] + + // First, ensure the counts match + XCTAssertEqual(actualDurations.count, expectedDurations.count, "Ended events count for key \(key) does not match expected count \(expectedDurations.count)") + + // Create a mutable copy of actualDurations to modify + var mutableActualDurations = actualDurations + + // Check each duration matches + for (index, expectedDuration) in expectedDurations.enumerated() { + // Check if the expected duration exists in the actual durations + XCTAssertTrue(mutableActualDurations.contains(expectedDuration), "Duration at index \(index) for key \(key) does not match expected duration \(expectedDuration)") + + // Remove the expectedDuration from mutableActualDurations + if let foundIndex = mutableActualDurations.firstIndex(of: expectedDuration) { + mutableActualDurations.remove(at: foundIndex) + } + } + + // Optionally, check if all expected durations have been matched + XCTAssertTrue(mutableActualDurations.isEmpty, "Not all actual durations were matched with expected durations for key \(key)") + } + + } + + func validateQueuedViews(startedEventsCount: [String: Int], endedEventsDurations: [String: [Int]]) { + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + // Filter out requests containing "events=" + let eventRequests = queuedRequests.filter { $0.contains("events=") } + + // Initialize dictionaries to track actual counts and durations for verification + var actualStartedEventsCount: [String: Int] = [:] + var actualEndedEventsDurations: [String: [Int]] = [:] + + // Loop through each event request to process events + for request in eventRequests { + // Parse the query parameters + let parsedRequest = parseQueryString(request) + + // Check if "events" parameter exists and parse it + if let eventsJSON = parsedRequest["events"] as? String, + let jsonData = eventsJSON.data(using: .utf8) { + do { + // Decode JSON data into an array of events + let events = try JSONDecoder().decode([CountlyEventStruct].self, from: jsonData) + + // Process each event to check if it’s a start or stop event + for event in events { + if event.key == kCountlyReservedEventView { + let eventKey = event.segmentation?["name"] as? String ?? "" + + // Check for start events with "visit": "1" + if let visit = event.segmentation?["visit"] as? Int, visit == 1 { + actualStartedEventsCount[eventKey, default: 0] += 1 + } + // Check for stop events with "dur" for duration + else { + actualEndedEventsDurations[eventKey, default: []].append(Int(event.duration)) + } + } + } + } catch { + print("Failed to decode events JSON: \(error.localizedDescription)") + } + } + } + + // Validate started events count + for (key, expectedCount) in startedEventsCount { + let actualCount = actualStartedEventsCount[key] ?? 0 + XCTAssertEqual(actualCount, expectedCount, "Started events count for key \(key) does not match expected count \(expectedCount)") + } + + // Validate ended events durations + for (key, expectedDurations) in endedEventsDurations { + let actualDurations = actualEndedEventsDurations[key] ?? [] + XCTAssertEqual(actualDurations.count, expectedDurations.count, "Ended events count for key \(key) does not match expected count \(expectedDurations.count)") + + // Check each duration matches + for (index, expectedDuration) in expectedDurations.enumerated() { + XCTAssertEqual(actualDurations[index], expectedDuration, "Duration at index \(index) for key \(key) does not match expected duration \(expectedDuration)") + } + } + } } + + + From 81dd221d15961e9dc6b3ec799884cd1833725bf1 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 28 Oct 2024 21:24:31 +0500 Subject: [PATCH 5/9] View test finalized --- CountlyTests/CountlyViewTests.swift | 306 +++++++++++++++------------- 1 file changed, 165 insertions(+), 141 deletions(-) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index e4dccec8..896758fe 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -73,6 +73,8 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Call validateRecordedEvents to check if the events match expectations validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) + + validateRecordedEventSegmentations(forEventID: viewID ?? "", expectedSegmentations: ["name": "View1", "visit": 1, "key": "value", "segment": "iOS"]) } func testStartViewAndStopViewWithID() throws { @@ -436,75 +438,11 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } - func testAddSegmentationToAlreadyStartedViewUsingViewName() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - // Start the view and assert it starts successfully - let viewID = Countly.sharedInstance().views().startView("View1") - XCTAssertNotNil(viewID, "View1 should be started successfully.") - - // Create expectations - let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") - let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - - // Add segmentation after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value"]) - addSegmentationExpectation.fulfill() - - // Stop the view after another 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - stopViewExpectation.fulfill() - } - } - - // Wait for all expectations to be fulfilled - wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) - - // Check if the recorded events include the added segmentation - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) - } - - func testAddSegmentationToAlreadyStartedViewUsingViewID() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - // Start the view and assert it starts successfully - let viewID = Countly.sharedInstance().views().startView("View1") - XCTAssertNotNil(viewID, "View1 should be started successfully.") - - // Create expectations - let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") - let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - - // Add segmentation after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().addSegmentationToView(withID: viewID, segmentation: ["key": "value"]) - addSegmentationExpectation.fulfill() - - // Stop the view after another 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withID: viewID) - stopViewExpectation.fulfill() - } - } - - // Wait for all expectations to be fulfilled - wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) - - // Check if the recorded events include the added segmentation - checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) - } - - // Refactor remaining tests similarly... - func testUpdateSegmentationMultipleTimesOnTheSameView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("View1") + let viewID = Countly.sharedInstance().views().startView("View1", segmentation: ["startKey": "startValue"]) XCTAssertNotNil(viewID, "View should be started successfully.") // Create expectations @@ -514,12 +452,12 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Add first segmentation DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key1": "value1"]) waitForStart.fulfill() // Add second segmentation DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key2": "value2"]) waitForSecondSegmentation.fulfill() // Stop the view @@ -533,8 +471,8 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Wait for all expectations to be fulfilled wait(for: [waitForStart, waitForSecondSegmentation, waitForStop], timeout: 12.0) - // Check recorded events for the view with the last segmentation - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply + validateRecordedEventSegmentations(forEventID: viewID ?? "", expectedSegmentations: ["name": "View1", "visit": 1, "startKey": "startValue", "segment": "iOS"]) + validateRecordedEventSegmentations(forEventID: viewID ?? "", expectedSegmentations: ["name": "View1", "key1": "value1", "key2": "value2", "segment": "iOS"]) } func testStartViewWithConsentNotGiven() throws { @@ -542,11 +480,18 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { config.requiresConsent = true Countly.sharedInstance().start(with: config) + + let beforeEventCount = getRecordedViews().count; + let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNil(viewID, "Event should not be recorded when consent is not given.") Countly.sharedInstance().views().stopView(withName: "View1") // This should also not affect recorded events - checkNoRecordedEvents() + + let afterEventCount = getRecordedViews().count + + XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not record any new event.") + } func testSetAndUpdateGlobalViewSegmentationWithViewInteractions() throws { @@ -560,7 +505,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { let stopView1Expectation = XCTestExpectation(description: "Expect View1 to be stopped after 4 seconds.") let startView2Expectation = XCTestExpectation(description: "Expect View2 to start after 3 seconds.") let stopView2Expectation = XCTestExpectation(description: "Expect View2 to be stopped after 4 seconds.") - + var viewID2 = "" // Stop View1 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") @@ -571,7 +516,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Start View2 after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().startView("View2") + viewID2 = Countly.sharedInstance().views().startView("View2") startView2Expectation.fulfill() // Fulfill View2 start expectation // Update global view segmentation @@ -588,53 +533,13 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Wait for all expectations to be fulfilled wait(for: [stopView1Expectation, startView2Expectation, stopView2Expectation], timeout: 12.0) - // Check if the global segmentation has been applied - checkGlobalSegmentationApplied(expected: ["key": "newValue"]) + validateRecordedEventSegmentations(forEventID: viewID2, expectedSegmentations: ["visit": 1, "key": "value", "name": "View2", "segment": "iOS"]) + validateRecordedEventSegmentations(forEventID: viewID2, expectedSegmentations: ["key": "newValue", "name": "View2", "segment": "iOS"]) } } class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { - func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - // Start the first view - Countly.sharedInstance().views().startView("View1") - - // Create expectations for various events - let backgroundAppExpectation = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") - let waitInBackgroundExpectation = XCTestExpectation(description: "Wait for 4 seconds in background.") - let waitAfterForegroundExpectation = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") - - // Background the app after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulate app going to background - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - backgroundAppExpectation.fulfill() // Fulfill background app expectation - - // Wait in background for 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulate app returning to foreground - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - waitInBackgroundExpectation.fulfill() // Fulfill wait in background expectation - - // Wait after foreground for 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Stop the view after returning to foreground - Countly.sharedInstance().views().stopView(withName: "View1") - waitAfterForegroundExpectation.fulfill() // Fulfill wait after foreground expectation - } - } - } - - // Wait for all expectations to be fulfilled - wait(for: [backgroundAppExpectation, waitInBackgroundExpectation, waitAfterForegroundExpectation], timeout: 12.0) - - // Check recorded events for the view after transitions - checkRecordedEventsForView(viewName: "View1") - } - func testStartMultipleViewsMoveAppToBackgroundAndReturnToForeground() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) @@ -705,7 +610,7 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") // Start the timer for moving the app to the background - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) waitForStart.fulfill() // Fulfill the start expectation @@ -726,10 +631,21 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { } // Wait for all expectations to be fulfilled - wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 20.0) + + let startedQueuedEventsCount = ["View1": 1] + + let endedQueuedEventsDurations = ["View1": [5]] + + // Call validateRecordedEvents to check if the events match expectations + validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) + + let startedEventsCount = ["View1": 1] + + let endedEventsDurations = ["View1": [3]] - // Check recorded events for the view after transitions - checkRecordedEventsForView(viewName: "View1") + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testAttemptToStopANonStartedView() throws { @@ -737,11 +653,11 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { Countly.sharedInstance().start(with: config) // Attempt to stop a non-started view - // let beforeEventCount = getRecordedEventCount() + let beforeEventCount = getRecordedViews().count; Countly.sharedInstance().views().stopView(withName: "ViewNotStarted") - // let afterEventCount = getRecordedEventCount() + let afterEventCount = getRecordedViews().count - // XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") + XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not record any new event.") } func testBackgroundAndForegroundTriggers() throws { @@ -775,32 +691,27 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { } // Wait for all expectations to be fulfilled - wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 15.0) + + let startedQueuedEventsCount = ["View1": 1] + + let endedQueuedEventsDurations = ["View1": [3]] + + // Call validateRecordedEvents to check if the events match expectations + validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) + + let startedEventsCount = ["View1": 1] + + let endedEventsDurations: [String: [Int]] = [:] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } } class CountlyViewBaseTest: CountlyBaseTestCase { // Helper methods to validate results - func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { - // Implement your logic to check recorded events for the specified view - } - - func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { - // Implement your logic to check recorded events for the specified view ID - } - - func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { - // Implement your logic to check that all views have been stopped with specific segmentation - } - - func checkGlobalSegmentationApplied(expected: [String: String]) { - // Implement your logic to verify global segmentation applied correctly - } - - func checkNoRecordedEvents() { - // Implement logic to verify no recorded events - } func validateRecordedViews(startedEventsCount: [String: Int], endedEventsDurations: [String: [Int]]) { // Access recorded events @@ -925,6 +836,119 @@ class CountlyViewBaseTest: CountlyBaseTestCase { } } } + + func getRecordedViews() -> [CountlyEvent] { + // Access recorded events + guard let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? [CountlyEvent] else { + fatalError("Failed to get recordedEvents from CountlyPersistency") + } + + // Filter and return events with the key `kCountlyReservedEventView` + return recordedEvents.filter { $0.key == kCountlyReservedEventView } + } + + func getQueuedViews() -> [CountlyEventStruct] { + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + // Filter out requests containing "events=" + let eventRequests = queuedRequests.filter { $0.contains("events=") } + var queuedViews: [CountlyEventStruct] = [] + + // Process each event request to extract and filter events + for request in eventRequests { + // Parse the query parameters + let parsedRequest = parseQueryString(request) + + // Check if "events" parameter exists and parse it + if let eventsJSON = parsedRequest["events"] as? String, + let jsonData = eventsJSON.data(using: .utf8) { + do { + // Decode JSON data into an array of events + let events = try JSONDecoder().decode([CountlyEventStruct].self, from: jsonData) + + // Filter and add events with the key `kCountlyReservedEventView` + queuedViews.append(contentsOf: events.filter { $0.key == kCountlyReservedEventView }) + } catch { + print("Failed to decode events JSON: \(error.localizedDescription)") + } + } + } + + return queuedViews + } + + + func validateRecordedEventSegmentations(forEventID eventID: String, expectedSegmentations: [String: Any]) { + // Get recorded views filtered by key + let recordedViews = getRecordedViews() + + // Determine if "visit" is specified in expectedSegmentations + let requiresVisit = expectedSegmentations["visit"] as? Int == 1 + + // Filter events based on the presence and value of "visit" + let filteredEvents = recordedViews.filter { event in + event.id == eventID && + (requiresVisit ? (event.segmentation?["visit"] as? Int == 1) : (event.segmentation?["visit"] == nil)) + } + + // Ensure there are events with the specified ID and segmentation criteria + XCTAssertFalse(filteredEvents.isEmpty, "No recorded events found with ID \(eventID) matching expected segmentation criteria") + + // Validate segmentations for each filtered event + for event in filteredEvents { + guard let eventSegmentations = event.segmentation as? [String: Any] else { + XCTFail("Event segmentation is missing or invalid for event with ID \(eventID)") + continue + } + + // Validate each expected segmentation + for (key, expectedValue) in expectedSegmentations { + if let actualValue = eventSegmentations[key] { + XCTAssertEqual("\(actualValue)", "\(expectedValue)", "Segmentation mismatch for key \(key) in recorded event with ID \(eventID): expected \(expectedValue), found \(actualValue)") + } else { + XCTFail("Segmentation key \(key) missing in recorded event with ID \(eventID)") + } + } + } + } + + func validateQueuedEventSegmentations(forEventID eventID: String, expectedSegmentations: [String: Any]) { + // Get queued views filtered by key + let queuedViews = getQueuedViews() + + // Determine if "visit" is specified in expectedSegmentations + let requiresVisit = expectedSegmentations["visit"] as? Int == 1 + + // Filter events based on the presence and value of "visit" + let filteredEvents = queuedViews.filter { event in + event.ID == eventID && + (requiresVisit ? (event.segmentation?["visit"] as? Int == 1) : (event.segmentation?["visit"] == nil)) + } + + // Ensure there are events with the specified ID and segmentation criteria + XCTAssertFalse(filteredEvents.isEmpty, "No queued events found with ID \(eventID) matching expected segmentation criteria") + + // Validate segmentations for each filtered event + for event in filteredEvents { + guard let eventSegmentations = event.segmentation as? [String: Any] else { + XCTFail("Event segmentation is missing or invalid for event with ID \(eventID)") + continue + } + + // Validate each expected segmentation + for (key, expectedValue) in expectedSegmentations { + if let actualValue = eventSegmentations[key] { + XCTAssertEqual("\(actualValue)", "\(expectedValue)", "Segmentation mismatch for key \(key) in queued event with ID \(eventID): expected \(expectedValue), found \(actualValue)") + } else { + XCTFail("Segmentation key \(key) missing in queued event with ID \(eventID)") + } + } + } + } + + } From b1313a2415b0585e32e861a3cbeeebfb90350ebe Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 28 Oct 2024 21:31:59 +0500 Subject: [PATCH 6/9] Added dummyTest --- CountlyTests/CountlyViewTests.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 896758fe..be0055f8 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -10,6 +10,16 @@ import XCTest class CountlyViewTrackingTests: CountlyViewBaseTest { + // Run this test first if you are facing cache not clear or instances are not reset properly + // This is a dummy test to cover the edge case clear the cache when SDK is not initialized + func testDummy() { + let config = createBaseConfig() + config.requiresConsent = false; + config.manualSessionHandling = true; + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().halt(true) + } + func testStartAndStopView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) From c2f5e6560e80c6956a03e96b396069ddeb8b476b Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 30 Oct 2024 17:57:54 +0500 Subject: [PATCH 7/9] Updated test for views --- CountlyTests/CountlyViewTests.swift | 56 +++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index be0055f8..387a6ea4 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -348,7 +348,6 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Stop both views after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().stopView(withID: viewID1) Countly.sharedInstance().views().stopView(withID: viewID2) // Fulfill stopExpectation after stopping both views @@ -446,6 +445,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Call validateRecordedEvents to check if the events match expectations validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) + //TODO: check segmentations also } func testUpdateSegmentationMultipleTimesOnTheSameView() throws { @@ -498,6 +498,11 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { Countly.sharedInstance().views().stopView(withName: "View1") // This should also not affect recorded events + let viewID2 = Countly.sharedInstance().views().startView("View2") + Countly.sharedInstance().views().stopView(withID: viewID2) + //TODO: Add all the public methods + + let afterEventCount = getRecordedViews().count XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not record any new event.") @@ -510,6 +515,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Start the first view Countly.sharedInstance().views().startView("View1") + //TODO: validate that view or remove it // Create expectations for various events let stopView1Expectation = XCTestExpectation(description: "Expect View1 to be stopped after 4 seconds.") @@ -527,6 +533,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Start View2 after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { viewID2 = Countly.sharedInstance().views().startView("View2") + //TODO: also start with segmentation to check the precedence of user provided and global segmentation startView2Expectation.fulfill() // Fulfill View2 start expectation // Update global view segmentation @@ -555,13 +562,15 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { Countly.sharedInstance().start(with: config) // Start the views - Countly.sharedInstance().views().startView("View1") - Countly.sharedInstance().views().startAutoStoppedView("View2") + Countly.sharedInstance().views().startView("V1") + Countly.sharedInstance().views().startAutoStoppedView("A1") // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + let waitBGStartView = XCTestExpectation(description: "Wait for 1 seconds after background.") + let waitFGStartView = XCTestExpectation(description: "Wait for 1 seconds after background.") // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { @@ -569,17 +578,28 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) waitForStart.fulfill() // Fulfill the start expectation + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Countly.sharedInstance().views().startView("BGV1") + Countly.sharedInstance().views().startAutoStoppedView("BGA1") + waitBGStartView.fulfill() // Fulfill the foreground expectation + } + + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) waitForBackground.fulfill() // Fulfill the background expectation + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Countly.sharedInstance().views().startView("FGV1") + Countly.sharedInstance().views().startAutoStoppedView("FGA1") + waitFGStartView.fulfill() // Fulfill the foreground expectation + } // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // Stop the views after returning to foreground - Countly.sharedInstance().views().stopView(withName: "View1") - Countly.sharedInstance().views().stopView(withName: "View2") + Countly.sharedInstance().views().stopAllViews(nil); waitForForeground.fulfill() // Fulfill the foreground expectation } } @@ -588,20 +608,28 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { // Wait for all expectations to be fulfilled wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 20) - let startedQueuedEventsCount = ["View1": 1, - "View2": 1] + let startedQueuedEventsCount = ["V1": 1, + "A1": 1] - let endedQueuedEventsDurations = ["View1": [3], - "View2": [3]] + let endedQueuedEventsDurations = ["V1": [3], + "A1": [3]] // Call validateRecordedEvents to check if the events match expectations validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) - let startedEventsCount = ["View1": 1, - "View2": 1] - - let endedEventsDurations = ["View1": [5], - "View2": [5]] + let startedEventsCount = ["BGV1": 1, + "BGA1": 1, + "V1": 1, + "A1": 1, + "FGV1": 1, + "FGA1": 1] + + let endedEventsDurations = ["BGA1": [3], + "A1": [1], + "V1": [5], + "BGV1": [8], + "FGV1": [4], + "FGA1": [4]] // Call validateRecordedEvents to check if the events match expectations validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) From b02bbe9a07eab55e81706b0e574733739de2f4c8 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 31 Oct 2024 12:19:30 +0500 Subject: [PATCH 8/9] Updated changelog for view fixes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f56a0e..a7bf827f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## xx.xx.xx +* Mitigated an issue where pausing a view resulted in a '0' view duration. +* Mitigated an issue where an `autoStoppedView` would stop again upon returning to the foreground, after already being stopped by the SDK when the app went to the background. +* Improved view duration reporting by sending it as an `NSInteger`, eliminating lengthy decimal sequences previously sent as `NSTimeInterval` + ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) * Updated the SDK to ensure compatibility with the latest server response models From f267f7ac7c5d86cc075e83a59097ceb920f7aec9 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 31 Oct 2024 14:52:38 +0500 Subject: [PATCH 9/9] Updated changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7bf827f..5fa2791b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## xx.xx.xx * Mitigated an issue where pausing a view resulted in a '0' view duration. -* Mitigated an issue where an `autoStoppedView` would stop again upon returning to the foreground, after already being stopped by the SDK when the app went to the background. -* Improved view duration reporting by sending it as an `NSInteger`, eliminating lengthy decimal sequences previously sent as `NSTimeInterval` +* Mitigated an issue where an internal timer was not reset when going to foreground for `autoStoppedViews` +* Mitigated an issue for `autoStoppedViews` could have not started when multiple views were open at the same time while going to foreground ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!)