// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Combine import GRDB import Quick import Nimble @testable import Session class ThreadSettingsViewModelSpec: QuickSpec { typealias ParentType = SessionTableViewModel // MARK: - Spec override func spec() { var mockStorage: Storage! var mockGeneralCache: MockGeneralCache! var cancellables: [AnyCancellable] = [] var dependencies: Dependencies! var viewModel: ThreadSettingsViewModel! var didTriggerSearchCallbackTriggered: Bool = false describe("a ThreadSettingsViewModel") { // MARK: - Configuration beforeEach { mockStorage = SynchronousStorage( customWriter: try! DatabaseQueue(), customMigrations: [ SNUtilitiesKit.migrations(), SNSnodeKit.migrations(), SNMessagingKit.migrations(), SNUIKit.migrations() ] ) mockGeneralCache = MockGeneralCache() dependencies = Dependencies( generalCache: Atomic(mockGeneralCache), storage: mockStorage, scheduler: .immediate ) mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") mockStorage.write { db in try SessionThread( id: "TestId", variant: .contact ).insert(db) try Identity( variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey) ).insert(db) try Profile( id: "05\(TestConstants.publicKey)", name: "TestMe" ).insert(db) try Profile( id: "TestId", name: "TestUser" ).insert(db) } viewModel = ThreadSettingsViewModel( dependencies: dependencies, threadId: "TestId", threadVariant: .contact, didTriggerSearch: { didTriggerSearchCallbackTriggered = true } ) cancellables.append( viewModel.observableSettingsData .receiveOnMain(immediately: true) .sink( receiveCompletion: { _ in }, receiveValue: { viewModel.updateSettings($0) } ) ) } afterEach { cancellables.forEach { $0.cancel() } mockStorage = nil cancellables = [] dependencies = nil viewModel = nil didTriggerSearchCallbackTriggered = false } // MARK: - Basic Tests context("with any conversation type") { it("triggers the search callback when tapping search") { viewModel.settingsData .first(where: { $0.model == .content })? .elements .first(where: { $0.id == .searchConversation })? .onTap?(nil) expect(didTriggerSearchCallbackTriggered).to(beTrue()) } it("mutes a conversation") { viewModel.settingsData .first(where: { $0.model == .content })? .elements .first(where: { $0.id == .notificationMute })? .onTap?(nil) expect( mockStorage .read { db in try SessionThread.fetchOne(db, id: "TestId") }? .mutedUntilTimestamp ) .toNot(beNil()) } it("unmutes a conversation") { mockStorage.write { db in try SessionThread .updateAll( db, SessionThread.Columns.mutedUntilTimestamp.set(to: 1234567890) ) } expect( mockStorage .read { db in try SessionThread.fetchOne(db, id: "TestId") }? .mutedUntilTimestamp ) .toNot(beNil()) viewModel.settingsData .first(where: { $0.model == .content })? .elements .first(where: { $0.id == .notificationMute })? .onTap?(nil) expect( mockStorage .read { db in try SessionThread.fetchOne(db, id: "TestId") }? .mutedUntilTimestamp ) .to(beNil()) } } context("with a note-to-self conversation") { beforeEach { mockStorage.write { db in try SessionThread.deleteAll(db) try SessionThread( id: "05\(TestConstants.publicKey)", variant: .contact ).insert(db) } viewModel = ThreadSettingsViewModel( dependencies: dependencies, threadId: "05\(TestConstants.publicKey)", threadVariant: .contact, didTriggerSearch: { didTriggerSearchCallbackTriggered = true } ) cancellables.append( viewModel.observableSettingsData .receiveOnMain(immediately: true) .sink( receiveCompletion: { _ in }, receiveValue: { viewModel.updateSettings($0) } ) ) } it("has the correct title") { expect(viewModel.title).to(equal("vc_settings_title".localized())) } it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .edit, systemItem: .edit, accessibilityIdentifier: "Edit button" ) ])) } it("has no mute button") { expect( viewModel.settingsData .first(where: { $0.model == .content })? .elements .first(where: { $0.id == .notificationMute }) ).to(beNil()) } context("when entering edit mode") { beforeEach { viewModel.rightNavItems.firstValue()??.first?.action?() let leftAccessory: SessionCell.Accessory? = viewModel.settingsData.first? .elements.first? .leftAccessory switch leftAccessory { case .threadInfo(_, _, _, _, let titleChanged): titleChanged?("TestNew") default: break } } it("enters the editing state") { expect(viewModel.navState.firstValue()) .to(equal(.editing)) expect(viewModel.leftNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .cancel, systemItem: .cancel, accessibilityIdentifier: "Cancel button" ) ])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .done, systemItem: .done, accessibilityIdentifier: "Done" ) ])) } context("when cancelling edit mode") { beforeEach { viewModel.leftNavItems.firstValue()??.first?.action?() } it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .edit, systemItem: .edit, accessibilityIdentifier: "Edit button" ) ])) } it("does not update the nickname for the current user") { expect( mockStorage .read { db in try Profile.fetchOne(db, id: "05\(TestConstants.publicKey)") }? .nickname ) .to(beNil()) } } context("when saving edit mode") { beforeEach { viewModel.rightNavItems.firstValue()??.first?.action?() } it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .edit, systemItem: .edit, accessibilityIdentifier: "Edit button" ) ])) } it("updates the nickname for the current user") { expect( mockStorage .read { db in try Profile.fetchOne(db, id: "05\(TestConstants.publicKey)") }? .nickname ) .to(equal("TestNew")) } } } } context("with a one-to-one conversation") { beforeEach { mockStorage.write { db in try SessionThread.deleteAll(db) try SessionThread( id: "TestId", variant: .contact ).insert(db) } } it("has the correct title") { expect(viewModel.title).to(equal("vc_settings_title".localized())) } it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .edit, systemItem: .edit, accessibilityIdentifier: "Edit button" ) ])) } context("when entering edit mode") { beforeEach { viewModel.rightNavItems.firstValue()??.first?.action?() let leftAccessory: SessionCell.Accessory? = viewModel.settingsData.first? .elements.first? .leftAccessory switch leftAccessory { case .threadInfo(_, _, _, _, let titleChanged): titleChanged?("TestUserNew") default: break } } it("enters the editing state") { expect(viewModel.navState.firstValue()) .to(equal(.editing)) expect(viewModel.leftNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .cancel, systemItem: .cancel, accessibilityIdentifier: "Cancel button" ) ])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .done, systemItem: .done, accessibilityIdentifier: "Done" ) ])) } context("when cancelling edit mode") { beforeEach { viewModel.leftNavItems.firstValue()??.first?.action?() } it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .edit, systemItem: .edit, accessibilityIdentifier: "Edit button" ) ])) } it("does not update the nickname for the current user") { expect( mockStorage .read { db in try Profile.fetchOne(db, id: "TestId") }? .nickname ) .to(beNil()) } } context("when saving edit mode") { beforeEach { viewModel.rightNavItems.firstValue()??.first?.action?() } it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()) .to(equal([ ParentType.NavItem( id: .edit, systemItem: .edit, accessibilityIdentifier: "Edit button" ) ])) } it("updates the nickname for the current user") { expect( mockStorage .read { db in try Profile.fetchOne(db, id: "TestId") }? .nickname ) .to(equal("TestUserNew")) } } } } context("with a group conversation") { beforeEach { mockStorage.write { db in try SessionThread.deleteAll(db) try SessionThread( id: "TestId", variant: .closedGroup ).insert(db) } viewModel = ThreadSettingsViewModel( dependencies: dependencies, threadId: "TestId", threadVariant: .closedGroup, didTriggerSearch: { didTriggerSearchCallbackTriggered = true } ) cancellables.append( viewModel.observableSettingsData .receiveOnMain(immediately: true) .sink( receiveCompletion: { _ in }, receiveValue: { viewModel.updateSettings($0) } ) ) } it("has the correct title") { expect(viewModel.title).to(equal("vc_group_settings_title".localized())) } it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()).to(equal([])) } } context("with a community conversation") { beforeEach { mockStorage.write { db in try SessionThread.deleteAll(db) try SessionThread( id: "TestId", variant: .openGroup ).insert(db) } viewModel = ThreadSettingsViewModel( dependencies: dependencies, threadId: "TestId", threadVariant: .openGroup, didTriggerSearch: { didTriggerSearchCallbackTriggered = true } ) cancellables.append( viewModel.observableSettingsData .receiveOnMain(immediately: true) .sink( receiveCompletion: { _ in }, receiveValue: { viewModel.updateSettings($0) } ) ) } it("has the correct title") { expect(viewModel.title).to(equal("vc_group_settings_title".localized())) } it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) expect(viewModel.leftNavItems.firstValue()).to(equal([])) expect(viewModel.rightNavItems.firstValue()).to(equal([])) } } } } }