From d95a5209296a086b5376b35b2fb29d12346d2f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gonc=CC=A7alves?= Date: Fri, 28 Nov 2025 04:11:53 +0000 Subject: [PATCH 1/3] pm-25906 Add tests --- .../Auth/Landing/LandingProcessorTests.swift | 367 ++++++++++++++ .../VaultUnlockProcessorTests.swift | 479 +++++++++++++++++- .../VaultAutofillListProcessorTests.swift | 49 ++ .../VaultItemSelectionProcessorTests.swift | 56 ++ .../VaultList/VaultListProcessorTests.swift | 264 ++++++++++ 5 files changed, 1211 insertions(+), 4 deletions(-) diff --git a/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift b/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift index 1fbd665a9a..ad33fdbfbd 100644 --- a/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift +++ b/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift @@ -511,6 +511,42 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLockedSuccessfully)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// lock the selected account, dismissing the profile switcher after confirmation. + @MainActor + func test_receive_accountLongPressed_lock_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify the profile switcher sheet is dismissed after lock action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual( + coordinator.events.last, + .action(.lockVault(userId: otherProfile.userId, isManuallyLocking: true)), + ) + XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLockedSuccessfully)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` records any errors from locking the account. @MainActor func test_receive_accountLongPressed_lock_error() async throws { @@ -541,6 +577,38 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 records any errors from locking + /// the account and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_lock_error_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "2") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = nil + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify the profile switcher sheet is dismissed even after error + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// log out of the selected account. @MainActor @@ -580,6 +648,46 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ ) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// log out of the selected account, dismissing the profile switcher after confirmation. + @MainActor + func test_receive_accountLongPressed_logout_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "2") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed after logout action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLoggedOutSuccessfully)) + XCTAssertEqual( + coordinator.events.last, + .action(.logout(userId: otherProfile.userId, userInitiated: true)), + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` records any errors from logging out the /// account. @MainActor @@ -615,6 +723,42 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 records any errors from logging out + /// the account and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_error_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "2") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = nil + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed even after error + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` with the active account /// dismisses the profile switcher. @MainActor @@ -654,6 +798,40 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ ) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 with the active unlocked account + /// dismisses the profile switcher. + @MainActor + func test_receive_accountPressed_active_unlocked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture() + authRepository.profileSwitcherState = .init( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual( + coordinator.events, + [ + .action(.switchAccount(isAutomatic: false, userId: profile.userId)), + ], + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` with the active account /// dismisses the profile switcher. @MainActor @@ -697,6 +875,44 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ ) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 with the active locked account + /// dismisses the profile switcher. + @MainActor + func test_receive_accountPressed_active_locked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture(isUnlocked: false) + let account = Account.fixture(profile: .fixture( + userId: profile.userId, + )) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.accountForItemResult = .success(account) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual( + coordinator.events, + [ + .action(.switchAccount(isAutomatic: false, userId: profile.userId)), + ], + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_alternateUnlocked() throws { @@ -740,6 +956,45 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ ) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// and switches to an alternate unlocked account. + @MainActor + func test_receive_accountPressed_alternateUnlocked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture() + let active = ProfileSwitcherItem.fixture() + let account = Account.fixture( + profile: .fixture( + userId: profile.userId, + ), + ) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.accountForItemResult = .success(account) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual( + coordinator.events, + [.action(.switchAccount(isAutomatic: false, userId: profile.userId))], + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_alternateLocked() throws { @@ -781,6 +1036,43 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ ) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// and switches to an alternate locked account. + @MainActor + func test_receive_accountPressed_alternateLocked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture(isUnlocked: false) + let active = ProfileSwitcherItem.fixture() + let account = Account.fixture(profile: .fixture( + userId: profile.userId, + )) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.accountForItemResult = .success(account) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual( + coordinator.events, + [.action(.switchAccount(isAutomatic: false, userId: profile.userId))], + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_noMatch() throws { @@ -818,6 +1110,39 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ ) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// and switches to an account that doesn't match. + @MainActor + func test_receive_accountPressed_noMatch_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture() + let active = ProfileSwitcherItem.fixture() + authRepository.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual( + coordinator.events, + [.action(.switchAccount(isAutomatic: false, userId: profile.userId))], + ) + } + /// `receive(_:)` with `.profileSwitcher(.addAccountPressed)` updates the state to reflect the changes. @MainActor func test_receive_addAccountPressed() throws { @@ -845,6 +1170,27 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(coordinator.routes, []) } + /// `receive(_:)` with `.profileSwitcher(.addAccountPressed)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_receive_addAccountPressed_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let active = ProfileSwitcherItem.fixture() + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.addAccountPressed)) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.profileSwitcher(.backgroundTapped)` updates the state to reflect the changes. @MainActor func test_receive_backgroundTapped() throws { @@ -872,6 +1218,27 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(coordinator.routes, []) } + /// `receive(_:)` with `.profileSwitcher(.backgroundTapped)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_receive_backgroundTapped_iOS26() throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let active = ProfileSwitcherItem.fixture() + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + subject.receive(.profileSwitcher(.backgroundTapped)) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.showPreLoginSettings` requests the coordinator navigate to the /// pre-login settings. @MainActor diff --git a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift index b3c1f90332..ca9455942b 100644 --- a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift +++ b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift @@ -288,8 +288,11 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_perform_requestedProfileSwitcherVisible_false() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher - throw XCTSkip("This test requires iOS 18.6 or earlier") + // NOTE: This test is not applicable for iOS 26. + // On iOS 26, the profile switcher is presented as a separate sheet, not toggled via visibility state. + // There's no "hide" operation via `.requestedProfileSwitcher(visible: false)` - dismissal happens + // via coordinator navigation (.dismiss) triggered by user actions or explicit dismissal calls. + throw XCTSkip("This test is only applicable for iOS < 26") } let active = ProfileSwitcherItem.fixture() @@ -311,7 +314,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_perform_requestedProfileSwitcherVisible_true() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -330,6 +332,26 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertTrue(subject.state.profileSwitcherState.isVisible) } + /// `showProfileSwitcher()` navigates to present the profile switcher sheet on iOS 26. + @MainActor + func test_perform_requestedProfileSwitcherVisible_true_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let active = ProfileSwitcherItem.fixture() + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.requestedProfileSwitcher(visible: true))) + + XCTAssertEqual(coordinator.routes.last, .viewProfileSwitcher) + } + /// `perform(.profileSwitcher(.rowAppeared))` should not update the state for add Account @MainActor func test_perform_rowAppeared_add() async { @@ -901,11 +923,51 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLockedSuccessfully)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows alert and allows locking + /// another account, dismissing the profile switcher after confirmation. + @MainActor + func test_receive_accountLongPressed_lock_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture(userId: "1") + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = Account.fixture(profile: .fixture(userId: "1")) + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + // On iOS 26, the alert presents from the sheet, so the sheet stays visible + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify dismissal happens after lock action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + // Verify the results. + XCTAssertEqual( + coordinator.events.last, + .action( + .lockVault( + userId: otherProfile.userId, + isManuallyLocking: true, + ), + ), + ) + XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLockedSuccessfully)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` records any errors from locking the account. @MainActor func test_receive_accountLongPressed_lock_error() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -931,6 +993,37 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 records any errors from locking. + @MainActor + func test_receive_accountLongPressed_lock_error_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = nil + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify dismissal happens after lock error + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// log out of the selected account, which navigates back to the landing page for the active account. @MainActor @@ -971,6 +1064,48 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t ) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// log out of the selected account, which navigates back to the landing page for the active account + /// and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_activeAccount_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + + await subject.perform(.profileSwitcher(.accountLongPressed(activeProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed after logout action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual( + coordinator.events.last, + .action( + .logout(userId: activeProfile.userId, userInitiated: true), + ), + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// log out of the selected account, which triggers an account switch. @MainActor @@ -1018,6 +1153,54 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t ) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// log out of the selected account, which triggers an account switch and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_activeAccount_withAlternate_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + stateService.accounts = [ + .fixture( + profile: .fixture( + userId: "42", + ), + ), + ] + + await subject.perform(.profileSwitcher(.accountLongPressed(activeProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed after logout action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual( + coordinator.events.last, + .action( + .logout(userId: activeProfile.userId, userInitiated: true), + ), + ) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// log out of the selected account, which displays a toast. @MainActor @@ -1062,6 +1245,51 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLoggedOutSuccessfully)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// log out of the selected account, which displays a toast and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_otherAccount_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed after logout action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual( + coordinator.events.last, + .action(.logout(userId: otherProfile.userId, userInitiated: true)), + ) + XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLoggedOutSuccessfully)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` records any errors from logging out the /// account. @MainActor @@ -1097,6 +1325,42 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 records any errors from logging out + /// the account and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_error_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = nil + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed even after error + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_active_unlocked() throws { @@ -1131,6 +1395,36 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.events, []) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// when pressing the active unlocked account. + @MainActor + func test_receive_accountPressed_active_unlocked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture() + authRepository.profileSwitcherState = .init( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.events, []) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_active_locked() throws { @@ -1168,6 +1462,39 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.events, []) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// when pressing the active locked account. + @MainActor + func test_receive_accountPressed_active_locked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture(isUnlocked: false) + let account = Account.fixture(profile: .fixture( + userId: profile.userId, + )) + authRepository.profileSwitcherState = .init( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.accountForItemResult = .success(account) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile], + activeAccountId: profile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.events, []) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_alternateUnlocked() throws { @@ -1207,6 +1534,41 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.events, [.action(.switchAccount(isAutomatic: false, userId: profile.userId))]) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// and switches to an alternate unlocked account. + @MainActor + func test_receive_accountPressed_alternateUnlocked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture(isUnlocked: true) + let active = ProfileSwitcherItem.fixture() + let account = Account.fixture(profile: .fixture( + userId: profile.userId, + )) + authRepository.profileSwitcherState = .init( + accounts: [active, profile], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.accountForItemResult = .success(account) + authRepository.isLockedResult = .success(false) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.events, [.action(.switchAccount(isAutomatic: false, userId: profile.userId))]) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_alternateLocked() throws { @@ -1245,6 +1607,40 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.events, [.action(.switchAccount(isAutomatic: false, userId: profile.userId))]) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// and switches to an alternate locked account. + @MainActor + func test_receive_accountPressed_alternateLocked_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture(isUnlocked: false) + let active = ProfileSwitcherItem.fixture() + let account = Account.fixture(profile: .fixture( + userId: profile.userId, + )) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.accountForItemResult = .success(account) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.events, [.action(.switchAccount(isAutomatic: false, userId: profile.userId))]) + } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` updates the state to reflect the changes. @MainActor func test_receive_accountPressed_noMatch() throws { @@ -1279,6 +1675,36 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.events, [.action(.switchAccount(isAutomatic: false, userId: profile.userId))]) } + /// `receive(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher + /// and switches to an account that doesn't match. + @MainActor + func test_receive_accountPressed_noMatch_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let profile = ProfileSwitcherItem.fixture() + let active = ProfileSwitcherItem.fixture() + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [profile, active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.accountPressed(profile))) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.events, [.action(.switchAccount(isAutomatic: false, userId: profile.userId))]) + } + /// `receive(_:)` with `.profileSwitcher(.addAccountPressed)` updates the state to reflect the changes. @MainActor func test_receive_addAccountPressed() throws { @@ -1306,6 +1732,29 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.routes, [.landing]) } + /// `receive(_:)` with `.profileSwitcher(.addAccountPressed)` for iOS 26 dismisses the profile switcher + /// and navigates to the landing page to add a new account. + @MainActor + func test_receive_addAccountPressed_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let active = ProfileSwitcherItem.fixture() + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + await subject.perform(.profileSwitcher(.addAccountPressed)) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.routes.last, .landing) + } + /// `receive(_:)` with `.profileSwitcher(.backgroundPressed)` updates the state to reflect the changes. @MainActor func test_receive_backgroundPressed() throws { @@ -1333,6 +1782,28 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(coordinator.routes, []) } + /// `receive(_:)` with `.profileSwitcher(.backgroundPressed)` for iOS 26 dismisses the profile switcher + /// when tapping the background. + @MainActor + func test_receive_backgroundPressed_iOS26() throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let active = ProfileSwitcherItem.fixture() + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [active], + activeAccountId: active.userId, + allowLockAndLogout: true, + isVisible: true, + ) + + subject.receive(.profileSwitcher(.backgroundTapped)) + + XCTAssertNotNil(subject.state.profileSwitcherState) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.cancelPressed` notifies the delegate that cancel was pressed. @MainActor func test_receive_cancelPressed() { diff --git a/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift index 2b50e6faaf..e277468905 100644 --- a/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift @@ -190,6 +190,28 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: XCTAssertEqual(coordinator.events.last, .switchAccount(isAutomatic: false, userId: "1")) } + /// `perform(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_perform_profileSwitcher_accountPressed_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = true + await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture(userId: "1")))) + authRepository.activeAccount = .fixture(profile: .fixture(userId: "42")) + authRepository.altAccounts = [ + .fixture(), + ] + authRepository.vaultTimeout = [ + "1": .fiveMinutes, + "42": .immediately, + ] + + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual(coordinator.events.last, .switchAccount(isAutomatic: false, userId: "1")) + } + /// `perform(_:)` with `.profileSwitcher(.lock)` does nothing. @MainActor func test_perform_profileSwitcher_lock() async { @@ -213,6 +235,20 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: XCTAssertTrue(subject.state.profileSwitcherState.isVisible) } + /// `perform(_:)` with `.profileSwitcher(.requestedProfileSwitcher(visible:))` + /// for iOS 26 navigates to present the profile switcher sheet. + @MainActor + func test_perform_profileSwitcher_toggleProfilesViewVisibility_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = false + await subject.perform(.profileSwitcher(.requestedProfileSwitcher(visible: true))) + + XCTAssertEqual(coordinator.routes.last, .viewProfileSwitcher) + } + /// `perform(_:)` with `.search()` performs a cipher search and updates the state with the results. @MainActor func test_perform_search() { @@ -398,6 +434,19 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: XCTAssertFalse(subject.state.profileSwitcherState.isVisible) } + /// `receive(_:)` with `.profileSwitcher(.backgroundTapped)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_receive_profileSwitcher_backgroundPressed_iOS26() throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = true + subject.receive(.profileSwitcher(.backgroundTapped)) + + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.profileSwitcher(.logout)` does nothing. @MainActor func test_receive_profileSwitcher_logout() async { diff --git a/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift index 38088d3e36..e453e226a0 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift @@ -162,6 +162,35 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable ) } + /// `perform(_:)` with `.profileSwitcher(.accountPressed)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_perform_profileSwitcher_accountPressed_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = true + await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture(userId: "1")))) + authRepository.activeAccount = .fixture(profile: .fixture(userId: "42")) + authRepository.altAccounts = [ + .fixture(), + ] + authRepository.vaultTimeout = [ + "1": .fiveMinutes, + "42": .immediately, + ] + + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + XCTAssertEqual( + coordinator.events.last, + .switchAccount( + isAutomatic: false, + userId: "1", + authCompletionRoute: .tab(.vault(.vaultItemSelection(.fixtureExample))), + ), + ) + } + /// `perform(_:)` with `.profileSwitcher(.lock)` does nothing. @MainActor func test_perform_profileSwitcher_lock() async { @@ -185,6 +214,20 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable XCTAssertTrue(subject.state.profileSwitcherState.isVisible) } + /// `perform(_:)` with `.profileSwitcher(.requestedProfileSwitcher(visible:))` + /// for iOS 26 navigates to present the profile switcher sheet. + @MainActor + func test_perform_profileSwitcher_toggleProfilesViewVisibility_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = false + await subject.perform(.profileSwitcher(.requestedProfileSwitcher(visible: true))) + + XCTAssertEqual(coordinator.routes.last, .viewProfileSwitcher) + } + /// `perform(_:)` with `.search()` performs a vault search and updates the state with the results. @MainActor func test_perform_search() throws { @@ -497,6 +540,19 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable XCTAssertFalse(subject.state.profileSwitcherState.isVisible) } + /// `receive(_:)` with `.profileSwitcher(.backgroundTapped)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_receive_profileSwitcher_backgroundPressed_iOS26() throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = true + subject.receive(.profileSwitcher(.backgroundTapped)) + + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.profileSwitcher(.logout)` does nothing. @MainActor func test_receive_profileSwitcher_logout() async { diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift index 2da7ded964..e08685d786 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift @@ -725,6 +725,28 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertTrue(authRepository.checkSessionTimeoutCalled) } + /// `perform(.profileSwitcher(.requestedProfileSwitcher))` + /// for iOS 26 navigates to present the profile switcher sheet. + @MainActor + func test_perform_requestedProfileSwitcher_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + let annAccount = ProfileSwitcherItem.anneAccount + let beeAccount = ProfileSwitcherItem.beeAccount + + subject.state.profileSwitcherState.accounts = [annAccount, beeAccount] + subject.state.profileSwitcherState.isVisible = false + + authRepository.profileSwitcherState = ProfileSwitcherState.maximumAccounts + await subject.perform(.profileSwitcher(.requestedProfileSwitcher(visible: true))) + + // Ensure the coordinator navigates to present the profile switcher + XCTAssertEqual(coordinator.routes.last, .viewProfileSwitcher) + XCTAssertTrue(authRepository.checkSessionTimeoutCalled) + } + /// `perform(.profileSwitcher(.rowAppeared))` should not update the state for add Account @MainActor func test_perform_rowAppeared_add() async { @@ -1205,6 +1227,42 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(coordinator.events.last, .lockVault(userId: activeProfile.userId, isManuallyLocking: true)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// lock the active account, dismissing the profile switcher after confirmation. + @MainActor + func test_receive_accountLongPressed_lock_activeAccount_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "1") + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + authRepository.vaultTimeout = [ + "1": .fiveMinutes, + "42": .fifteenMinutes, + ] + + await subject.perform(.profileSwitcher(.accountLongPressed(activeProfile))) + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify the profile switcher sheet is dismissed after lock action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(coordinator.events.last, .lockVault(userId: activeProfile.userId, isManuallyLocking: true)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// lock the selected account, which displays a toast. @MainActor @@ -1241,6 +1299,43 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLockedSuccessfully)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// lock the other account, which displays a toast and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_lock_otherAccount_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture(userId: "1") + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + authRepository.vaultTimeout = [ + "1": .fiveMinutes, + "42": .fifteenMinutes, + ] + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify the profile switcher sheet is dismissed after lock action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(coordinator.events.last, .lockVault(userId: otherProfile.userId, isManuallyLocking: true)) + XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLockedSuccessfully)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` records any errors from locking the account. @MainActor func test_receive_accountLongPressed_lock_error() async throws { @@ -1271,6 +1366,38 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 records any errors from locking + /// the account and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_lock_error_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture(userId: "1") + let otherProfile = ProfileSwitcherItem.fixture(isUnlocked: true, userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + stateService.activeAccount = nil + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to lock the account. + let lockAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await lockAction.handler?(lockAction, []) + + // Verify the profile switcher sheet is dismissed even after error + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// log out of the selected account, which navigates back to the landing page for the active account. @MainActor @@ -1306,6 +1433,42 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(coordinator.events.last, .logout(userId: activeProfile.userId, userInitiated: true)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// log out of the active account, dismissing the profile switcher after confirmation. + @MainActor + func test_receive_accountLongPressed_logout_activeAccount_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture(userId: "1") + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + + await subject.perform(.profileSwitcher(.accountLongPressed(activeProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed after logout action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(coordinator.events.last, .logout(userId: activeProfile.userId, userInitiated: true)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` shows the alert and allows the user to /// log out of the selected account, which displays a toast. @MainActor @@ -1344,6 +1507,45 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLoggedOutSuccessfully)) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 shows the alert and allows the user to + /// log out of the other account, which displays a toast and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_otherAccount_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.activeAccount = .fixture() + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed after logout action + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual( + coordinator.events.last, + .logout(userId: otherProfile.userId, userInitiated: true), + ) + XCTAssertEqual(subject.state.toast, Toast(title: Localizations.accountLoggedOutSuccessfully)) + } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` records any errors from logging out the /// account. @MainActor @@ -1379,6 +1581,42 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) } + /// `receive(_:)` with `.profileSwitcher(.accountLongPressed)` for iOS 26 records any errors from logging out + /// the account and dismisses the profile switcher. + @MainActor + func test_receive_accountLongPressed_logout_error_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + // Set up the mock data. + let activeProfile = ProfileSwitcherItem.fixture() + let otherProfile = ProfileSwitcherItem.fixture(userId: "42") + subject.state.profileSwitcherState = ProfileSwitcherState( + accounts: [otherProfile, activeProfile], + activeAccountId: activeProfile.userId, + allowLockAndLogout: true, + isVisible: true, + ) + authRepository.getAccountError = BitwardenTestError.example + + await subject.perform(.profileSwitcher(.accountLongPressed(otherProfile))) + + // Select the alert action to log out from the account. + let logoutAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await logoutAction.handler?(logoutAction, []) + + // Confirm logging out on the second alert. + let confirmAction = try XCTUnwrap(coordinator.alertShown.last?.alertActions.first) + await confirmAction.handler?(confirmAction, []) + + // Verify the profile switcher sheet is dismissed even after error + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + + // Verify the results. + XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) + } + /// `receive(_:)` with `.addAccountPressed` updates the state correctly @MainActor func test_receive_accountPressed() async throws { @@ -1393,6 +1631,19 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertFalse(subject.state.profileSwitcherState.isVisible) } + /// `receive(_:)` with `.addAccountPressed` for iOS 26 dismisses the profile switcher. + @MainActor + func test_receive_accountPressed_iOS26() async throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = true + await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture()))) + + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.addAccountPressed` updates the state correctly @MainActor func test_receive_addAccountPressed() async { @@ -1561,6 +1812,19 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertFalse(subject.state.profileSwitcherState.isVisible) } + /// `receive(_:)` with `.profileSwitcher(.backgroundTapped)` for iOS 26 dismisses the profile switcher. + @MainActor + func test_receive_profileSwitcherBackgroundPressed_iOS26() throws { + guard #available(iOS 26, *) else { + throw XCTSkip("This test requires iOS 26 or later") + } + + subject.state.profileSwitcherState.isVisible = true + subject.receive(.profileSwitcher(.backgroundTapped)) + + XCTAssertTrue(coordinator.routes.contains(.dismiss)) + } + /// `receive(_:)` with `.searchStateChanged(isSearching: false)` hides the profile switcher @MainActor func test_receive_searchTextChanged_false_noProfilesChange() { From bece6c9b8ce5ecc74547bd1450c2a5a56282a979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gonc=CC=A7alves?= Date: Fri, 28 Nov 2025 04:11:53 +0000 Subject: [PATCH 2/3] pm-25906 Remove TODOs --- .../Auth/Landing/LandingProcessorTests.swift | 11 ---------- .../VaultUnlockProcessorTests.swift | 20 ++----------------- .../VaultAutofillListProcessorTests.swift | 3 --- .../VaultItemSelectionProcessorTests.swift | 3 --- .../VaultList/VaultListProcessorTests.swift | 9 --------- 5 files changed, 2 insertions(+), 44 deletions(-) diff --git a/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift b/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift index ad33fdbfbd..ed97e6e213 100644 --- a/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift +++ b/BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift @@ -481,7 +481,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountLongPressed_lock() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -551,7 +550,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountLongPressed_lock_error() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -614,7 +612,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountLongPressed_logout() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -693,7 +690,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountLongPressed_logout_error() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -764,7 +760,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountPressed_active_unlocked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -837,7 +832,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountPressed_active_locked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -917,7 +911,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountPressed_alternateUnlocked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -999,7 +992,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountPressed_alternateLocked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1077,7 +1069,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_accountPressed_noMatch() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1147,7 +1138,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_addAccountPressed() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1195,7 +1185,6 @@ class LandingProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_ @MainActor func test_receive_backgroundTapped() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } diff --git a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift index ca9455942b..a3c5669d6f 100644 --- a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift +++ b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift @@ -288,11 +288,7 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_perform_requestedProfileSwitcherVisible_false() async throws { guard #unavailable(iOS 26) else { - // NOTE: This test is not applicable for iOS 26. - // On iOS 26, the profile switcher is presented as a separate sheet, not toggled via visibility state. - // There's no "hide" operation via `.requestedProfileSwitcher(visible: false)` - dismissal happens - // via coordinator navigation (.dismiss) triggered by user actions or explicit dismissal calls. - throw XCTSkip("This test is only applicable for iOS < 26") + throw XCTSkip("This test requires iOS 18.6 or earlier") } let active = ProfileSwitcherItem.fixture() @@ -887,8 +883,7 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t /// lock the selected account. @MainActor func test_receive_accountLongPressed_lock() async throws { - guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher + guard #available(iOS 26, *) else { throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1029,7 +1024,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountLongPressed_logout_activeAccount() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1111,7 +1105,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountLongPressed_logout_activeAccount_withAlternate() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1206,7 +1199,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountLongPressed_logout_otherAccount() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1295,7 +1287,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountLongPressed_logout_error() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1365,7 +1356,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountPressed_active_unlocked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1429,7 +1419,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountPressed_active_locked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1499,7 +1488,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountPressed_alternateUnlocked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1573,7 +1561,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountPressed_alternateLocked() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1645,7 +1632,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_accountPressed_noMatch() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1709,7 +1695,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_addAccountPressed() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1759,7 +1744,6 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t @MainActor func test_receive_backgroundPressed() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } diff --git a/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift index e277468905..5b76d27a08 100644 --- a/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift @@ -171,7 +171,6 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: @MainActor func test_perform_profileSwitcher_accountPressed() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -225,7 +224,6 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: @MainActor func test_perform_profileSwitcher_toggleProfilesViewVisibility() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -424,7 +422,6 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: @MainActor func test_receive_profileSwitcher_backgroundPressed() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } diff --git a/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift index e453e226a0..01c8a2c37c 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift @@ -136,7 +136,6 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable @MainActor func test_perform_profileSwitcher_accountPressed() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -204,7 +203,6 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable @MainActor func test_perform_profileSwitcher_toggleProfilesViewVisibility() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -530,7 +528,6 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable @MainActor func test_receive_profileSwitcher_backgroundPressed() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift index e08685d786..c0bfbe8502 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift @@ -706,7 +706,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_perform_requestedProfileSwitcher() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1197,7 +1196,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountLongPressed_lock_activeAccount() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1268,7 +1266,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountLongPressed_lock_otherAccount() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1340,7 +1337,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountLongPressed_lock_error() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1403,7 +1399,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountLongPressed_logout_activeAccount() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1474,7 +1469,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountLongPressed_logout_otherAccount() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1551,7 +1545,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountLongPressed_logout_error() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1621,7 +1614,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_accountPressed() async throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } @@ -1802,7 +1794,6 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ @MainActor func test_receive_profileSwitcherBackgroundPressed() throws { guard #unavailable(iOS 26) else { - // TODO: PM-25906 - Backfill tests for new account switcher throw XCTSkip("This test requires iOS 18.6 or earlier") } From 5c1361a87eb97d899b6cf529717964bec2957d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gonc=CC=A7alves?= Date: Mon, 15 Dec 2025 17:41:21 +0000 Subject: [PATCH 3/3] pm-25906 Fix Claude comments --- .../Vault/AutofillList/VaultAutofillListProcessorTests.swift | 3 ++- .../VaultItemSelection/VaultItemSelectionProcessorTests.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift index 788c820dff..143f699fed 100644 --- a/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/AutofillList/VaultAutofillListProcessorTests.swift @@ -198,7 +198,6 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: } subject.state.profileSwitcherState.isVisible = true - await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture(userId: "1")))) authRepository.activeAccount = .fixture(profile: .fixture(userId: "42")) authRepository.altAccounts = [ .fixture(), @@ -208,6 +207,8 @@ class VaultAutofillListProcessorTests: BitwardenTestCase { // swiftlint:disable: "42": .immediately, ] + await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture(userId: "1")))) + XCTAssertTrue(coordinator.routes.contains(.dismiss)) XCTAssertEqual(coordinator.events.last, .switchAccount(isAutomatic: false, userId: "1")) } diff --git a/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift index bc290a4211..eca2faf387 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultItemSelection/VaultItemSelectionProcessorTests.swift @@ -170,7 +170,6 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable } subject.state.profileSwitcherState.isVisible = true - await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture(userId: "1")))) authRepository.activeAccount = .fixture(profile: .fixture(userId: "42")) authRepository.altAccounts = [ .fixture(), @@ -179,6 +178,8 @@ class VaultItemSelectionProcessorTests: BitwardenTestCase { // swiftlint:disable "1": .fiveMinutes, "42": .immediately, ] + + await subject.perform(.profileSwitcher(.accountPressed(ProfileSwitcherItem.fixture(userId: "1")))) XCTAssertTrue(coordinator.routes.contains(.dismiss)) XCTAssertEqual(