diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index e691ff827e2..e9ea2bc3dc1 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -150,8 +150,16 @@ limitations under the License. } .mx_MPollBody_totalVotes { + display: flex; + flex-direction: inline; + justify-content: start; color: $secondary-content; font-size: $font-12px; + + .mx_Spinner { + flex: 0; + margin-left: $spacing-8; + } } } diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 3d38fc5a70f..677565f7d81 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -195,7 +195,7 @@ export default class MessageContextMenu extends React.Component return ( M_POLL_START.matches(mxEvent.getType()) && this.state.canRedact && - !isPollEnded(mxEvent, MatrixClientPeg.get(), this.props.getRelationsForEvent) + !isPollEnded(mxEvent, MatrixClientPeg.get()) ); } diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index 946f209d31c..463605553e2 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -35,26 +35,34 @@ interface IProps extends IDialogProps { } export default class EndPollDialog extends React.Component { - private onFinished = (endPoll: boolean): void => { - const topAnswer = findTopAnswer(this.props.event, this.props.matrixClient, this.props.getRelationsForEvent); + private onFinished = async (endPoll: boolean): Promise => { + if (endPoll) { + const room = this.props.matrixClient.getRoom(this.props.event.getRoomId()); + const poll = room?.polls.get(this.props.event.getId()!); - const message = - topAnswer === "" - ? _t("The poll has ended. No votes were cast.") - : _t("The poll has ended. Top answer: %(topAnswer)s", { topAnswer }); + if (!poll) { + throw new Error("No poll instance found in room."); + } - if (endPoll) { - const endEvent = PollEndEvent.from(this.props.event.getId(), message).serialize(); + try { + const responses = await poll.getResponses(); + const topAnswer = findTopAnswer(this.props.event, responses); + + const message = + topAnswer === "" + ? _t("The poll has ended. No votes were cast.") + : _t("The poll has ended. Top answer: %(topAnswer)s", { topAnswer }); + + const endEvent = PollEndEvent.from(this.props.event.getId()!, message).serialize(); - this.props.matrixClient - .sendEvent(this.props.event.getRoomId(), endEvent.type, endEvent.content) - .catch((e: any) => { - console.error("Failed to submit poll response event:", e); - Modal.createDialog(ErrorDialog, { - title: _t("Failed to end poll"), - description: _t("Sorry, the poll did not end. Please try again."), - }); + await this.props.matrixClient.sendEvent(this.props.event.getRoomId()!, endEvent.type, endEvent.content); + } catch (e) { + console.error("Failed to submit poll response event:", e); + Modal.createDialog(ErrorDialog, { + title: _t("Failed to end poll"), + description: _t("Sorry, the poll did not end. Please try again."), }); + } } this.props.onFinished(endPoll); }; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index a9317957c6c..f5bb9e81146 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -14,17 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; -import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Relations } from "matrix-js-sdk/src/models/relations"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START } from "matrix-js-sdk/src/@types/polls"; +import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; -import { NamespacedValue } from "matrix-events-sdk"; import { PollStartEvent, PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent"; +import { Poll, PollEvent } from "matrix-js-sdk/src/models/poll"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -36,11 +36,14 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import { GetRelationsForEvent } from "../rooms/EventTile"; import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import Spinner from "../elements/Spinner"; interface IState { + poll?: Poll; + // poll instance has fetched at least one page of responses + pollInitialised: boolean; selected?: string | null | undefined; // Which option was clicked by the local user - voteRelations: RelatedRelations; // Voting (response) events - endRelations: RelatedRelations; // Poll end events + voteRelations?: Relations; // Voting (response) events } export function createVoteRelations(getRelationsForEvent: GetRelationsForEvent, eventId: string): RelatedRelations { @@ -59,15 +62,7 @@ export function createVoteRelations(getRelationsForEvent: GetRelationsForEvent, return new RelatedRelations(relationsList); } -export function findTopAnswer( - pollEvent: MatrixEvent, - matrixClient: MatrixClient, - getRelationsForEvent?: GetRelationsForEvent, -): string { - if (!getRelationsForEvent) { - return ""; - } - +export function findTopAnswer(pollEvent: MatrixEvent, voteRelations: Relations): string { const pollEventId = pollEvent.getId(); if (!pollEventId) { logger.warn( @@ -87,25 +82,7 @@ export function findTopAnswer( return poll.answers.find((a) => a.id === answerId)?.text ?? ""; }; - const voteRelations = createVoteRelations(getRelationsForEvent, pollEventId); - - const relationsList: Relations[] = []; - - const pollEndRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.name); - if (pollEndRelations) { - relationsList.push(pollEndRelations); - } - - const pollEndAltRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.altName); - if (pollEndAltRelations) { - relationsList.push(pollEndAltRelations); - } - - const endRelations = new RelatedRelations(relationsList); - - const userVotes: Map = collectUserVotes( - allVotes(pollEvent, matrixClient, voteRelations, endRelations), - ); + const userVotes: Map = collectUserVotes(allVotes(voteRelations)); const votes: Map = countVotes(userVotes, poll); const highestScore: number = Math.max(...votes.values()); @@ -122,62 +99,13 @@ export function findTopAnswer( return formatCommaSeparatedList(bestAnswerTexts, 3); } -export function isPollEnded( - pollEvent: MatrixEvent, - matrixClient: MatrixClient, - getRelationsForEvent?: GetRelationsForEvent, -): boolean { - if (!getRelationsForEvent) { - return false; - } - - const pollEventId = pollEvent.getId(); - if (!pollEventId) { - logger.warn( - "isPollEnded: Poll event must have event ID in order to determine whether it has ended " + - "- assuming poll has not ended", - ); - return false; - } - - const roomId = pollEvent.getRoomId(); - if (!roomId) { - logger.warn( - "isPollEnded: Poll event must have room ID in order to determine whether it has ended " + - "- assuming poll has not ended", - ); +export function isPollEnded(pollEvent: MatrixEvent, matrixClient: MatrixClient): boolean { + const room = matrixClient.getRoom(pollEvent.getRoomId()); + const poll = room?.polls.get(pollEvent.getId()!); + if (!poll || poll.isFetchingResponses) { return false; } - - const roomCurrentState = matrixClient.getRoom(roomId)?.currentState; - function userCanRedact(endEvent: MatrixEvent): boolean { - const endEventSender = endEvent.getSender(); - return ( - endEventSender && roomCurrentState && roomCurrentState.maySendRedactionForEvent(pollEvent, endEventSender) - ); - } - - const relationsList: Relations[] = []; - - const pollEndRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.name); - if (pollEndRelations) { - relationsList.push(pollEndRelations); - } - - const pollEndAltRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.altName); - if (pollEndAltRelations) { - relationsList.push(pollEndAltRelations); - } - - const endRelations = new RelatedRelations(relationsList); - - if (!endRelations) { - return false; - } - - const authorisedRelations = endRelations.getRelations().filter(userCanRedact); - - return authorisedRelations.length > 0; + return poll.isEnded; } export function pollAlreadyHasVotes(mxEvent: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): boolean { @@ -215,75 +143,58 @@ export default class MPollBody extends React.Component { public static contextType = MatrixClientContext; public context!: React.ContextType; private seenEventIds: string[] = []; // Events we have already seen - private voteRelationsReceived = false; - private endRelationsReceived = false; public constructor(props: IBodyProps) { super(props); this.state = { selected: null, - voteRelations: this.fetchVoteRelations(), - endRelations: this.fetchEndRelations(), + pollInitialised: false, }; + } - this.addListeners(this.state.voteRelations, this.state.endRelations); - this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onRelationsCreated); + public componentDidMount(): void { + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + const poll = room?.polls.get(this.props.mxEvent.getId()!); + if (poll) { + this.setPollInstance(poll); + } else { + room?.on(PollEvent.New, this.setPollInstance.bind(this)); + } } public componentWillUnmount(): void { - this.props.mxEvent.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated); - this.removeListeners(this.state.voteRelations, this.state.endRelations); + this.removeListeners(); } - private addListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations): void { - if (voteRelations) { - voteRelations.on(RelationsEvent.Add, this.onRelationsChange); - voteRelations.on(RelationsEvent.Remove, this.onRelationsChange); - voteRelations.on(RelationsEvent.Redaction, this.onRelationsChange); - } - if (endRelations) { - endRelations.on(RelationsEvent.Add, this.onRelationsChange); - endRelations.on(RelationsEvent.Remove, this.onRelationsChange); - endRelations.on(RelationsEvent.Redaction, this.onRelationsChange); + private async setPollInstance(poll: Poll): Promise { + if (poll.pollId !== this.props.mxEvent.getId()) { + return; } - } + this.setState({ poll }, () => { + this.addListeners(); + }); + const responses = await poll.getResponses(); + const voteRelations = responses; - private removeListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations): void { - if (voteRelations) { - voteRelations.off(RelationsEvent.Add, this.onRelationsChange); - voteRelations.off(RelationsEvent.Remove, this.onRelationsChange); - voteRelations.off(RelationsEvent.Redaction, this.onRelationsChange); - } - if (endRelations) { - endRelations.off(RelationsEvent.Add, this.onRelationsChange); - endRelations.off(RelationsEvent.Remove, this.onRelationsChange); - endRelations.off(RelationsEvent.Redaction, this.onRelationsChange); - } + this.setState({ pollInitialised: true, voteRelations }); } - private onRelationsCreated = (relationType: string, eventType: string): void => { - if (relationType !== "m.reference") { - return; - } + private addListeners(): void { + this.state.poll?.on(PollEvent.Responses, this.onResponsesChange); + this.state.poll?.on(PollEvent.End, this.onRelationsChange); + } - if (M_POLL_RESPONSE.matches(eventType)) { - this.voteRelationsReceived = true; - const newVoteRelations = this.fetchVoteRelations(); - this.addListeners(newVoteRelations); - this.removeListeners(this.state.voteRelations); - this.setState({ voteRelations: newVoteRelations }); - } else if (M_POLL_END.matches(eventType)) { - this.endRelationsReceived = true; - const newEndRelations = this.fetchEndRelations(); - this.addListeners(newEndRelations); - this.removeListeners(this.state.endRelations); - this.setState({ endRelations: newEndRelations }); + private removeListeners(): void { + if (this.state.poll) { + this.state.poll.off(PollEvent.Responses, this.onResponsesChange); + this.state.poll.off(PollEvent.End, this.onRelationsChange); } + } - if (this.voteRelationsReceived && this.endRelationsReceived) { - this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onRelationsCreated); - } + private onResponsesChange = (responses: Relations): void => { + this.setState({ voteRelations: responses }); + this.onRelationsChange(); }; private onRelationsChange = (): void => { @@ -295,19 +206,19 @@ export default class MPollBody extends React.Component { }; private selectOption(answerId: string): void { - if (this.isEnded()) { + if (this.state.poll?.isEnded) { return; } const userVotes = this.collectUserVotes(); - const userId = this.context.getUserId(); + const userId = this.context.getSafeUserId(); const myVote = userVotes.get(userId)?.answers[0]; if (answerId === myVote) { return; } - const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()).serialize(); + const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()!).serialize(); - this.context.sendEvent(this.props.mxEvent.getRoomId(), response.type, response.content).catch((e: any) => { + this.context.sendEvent(this.props.mxEvent.getRoomId()!, response.type, response.content).catch((e: any) => { console.error("Failed to submit poll response event:", e); Modal.createDialog(ErrorDialog, { @@ -323,51 +234,14 @@ export default class MPollBody extends React.Component { this.selectOption(e.currentTarget.value); }; - private fetchVoteRelations(): RelatedRelations | null { - return this.fetchRelations(M_POLL_RESPONSE); - } - - private fetchEndRelations(): RelatedRelations | null { - return this.fetchRelations(M_POLL_END); - } - - private fetchRelations(eventType: NamespacedValue): RelatedRelations | null { - if (this.props.getRelationsForEvent) { - const relationsList: Relations[] = []; - - const eventId = this.props.mxEvent.getId(); - if (!eventId) { - return null; - } - - const relations = this.props.getRelationsForEvent(eventId, "m.reference", eventType.name); - if (relations) { - relationsList.push(relations); - } - - // If there is an alternatve experimental event type, also look for that - if (eventType.altName) { - const altRelations = this.props.getRelationsForEvent(eventId, "m.reference", eventType.altName); - if (altRelations) { - relationsList.push(altRelations); - } - } - - return new RelatedRelations(relationsList); - } else { - return null; - } - } - /** * @returns userId -> UserVote */ private collectUserVotes(): Map { - return collectUserVotes( - allVotes(this.props.mxEvent, this.context, this.state.voteRelations, this.state.endRelations), - this.context.getUserId(), - this.state.selected, - ); + if (!this.state.voteRelations) { + return new Map(); + } + return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected); } /** @@ -379,10 +253,10 @@ export default class MPollBody extends React.Component { * have already seen. */ private unselectIfNewEventFromMe(): void { - const newEvents: MatrixEvent[] = this.state.voteRelations - .getRelations() - .filter(isPollResponse) - .filter((mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!)); + const relations = this.state.voteRelations?.getRelations() || []; + const newEvents: MatrixEvent[] = relations.filter( + (mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!), + ); let newSelected = this.state.selected; if (newEvents.length > 0) { @@ -392,7 +266,7 @@ export default class MPollBody extends React.Component { } } } - const newEventIds = newEvents.map((mxEvent: MatrixEvent) => mxEvent.getId()); + const newEventIds = newEvents.map((mxEvent: MatrixEvent) => mxEvent.getId()!); this.seenEventIds = this.seenEventIds.concat(newEventIds); this.setState({ selected: newSelected }); } @@ -405,30 +279,30 @@ export default class MPollBody extends React.Component { return sum; } - private isEnded(): boolean { - return isPollEnded(this.props.mxEvent, this.context, this.props.getRelationsForEvent); - } + public render(): ReactNode { + const { poll, pollInitialised } = this.state; + if (!poll?.pollEvent) { + return null; + } - public render(): JSX.Element { - const poll = this.props.mxEvent.unstableExtensibleEvent as PollStartEvent; - if (!poll?.isEquivalentTo(M_POLL_START)) return null; // invalid + const pollEvent = poll.pollEvent; - const ended = this.isEnded(); - const pollId = this.props.mxEvent.getId(); + const pollId = this.props.mxEvent.getId()!; + const isFetchingResponses = !pollInitialised || poll.isFetchingResponses; const userVotes = this.collectUserVotes(); - const votes = countVotes(userVotes, poll); + const votes = countVotes(userVotes, pollEvent); const totalVotes = this.totalVotes(votes); const winCount = Math.max(...votes.values()); const userId = this.context.getUserId(); const myVote = userVotes?.get(userId!)?.answers[0]; - const disclosed = M_POLL_KIND_DISCLOSED.matches(poll.kind.name); + const disclosed = M_POLL_KIND_DISCLOSED.matches(pollEvent.kind.name); // Disclosed: votes are hidden until I vote or the poll ends // Undisclosed: votes are hidden until poll ends - const showResults = ended || (disclosed && myVote !== undefined); + const showResults = poll.isEnded || (disclosed && myVote !== undefined); let totalText: string; - if (ended) { + if (poll.isEnded) { totalText = _t("Final result based on %(count)s votes", { count: totalVotes }); } else if (!disclosed) { totalText = _t("Results will be visible when the poll is ended"); @@ -449,11 +323,11 @@ export default class MPollBody extends React.Component { return (

- {poll.question.text} + {pollEvent.question.text} {editedSpan}

- {poll.answers.map((answer: PollAnswerSubevent) => { + {pollEvent.answers.map((answer: PollAnswerSubevent) => { let answerVotes = 0; let votesText = ""; @@ -462,11 +336,12 @@ export default class MPollBody extends React.Component { votesText = _t("%(count)s votes", { count: answerVotes }); } - const checked = (!ended && myVote === answer.id) || (ended && answerVotes === winCount); + const checked = + (!poll.isEnded && myVote === answer.id) || (poll.isEnded && answerVotes === winCount); const cls = classNames({ mx_MPollBody_option: true, mx_MPollBody_option_checked: checked, - mx_MPollBody_option_ended: ended, + mx_MPollBody_option_ended: poll.isEnded, }); const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes); @@ -477,7 +352,7 @@ export default class MPollBody extends React.Component { className={cls} onClick={() => this.selectOption(answer.id)} > - {ended ? ( + {poll.isEnded ? ( ) : ( {
{totalText} + {isFetchingResponses && }
); @@ -562,68 +438,17 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote { throw new Error("Failed to parse Poll Response Event to determine user response"); } - return new UserVote(event.getTs(), event.getSender(), response.answerIds); + return new UserVote(event.getTs(), event.getSender()!, response.answerIds); } -export function allVotes( - pollEvent: MatrixEvent, - matrixClient: MatrixClient, - voteRelations: RelatedRelations, - endRelations: RelatedRelations, -): Array { - const endTs = pollEndTs(pollEvent, matrixClient, endRelations); - - function isOnOrBeforeEnd(responseEvent: MatrixEvent): boolean { - // From MSC3381: - // "Votes sent on or before the end event's timestamp are valid votes" - return endTs === null || responseEvent.getTs() <= endTs; - } - +export function allVotes(voteRelations: Relations): Array { if (voteRelations) { - return voteRelations - .getRelations() - .filter(isPollResponse) - .filter(isOnOrBeforeEnd) - .map(userResponseFromPollResponseEvent); + return voteRelations.getRelations().map(userResponseFromPollResponseEvent); } else { return []; } } -/** - * Returns the earliest timestamp from the supplied list of end_poll events - * or null if there are no authorised events. - */ -export function pollEndTs( - pollEvent: MatrixEvent, - matrixClient: MatrixClient, - endRelations: RelatedRelations, -): number | null { - if (!endRelations) { - return null; - } - - const roomCurrentState = matrixClient.getRoom(pollEvent.getRoomId()).currentState; - function userCanRedact(endEvent: MatrixEvent): boolean { - return roomCurrentState.maySendRedactionForEvent(pollEvent, endEvent.getSender()); - } - - const tss: number[] = endRelations - .getRelations() - .filter(userCanRedact) - .map((evt: MatrixEvent) => evt.getTs()); - - if (tss.length === 0) { - return null; - } else { - return Math.min(...tss); - } -} - -function isPollResponse(responseEvent: MatrixEvent): boolean { - return responseEvent.unstableExtensibleEvent?.isEquivalentTo(M_POLL_RESPONSE); -} - /** * Figure out the correct vote for each user. * @param userResponses current vote responses in the poll @@ -662,7 +487,7 @@ function countVotes(userVotes: Map, pollStart: PollStartEvent) if (!tempResponse.spoiled) { for (const answerId of tempResponse.answerIds) { if (collected.has(answerId)) { - collected.set(answerId, collected.get(answerId) + 1); + collected.set(answerId, collected.get(answerId)! + 1); } else { collected.set(answerId, 1); } diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index e57636c96fd..f393ddd9a7e 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -133,6 +133,7 @@ const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator if (event.isEncrypted()) { await cli.decryptEventIfNeeded(event); // TODO await? } + await room.processPollEvents([event]); if (event && PinningUtils.isPinnable(event)) { // Inject sender information diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 26b7f63c256..307ad27f4ec 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -19,8 +19,6 @@ import React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Relations } from "matrix-js-sdk/src/models/relations"; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; -import { logger } from "matrix-js-sdk/src/logger"; -import { M_POLL_START, M_POLL_RESPONSE, M_POLL_END } from "matrix-js-sdk/src/@types/polls"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; @@ -69,47 +67,6 @@ export default class PinnedEventTile extends React.Component { } }; - public async componentDidMount(): Promise { - // Fetch poll responses - if (M_POLL_START.matches(this.props.event.getType())) { - const eventId = this.props.event.getId(); - const roomId = this.props.event.getRoomId(); - const room = this.context.getRoom(roomId); - - try { - await Promise.all( - [M_POLL_RESPONSE.name, M_POLL_RESPONSE.altName, M_POLL_END.name, M_POLL_END.altName].map( - async (eventType): Promise => { - const relations = new Relations(RelationType.Reference, eventType, room); - relations.setTargetEvent(this.props.event); - - if (!this.relations.has(RelationType.Reference)) { - this.relations.set(RelationType.Reference, new Map()); - } - this.relations.get(RelationType.Reference).set(eventType, relations); - - let nextBatch: string | undefined; - do { - const page = await this.context.relations( - roomId, - eventId, - RelationType.Reference, - eventType, - { from: nextBatch }, - ); - nextBatch = page.nextBatch; - page.events.forEach((event) => relations.addEvent(event)); - } while (nextBatch); - }, - ), - ); - } catch (err) { - logger.error(`Error fetching responses to pinned poll ${eventId} in room ${roomId}`); - logger.error(err); - } - } - } - public render(): JSX.Element { const sender = this.props.event.getSender(); diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index a6f9b5e11c6..574a552a0ea 100644 --- a/test/components/views/messages/MPollBody-test.tsx +++ b/test/components/views/messages/MPollBody-test.tsx @@ -16,9 +16,8 @@ limitations under the License. import React from "react"; import { fireEvent, render, RenderResult } from "@testing-library/react"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { Relations } from "matrix-js-sdk/src/models/relations"; -import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; import { M_POLL_END, M_POLL_KIND_DISCLOSED, @@ -29,126 +28,60 @@ import { PollAnswer, } from "matrix-js-sdk/src/@types/polls"; import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events"; -import { MockedObject } from "jest-mock"; -import { - UserVote, - allVotes, - findTopAnswer, - pollEndTs, - isPollEnded, -} from "../../../../src/components/views/messages/MPollBody"; -import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { allVotes, findTopAnswer, isPollEnded } from "../../../../src/components/views/messages/MPollBody"; import { IBodyProps } from "../../../../src/components/views/messages/IBodyProps"; -import { getMockClientWithEventEmitter } from "../../../test-utils"; +import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import MPollBody from "../../../../src/components/views/messages/MPollBody"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; const CHECKED = "mx_MPollBody_option_checked"; +const userId = "@me:example.com"; const mockClient = getMockClientWithEventEmitter({ - getUserId: jest.fn().mockReturnValue("@me:example.com"), + ...mockClientMethodsUser(userId), sendEvent: jest.fn().mockReturnValue(Promise.resolve({ event_id: "fake_send_id" })), getRoom: jest.fn(), + decryptEventIfNeeded: jest.fn().mockResolvedValue(true), + relations: jest.fn(), }); -setRedactionAllowedForMeOnly(mockClient); - describe("MPollBody", () => { beforeEach(() => { mockClient.sendEvent.mockClear(); - }); - - it("finds no votes if there are none", () => { - expect( - allVotes( - { getRoomId: () => "$room" } as MatrixEvent, - MatrixClientPeg.get(), - new RelatedRelations([newVoteRelations([])]), - new RelatedRelations([newEndRelations([])]), - ), - ).toEqual([]); - }); - it("can find all the valid responses to a poll", () => { - const ev1 = responseEvent(); - const ev2 = responseEvent(); - const badEvent = badResponseEvent(); - - const voteRelations = new RelatedRelations([newVoteRelations([ev1, badEvent, ev2])]); - expect( - allVotes( - { getRoomId: () => "$room" } as MatrixEvent, - MatrixClientPeg.get(), - voteRelations, - new RelatedRelations([newEndRelations([])]), - ), - ).toEqual([ - new UserVote(ev1.getTs(), ev1.getSender()!, ev1.getContent()[M_POLL_RESPONSE.name].answers), - new UserVote( - badEvent.getTs(), - badEvent.getSender()!, - [], // should be spoiled - ), - new UserVote(ev2.getTs(), ev2.getSender()!, ev2.getContent()[M_POLL_RESPONSE.name].answers), - ]); + mockClient.getRoom.mockReturnValue(null); + mockClient.relations.mockResolvedValue({ events: [] }); }); - it("finds the first end poll event", () => { - const endRelations = new RelatedRelations([ - newEndRelations([ - endEvent("@me:example.com", 25), - endEvent("@me:example.com", 12), - endEvent("@me:example.com", 45), - endEvent("@me:example.com", 13), - ]), - ]); - - setRedactionAllowedForMeOnly(mockClient); - - expect(pollEndTs({ getRoomId: () => "$room" } as MatrixEvent, mockClient, endRelations)).toBe(12); + it("finds no votes if there are none", () => { + expect(allVotes(newVoteRelations([]))).toEqual([]); }); - it("ignores unauthorised end poll event when finding end ts", () => { - const endRelations = new RelatedRelations([ - newEndRelations([ - endEvent("@me:example.com", 25), - endEvent("@unauthorised:example.com", 12), - endEvent("@me:example.com", 45), - endEvent("@me:example.com", 13), - ]), - ]); - - setRedactionAllowedForMeOnly(mockClient); - - expect(pollEndTs({ getRoomId: () => "$room" } as MatrixEvent, mockClient, endRelations)).toBe(13); - }); + it("renders a loader while responses are still loading", async () => { + const votes = [ + responseEvent("@me:example.com", "pizza"), + responseEvent("@bellc:example.com", "pizza"), + responseEvent("@catrd:example.com", "poutine"), + responseEvent("@dune2:example.com", "wings"), + ]; + // render without waiting for responses + const renderResult = await newMPollBody(votes, [], undefined, undefined, false); - it("counts only votes before the end poll event", () => { - const voteRelations = new RelatedRelations([ - newVoteRelations([ - responseEvent("sf@matrix.org", "wings", 13), - responseEvent("jr@matrix.org", "poutine", 40), - responseEvent("ak@matrix.org", "poutine", 37), - responseEvent("id@matrix.org", "wings", 13), - responseEvent("ps@matrix.org", "wings", 19), - ]), - ]); - const endRelations = new RelatedRelations([newEndRelations([endEvent("@me:example.com", 25)])]); - expect( - allVotes({ getRoomId: () => "$room" } as MatrixEvent, MatrixClientPeg.get(), voteRelations, endRelations), - ).toEqual([ - new UserVote(13, "sf@matrix.org", ["wings"]), - new UserVote(13, "id@matrix.org", ["wings"]), - new UserVote(19, "ps@matrix.org", ["wings"]), - ]); + // votes still displayed + expect(votesCount(renderResult, "pizza")).toBe("2 votes"); + expect(votesCount(renderResult, "poutine")).toBe("1 vote"); + expect(votesCount(renderResult, "italian")).toBe("0 votes"); + expect(votesCount(renderResult, "wings")).toBe("1 vote"); + // spinner rendered + expect(renderResult.getByTestId("totalVotes").innerHTML).toMatchSnapshot(); }); - it("renders no votes if none were made", () => { + it("renders no votes if none were made", async () => { const votes: MatrixEvent[] = []; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe(""); expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); @@ -157,14 +90,14 @@ describe("MPollBody", () => { expect(renderResult.getByText("What should we order for the party?")).toBeTruthy(); }); - it("finds votes from multiple people", () => { + it("finds votes from multiple people", async () => { const votes = [ responseEvent("@me:example.com", "pizza"), responseEvent("@bellc:example.com", "pizza"), responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe("2 votes"); expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); @@ -172,7 +105,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); }); - it("ignores end poll events from unauthorised users", () => { + it("ignores end poll events from unauthorised users", async () => { const votes = [ responseEvent("@me:example.com", "pizza"), responseEvent("@bellc:example.com", "pizza"), @@ -180,7 +113,7 @@ describe("MPollBody", () => { responseEvent("@dune2:example.com", "wings"), ]; const ends = [endEvent("@notallowed:example.com", 12)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); // Even though an end event was sent, we render the poll as unfinished // because this person is not allowed to send these events @@ -191,14 +124,14 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); }); - it("hides scores if I have not voted", () => { + it("hides scores if I have not voted", async () => { const votes = [ responseEvent("@alice:example.com", "pizza"), responseEvent("@bellc:example.com", "pizza"), responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe(""); expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); @@ -206,9 +139,9 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("4 votes cast. Vote to see the results"); }); - it("hides a single vote if I have not voted", () => { + it("hides a single vote if I have not voted", async () => { const votes = [responseEvent("@alice:example.com", "pizza")]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe(""); expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); @@ -216,7 +149,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("1 vote cast. Vote to see the results"); }); - it("takes someone's most recent vote if they voted several times", () => { + it("takes someone's most recent vote if they voted several times", async () => { const votes = [ responseEvent("@me:example.com", "pizza", 12), responseEvent("@me:example.com", "wings", 20), // latest me @@ -224,7 +157,7 @@ describe("MPollBody", () => { responseEvent("@qbert:example.com", "poutine", 16), // latest qbert responseEvent("@qbert:example.com", "wings", 15), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe("0 votes"); expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); @@ -232,14 +165,14 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); }); - it("uses my local vote", () => { + it("uses my local vote", async () => { // Given I haven't voted const votes = [ responseEvent("@nf:example.com", "pizza", 15), responseEvent("@fg:example.com", "pizza", 15), responseEvent("@hi:example.com", "pizza", 15), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); // When I vote for Italian clickOption(renderResult, "italian"); @@ -253,7 +186,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); }); - it("overrides my other votes with my local vote", () => { + it("overrides my other votes with my local vote", async () => { // Given two of us have voted for Italian const votes = [ responseEvent("@me:example.com", "pizza", 12), @@ -261,7 +194,7 @@ describe("MPollBody", () => { responseEvent("@me:example.com", "italian", 14), responseEvent("@nf:example.com", "italian", 15), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); // When I click Wings clickOption(renderResult, "wings"); @@ -279,7 +212,7 @@ describe("MPollBody", () => { expect(voteButton(renderResult, "italian").className.includes(CHECKED)).toBe(false); }); - it("cancels my local vote if another comes in", () => { + it("cancels my local vote if another comes in", async () => { // Given I voted locally const votes = [responseEvent("@me:example.com", "pizza", 100)]; const mxEvent = new MatrixEvent({ @@ -288,14 +221,15 @@ describe("MPollBody", () => { room_id: "#myroom:example.com", content: newPollStart(undefined, undefined, true), }); - const props = getMPollBodyPropsFromEvent(mxEvent, votes); + const props = getMPollBodyPropsFromEvent(mxEvent); + const room = await setupRoomWithPollEvents(mxEvent, votes); const renderResult = renderMPollBodyWithWrapper(props); - const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); - expect(voteRelations).toBeDefined(); + // wait for /relations promise to resolve + await flushPromises(); clickOption(renderResult, "pizza"); // When a new vote from me comes in - voteRelations!.addEvent(responseEvent("@me:example.com", "wings", 101)); + await room.processPollEvents([responseEvent("@me:example.com", "wings", 101)]); // Then the new vote is counted, not the old one expect(votesCount(renderResult, "pizza")).toBe("0 votes"); @@ -306,7 +240,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); }); - it("doesn't cancel my local vote if someone else votes", () => { + it("doesn't cancel my local vote if someone else votes", async () => { // Given I voted locally const votes = [responseEvent("@me:example.com", "pizza")]; const mxEvent = new MatrixEvent({ @@ -315,15 +249,16 @@ describe("MPollBody", () => { room_id: "#myroom:example.com", content: newPollStart(undefined, undefined, true), }); - const props = getMPollBodyPropsFromEvent(mxEvent, votes); + const props = getMPollBodyPropsFromEvent(mxEvent); + const room = await setupRoomWithPollEvents(mxEvent, votes); const renderResult = renderMPollBodyWithWrapper(props); + // wait for /relations promise to resolve + await flushPromises(); - const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); - expect(voteRelations).toBeDefined(); clickOption(renderResult, "pizza"); // When a new vote from someone else comes in - voteRelations!.addEvent(responseEvent("@xx:example.com", "wings", 101)); + await room.processPollEvents([responseEvent("@xx:example.com", "wings", 101)]); // Then my vote is still for pizza // NOTE: the new event does not affect the counts for other people - @@ -341,10 +276,10 @@ describe("MPollBody", () => { expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(false); }); - it("highlights my vote even if I did it on another device", () => { + it("highlights my vote even if I did it on another device", async () => { // Given I voted italian const votes = [responseEvent("@me:example.com", "italian"), responseEvent("@nf:example.com", "wings")]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); // But I didn't click anything locally @@ -353,10 +288,10 @@ describe("MPollBody", () => { expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(false); }); - it("ignores extra answers", () => { + it("ignores extra answers", async () => { // When cb votes for 2 things, we consider the first only const votes = [responseEvent("@cb:example.com", ["pizza", "wings"]), responseEvent("@me:example.com", "wings")]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe("1 vote"); expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); @@ -364,13 +299,13 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); }); - it("allows un-voting by passing an empty vote", () => { + it("allows un-voting by passing an empty vote", async () => { const votes = [ responseEvent("@nc:example.com", "pizza", 12), responseEvent("@nc:example.com", [], 13), responseEvent("@me:example.com", "italian"), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe("0 votes"); expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("1 vote"); @@ -378,14 +313,14 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); }); - it("allows re-voting after un-voting", () => { + it("allows re-voting after un-voting", async () => { const votes = [ responseEvent("@op:example.com", "pizza", 12), responseEvent("@op:example.com", [], 13), responseEvent("@op:example.com", "italian", 14), responseEvent("@me:example.com", "italian"), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe("0 votes"); expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("2 votes"); @@ -393,7 +328,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); }); - it("treats any invalid answer as a spoiled ballot", () => { + it("treats any invalid answer as a spoiled ballot", async () => { // Note that uy's second vote has a valid first answer, but // the ballot is still spoiled because the second answer is // invalid, even though we would ignore it if we continued. @@ -403,7 +338,7 @@ describe("MPollBody", () => { responseEvent("@uy:example.com", "italian", 14), responseEvent("@uy:example.com", "doesntexist", 15), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe("0 votes"); expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); @@ -411,7 +346,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 0 votes"); }); - it("allows re-voting after a spoiled ballot", () => { + it("allows re-voting after a spoiled ballot", async () => { const votes = [ responseEvent("@me:example.com", "pizza", 12), responseEvent("@me:example.com", ["pizza", "doesntexist"], 13), @@ -419,7 +354,7 @@ describe("MPollBody", () => { responseEvent("@uy:example.com", "doesntexist", 15), responseEvent("@uy:example.com", "poutine", 16), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); expect(renderResult.container.querySelectorAll('input[type="radio"]')).toHaveLength(4); expect(votesCount(renderResult, "pizza")).toBe("0 votes"); expect(votesCount(renderResult, "poutine")).toBe("1 vote"); @@ -428,25 +363,25 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); }); - it("renders nothing if poll has no answers", () => { + it("renders nothing if poll has no answers", async () => { const answers: PollAnswer[] = []; const votes: MatrixEvent[] = []; const ends: MatrixEvent[] = []; - const { container } = newMPollBody(votes, ends, answers); + const { container } = await newMPollBody(votes, ends, answers); expect(container.childElementCount).toEqual(0); }); - it("renders the first 20 answers if 21 were given", () => { + it("renders the first 20 answers if 21 were given", async () => { const answers = Array.from(Array(21).keys()).map((i) => { return { id: `id${i}`, [M_TEXT.name]: `Name ${i}` }; }); const votes: MatrixEvent[] = []; const ends: MatrixEvent[] = []; - const { container } = newMPollBody(votes, ends, answers); + const { container } = await newMPollBody(votes, ends, answers); expect(container.querySelectorAll(".mx_MPollBody_option").length).toBe(20); }); - it("hides scores if I voted but the poll is undisclosed", () => { + it("hides scores if I voted but the poll is undisclosed", async () => { const votes = [ responseEvent("@me:example.com", "pizza"), responseEvent("@alice:example.com", "pizza"), @@ -454,7 +389,7 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const renderResult = newMPollBody(votes, [], undefined, false); + const renderResult = await newMPollBody(votes, [], undefined, false); expect(votesCount(renderResult, "pizza")).toBe(""); expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); @@ -462,7 +397,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Results will be visible when the poll is ended"); }); - it("highlights my vote if the poll is undisclosed", () => { + it("highlights my vote if the poll is undisclosed", async () => { const votes = [ responseEvent("@me:example.com", "pizza"), responseEvent("@alice:example.com", "poutine"), @@ -470,7 +405,7 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const { container } = newMPollBody(votes, [], undefined, false); + const { container } = await newMPollBody(votes, [], undefined, false); // My vote is marked expect(container.querySelector('input[value="pizza"]')!).toBeChecked(); @@ -479,7 +414,7 @@ describe("MPollBody", () => { expect(container.querySelector('input[value="poutine"]')!).not.toBeChecked(); }); - it("shows scores if the poll is undisclosed but ended", () => { + it("shows scores if the poll is undisclosed but ended", async () => { const votes = [ responseEvent("@me:example.com", "pizza"), responseEvent("@alice:example.com", "pizza"), @@ -488,7 +423,7 @@ describe("MPollBody", () => { responseEvent("@dune2:example.com", "wings"), ]; const ends = [endEvent("@me:example.com", 12)]; - const renderResult = newMPollBody(votes, ends, undefined, false); + const renderResult = await newMPollBody(votes, ends, undefined, false); expect(endedVotesCount(renderResult, "pizza")).toBe("3 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); @@ -496,16 +431,16 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); - it("sends a vote event when I choose an option", () => { + it("sends a vote event when I choose an option", async () => { const votes: MatrixEvent[] = []; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); clickOption(renderResult, "wings"); expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings")); }); - it("sends only one vote event when I click several times", () => { + it("sends only one vote event when I click several times", async () => { const votes: MatrixEvent[] = []; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); clickOption(renderResult, "wings"); clickOption(renderResult, "wings"); clickOption(renderResult, "wings"); @@ -513,9 +448,9 @@ describe("MPollBody", () => { expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings")); }); - it("sends no vote event when I click what I already chose", () => { + it("sends no vote event when I click what I already chose", async () => { const votes = [responseEvent("@me:example.com", "wings")]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); clickOption(renderResult, "wings"); clickOption(renderResult, "wings"); clickOption(renderResult, "wings"); @@ -523,9 +458,9 @@ describe("MPollBody", () => { expect(mockClient.sendEvent).not.toHaveBeenCalled(); }); - it("sends several events when I click different options", () => { + it("sends several events when I click different options", async () => { const votes: MatrixEvent[] = []; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); clickOption(renderResult, "wings"); clickOption(renderResult, "italian"); clickOption(renderResult, "poutine"); @@ -535,17 +470,17 @@ describe("MPollBody", () => { expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("poutine")); }); - it("sends no events when I click in an ended poll", () => { + it("sends no events when I click in an ended poll", async () => { const ends = [endEvent("@me:example.com", 25)]; const votes = [responseEvent("@uy:example.com", "wings", 15), responseEvent("@uy:example.com", "poutine", 15)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); clickOption(renderResult, "wings"); clickOption(renderResult, "italian"); clickOption(renderResult, "poutine"); expect(mockClient.sendEvent).not.toHaveBeenCalled(); }); - it("finds the top answer among several votes", () => { + it("finds the top answer among several votes", async () => { // 2 votes for poutine, 1 for pizza. "me" made an invalid vote. const votes = [ responseEvent("@me:example.com", "pizza", 12), @@ -557,46 +492,30 @@ describe("MPollBody", () => { responseEvent("@fa:example.com", "poutine", 18), ]; - expect(runFindTopAnswer(votes, [])).toEqual("Poutine"); - }); - - it("finds all top answers when there is a draw", () => { - const votes = [ - responseEvent("@uy:example.com", "italian", 14), - responseEvent("@ab:example.com", "pizza", 17), - responseEvent("@fa:example.com", "poutine", 18), - ]; - expect(runFindTopAnswer(votes, [])).toEqual("Italian, Pizza and Poutine"); + expect(runFindTopAnswer(votes)).toEqual("Poutine"); }); - it("finds all top answers ignoring late votes", () => { + it("finds all top answers when there is a draw", async () => { const votes = [ responseEvent("@uy:example.com", "italian", 14), responseEvent("@ab:example.com", "pizza", 17), - responseEvent("@io:example.com", "poutine", 30), // Late responseEvent("@fa:example.com", "poutine", 18), - responseEvent("@of:example.com", "poutine", 31), // Late ]; - const ends = [endEvent("@me:example.com", 25)]; - expect(runFindTopAnswer(votes, ends)).toEqual("Italian, Pizza and Poutine"); + expect(runFindTopAnswer(votes)).toEqual("Italian, Pizza and Poutine"); }); - it("is silent about the top answer if there are no votes", () => { - expect(runFindTopAnswer([], [])).toEqual(""); + it("is silent about the top answer if there are no votes", async () => { + expect(runFindTopAnswer([])).toEqual(""); }); - it("is silent about the top answer if there are no votes when ended", () => { - expect(runFindTopAnswer([], [endEvent("@me:example.com", 13)])).toEqual(""); - }); - - it("shows non-radio buttons if the poll is ended", () => { + it("shows non-radio buttons if the poll is ended", async () => { const events = [endEvent()]; - const { container } = newMPollBody([], events); + const { container } = await newMPollBody([], events); expect(container.querySelector(".mx_StyledRadioButton")).not.toBeInTheDocument(); expect(container.querySelector('input[type="radio"]')).not.toBeInTheDocument(); }); - it("counts votes as normal if the poll is ended", () => { + it("counts votes as normal if the poll is ended", async () => { const votes = [ responseEvent("@me:example.com", "pizza", 12), responseEvent("@me:example.com", "wings", 20), // latest me @@ -605,7 +524,7 @@ describe("MPollBody", () => { responseEvent("@qbert:example.com", "wings", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(endedVotesCount(renderResult, "pizza")).toBe("0 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); @@ -613,10 +532,10 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 2 votes"); }); - it("counts a single vote as normal if the poll is ended", () => { + it("counts a single vote as normal if the poll is ended", async () => { const votes = [responseEvent("@qbert:example.com", "poutine", 16)]; const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(endedVotesCount(renderResult, "pizza")).toBe("0 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); @@ -624,7 +543,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 1 vote"); }); - it("shows ended vote counts of different numbers", () => { + it("shows ended vote counts of different numbers", async () => { const votes = [ responseEvent("@me:example.com", "wings", 20), responseEvent("@qb:example.com", "wings", 14), @@ -633,7 +552,7 @@ describe("MPollBody", () => { responseEvent("@hi:example.com", "pizza", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(renderResult.container.querySelectorAll(".mx_StyledRadioButton")).toHaveLength(0); expect(renderResult.container.querySelectorAll('input[type="radio"]')).toHaveLength(0); @@ -644,7 +563,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); - it("ignores votes that arrived after poll ended", () => { + it("ignores votes that arrived after poll ended", async () => { const votes = [ responseEvent("@sd:example.com", "wings", 30), // Late responseEvent("@ff:example.com", "wings", 20), @@ -655,7 +574,7 @@ describe("MPollBody", () => { responseEvent("@ld:example.com", "pizza", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); @@ -664,7 +583,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); - it("counts votes that arrived after an unauthorised poll end event", () => { + it("counts votes that arrived after an unauthorised poll end event", async () => { const votes = [ responseEvent("@sd:example.com", "wings", 30), // Late responseEvent("@ff:example.com", "wings", 20), @@ -678,7 +597,7 @@ describe("MPollBody", () => { endEvent("@unauthorised:example.com", 5), // Should be ignored endEvent("@me:example.com", 25), ]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); @@ -687,7 +606,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); - it("ignores votes that arrived after the first end poll event", () => { + it("ignores votes that arrived after the first end poll event", async () => { // From MSC3381: // "Votes sent on or before the end event's timestamp are valid votes" @@ -705,7 +624,7 @@ describe("MPollBody", () => { endEvent("@me:example.com", 25), endEvent("@me:example.com", 75), ]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); @@ -714,7 +633,7 @@ describe("MPollBody", () => { expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); }); - it("highlights the winning vote in an ended poll", () => { + it("highlights the winning vote in an ended poll", async () => { // Given I voted for pizza but the winner is wings const votes = [ responseEvent("@me:example.com", "pizza", 20), @@ -722,7 +641,7 @@ describe("MPollBody", () => { responseEvent("@xy:example.com", "wings", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); // Then the winner is highlighted expect(endedVoteChecked(renderResult, "wings")).toBe(true); @@ -733,14 +652,14 @@ describe("MPollBody", () => { expect(endedVoteDiv(renderResult, "pizza").className.includes("mx_MPollBody_endedOptionWinner")).toBe(false); }); - it("highlights multiple winning votes", () => { + it("highlights multiple winning votes", async () => { const votes = [ responseEvent("@me:example.com", "pizza", 20), responseEvent("@xy:example.com", "wings", 15), responseEvent("@fg:example.com", "poutine", 15), ]; const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody(votes, ends); + const renderResult = await newMPollBody(votes, ends); expect(endedVoteChecked(renderResult, "pizza")).toBe(true); expect(endedVoteChecked(renderResult, "wings")).toBe(true); @@ -749,53 +668,41 @@ describe("MPollBody", () => { expect(renderResult.container.getElementsByClassName("mx_MPollBody_option_checked")).toHaveLength(3); }); - it("highlights nothing if poll has no votes", () => { + it("highlights nothing if poll has no votes", async () => { const ends = [endEvent("@me:example.com", 25)]; - const renderResult = newMPollBody([], ends); + const renderResult = await newMPollBody([], ends); expect(renderResult.container.getElementsByClassName("mx_MPollBody_option_checked")).toHaveLength(0); }); - it("says poll is not ended if there is no end event", () => { + it("says poll is not ended if there is no end event", async () => { const ends: MatrixEvent[] = []; - expect(runIsPollEnded(ends)).toBe(false); + const result = await runIsPollEnded(ends); + expect(result).toBe(false); }); - it("says poll is ended if there is an end event", () => { + it("says poll is ended if there is an end event", async () => { const ends = [endEvent("@me:example.com", 25)]; - expect(runIsPollEnded(ends)).toBe(true); - }); - - it("says poll is not ended if endRelations is undefined", () => { - const pollEvent = new MatrixEvent(); - setRedactionAllowedForMeOnly(mockClient); - expect(isPollEnded(pollEvent, mockClient, undefined)).toBe(false); + const result = await runIsPollEnded(ends); + expect(result).toBe(true); }); - it("says poll is not ended if asking for relations returns undefined", () => { + it("says poll is not ended if poll is fetching responses", async () => { const pollEvent = new MatrixEvent({ + type: M_POLL_START.name, event_id: "$mypoll", room_id: "#myroom:example.com", content: newPollStart([]), }); - mockClient.getRoom.mockImplementation((_roomId) => { - return { - currentState: { - maySendRedactionForEvent: (_evt: MatrixEvent, userId: string) => { - return userId === "@me:example.com"; - }, - }, - } as unknown as Room; - }); - const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - expect(M_POLL_END.matches(eventType)).toBe(true); - return undefined; - }; - expect(isPollEnded(pollEvent, MatrixClientPeg.get(), getRelationsForEvent)).toBe(false); + const ends = [endEvent("@me:example.com", 25)]; + + await setupRoomWithPollEvents(pollEvent, [], ends); + const poll = mockClient.getRoom(pollEvent.getRoomId()!)!.polls.get(pollEvent.getId()!)!; + // start fetching, dont await + poll.getResponses(); + expect(isPollEnded(pollEvent, mockClient)).toBe(false); }); - it("Displays edited content and new answer IDs if the poll has been edited", () => { + it("Displays edited content and new answer IDs if the poll has been edited", async () => { const pollEvent = new MatrixEvent({ type: M_POLL_START.name, event_id: "$mypoll", @@ -824,7 +731,7 @@ describe("MPollBody", () => { }, }); pollEvent.makeReplaced(replacingEvent); - const { getByTestId, container } = newMPollBodyFromEvent(pollEvent, []); + const { getByTestId, container } = await newMPollBodyFromEvent(pollEvent, []); expect(getByTestId("pollQuestion").innerHTML).toEqual( 'new question (edited)', ); @@ -840,13 +747,13 @@ describe("MPollBody", () => { expect(options[2].innerHTML).toEqual("new answer 3"); }); - it("renders a poll with no votes", () => { + it("renders a poll with no votes", async () => { const votes: MatrixEvent[] = []; - const { container } = newMPollBody(votes); + const { container } = await newMPollBody(votes); expect(container).toMatchSnapshot(); }); - it("renders a poll with only non-local votes", () => { + it("renders a poll with only non-local votes", async () => { const votes = [ responseEvent("@op:example.com", "pizza", 12), responseEvent("@op:example.com", [], 13), @@ -854,11 +761,11 @@ describe("MPollBody", () => { responseEvent("@me:example.com", "wings", 15), responseEvent("@qr:example.com", "italian", 16), ]; - const { container } = newMPollBody(votes); + const { container } = await newMPollBody(votes); expect(container).toMatchSnapshot(); }); - it("renders a poll with local, non-local and invalid votes", () => { + it("renders a poll with local, non-local and invalid votes", async () => { const votes = [ responseEvent("@a:example.com", "pizza", 12), responseEvent("@b:example.com", [], 13), @@ -867,12 +774,13 @@ describe("MPollBody", () => { responseEvent("@e:example.com", "wings", 15), responseEvent("@me:example.com", "italian", 16), ]; - const renderResult = newMPollBody(votes); + const renderResult = await newMPollBody(votes); clickOption(renderResult, "italian"); + expect(renderResult.container).toMatchSnapshot(); }); - it("renders a poll that I have not voted in", () => { + it("renders a poll that I have not voted in", async () => { const votes = [ responseEvent("@op:example.com", "pizza", 12), responseEvent("@op:example.com", [], 13), @@ -880,17 +788,17 @@ describe("MPollBody", () => { responseEvent("@yo:example.com", "wings", 15), responseEvent("@qr:example.com", "italian", 16), ]; - const { container } = newMPollBody(votes); + const { container } = await newMPollBody(votes); expect(container).toMatchSnapshot(); }); - it("renders a finished poll with no votes", () => { + it("renders a finished poll with no votes", async () => { const ends = [endEvent("@me:example.com", 25)]; - const { container } = newMPollBody([], ends); + const { container } = await newMPollBody([], ends); expect(container).toMatchSnapshot(); }); - it("renders a finished poll", () => { + it("renders a finished poll", async () => { const votes = [ responseEvent("@op:example.com", "pizza", 12), responseEvent("@op:example.com", [], 13), @@ -899,11 +807,11 @@ describe("MPollBody", () => { responseEvent("@qr:example.com", "italian", 16), ]; const ends = [endEvent("@me:example.com", 25)]; - const { container } = newMPollBody(votes, ends); + const { container } = await newMPollBody(votes, ends); expect(container).toMatchSnapshot(); }); - it("renders a finished poll with multiple winners", () => { + it("renders a finished poll with multiple winners", async () => { const votes = [ responseEvent("@ed:example.com", "pizza", 12), responseEvent("@rf:example.com", "pizza", 12), @@ -913,11 +821,11 @@ describe("MPollBody", () => { responseEvent("@yh:example.com", "poutine", 14), ]; const ends = [endEvent("@me:example.com", 25)]; - const { container } = newMPollBody(votes, ends); + const { container } = await newMPollBody(votes, ends); expect(container).toMatchSnapshot(); }); - it("renders an undisclosed, unfinished poll", () => { + it("renders an undisclosed, unfinished poll", async () => { const votes = [ responseEvent("@ed:example.com", "pizza", 12), responseEvent("@rf:example.com", "pizza", 12), @@ -927,11 +835,11 @@ describe("MPollBody", () => { responseEvent("@yh:example.com", "poutine", 14), ]; const ends: MatrixEvent[] = []; - const { container } = newMPollBody(votes, ends, undefined, false); + const { container } = await newMPollBody(votes, ends, undefined, false); expect(container).toMatchSnapshot(); }); - it("renders an undisclosed, finished poll", () => { + it("renders an undisclosed, finished poll", async () => { const votes = [ responseEvent("@ed:example.com", "pizza", 12), responseEvent("@rf:example.com", "pizza", 12), @@ -941,65 +849,47 @@ describe("MPollBody", () => { responseEvent("@yh:example.com", "poutine", 14), ]; const ends = [endEvent("@me:example.com", 25)]; - const { container } = newMPollBody(votes, ends, undefined, false); + const { container } = await newMPollBody(votes, ends, undefined, false); expect(container).toMatchSnapshot(); }); }); function newVoteRelations(relationEvents: Array): Relations { - return newRelations(relationEvents, M_POLL_RESPONSE.name); + return newRelations(relationEvents, M_POLL_RESPONSE.name, [M_POLL_RESPONSE.altName!]); } -function newEndRelations(relationEvents: Array): Relations { - return newRelations(relationEvents, M_POLL_END.name); -} - -function newRelations(relationEvents: Array, eventType: string): Relations { - const voteRelations = new Relations("m.reference", eventType, mockClient); +function newRelations(relationEvents: Array, eventType: string, altEventTypes?: string[]): Relations { + const voteRelations = new Relations("m.reference", eventType, mockClient, altEventTypes); for (const ev of relationEvents) { voteRelations.addEvent(ev); } return voteRelations; } -function newMPollBody( +async function newMPollBody( relationEvents: Array, endEvents: Array = [], answers?: PollAnswer[], disclosed = true, -): RenderResult { + waitForResponsesLoad = true, +): Promise { const mxEvent = new MatrixEvent({ type: M_POLL_START.name, event_id: "$mypoll", room_id: "#myroom:example.com", content: newPollStart(answers, undefined, disclosed), }); - return newMPollBodyFromEvent(mxEvent, relationEvents, endEvents); + const result = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents); + // flush promises from loading relations + if (waitForResponsesLoad) { + await flushPromises(); + } + return result; } -function getMPollBodyPropsFromEvent( - mxEvent: MatrixEvent, - relationEvents: Array, - endEvents: Array = [], -): IBodyProps { - const voteRelations = newVoteRelations(relationEvents); - const endRelations = newEndRelations(endEvents); - - const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - if (M_POLL_RESPONSE.matches(eventType)) { - return voteRelations; - } else if (M_POLL_END.matches(eventType)) { - return endRelations; - } else { - fail("Unexpected eventType: " + eventType); - } - }; - +function getMPollBodyPropsFromEvent(mxEvent: MatrixEvent): IBodyProps { return { mxEvent, - getRelationsForEvent, // We don't use any of these props, but they're required. highlightLink: "unused", highlights: [], @@ -1018,15 +908,35 @@ function renderMPollBodyWithWrapper(props: IBodyProps): RenderResult { }); } -function newMPollBodyFromEvent( +async function newMPollBodyFromEvent( mxEvent: MatrixEvent, relationEvents: Array, endEvents: Array = [], -): RenderResult { - const props = getMPollBodyPropsFromEvent(mxEvent, relationEvents, endEvents); +): Promise { + const props = getMPollBodyPropsFromEvent(mxEvent); + + await setupRoomWithPollEvents(mxEvent, relationEvents, endEvents); + return renderMPollBodyWithWrapper(props); } +async function setupRoomWithPollEvents( + mxEvent: MatrixEvent, + relationEvents: Array, + endEvents: Array = [], +): Promise { + const room = new Room(mxEvent.getRoomId()!, mockClient, userId); + room.processPollEvents([mxEvent, ...relationEvents, ...endEvents]); + setRedactionAllowedForMeOnly(room); + // wait for events to process on room + await flushPromises(); + mockClient.getRoom.mockReturnValue(room); + mockClient.relations.mockResolvedValue({ + events: [...relationEvents, ...endEvents], + }); + return room; +} + function clickOption({ getByTestId }: RenderResult, value: string) { fireEvent.click(getByTestId(`pollOption-${value}`)); } @@ -1081,21 +991,6 @@ function newPollStart(answers?: PollAnswer[], question?: string, disclosed = tru }; } -function badResponseEvent(): MatrixEvent { - return new MatrixEvent({ - event_id: nextId(), - type: M_POLL_RESPONSE.name, - sender: "@malicious:example.com", - content: { - "m.relates_to": { - rel_type: "m.reference", - event_id: "$mypoll", - }, - // Does not actually contain a response - }, - }); -} - function responseEvent( sender = "@alice:example.com", answers: string | Array = "italian", @@ -1133,8 +1028,7 @@ function expectedResponseEvent(answer: string) { }, roomId: "#myroom:example.com", eventType: M_POLL_RESPONSE.name, - txnId: undefined, - callback: undefined, + txnId: "$123", }; } function expectedResponseEventCall(answer: string) { @@ -1160,7 +1054,7 @@ function endEvent(sender = "@me:example.com", ts = 0): MatrixEvent { }); } -function runIsPollEnded(ends: MatrixEvent[]) { +async function runIsPollEnded(ends: MatrixEvent[]) { const pollEvent = new MatrixEvent({ event_id: "$mypoll", room_id: "#myroom:example.com", @@ -1168,19 +1062,12 @@ function runIsPollEnded(ends: MatrixEvent[]) { content: newPollStart(), }); - setRedactionAllowedForMeOnly(mockClient); + await setupRoomWithPollEvents(pollEvent, [], ends); - const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - expect(M_POLL_END.matches(eventType)).toBe(true); - return newEndRelations(ends); - }; - - return isPollEnded(pollEvent, mockClient, getRelationsForEvent); + return isPollEnded(pollEvent, mockClient); } -function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) { +function runFindTopAnswer(votes: MatrixEvent[]) { const pollEvent = new MatrixEvent({ event_id: "$mypoll", room_id: "#myroom:example.com", @@ -1188,30 +1075,12 @@ function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) { content: newPollStart(), }); - const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - if (M_POLL_RESPONSE.matches(eventType)) { - return newVoteRelations(votes); - } else if (M_POLL_END.matches(eventType)) { - return newEndRelations(ends); - } else { - fail(`eventType should be end or vote but was ${eventType}`); - } - }; - - return findTopAnswer(pollEvent, MatrixClientPeg.get(), getRelationsForEvent); + return findTopAnswer(pollEvent, newVoteRelations(votes)); } -function setRedactionAllowedForMeOnly(matrixClient: MockedObject) { - matrixClient.getRoom.mockImplementation((_roomId: string) => { - return { - currentState: { - maySendRedactionForEvent: (_evt: MatrixEvent, userId: string) => { - return userId === "@me:example.com"; - }, - }, - } as Room; +function setRedactionAllowedForMeOnly(room: Room) { + jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation((_evt: MatrixEvent, id: string) => { + return id === userId; }); } diff --git a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index 2263527148b..7bc530048cc 100644 --- a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap @@ -456,6 +456,8 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` `; +exports[`MPollBody renders a loader while responses are still loading 1`] = `"Based on 4 votes
"`; + exports[`MPollBody renders a poll that I have not voted in 1`] = `
", () => { stubClient(); const cli = mocked(MatrixClientPeg.get()); cli.getUserId.mockReturnValue("@alice:example.org"); - cli.setRoomAccountData.mockReturnValue(undefined); + cli.setRoomAccountData.mockResolvedValue({}); cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] }); const mkRoom = (localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]): Room => { - const room = mkStubRoom("!room:example.org", "room", cli); + const room = new Room("!room:example.org", cli, "@me:example.org"); // Deferred since we may be adding or removing pins later const pins = () => [...localPins, ...nonLocalPins]; // Insert pin IDs into room state - mocked(room.currentState).getStateEvents.mockImplementation((): any => + jest.spyOn(room.currentState, "getStateEvents").mockImplementation((): any => mkEvent({ event: true, type: EventType.RoomPinnedEvents, @@ -61,6 +61,8 @@ describe("", () => { }), ); + jest.spyOn(room.currentState, "on"); + // Insert local pins into local timeline set room.getUnfilteredTimelineSet = () => ({ @@ -75,6 +77,8 @@ describe("", () => { return Promise.resolve(event as IMinimalEvent); }); + cli.getRoom.mockReturnValue(room); + return room; }; @@ -131,8 +135,8 @@ describe("", () => { it("updates when messages are pinned", async () => { // Start with nothing pinned - const localPins = []; - const nonLocalPins = []; + const localPins: MatrixEvent[] = []; + const nonLocalPins: MatrixEvent[] = []; const pins = await mountPins(mkRoom(localPins, nonLocalPins)); expect(pins.find(PinnedEventTile).length).toBe(0); @@ -240,31 +244,27 @@ describe("", () => { ["@eve:example.org", 1], ].map(([user, option], i) => mkEvent({ - ...PollResponseEvent.from([answers[option].id], poll.getId()).serialize(), + ...PollResponseEvent.from([answers[option as number].id], poll.getId()!).serialize(), event: true, room: "!room:example.org", user: user as string, }), ); + const end = mkEvent({ - ...PollEndEvent.from(poll.getId(), "Closing the poll").serialize(), + ...PollEndEvent.from(poll.getId()!, "Closing the poll").serialize(), event: true, room: "!room:example.org", user: "@alice:example.org", }); // Make the responses available - cli.relations.mockImplementation(async (roomId, eventId, relationType, eventType, { from }) => { + cli.relations.mockImplementation(async (roomId, eventId, relationType, eventType, opts) => { if (eventId === poll.getId() && relationType === RelationType.Reference) { - switch (eventType) { - case M_POLL_RESPONSE.name: - // Paginate the results, for added challenge - return from === "page2" - ? { originalEvent: poll, events: responses.slice(2) } - : { originalEvent: poll, events: responses.slice(0, 2), nextBatch: "page2" }; - case M_POLL_END.name: - return { originalEvent: null, events: [end] }; - } + // Paginate the results, for added challenge + return opts?.from === "page2" + ? { originalEvent: poll, events: responses.slice(2) } + : { originalEvent: poll, events: [...responses.slice(0, 2), end], nextBatch: "page2" }; } // type does not allow originalEvent to be falsy // but code seems to @@ -272,8 +272,20 @@ describe("", () => { return { originalEvent: undefined as unknown as MatrixEvent, events: [] }; }); - const pins = await mountPins(mkRoom([], [poll])); + const room = mkRoom([], [poll]); + // poll end event validates against this + jest.spyOn(room.currentState, "maySendRedactionForEvent").mockReturnValue(true); + + const pins = await mountPins(room); + // two pages of results + await flushPromises(); + await flushPromises(); + + const pollInstance = room.polls.get(poll.getId()!); + expect(pollInstance).toBeTruthy(); + const pinTile = pins.find(MPollBody); + expect(pinTile.length).toEqual(1); expect(pinTile.find(".mx_MPollBody_option_ended").length).toEqual(2); expect(pinTile.find(".mx_MPollBody_optionVoteCount").first().text()).toEqual("2 votes");