mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
438 lines
19 KiB
Swift
438 lines
19 KiB
Swift
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Combine
|
|
import GRDB
|
|
import Quick
|
|
import Nimble
|
|
import SessionUIKit
|
|
import SessionNetworkingKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
|
|
@testable import Session
|
|
|
|
class ThreadNotificationSettingsViewModelSpec: AsyncSpec {
|
|
override class func spec() {
|
|
// MARK: Configuration
|
|
|
|
@TestState var dependencies: TestDependencies! = TestDependencies { dependencies in
|
|
dependencies.forceSynchronous = true
|
|
dependencies[singleton: .scheduler] = .immediate
|
|
}
|
|
@TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage(
|
|
customWriter: try! DatabaseQueue(),
|
|
migrations: SNMessagingKit.migrations,
|
|
using: dependencies,
|
|
initialData: { db in
|
|
try SessionThread(
|
|
id: "TestId",
|
|
variant: .contact,
|
|
creationDateTimestamp: 0
|
|
).insert(db)
|
|
}
|
|
)
|
|
@TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner(
|
|
initialSetup: { jobRunner in
|
|
jobRunner
|
|
.when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }
|
|
.thenReturn(nil)
|
|
jobRunner
|
|
.when { $0.upsert(.any, job: .any, canStartJob: .any) }
|
|
.thenReturn(nil)
|
|
}
|
|
)
|
|
@TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = MockNotificationsManager(
|
|
initialSetup: { $0.defaultInitialSetup() }
|
|
)
|
|
@TestState var viewModel: ThreadNotificationSettingsViewModel! = TestState.create {
|
|
await ThreadNotificationSettingsViewModel(
|
|
threadId: "TestId",
|
|
threadVariant: .contact,
|
|
threadOnlyNotifyForMentions: nil,
|
|
threadMutedUntilTimestamp: nil,
|
|
using: dependencies
|
|
)
|
|
}
|
|
|
|
@TestState var cancellables: [AnyCancellable]! = [
|
|
viewModel.tableDataPublisher
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { viewModel.updateTableData($0) }
|
|
)
|
|
]
|
|
|
|
// MARK: - a ThreadNotificationSettingsViewModel
|
|
describe("a ThreadNotificationSettingsViewModel") {
|
|
beforeEach {
|
|
// Wait for the state to load
|
|
await expect(viewModel.tableData).toEventuallyNot(beEmpty())
|
|
}
|
|
|
|
// MARK: -- has the correct title
|
|
it("has the correct title") {
|
|
expect(viewModel.title).to(equal("sessionNotifications".localized()))
|
|
}
|
|
|
|
// MARK: -- has the correct number of items
|
|
it("has the correct number of items") {
|
|
// The default disappearing messages configure is Off
|
|
// Should only show one section of Disappearing Messages Type
|
|
expect(viewModel.tableData.count).to(equal(1))
|
|
|
|
// All Messages
|
|
// Mentions Only
|
|
// Mute
|
|
expect(viewModel.tableData.first?.elements.count).to(equal(3))
|
|
}
|
|
|
|
// MARK: -- has the correct default state
|
|
it("has the correct default state") {
|
|
// First option is always All Messages
|
|
expect(viewModel.tableData.first?.elements.first)
|
|
.to(
|
|
equal(
|
|
SessionCell.Info(
|
|
id: .allMessages,
|
|
position: .top,
|
|
leadingAccessory: .icon(.volume2),
|
|
title: "notificationsAllMessages".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: true,
|
|
accessibility: Accessibility(
|
|
identifier: "All messages - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "All messages notification setting",
|
|
label: "All messages"
|
|
),
|
|
onTap: {}
|
|
)
|
|
)
|
|
)
|
|
// Last option is always Mute
|
|
expect(viewModel.tableData.first?.elements.last)
|
|
.to(
|
|
equal(
|
|
SessionCell.Info(
|
|
id: .mute,
|
|
position: .bottom,
|
|
leadingAccessory: .icon(.volumeOff),
|
|
title: "notificationsMute".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: false,
|
|
accessibility: Accessibility(
|
|
identifier: "Mute - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "\(ThreadSettingsViewModel.self).mute",
|
|
label: "Mute notifications"
|
|
),
|
|
onTap: {}
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
// MARK: -- starts with the correct item active if not default
|
|
it("starts with the correct item active if not default") {
|
|
// Test settings: Mute
|
|
mockStorage.write { db in
|
|
try SessionThread
|
|
.filter(id: "TestId")
|
|
.updateAll(
|
|
db,
|
|
SessionThread.Columns.mutedUntilTimestamp.set(
|
|
to: Date.distantFuture.timeIntervalSince1970
|
|
)
|
|
)
|
|
}
|
|
viewModel = await ThreadNotificationSettingsViewModel(
|
|
threadId: "TestId",
|
|
threadVariant: .contact,
|
|
threadOnlyNotifyForMentions: false,
|
|
threadMutedUntilTimestamp: Date.distantFuture.timeIntervalSince1970,
|
|
using: dependencies
|
|
)
|
|
cancellables.append(
|
|
viewModel.tableDataPublisher
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { viewModel.updateTableData($0) }
|
|
)
|
|
)
|
|
|
|
// Wait for the state to load
|
|
await expect(viewModel.tableData).toEventuallyNot(beEmpty())
|
|
|
|
expect(viewModel.tableData.first?.elements.first)
|
|
.to(
|
|
equal(
|
|
SessionCell.Info(
|
|
id: .allMessages,
|
|
position: .top,
|
|
leadingAccessory: .icon(.volume2),
|
|
title: "notificationsAllMessages".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: false,
|
|
accessibility: Accessibility(
|
|
identifier: "All messages - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "All messages notification setting",
|
|
label: "All messages"
|
|
),
|
|
onTap: {}
|
|
)
|
|
)
|
|
)
|
|
|
|
expect(viewModel.tableData.first?.elements.last)
|
|
.to(
|
|
equal(
|
|
SessionCell.Info(
|
|
id: .mute,
|
|
position: .bottom,
|
|
leadingAccessory: .icon(.volumeOff),
|
|
title: "notificationsMute".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: true,
|
|
accessibility: Accessibility(
|
|
identifier: "Mute - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "\(ThreadSettingsViewModel.self).mute",
|
|
label: "Mute notifications"
|
|
),
|
|
onTap: {}
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
// MARK: -- has a disabled footer button
|
|
it("has a disabled footer button") {
|
|
var footerButtonInfo: SessionButton.Info?
|
|
|
|
cancellables.append(
|
|
viewModel.footerButtonInfo
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { info in footerButtonInfo = info }
|
|
)
|
|
)
|
|
await expect(footerButtonInfo).toEventuallyNot(beNil())
|
|
|
|
expect(footerButtonInfo).to(equal(
|
|
SessionButton.Info(
|
|
style: .bordered,
|
|
title: "set".localized(),
|
|
isEnabled: false,
|
|
accessibility: Accessibility(
|
|
identifier: "Set button",
|
|
label: "Set button"
|
|
),
|
|
minWidth: 110,
|
|
onTap: {}
|
|
)
|
|
))
|
|
}
|
|
|
|
// MARK: -- can change to another setting and change back
|
|
it("can change to another setting and change back") {
|
|
var footerButtonInfo: SessionButton.Info?
|
|
|
|
// Test settings: Mute
|
|
mockStorage.write { db in
|
|
try SessionThread
|
|
.filter(id: "TestId")
|
|
.updateAll(
|
|
db,
|
|
SessionThread.Columns.mutedUntilTimestamp.set(to: Date.distantFuture.timeIntervalSince1970)
|
|
)
|
|
}
|
|
viewModel = await ThreadNotificationSettingsViewModel(
|
|
threadId: "TestId",
|
|
threadVariant: .contact,
|
|
threadOnlyNotifyForMentions: false,
|
|
threadMutedUntilTimestamp: Date.distantFuture.timeIntervalSince1970,
|
|
using: dependencies
|
|
)
|
|
cancellables.append(
|
|
viewModel.tableDataPublisher
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { viewModel.updateTableData($0) }
|
|
)
|
|
)
|
|
cancellables.append(
|
|
viewModel.footerButtonInfo
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { info in
|
|
footerButtonInfo = info
|
|
}
|
|
)
|
|
)
|
|
await expect(footerButtonInfo).toEventuallyNot(beNil())
|
|
|
|
// Wait for the state to load
|
|
await expect(viewModel.tableData).toEventuallyNot(beEmpty())
|
|
|
|
// Change to another setting
|
|
await viewModel.tableData.first?.elements.first?.onTap?()
|
|
|
|
// Gets enabled
|
|
await expect(footerButtonInfo).toEventually(equal(
|
|
SessionButton.Info(
|
|
style: .bordered,
|
|
title: "set".localized(),
|
|
isEnabled: true,
|
|
accessibility: Accessibility(
|
|
identifier: "Set button",
|
|
label: "Set button"
|
|
),
|
|
minWidth: 110,
|
|
onTap: {}
|
|
)
|
|
))
|
|
|
|
// Change back
|
|
await viewModel.tableData.first?.elements.last?.onTap?()
|
|
|
|
await expect(viewModel.tableData.first?.elements.last)
|
|
.toEventually(
|
|
equal(
|
|
SessionCell.Info(
|
|
id: .mute,
|
|
position: .bottom,
|
|
leadingAccessory: .icon(.volumeOff),
|
|
title: "notificationsMute".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: true,
|
|
accessibility: Accessibility(
|
|
identifier: "Mute - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "\(ThreadSettingsViewModel.self).mute",
|
|
label: "Mute notifications"
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
cancellables.append(
|
|
viewModel.footerButtonInfo
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { info in footerButtonInfo = info }
|
|
)
|
|
)
|
|
await expect(footerButtonInfo).toEventuallyNot(beNil())
|
|
|
|
// Disabled again
|
|
expect(footerButtonInfo).to(equal(
|
|
SessionButton.Info(
|
|
style: .bordered,
|
|
title: "set".localized(),
|
|
isEnabled: false,
|
|
accessibility: Accessibility(
|
|
identifier: "Set button",
|
|
label: "Set button"
|
|
),
|
|
minWidth: 110,
|
|
onTap: {}
|
|
)
|
|
))
|
|
}
|
|
|
|
// MARK: -- when changed from the previous setting
|
|
context("when changed from the previous setting") {
|
|
@TestState var footerButtonInfo: SessionButton.Info?
|
|
|
|
beforeEach {
|
|
cancellables.append(
|
|
viewModel.footerButtonInfo
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { info in footerButtonInfo = info }
|
|
)
|
|
)
|
|
|
|
await viewModel.tableData.first?.elements.last?.onTap?()
|
|
await expect(footerButtonInfo).toEventuallyNot(beNil())
|
|
}
|
|
|
|
// MARK: ---- shows the set button
|
|
it("shows the set button") {
|
|
await expect(footerButtonInfo)
|
|
.toEventually(
|
|
equal(
|
|
SessionButton.Info(
|
|
style: .bordered,
|
|
title: "set".localized(),
|
|
isEnabled: true,
|
|
accessibility: Accessibility(
|
|
identifier: "Set button",
|
|
label: "Set button"
|
|
),
|
|
minWidth: 110,
|
|
onTap: {}
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
// MARK: ---- and saving
|
|
context("and saving") {
|
|
// MARK: ------ dismisses the screen
|
|
it("dismisses the screen") {
|
|
var didDismissScreen: Bool = false
|
|
|
|
cancellables.append(
|
|
viewModel.navigatableState.dismissScreen
|
|
.receive(on: ImmediateScheduler.shared)
|
|
.sink(
|
|
receiveCompletion: { _ in },
|
|
receiveValue: { _ in didDismissScreen = true }
|
|
)
|
|
)
|
|
await expect(footerButtonInfo).toEventuallyNot(beNil())
|
|
|
|
await MainActor.run { [footerButtonInfo] in footerButtonInfo?.onTap() }
|
|
|
|
await expect(didDismissScreen).toEventually(beTrue())
|
|
}
|
|
|
|
// MARK: ------ saves the updated settings
|
|
it("saves the updated settings") {
|
|
await MainActor.run { [footerButtonInfo] in footerButtonInfo?.onTap() }
|
|
|
|
await expect(mockNotificationsManager)
|
|
.toEventually(call(.exactly(times: 1), matchingParameters: .all) {
|
|
$0.updateSettings(
|
|
threadId: "TestId",
|
|
threadVariant: .contact,
|
|
mentionsOnly: false,
|
|
mutedUntil: Date.distantFuture.timeIntervalSince1970
|
|
)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|