session-ios/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec...

524 lines
23 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Combine
import GRDB
import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
import SessionUtilitiesKit
@testable import Session
class ThreadSettingsViewModelSpec: QuickSpec {
override class func spec() {
// MARK: Configuration
@TestState var mockStorage: Storage! = SynchronousStorage(
customWriter: try! DatabaseQueue(),
migrationTargets: [
SNUtilitiesKit.self,
SNSnodeKit.self,
SNMessagingKit.self,
SNUIKit.self
],
initialData: { db in
try Identity(
variant: .x25519PublicKey,
data: Data(hex: TestConstants.publicKey)
).insert(db)
try SessionThread(id: "TestId",variant: .contact).insert(db)
try Profile(id: "05\(TestConstants.publicKey)", name: "TestMe").insert(db)
try Profile(id: "TestId", name: "TestUser").insert(db)
}
)
@TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache(
initialSetup: { cache in
cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)")
}
)
@TestState var mockCaches: MockCaches! = MockCaches()
.setting(cache: .general, to: mockGeneralCache)
@TestState var dependencies: Dependencies! = Dependencies(
storage: mockStorage,
caches: mockCaches,
scheduler: .immediate
)
@TestState var threadVariant: SessionThread.Variant! = .contact
@TestState var didTriggerSearchCallbackTriggered: Bool! = false
@TestState var viewModel: ThreadSettingsViewModel! = ThreadSettingsViewModel(
threadId: "TestId",
threadVariant: .contact,
didTriggerSearch: {
didTriggerSearchCallbackTriggered = true
},
using: dependencies
)
@TestState var disposables: [AnyCancellable]! = [
viewModel.observableTableData
.receive(on: ImmediateScheduler.shared)
.sink(
receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) }
)
]
// MARK: - a ThreadSettingsViewModel
describe("a ThreadSettingsViewModel") {
// MARK: -- with any conversation type
context("with any conversation type") {
// MARK: ---- triggers the search callback when tapping search
it("triggers the search callback when tapping search") {
viewModel.tableData
.first(where: { $0.model == .content })?
.elements
.first(where: { $0.id == .searchConversation })?
.onTap?()
expect(didTriggerSearchCallbackTriggered).to(beTrue())
}
// MARK: ---- mutes a conversation
it("mutes a conversation") {
viewModel.tableData
.first(where: { $0.model == .content })?
.elements
.first(where: { $0.id == .notificationMute })?
.onTap?()
expect(
mockStorage
.read { db in try SessionThread.fetchOne(db, id: "TestId") }?
.mutedUntilTimestamp
)
.toNot(beNil())
}
// MARK: ---- unmutes a conversation
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.tableData
.first(where: { $0.model == .content })?
.elements
.first(where: { $0.id == .notificationMute })?
.onTap?()
expect(
mockStorage
.read { db in try SessionThread.fetchOne(db, id: "TestId") }?
.mutedUntilTimestamp
)
.to(beNil())
}
}
// MARK: -- with a note-to-self conversation
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(
threadId: "05\(TestConstants.publicKey)",
threadVariant: .contact,
didTriggerSearch: {
didTriggerSearchCallbackTriggered = true
},
using: dependencies
)
disposables.append(
viewModel.observableTableData
.receive(on: ImmediateScheduler.shared)
.sink(
receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) }
)
)
}
// MARK: ---- has the correct title
it("has the correct title") {
expect(viewModel.title).to(equal("vc_settings_title".localized()))
}
// MARK: ---- starts in the standard nav state
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"
)
]))
}
// MARK: ---- has no mute button
it("has no mute button") {
expect(
viewModel.tableData
.first(where: { $0.model == .content })?
.elements
.first(where: { $0.id == .notificationMute })
).to(beNil())
}
// MARK: ---- when entering edit mode
context("when entering edit mode") {
beforeEach {
viewModel.navState.sinkAndStore(in: &disposables)
viewModel.rightNavItems.firstValue()??.first?.action?()
viewModel.textChanged("TestNew", for: .nickname)
}
// MARK: ------ enters the editing state
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"
)
]))
}
// MARK: ------ when cancelling edit mode
context("when cancelling edit mode") {
beforeEach {
viewModel.leftNavItems.firstValue()??.first?.action?()
}
// MARK: -------- exits editing mode
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"
)
]))
}
// MARK: -------- does not update the nickname for the current user
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())
}
}
// MARK: ------ when saving edit mode
context("when saving edit mode") {
beforeEach {
viewModel.rightNavItems.firstValue()??.first?.action?()
}
// MARK: -------- exits editing mode
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"
)
]))
}
// MARK: -------- updates the nickname for the current user
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"))
}
}
}
}
// MARK: -- with a one-to-one conversation
context("with a one-to-one conversation") {
beforeEach {
mockStorage.write { db in
try SessionThread.deleteAll(db)
try SessionThread(
id: "TestId",
variant: .contact
).insert(db)
}
}
// MARK: ---- has the correct title
it("has the correct title") {
expect(viewModel.title).to(equal("vc_settings_title".localized()))
}
// MARK: ---- starts in the standard nav state
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"
)
]))
}
// MARK: ---- when entering edit mode
context("when entering edit mode") {
beforeEach {
viewModel.navState.sinkAndStore(in: &disposables)
viewModel.rightNavItems.firstValue()??.first?.action?()
viewModel.textChanged("TestUserNew", for: .nickname)
}
// MARK: ------ enters the editing state
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"
)
]))
}
// MARK: ------ when cancelling edit mode
context("when cancelling edit mode") {
beforeEach {
viewModel.leftNavItems.firstValue()??.first?.action?()
}
// MARK: -------- exits editing mode
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"
)
]))
}
// MARK: -------- does not update the nickname for the current user
it("does not update the nickname for the current user") {
expect(
mockStorage
.read { db in try Profile.fetchOne(db, id: "TestId") }?
.nickname
)
.to(beNil())
}
}
// MARK: ------ when saving edit mode
context("when saving edit mode") {
beforeEach {
viewModel.rightNavItems.firstValue()??.first?.action?()
}
// MARK: -------- exits editing mode
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"
)
]))
}
// MARK: -------- updates the nickname for the current user
it("updates the nickname for the current user") {
expect(
mockStorage
.read { db in try Profile.fetchOne(db, id: "TestId") }?
.nickname
)
.to(equal("TestUserNew"))
}
}
}
}
// MARK: -- with a group conversation
context("with a group conversation") {
beforeEach {
mockStorage.write { db in
try SessionThread.deleteAll(db)
try SessionThread(
id: "TestId",
variant: .legacyGroup
).insert(db)
}
viewModel = ThreadSettingsViewModel(
threadId: "TestId",
threadVariant: .legacyGroup,
didTriggerSearch: {
didTriggerSearchCallbackTriggered = true
},
using: dependencies
)
disposables.append(
viewModel.observableTableData
.receive(on: ImmediateScheduler.shared)
.sink(
receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) }
)
)
}
// MARK: ---- has the correct title
it("has the correct title") {
expect(viewModel.title).to(equal("vc_group_settings_title".localized()))
}
// MARK: ---- starts in the standard nav state
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([]))
}
}
// MARK: -- with a community conversation
context("with a community conversation") {
beforeEach {
mockStorage.write { db in
try SessionThread.deleteAll(db)
try SessionThread(
id: "TestId",
variant: .community
).insert(db)
}
viewModel = ThreadSettingsViewModel(
threadId: "TestId",
threadVariant: .community,
didTriggerSearch: {
didTriggerSearchCallbackTriggered = true
},
using: dependencies
)
disposables.append(
viewModel.observableTableData
.receive(on: ImmediateScheduler.shared)
.sink(
receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) }
)
)
}
// MARK: ---- has the correct title
it("has the correct title") {
expect(viewModel.title).to(equal("vc_group_settings_title".localized()))
}
// MARK: ---- starts in the standard nav state
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([]))
}
}
}
}
}
// MARK: - Test Types
fileprivate typealias ParentType = SessionTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting>