Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Some fixes for view duration and added tests #347

Open
wants to merge 2 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Countly.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down
4 changes: 4 additions & 0 deletions Countly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -131,6 +132,7 @@
3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyWebViewManager.m; sourceTree = "<group>"; };
3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlySegmentationTests.swift; sourceTree = "<group>"; };
3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyDeviceIDTests.swift; sourceTree = "<group>"; };
3969D0222CB80848000F8A32 /* CountlyViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyViewTests.swift; sourceTree = "<group>"; };
3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyEventStruct.swift; sourceTree = "<group>"; };
3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyUserProfileTests.swift; sourceTree = "<group>"; };
399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyContentBuilder.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -217,6 +219,7 @@
3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */,
3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */,
399B464F2C52813700AD384E /* CountlyLocationTests.swift */,
3969D0222CB80848000F8A32 /* CountlyViewTests.swift */,
);
path = CountlyTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
199 changes: 199 additions & 0 deletions CountlyTests/CountlyViewTests.swift
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addition of test comments would be great

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.")
}
}
2 changes: 1 addition & 1 deletion CountlyViewData.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* Duration of the view
* @discussion it returns the duration of view in foreground after view started.
*/
- (NSTimeInterval)duration;
- (NSInteger)duration;


/**
Expand Down
7 changes: 4 additions & 3 deletions CountlyViewData.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
8 changes: 4 additions & 4 deletions CountlyViewTrackingInternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down Expand Up @@ -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];
}
Expand All @@ -537,8 +537,8 @@ - (void)stopRunningViewsInternal

- (void)pauseViewInternal:(CountlyViewData*) viewData
{
[viewData pauseView];
[self stopViewWithIDInternal:viewData.viewID customSegmentation:nil autoPaused:YES];
[viewData pauseView];
}

- (void)startStoppedViewsInternal
Expand Down
Loading