From 1fd565414f707c7311e20e3cc86044894bc9520f Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 9 Jan 2023 17:51:50 +1300 Subject: [PATCH 01/34] wip --- src/components/views/messages/MPollBody.tsx | 129 +++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index ae90046abb3..d863f16bbba 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -42,8 +42,11 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import { GetRelationsForEvent } from "../rooms/EventTile"; import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { Poll, PollEvent } from "matrix-js-sdk/src/models/poll"; interface IState { + poll?: Poll, + pollReady: boolean; selected?: string | null | undefined; // Which option was clicked by the local user voteRelations: RelatedRelations; // Voting (response) events endRelations: RelatedRelations; // Poll end events @@ -229,19 +232,46 @@ export default class MPollBody extends React.Component { this.state = { selected: null, + pollReady: false, voteRelations: this.fetchVoteRelations(), endRelations: this.fetchEndRelations(), }; + + 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() { this.props.mxEvent.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated); this.removeListeners(this.state.voteRelations, this.state.endRelations); } + private async setPollInstance(poll: Poll) { + if (poll.pollId !== this.props.mxEvent.getId()) { + return; + } + console.log('hhh MPollBody setPollInstance', { poll, e: this.props.mxEvent }); + this.setState({ poll }); + const responses = await poll.getResponses(); + const voteRelations = new RelatedRelations([responses]); + this.setState({ pollReady: true, voteRelations }, () => { + this.addListeners(this.state.voteRelations, this.state.endRelations); + }); + } + private addListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations) { if (voteRelations) { voteRelations.on(RelationsEvent.Add, this.onRelationsChange); @@ -293,6 +323,7 @@ export default class MPollBody extends React.Component { }; private onRelationsChange = () => { + console.log('hhh', 'onRelationsChange'); // We hold Relations in our state, and they changed under us. // Check whether we should delete our selection, and then // re-render. @@ -385,12 +416,14 @@ export default class MPollBody extends React.Component { * have already seen. */ private unselectIfNewEventFromMe() { + // @TODO(kerrya) removed filter because vote relations are only poll responses now const newEvents: MatrixEvent[] = this.state.voteRelations .getRelations() - .filter(isPollResponse) .filter((mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!)); let newSelected = this.state.selected; + console.log('hhh unselectIfNewEvent', newEvents, this.seenEventIds); + if (newEvents.length > 0) { for (const mxEvent of newEvents) { if (mxEvent.getSender() === this.context.getUserId()) { @@ -416,6 +449,100 @@ export default class MPollBody extends React.Component { } public render() { + const { poll, pollReady } = this.state; + console.log('hhh', 'MPollBody render', poll, pollReady) + if (!poll) { + return null; + } + + const pollEvent = poll.getPollStartEvent(); + + + const pollId = this.props.mxEvent.getId(); + const userVotes = this.collectUserVotes(); + 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(pollEvent.kind.name); + + // Disclosed: votes are hidden until I vote or the poll ends + // Undisclosed: votes are hidden until poll ends + const showResults = poll.isEnded || (disclosed && myVote !== undefined); + + let totalText: string; + 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"); + } else if (myVote === undefined) { + if (totalVotes === 0) { + totalText = _t("No votes cast"); + } else { + totalText = _t("%(count)s votes cast. Vote to see the results", { count: totalVotes }); + } + } else { + totalText = _t("Based on %(count)s votes", { count: totalVotes }); + } + + const editedSpan = this.props.mxEvent.replacingEvent() ? ( + ({_t("edited")}) + ) : null; + + return ( +
+

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

+
+ {pollEvent.answers.map((answer: PollAnswerSubevent) => { + let answerVotes = 0; + let votesText = ""; + + if (showResults) { + answerVotes = votes.get(answer.id) ?? 0; + votesText = _t("%(count)s votes", { count: answerVotes }); + } + + 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: poll.isEnded, + }); + + const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes); + return ( +
this.selectOption(answer.id)}> + {poll.isEnded ? ( + + ) : ( + + )} +
+
+
+
+ ); + })} +
+
{totalText}
+
+ ); + } + + public render2() { const poll = this.props.mxEvent.unstableExtensibleEvent as PollStartEvent; if (!poll?.isEquivalentTo(M_POLL_START)) return null; // invalid From 2c01d2ebedb832f11d7d977169373a2a6ce7a975 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 9 Jan 2023 17:54:05 +1300 Subject: [PATCH 02/34] remove dupe --- src/components/views/messages/MPollBody.tsx | 89 --------------------- 1 file changed, 89 deletions(-) diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index d863f16bbba..f35c04f3026 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -541,95 +541,6 @@ export default class MPollBody extends React.Component {
); } - - public render2() { - const poll = this.props.mxEvent.unstableExtensibleEvent as PollStartEvent; - if (!poll?.isEquivalentTo(M_POLL_START)) return null; // invalid - - const ended = this.isEnded(); - const pollId = this.props.mxEvent.getId(); - const userVotes = this.collectUserVotes(); - const votes = countVotes(userVotes, poll); - 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); - - // Disclosed: votes are hidden until I vote or the poll ends - // Undisclosed: votes are hidden until poll ends - const showResults = ended || (disclosed && myVote !== undefined); - - let totalText: string; - if (ended) { - 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"); - } else if (myVote === undefined) { - if (totalVotes === 0) { - totalText = _t("No votes cast"); - } else { - totalText = _t("%(count)s votes cast. Vote to see the results", { count: totalVotes }); - } - } else { - totalText = _t("Based on %(count)s votes", { count: totalVotes }); - } - - const editedSpan = this.props.mxEvent.replacingEvent() ? ( - ({_t("edited")}) - ) : null; - - return ( -
-

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

-
- {poll.answers.map((answer: PollAnswerSubevent) => { - let answerVotes = 0; - let votesText = ""; - - if (showResults) { - answerVotes = votes.get(answer.id) ?? 0; - votesText = _t("%(count)s votes", { count: answerVotes }); - } - - const checked = (!ended && myVote === answer.id) || (ended && answerVotes === winCount); - const cls = classNames({ - mx_MPollBody_option: true, - mx_MPollBody_option_checked: checked, - mx_MPollBody_option_ended: ended, - }); - - const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes); - return ( -
this.selectOption(answer.id)}> - {ended ? ( - - ) : ( - - )} -
-
-
-
- ); - })} -
-
{totalText}
-
- ); - } } interface IEndedPollOptionProps { From 563bffbdb158b4969ed77b6266cfd20d2d38a958 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 26 Jan 2023 18:27:26 +1300 Subject: [PATCH 03/34] use poll model relations in all cases --- .../views/dialogs/EndPollDialog.tsx | 7 +- src/components/views/messages/MPollBody.tsx | 235 +++--------------- .../views/messages/MPollBody-test.tsx | 147 ++--------- 3 files changed, 59 insertions(+), 330 deletions(-) diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index 946f209d31c..4b54333054f 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -35,8 +35,11 @@ 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 => { + const room = this.props.matrixClient.getRoom(this.props.event.getRoomId()); + const poll = room?.polls.get(this.props.event.getId()); + const responses = await poll.getResponses(); + const topAnswer = findTopAnswer(this.props.event, responses); const message = topAnswer === "" diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index d6fb083faf9..383387abef3 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -17,12 +17,11 @@ limitations under the License. import React 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 { 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"; @@ -39,11 +38,10 @@ import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; interface IState { - poll?: Poll, + poll?: Poll; pollReady: 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 { @@ -62,15 +60,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( @@ -90,25 +80,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()); @@ -218,8 +190,6 @@ 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); @@ -227,14 +197,7 @@ export default class MPollBody extends React.Component { this.state = { selected: null, pollReady: false, - voteRelations: this.fetchVoteRelations(), - endRelations: this.fetchEndRelations(), }; - - - - this.addListeners(this.state.voteRelations, this.state.endRelations); - this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onRelationsCreated); } public componentDidMount(): void { @@ -243,77 +206,42 @@ export default class MPollBody extends React.Component { if (poll) { this.setPollInstance(poll); } else { - room.on(PollEvent.New, this.setPollInstance.bind(this)); + room?.on(PollEvent.New, this.setPollInstance.bind(this)); } - } - public componentWillUnmount() { - this.props.mxEvent.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated); - this.removeListeners(this.state.voteRelations, this.state.endRelations); + public componentWillUnmount(): void { + this.removeListeners(); } - private async setPollInstance(poll: Poll) { + private async setPollInstance(poll: Poll): Promise { if (poll.pollId !== this.props.mxEvent.getId()) { return; } - console.log('hhh MPollBody setPollInstance', { poll, e: this.props.mxEvent }); - this.setState({ poll }); - const responses = await poll.getResponses(); - const voteRelations = new RelatedRelations([responses]); - this.setState({ pollReady: true, voteRelations }, () => { - this.addListeners(this.state.voteRelations, this.state.endRelations); + this.setState({ poll }, () => { + this.addListeners(); }); - } + const responses = await poll.getResponses(); + const voteRelations = responses; - 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); - } + this.setState({ pollReady: true, voteRelations }); } - 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); - } + private addListeners(): void { + this.state.poll?.on(PollEvent.Responses, this.onResponsesChange); + this.state.poll?.on(PollEvent.End, this.onRelationsChange); } - private onRelationsCreated = (relationType: string, eventType: string): void => { - if (relationType !== "m.reference") { - return; - } - - 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 => { @@ -325,7 +253,7 @@ 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(); @@ -353,51 +281,11 @@ 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, - ); + return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected); } /** @@ -415,8 +303,6 @@ export default class MPollBody extends React.Component { .filter((mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!)); let newSelected = this.state.selected; - console.log('hhh unselectIfNewEvent', newEvents, this.seenEventIds); - if (newEvents.length > 0) { for (const mxEvent of newEvents) { if (mxEvent.getSender() === this.context.getUserId()) { @@ -437,20 +323,15 @@ export default class MPollBody extends React.Component { return sum; } - private isEnded(): boolean { - return isPollEnded(this.props.mxEvent, this.context, this.props.getRelationsForEvent); - } - public render(): JSX.Element { const { poll, pollReady } = this.state; - console.log('hhh', 'MPollBody render', poll, pollReady) + console.log("hhh", "MPollBody render", poll, pollReady); if (!poll?.pollEvent) { return null; } const pollEvent = poll.pollEvent; - const pollId = this.props.mxEvent.getId(); const userVotes = this.collectUserVotes(); const votes = countVotes(userVotes, pollEvent); @@ -499,7 +380,8 @@ export default class MPollBody extends React.Component { votesText = _t("%(count)s votes", { count: answerVotes }); } - const checked = (!poll.isEnded && myVote === answer.id) || (poll.isEnded && 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, @@ -602,65 +484,14 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote { 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 diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index a6f9b5e11c6..a234b21ebfc 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 { MatrixClient, MatrixEvent, Poll, 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, @@ -32,10 +31,8 @@ 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"; @@ -47,9 +44,10 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink 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"), + getUserId: jest.fn().mockReturnValue(userId), sendEvent: jest.fn().mockReturnValue(Promise.resolve({ event_id: "fake_send_id" })), getRoom: jest.fn(), }); @@ -59,94 +57,19 @@ setRedactionAllowedForMeOnly(mockClient); describe("MPollBody", () => { beforeEach(() => { mockClient.sendEvent.mockClear(); + + mockClient.getRoom.mockReturnValue(undefined); }); it("finds no votes if there are none", () => { expect( allVotes( - { getRoomId: () => "$room" } as MatrixEvent, - MatrixClientPeg.get(), - new RelatedRelations([newVoteRelations([])]), - new RelatedRelations([newEndRelations([])]), + newVoteRelations([]), ), ).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), - ]); - }); - - 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("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("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"]), - ]); - }); - - it("renders no votes if none were made", () => { + fit("renders no votes if none were made", () => { const votes: MatrixEvent[] = []; const renderResult = newMPollBody(votes); expect(votesCount(renderResult, "pizza")).toBe(""); @@ -557,7 +480,7 @@ describe("MPollBody", () => { responseEvent("@fa:example.com", "poutine", 18), ]; - expect(runFindTopAnswer(votes, [])).toEqual("Poutine"); + expect(runFindTopAnswer(votes)).toEqual("Poutine"); }); it("finds all top answers when there is a draw", () => { @@ -566,27 +489,11 @@ describe("MPollBody", () => { responseEvent("@ab:example.com", "pizza", 17), responseEvent("@fa:example.com", "poutine", 18), ]; - expect(runFindTopAnswer(votes, [])).toEqual("Italian, Pizza and Poutine"); - }); - - it("finds all top answers ignoring late votes", () => { - 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 when ended", () => { - expect(runFindTopAnswer([], [endEvent("@me:example.com", 13)])).toEqual(""); + expect(runFindTopAnswer([])).toEqual(""); }); it("shows non-radio buttons if the poll is ended", () => { @@ -779,6 +686,7 @@ describe("MPollBody", () => { }); mockClient.getRoom.mockImplementation((_roomId) => { return { + polls: new Map(), currentState: { maySendRedactionForEvent: (_evt: MatrixEvent, userId: string) => { return userId === "@me:example.com"; @@ -1180,7 +1088,7 @@ function runIsPollEnded(ends: MatrixEvent[]) { return isPollEnded(pollEvent, mockClient, getRelationsForEvent); } -function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) { +function runFindTopAnswer(votes: MatrixEvent[]) { const pollEvent = new MatrixEvent({ event_id: "$mypoll", room_id: "#myroom:example.com", @@ -1188,31 +1096,18 @@ 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; - }); + const room = new Room("!room:server.org", matrixClient, userId); + jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation( + (_evt: MatrixEvent, userId: string) => { + return userId === userId; + }, + ); + + matrixClient.getRoom.mockReturnValue(room); } let EVENT_ID = 0; From c87bf54faff2f7a53296d586606009b2ddffb7ef Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 27 Jan 2023 11:19:48 +1300 Subject: [PATCH 04/34] update mpollbody tests to use poll instance --- .../context_menus/MessageContextMenu.tsx | 2 +- .../views/elements/StyledRadioButton.tsx | 2 + src/components/views/messages/MPollBody.tsx | 70 +--- .../views/messages/MPollBody-test.tsx | 317 +++++++++--------- .../__snapshots__/MPollBody-test.tsx.snap | 208 ++++++++++++ 5 files changed, 372 insertions(+), 227 deletions(-) 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/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx index 0b7ff491ef3..cc1db4ea19f 100644 --- a/src/components/views/elements/StyledRadioButton.tsx +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -43,6 +43,8 @@ export default class StyledRadioButton extends React.PureComponent 0; + return poll.isEnded; } export function pollAlreadyHasVotes(mxEvent: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): boolean { @@ -297,10 +248,10 @@ export default class MPollBody extends React.Component { * have already seen. */ private unselectIfNewEventFromMe(): void { - // @TODO(kerrya) removed filter because vote relations are only poll responses now - const newEvents: MatrixEvent[] = this.state.voteRelations - .getRelations() - .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) { @@ -325,7 +276,6 @@ export default class MPollBody extends React.Component { public render(): JSX.Element { const { poll, pollReady } = this.state; - console.log("hhh", "MPollBody render", poll, pollReady); if (!poll?.pollEvent) { return null; } diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index a234b21ebfc..55d5dbff3e7 100644 --- a/test/components/views/messages/MPollBody-test.tsx +++ b/test/components/views/messages/MPollBody-test.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import { fireEvent, render, RenderResult } from "@testing-library/react"; -import { MatrixClient, MatrixEvent, Poll, 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 { M_POLL_END, @@ -28,16 +28,10 @@ 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 { - allVotes, - findTopAnswer, - 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 } from "../../../test-utils"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import MPollBody from "../../../../src/components/views/messages/MPollBody"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; @@ -50,28 +44,26 @@ const mockClient = getMockClientWithEventEmitter({ getUserId: jest.fn().mockReturnValue(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(); mockClient.getRoom.mockReturnValue(undefined); + mockClient.relations.mockResolvedValue({ events: [] }); }); it("finds no votes if there are none", () => { - expect( - allVotes( - newVoteRelations([]), - ), - ).toEqual([]); + expect(allVotes(newVoteRelations([]))).toEqual([]); }); - fit("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); + await flushPromises(); expect(votesCount(renderResult, "pizza")).toBe(""); expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); @@ -80,14 +72,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"); @@ -95,7 +87,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"), @@ -103,7 +95,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 @@ -114,14 +106,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(""); @@ -129,9 +121,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(""); @@ -139,7 +131,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 @@ -147,7 +139,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"); @@ -155,14 +147,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"); @@ -176,7 +168,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), @@ -184,7 +176,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"); @@ -202,7 +194,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({ @@ -212,13 +204,12 @@ describe("MPollBody", () => { content: newPollStart(undefined, undefined, true), }); const props = getMPollBodyPropsFromEvent(mxEvent, votes); + const room = await setupRoomWithPollEvents(mxEvent, votes); const renderResult = renderMPollBodyWithWrapper(props); - const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); - expect(voteRelations).toBeDefined(); 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"); @@ -229,7 +220,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({ @@ -239,14 +230,13 @@ describe("MPollBody", () => { content: newPollStart(undefined, undefined, true), }); const props = getMPollBodyPropsFromEvent(mxEvent, votes); + const room = await setupRoomWithPollEvents(mxEvent, votes); const renderResult = renderMPollBodyWithWrapper(props); - 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 - @@ -264,10 +254,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 @@ -276,10 +266,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"); @@ -287,13 +277,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"); @@ -301,14 +291,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"); @@ -316,7 +306,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. @@ -326,7 +316,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"); @@ -334,7 +324,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), @@ -342,7 +332,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"); @@ -351,25 +341,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"), @@ -377,7 +367,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(""); @@ -385,7 +375,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"), @@ -393,7 +383,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(); @@ -402,7 +392,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"), @@ -411,7 +401,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"); @@ -419,16 +409,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"); @@ -436,9 +426,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"); @@ -446,9 +436,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"); @@ -458,17 +448,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), @@ -483,7 +473,7 @@ describe("MPollBody", () => { expect(runFindTopAnswer(votes)).toEqual("Poutine"); }); - it("finds all top answers when there is a draw", () => { + 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), @@ -492,18 +482,18 @@ describe("MPollBody", () => { expect(runFindTopAnswer(votes)).toEqual("Italian, Pizza and Poutine"); }); - it("is silent about the top answer if there are no votes", () => { + it("is silent about the top answer if there are no votes", async () => { expect(runFindTopAnswer([])).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 @@ -512,7 +502,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"); @@ -520,10 +510,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"); @@ -531,7 +521,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), @@ -540,7 +530,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); @@ -551,7 +541,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), @@ -562,7 +552,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"); @@ -571,7 +561,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), @@ -585,7 +575,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"); @@ -594,7 +584,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" @@ -612,7 +602,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"); @@ -621,7 +611,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), @@ -629,7 +619,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); @@ -640,14 +630,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); @@ -656,54 +646,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); + const result = await runIsPollEnded(ends); + expect(result).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); - }); - - 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 { - polls: new Map(), - 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", @@ -732,7 +709,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)', ); @@ -748,13 +725,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), @@ -762,11 +739,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), @@ -775,12 +752,12 @@ 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), @@ -788,17 +765,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), @@ -807,11 +784,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), @@ -821,11 +798,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), @@ -835,11 +812,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), @@ -849,7 +826,7 @@ 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(); }); }); @@ -870,12 +847,12 @@ function newRelations(relationEvents: Array, eventType: string): Re return voteRelations; } -function newMPollBody( +async function newMPollBody( relationEvents: Array, endEvents: Array = [], answers?: PollAnswer[], disclosed = true, -): RenderResult { +): Promise { const mxEvent = new MatrixEvent({ type: M_POLL_START.name, event_id: "$mypoll", @@ -926,15 +903,35 @@ function renderMPollBodyWithWrapper(props: IBodyProps): RenderResult { }); } -function newMPollBodyFromEvent( +async function newMPollBodyFromEvent( mxEvent: MatrixEvent, relationEvents: Array, endEvents: Array = [], -): RenderResult { +): Promise { const props = getMPollBodyPropsFromEvent(mxEvent, relationEvents, endEvents); + + 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}`)); } @@ -1068,7 +1065,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", @@ -1076,16 +1073,9 @@ function runIsPollEnded(ends: MatrixEvent[]) { content: newPollStart(), }); - setRedactionAllowedForMeOnly(mockClient); - - 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); - }; + await setupRoomWithPollEvents(pollEvent, [], ends); - return isPollEnded(pollEvent, mockClient, getRelationsForEvent); + return isPollEnded(pollEvent, mockClient); } function runFindTopAnswer(votes: MatrixEvent[]) { @@ -1099,15 +1089,10 @@ function runFindTopAnswer(votes: MatrixEvent[]) { return findTopAnswer(pollEvent, newVoteRelations(votes)); } -function setRedactionAllowedForMeOnly(matrixClient: MockedObject) { - const room = new Room("!room:server.org", matrixClient, userId); - jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation( - (_evt: MatrixEvent, userId: string) => { - return userId === userId; - }, - ); - - matrixClient.getRoom.mockReturnValue(room); +function setRedactionAllowedForMeOnly(room: Room) { + jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation((_evt: MatrixEvent, id: string) => { + return id === userId; + }); } let EVENT_ID = 0; diff --git a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index 2263527148b..edb72d8f479 100644 --- a/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap @@ -865,6 +865,214 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] =
`; +exports[`MPollBody renders a poll with local, non-local and invalid votes 2`] = ` +
+
+

+ What should we order for the party? +

+
+
+