Add max selections support for polls (#5726)

This commit is contained in:
svetoslav-sportinno
2026-06-22 14:39:12 +03:00
committed by GitHub
parent 173a927bf8
commit 30e4e6339d
49 changed files with 506 additions and 251 deletions
@@ -26,6 +26,7 @@ struct PollFormScreenViewModelTests {
let isEmpty = context.options.allSatisfy(\.text.isEmpty)
#expect(isEmpty)
#expect(context.question.isEmpty)
#expect(context.maxSelections == 1)
#expect(context.viewState.isSubmitButtonDisabled)
#expect(!context.viewState.bindings.isUndisclosed)
@@ -44,6 +45,7 @@ struct PollFormScreenViewModelTests {
#expect(context.options.count == 3)
#expect(context.options.allSatisfy { !$0.text.isEmpty })
#expect(!context.question.isEmpty)
#expect(context.maxSelections == 1)
#expect(context.viewState.isSubmitButtonDisabled)
#expect(!context.viewState.bindings.isUndisclosed)
@@ -90,22 +92,58 @@ struct PollFormScreenViewModelTests {
#expect(context.alertInfo != nil)
}
@Test
mutating func maxSelectionsBounds() async throws {
setupViewModel()
context.send(viewAction: .decrementMaxSelections)
#expect(context.maxSelections == 1)
context.send(viewAction: .incrementMaxSelections)
#expect(context.maxSelections == 2)
context.send(viewAction: .incrementMaxSelections)
#expect(context.maxSelections == 2)
context.send(viewAction: .addOption)
context.send(viewAction: .incrementMaxSelections)
#expect(context.maxSelections == 3)
let deferredMaxSelections = deferFulfillment(context.observe(\.viewState.bindings.maxSelections)) { $0 == 2 }
context.send(viewAction: .deleteOption(index: 2))
try await deferredMaxSelections.fulfill()
#expect(context.maxSelections == 2)
}
@Test
mutating func editPollInitialMaxSelections() {
setupViewModel(mode: .edit(eventID: "foo", poll: .mock(question: "Pick two",
maxSelections: 2,
options: [.mock(text: "One"),
.mock(text: "Two"),
.mock(text: "Three")])))
#expect(context.maxSelections == 2)
}
@Test
mutating func newPollSubmit() async throws {
setupViewModel()
context.question = "foo"
context.options[0].text = "bla1"
context.options[1].text = "bla2"
context.send(viewAction: .incrementMaxSelections)
#expect(!context.viewState.isSubmitButtonDisabled)
let deferred = deferFulfillment(viewModel.actions) { $0 == .close }
try await confirmation { confirmation in
timelineProxy.createPollQuestionAnswersPollKindClosure = { question, options, kind in
timelineProxy.createPollQuestionAnswersMaxSelectionsPollKindClosure = { question, options, maxSelections, kind in
#expect(question == "foo")
#expect(options.count == 2)
#expect(options[0] == "bla1")
#expect(options[1] == "bla2")
#expect(maxSelections == 2)
#expect(kind == .disclosed)
confirmation()
return .success(())
@@ -122,12 +160,13 @@ struct PollFormScreenViewModelTests {
context.question = "What is your favorite country?"
context.options.append(.init(text: "France 🇫🇷"))
context.send(viewAction: .incrementMaxSelections)
#expect(!context.viewState.isSubmitButtonDisabled)
let deferred = deferFulfillment(viewModel.actions) { $0 == .close }
try await confirmation { confirmation in
timelineProxy.editPollOriginalQuestionAnswersPollKindClosure = { eventID, question, options, kind in
timelineProxy.editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure = { eventID, question, options, maxSelections, kind in
#expect(eventID == "foo")
#expect(question == "What is your favorite country?")
#expect(options.count == 4)
@@ -135,6 +174,7 @@ struct PollFormScreenViewModelTests {
#expect(options[1] == "China 🇨🇳")
#expect(options[2] == "USA 🇺🇸")
#expect(options[3] == "France 🇫🇷")
#expect(maxSelections == 2)
#expect(kind == .disclosed)
confirmation()
return .success(())
+74
View File
@@ -0,0 +1,74 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import Testing
struct PollTests {
@Test
func singleSelectionReplacesSelectedAnswer() {
let poll = Poll.mock(question: "Question?",
maxSelections: 1,
options: [
.mock(id: "first", text: "First", isSelected: true),
.mock(id: "second", text: "Second")
])
#expect(poll.answerIDsAfterSelecting(optionID: "second") == ["second"])
#expect(poll.answerIDsAfterSelecting(optionID: "first") == nil)
}
@Test
func multipleSelectionAddsAnswerUntilLimit() {
let poll = Poll.mock(question: "Question?",
maxSelections: 2,
options: [
.mock(id: "first", text: "First", isSelected: true),
.mock(id: "second", text: "Second"),
.mock(id: "third", text: "Third")
])
#expect(poll.answerIDsAfterSelecting(optionID: "second") == ["first", "second"])
}
@Test
func multipleSelectionCanRemoveAnswerWhenMoreThanOneIsSelected() {
let poll = Poll.mock(question: "Question?",
maxSelections: 2,
options: [
.mock(id: "first", text: "First", isSelected: true),
.mock(id: "second", text: "Second", isSelected: true),
.mock(id: "third", text: "Third")
])
#expect(poll.answerIDsAfterSelecting(optionID: "first") == ["second"])
}
@Test
func multipleSelectionDoesNotAddAnswerOverLimit() {
let poll = Poll.mock(question: "Question?",
maxSelections: 2,
options: [
.mock(id: "first", text: "First", isSelected: true),
.mock(id: "second", text: "Second", isSelected: true),
.mock(id: "third", text: "Third")
])
#expect(poll.answerIDsAfterSelecting(optionID: "third") == nil)
}
}
private extension Poll.Option {
static func mock(id: String, text: String, isSelected: Bool = false) -> Self {
.init(id: id,
text: text,
votes: 0,
allVotes: 0,
isSelected: isSelected,
isWinning: false)
}
}
@@ -139,14 +139,14 @@ struct RoomPollsHistoryScreenViewModelTests {
func sendPollResponse() async throws {
let deferred = deferFulfillment(interactionHandler.publisher.delay(for: 0.1, scheduler: DispatchQueue.main)) { _ in true }
interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .success(())
viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID"))
interactionHandler.sendPollResponsePollStartIDAnswerIDsReturnValue = .success(())
viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", answerIDs: ["firstOptionID", "secondOptionID"]))
try await deferred.fulfill()
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled)
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID")
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID")
#expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsCalled)
#expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].pollStartID == "somePollID")
#expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].answerIDs == ["firstOptionID", "secondOptionID"])
}
@Test
@@ -155,14 +155,14 @@ struct RoomPollsHistoryScreenViewModelTests {
value.bindings.alertInfo != nil
}
interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .failure(SDKError.generic)
viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID"))
interactionHandler.sendPollResponsePollStartIDAnswerIDsReturnValue = .failure(SDKError.generic)
viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", answerIDs: ["someOptionID"]))
try await deferred.fulfill()
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled)
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID")
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID")
#expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsCalled)
#expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].pollStartID == "somePollID")
#expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].answerIDs == ["someOptionID"])
}
@Test