From 5e6b0bd95cff275073111e24ad368d4e46f69fa3 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 13 Aug 2024 16:50:01 +1000 Subject: [PATCH] Refactor logic to support defining App variants in tests and add onboarding tests --- .../01_control_group_onboarding.yaml | 29 +++++++ .../02_control_group_hide_onboarding.yaml | 33 ++++++++ ...03_experiment_group_linear_onboarding.yaml | 52 ++++++++++++ .maestro/shared/setup.yaml | 6 +- Core/DefaultVariantManager.swift | 25 +++++- DuckDuckGo.xcodeproj/project.pbxproj | 10 +-- DuckDuckGo/LaunchOptionsHandler.swift | 39 ++++++++- .../LaunchOptionsHandlerTests.swift | 82 ++++++++++++++++++- DuckDuckGoTests/VariantManagerTests.swift | 72 +++++++++++++--- 9 files changed, 322 insertions(+), 26 deletions(-) create mode 100644 .maestro/onboarding_tests/01_control_group_onboarding.yaml create mode 100644 .maestro/onboarding_tests/02_control_group_hide_onboarding.yaml create mode 100644 .maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml diff --git a/.maestro/onboarding_tests/01_control_group_onboarding.yaml b/.maestro/onboarding_tests/01_control_group_onboarding.yaml new file mode 100644 index 0000000000..aaf2fbb3e5 --- /dev/null +++ b/.maestro/onboarding_tests/01_control_group_onboarding.yaml @@ -0,0 +1,29 @@ +appId: com.duckduckgo.mobile.ios +tags: + - onboarding + +--- + +# Set up +- runFlow: + file: ../shared/setup.yaml + env: + ONBOARDING_COMPLETED: "false" + ONBOARDING_VARIANT: "ma" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://www.duckduckgo.com" +- pressKey: Enter + +# Handle Onboarding +- assertVisible: "Got It" +- assertVisible: "Hide" +- tapOn: "Got It" +- assertVisible: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- assertVisible: "You’ve got this!\n\nRemember: Every time you browse with me, a creepy ad loses its wings. 👍" diff --git a/.maestro/onboarding_tests/02_control_group_hide_onboarding.yaml b/.maestro/onboarding_tests/02_control_group_hide_onboarding.yaml new file mode 100644 index 0000000000..7779ec549b --- /dev/null +++ b/.maestro/onboarding_tests/02_control_group_hide_onboarding.yaml @@ -0,0 +1,33 @@ +appId: com.duckduckgo.mobile.ios +tags: + - onboarding + +--- + +# Set up +- runFlow: + file: ../shared/setup.yaml + env: + ONBOARDING_COMPLETED: "false" + ONBOARDING_VARIANT: "ma" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://www.duckduckgo.com" +- pressKey: Enter + +# Handle Onboarding +- assertVisible: "Got It" +- assertVisible: "Hide" +- tapOn: "Hide" +- assertVisible: "Hide Tips Forever" +- tapOn: "Hide Tips Forever" + +# Handle Fire Button +- assertVisible: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- assertNotVisible: "You’ve got this!\n\nRemember: Every time you browse with me, a creepy ad loses its wings. 👍" diff --git a/.maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml b/.maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml new file mode 100644 index 0000000000..ce98136174 --- /dev/null +++ b/.maestro/onboarding_tests/03_experiment_group_linear_onboarding.yaml @@ -0,0 +1,52 @@ +appId: com.duckduckgo.mobile.ios +tags: + - onboarding + +--- + +# Set up +- runFlow: + file: ../shared/setup.yaml + env: + ONBOARDING_COMPLETED: "false" + ONBOARDING_VARIANT: "mb" + +# Handle Search Suggestions +- assertVisible: "Ready to get started?\nTry a search!" +- assertVisible: "Surprise Me!" +- tapOn: "Surprise Me!" + +# Handle First Dax Dialog +- assertVisible: "That’s DuckDuckGo Search. Private. Fast. Fewer ads." +- assertVisible: "Got It!" +- tapOn: "Got It!" + +# Handle Site Suggestions +- assertVisible: "Next, try visiting a site!" +- assertVisible: "Surprise Me!" +- tapOn: "Surprise Me!" + +# Handle Privacy Dashboard +- assertVisible: "Got It!" +- tapOn: + point: "6%,10%" # Shield icon. +- assertVisible: + text: "View Tracker Companies" +- assertVisible: + text: "Done" +- tapOn: "Done" + +# Handle Fire Message +- assertVisible: "Got It!" +- tapOn: "Got It!" +- assertVisible: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥" + +# Handle Fire Button +- assertVisible: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" + +# Handle End of Journey Dialog +- assertVisible: "You’ve got this!" +- assertVisible: "High five!" +- tapOn: "High five!" diff --git a/.maestro/shared/setup.yaml b/.maestro/shared/setup.yaml index 2946963c6c..a8a9d35eff 100644 --- a/.maestro/shared/setup.yaml +++ b/.maestro/shared/setup.yaml @@ -12,8 +12,10 @@ appId: com.duckduckgo.mobile.ios clearState: true clearKeychain: true arguments: - isUITesting: true - isOnboardingCompleted: ${ONBOARDING_COMPLETED} + isUITesting: true # Renaming `isUITesting` requires to update LaunchOptionsHandler `isUITesting` key + isOnboardingCompleted: ${ONBOARDING_COMPLETED} # Renaming `isOnboardingCompleted` requires to update LaunchOptionsHandler `isOnboardingCompleted` key + onboardingVariant: ${ONBOARDING_VARIANT} # Renaming `onboardingVariant` requires to update LaunchOptionsHandler `onboardingVariant` key + currentAppVariant: ${APP_VARIANT} # Renaming `currentAppVariant` requires to update LaunchOptionsHandler `currentAppVariant` key # Get past onboarding screens - runFlow: diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index d7f4b63855..18f30ee82e 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -81,10 +81,19 @@ public protocol VariantRNG { } +public protocol VariantNameOverriding { + var overriddenAppVariantName: String? { get } + var overriddenOnboardingVariantName: String? { get } +} + public class DefaultVariantManager: VariantManager { public var currentVariant: Variant? { - let variantName = ProcessInfo.processInfo.environment["VARIANT", default: storage.variant ?? "" ] + let variantName = variantNameOverride.overriddenAppVariantName + ?? ProcessInfo.processInfo.environment["VARIANT"] + ?? storage.variant + ?? "" + return variants.first(where: { $0.name == variantName }) } @@ -92,16 +101,19 @@ public class DefaultVariantManager: VariantManager { private let storage: StatisticsStore private let rng: VariantRNG private let returningUserMeasurement: ReturnUserMeasurement + private let variantNameOverride: VariantNameOverriding init(variants: [Variant], storage: StatisticsStore, rng: VariantRNG, - returningUserMeasurement: ReturnUserMeasurement) { - + returningUserMeasurement: ReturnUserMeasurement, + variantNameOverride: VariantNameOverriding + ) { self.variants = variants self.storage = storage self.rng = rng self.returningUserMeasurement = returningUserMeasurement + self.variantNameOverride = variantNameOverride } public convenience init() { @@ -109,7 +121,8 @@ public class DefaultVariantManager: VariantManager { variants: VariantIOS.defaultVariants, storage: StatisticsUserDefaults(), rng: Arc4RandomUniformVariantRNG(), - returningUserMeasurement: KeychainReturnUserMeasurement() + returningUserMeasurement: KeychainReturnUserMeasurement(), + variantNameOverride: LaunchOptionsHandler() ) } @@ -141,6 +154,10 @@ public class DefaultVariantManager: VariantManager { } private func selectVariant() -> Variant? { + if let overriddenOnboardingVariant = variantNameOverride.overriddenOnboardingVariantName { + return variants.first(where: { $0.name == overriddenOnboardingVariant }) + } + if returningUserMeasurement.isReturningUser { return VariantIOS.returningUser } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 097e5aeb1f..cdffb65139 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -694,8 +694,8 @@ 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */; }; 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */; }; 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */; }; - 9FCFCD7E2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */; }; + 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */; }; 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */; }; 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */; }; @@ -2433,7 +2433,7 @@ 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsersComparisonModel.swift; sourceTree = ""; }; 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModel.swift; sourceTree = ""; }; - 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandler.swift; sourceTree = ""; }; + 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LaunchOptionsHandler.swift; path = ../DuckDuckGo/LaunchOptionsHandler.swift; sourceTree = ""; }; 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandlerTests.swift; sourceTree = ""; }; 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporter.swift; sourceTree = ""; }; 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterTests.swift; sourceTree = ""; }; @@ -5537,6 +5537,7 @@ F143C3191E4A99DD00CFDE3A /* Utilities */ = { isa = PBXGroup; children = ( + 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */, B603974829C19F6F00902A34 /* Assertions.swift */, CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */, 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */, @@ -5621,7 +5622,6 @@ 4B62C4B925B930DD008912C6 /* AppConfigurationFetchTests.swift */, 85AFA1202B45D14F0028A504 /* BookmarksMigrationAssertionTests.swift */, 987243132C5232B5007ECC76 /* BookmarksDatabaseSetupTests.swift */, - 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */, ); name = Application; sourceTree = ""; @@ -5701,6 +5701,7 @@ F198D78F1E3976300088DA8A /* Utilities */ = { isa = PBXGroup; children = ( + 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */, F198D78D1E39762C0088DA8A /* StringExtensionTests.swift */, F14E491E1E391CE900DC037C /* URLExtensionTests.swift */, F1DA2F7C1EBCF23700313F51 /* ExternalUrlSchemeTests.swift */, @@ -5834,7 +5835,6 @@ 85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */, 9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */, 9821234F2B6D233E00F08C57 /* UserSession.swift */, - 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */, ); name = Application; sourceTree = ""; @@ -6969,7 +6969,6 @@ 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */, 1E8AD1D927C4FEC100ABA377 /* DownloadsListSectioningHelper.swift in Sources */, D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */, - 9FCFCD7E2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift in Sources */, 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */, C12726F02A5FF89900215B02 /* EmailSignupPromptViewModel.swift in Sources */, 31669B9A28020A460071CC18 /* SaveLoginViewModel.swift in Sources */, @@ -7867,6 +7866,7 @@ F1134EA61F3E2AF400B73467 /* StatisticsStore.swift in Sources */, F17D723C1E8BB374003E8B0E /* AppDeepLinkSchemes.swift in Sources */, 1E8AD1DB27C51AE000ABA377 /* TimeIntervalExtension.swift in Sources */, + 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */, B652DF0D287C2A6300C12A9C /* PrivacyFeatures.swift in Sources */, F10E522D1E946F8800CE1253 /* NSAttributedStringExtension.swift in Sources */, 9887DC252354D2AA005C85F5 /* Database.swift in Sources */, diff --git a/DuckDuckGo/LaunchOptionsHandler.swift b/DuckDuckGo/LaunchOptionsHandler.swift index 7fa3151910..b9ca8ee518 100644 --- a/DuckDuckGo/LaunchOptionsHandler.swift +++ b/DuckDuckGo/LaunchOptionsHandler.swift @@ -19,23 +19,54 @@ import Foundation -final class LaunchOptionsHandler { +public final class LaunchOptionsHandler { private static let isUITesting = "isUITesting" private static let isOnboardingcompleted = "isOnboardingCompleted" + private static let onboardingVariantName = "onboardingVariant" + private static let appVariantName = "currentAppVariant" private let launchArguments: [String] private let userDefaults: UserDefaults - init(launchArguments: [String] = ProcessInfo.processInfo.arguments, userDefaults: UserDefaults = .app) { + public init(launchArguments: [String] = ProcessInfo.processInfo.arguments, userDefaults: UserDefaults = .app) { self.launchArguments = launchArguments self.userDefaults = userDefaults } - var isUITesting: Bool { + public var isUITesting: Bool { launchArguments.contains(Self.isUITesting) } - var isOnboardingCompleted: Bool { + public var isOnboardingCompleted: Bool { userDefaults.string(forKey: Self.isOnboardingcompleted) == "true" } + + public var onboardingVariantName: String? { + sanitisedEnvParameter(string: userDefaults.string(forKey: Self.onboardingVariantName)) + } + + public var appVariantName: String? { + sanitisedEnvParameter(string: userDefaults.string(forKey: Self.appVariantName)) + } + + private func sanitisedEnvParameter(string: String?) -> String? { + guard let string, string != "null" else { return nil } + return string + } +} + +// MARK: - LaunchOptionsHandler + VariantManager + +extension LaunchOptionsHandler: VariantNameOverriding { + + public var overriddenAppVariantName: String? { + guard isUITesting else { return nil } + return appVariantName + } + + public var overriddenOnboardingVariantName: String? { + guard isUITesting else { return nil } + return onboardingVariantName + } + } diff --git a/DuckDuckGoTests/LaunchOptionsHandlerTests.swift b/DuckDuckGoTests/LaunchOptionsHandlerTests.swift index 0c29cf9f04..92799f47d5 100644 --- a/DuckDuckGoTests/LaunchOptionsHandlerTests.swift +++ b/DuckDuckGoTests/LaunchOptionsHandlerTests.swift @@ -18,7 +18,7 @@ // import XCTest -@testable import DuckDuckGo +@testable import Core final class LaunchOptionsHandlerTests: XCTestCase { private static let suiteName = "testing_launchOptionsHandler" @@ -35,6 +35,8 @@ final class LaunchOptionsHandlerTests: XCTestCase { try super.tearDownWithError() } + // MARK: - isUITesting + func testShouldReturnTrueWhenIsUITestingIsCalledAndLaunchArgumentContainsIsUITesting() { // GIVEN let launchArguments = ["isUITesting"] @@ -58,6 +60,8 @@ final class LaunchOptionsHandlerTests: XCTestCase { XCTAssertFalse(result) } + // MARK: - isOnboardingCompleted + func testShouldReturnTrueWhenIsOnboardingCompletedAndDefaultsIsOnboardingCompletedIsTrue() { // GIVEN userDefaults.set("true", forKey: "isOnboardingCompleted") @@ -81,4 +85,80 @@ final class LaunchOptionsHandlerTests: XCTestCase { // THEN XCTAssertFalse(result) } + + // MARK: - Onboarding Variant + + func testShouldReturnOnboardingVariantWhenOnboardingVariantIsCalledAndDefaultsContainsOnboardingVariant() { + // GIVEN + userDefaults.set("mb", forKey: "onboardingVariant") + let sut = LaunchOptionsHandler(launchArguments: [], userDefaults: userDefaults) + + // WHEN + let result = sut.onboardingVariantName + + // THEN + XCTAssertEqual(result, "mb") + } + + func testShouldReturnNilWhenOnboardingVariantIsCalledAndDefaultsDoesNotContainsOnboardingVariant() { + // GIVEN + userDefaults.removeObject(forKey: "onboardingVariant") + let sut = LaunchOptionsHandler(launchArguments: [], userDefaults: userDefaults) + + // WHEN + let result = sut.onboardingVariantName + + // THEN + XCTAssertNil(result) + } + + func testShouldReturnNilWhenOnboardingVariantIsCalledAndDefaultsContainsNullStringOnboardingVariant() { + // GIVEN + userDefaults.set("null", forKey: "onboardingVariant") + let sut = LaunchOptionsHandler(launchArguments: [], userDefaults: userDefaults) + + // WHEN + let result = sut.onboardingVariantName + + // THEN + XCTAssertNil(result) + } + + // MARK: - App Variant + + func testShouldReturnAppVariantWhenAppVariantIsCalledAndDefaultsContainsAppVariant() { + // GIVEN + userDefaults.set("mb", forKey: "currentAppVariant") + let sut = LaunchOptionsHandler(launchArguments: [], userDefaults: userDefaults) + + // WHEN + let result = sut.appVariantName + + // THEN + XCTAssertEqual(result, "mb") + } + + func testShouldReturnNilWhenAppVariantIsCalledAndDefaultsDoesNotContainsAppVariant() { + // GIVEN + userDefaults.removeObject(forKey: "currentAppVariant") + let sut = LaunchOptionsHandler(launchArguments: [], userDefaults: userDefaults) + + // WHEN + let result = sut.onboardingVariantName + + // THEN + XCTAssertNil(result) + } + + func testShouldReturnNilWhenAppVariantIsCalledAndDefaultsContainsNullStringAppVariant() { + // GIVEN + userDefaults.set("null", forKey: "currentAppVariant") + let sut = LaunchOptionsHandler(launchArguments: [], userDefaults: userDefaults) + + // WHEN + let result = sut.onboardingVariantName + + // THEN + XCTAssertNil(result) + } } diff --git a/DuckDuckGoTests/VariantManagerTests.swift b/DuckDuckGoTests/VariantManagerTests.swift index edb1c05c9c..ba78988a6d 100644 --- a/DuckDuckGoTests/VariantManagerTests.swift +++ b/DuckDuckGoTests/VariantManagerTests.swift @@ -34,7 +34,7 @@ class VariantManagerTests: XCTestCase { func testWhenVariantIsExcludedThenItIsNotInVariantList() { let subject = DefaultVariantManager(variants: testVariants, storage: MockStatisticsStore(), rng: MockVariantRNG(returnValue: 500), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) XCTAssertTrue(!subject.isSupported(feature: .dummy)) } @@ -48,7 +48,7 @@ class VariantManagerTests: XCTestCase { let mockStore = MockStatisticsStore() mockStore.variant = "test" let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) // temporarily use this feature name XCTAssertTrue(subject.isSupported(feature: .dummy)) @@ -65,7 +65,7 @@ class VariantManagerTests: XCTestCase { for i in 0 ..< 100 { let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: i), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) subject.assignVariantIfNeeded { _ in } XCTAssertNotEqual("mt", subject.currentVariant?.name) @@ -79,7 +79,7 @@ class VariantManagerTests: XCTestCase { mockStore.atb = "atb" let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) subject.assignVariantIfNeeded { _ in } XCTAssertNil(subject.currentVariant) @@ -89,7 +89,7 @@ class VariantManagerTests: XCTestCase { let variant = VariantIOS(name: "anything", weight: 100, isIncluded: VariantIOS.When.always, features: []) let subject = DefaultVariantManager(variants: [variant], storage: MockStatisticsStore(), rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) subject.assignVariantIfNeeded { _ in } XCTAssertEqual(variant.name, subject.currentVariant?.name) @@ -100,7 +100,7 @@ class VariantManagerTests: XCTestCase { let mockStore = MockStatisticsStore() mockStore.variant = "mb" let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) XCTAssertEqual("mb", subject.currentVariant?.name) XCTAssertEqual("mb", mockStore.variant) @@ -119,7 +119,7 @@ class VariantManagerTests: XCTestCase { let mockStore = MockStatisticsStore() let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) subject.assignVariantIfNeeded { _ in } XCTAssertEqual("mb", subject.currentVariant?.name) XCTAssertEqual("mb", mockStore.variant) @@ -130,20 +130,67 @@ class VariantManagerTests: XCTestCase { let mockStore = MockStatisticsStore() let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) XCTAssertNil(subject.currentVariant) } func testWhenNoVariantsThenAssignsNothing() { let subject = DefaultVariantManager(variants: [], storage: MockStatisticsStore(), rng: MockVariantRNG(returnValue: 0), - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) XCTAssertNil(subject.currentVariant) } + // MARK: - Variant Override + + func testWhenVariantOverrideHasAppVariantThenCurrentVariantIsAppVariant() { + // GIVEN + let expectedVariantName = "experiment-test" + let variants = testVariants + [VariantIOS(name: expectedVariantName, weight: 50, isIncluded: VariantIOS.When.always, features: [])] + let mockVariantOverride = MockVariantNameOverride() + mockVariantOverride.overriddenAppVariantName = expectedVariantName + let sut = DefaultVariantManager( + variants: variants, + storage: MockStatisticsStore(), + rng: MockVariantRNG(returnValue: 0), + returningUserMeasurement: MockReturningUserMeasurement(), + variantNameOverride: mockVariantOverride + ) + + // WHEN + let result = sut.currentVariant + + // THEN + XCTAssertEqual(result?.name, expectedVariantName) + } + + func testWhenVariantOverrideHasOnboardingVariantThenVariantIsReturnedWhenSelectingVariant() { + // GIVEN + let expectedVariantName = "experiment-test" + let variants = testVariants + [VariantIOS(name: expectedVariantName, weight: 50, isIncluded: VariantIOS.When.always, features: [])] + let mockVariantOverride = MockVariantNameOverride() + mockVariantOverride.overriddenOnboardingVariantName = expectedVariantName + let sut = DefaultVariantManager( + variants: variants, + storage: MockStatisticsStore(), + rng: MockVariantRNG(returnValue: 0), + returningUserMeasurement: MockReturningUserMeasurement(), + variantNameOverride: mockVariantOverride + ) + sut.assignVariantIfNeeded { _ in } + + // WHEN + let result = sut.currentVariant + + // THEN + XCTAssertEqual(result?.name, expectedVariantName) + } + + // MARK: - Helpers + private func assignedVariantManager(withRNG rng: VariantRNG) -> VariantManager { let variantManager = DefaultVariantManager(variants: testVariants, storage: MockStatisticsStore(), rng: rng, - returningUserMeasurement: MockReturningUserMeasurement()) + returningUserMeasurement: MockReturningUserMeasurement(), variantNameOverride: MockVariantNameOverride()) variantManager.assignVariantIfNeeded { _ in } return variantManager } @@ -167,3 +214,8 @@ class MockReturningUserMeasurement: ReturnUserMeasurement { func updateStoredATB(_ atb: Core.Atb) { } } + +final class MockVariantNameOverride: VariantNameOverriding { + var overriddenAppVariantName: String? + var overriddenOnboardingVariantName: String? +}