From 413e13f5d44c36ed8310804a83a3ec38dcad25ff Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 13 Nov 2025 17:50:48 +0000 Subject: [PATCH 01/15] Remove poll ended event UI. --- res/css/_components.pcss | 1 - res/css/views/messages/_MPollEndBody.pcss | 14 -- res/css/views/rooms/_EventBubbleTile.pcss | 5 - .../views/messages/MPollEndBody.tsx | 108 ---------- .../views/messages/MessageEvent.tsx | 3 - src/shouldHideEvent.ts | 5 +- .../views/messages/MPollEndBody-test.tsx | 193 ------------------ .../__snapshots__/MPollEndBody-test.tsx.snap | 117 ----------- 8 files changed, 4 insertions(+), 442 deletions(-) delete mode 100644 res/css/views/messages/_MPollEndBody.pcss delete mode 100644 src/components/views/messages/MPollEndBody.tsx delete mode 100644 test/unit-tests/components/views/messages/MPollEndBody-test.tsx delete mode 100644 test/unit-tests/components/views/messages/__snapshots__/MPollEndBody-test.tsx.snap diff --git a/res/css/_components.pcss b/res/css/_components.pcss index bb0f769b687..66eed090837 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -238,7 +238,6 @@ @import "./views/messages/_MLocationBody.pcss"; @import "./views/messages/_MNoticeBody.pcss"; @import "./views/messages/_MPollBody.pcss"; -@import "./views/messages/_MPollEndBody.pcss"; @import "./views/messages/_MStickerBody.pcss"; @import "./views/messages/_MTextBody.pcss"; @import "./views/messages/_MVideoBody.pcss"; diff --git a/res/css/views/messages/_MPollEndBody.pcss b/res/css/views/messages/_MPollEndBody.pcss deleted file mode 100644 index 655f444e357..00000000000 --- a/res/css/views/messages/_MPollEndBody.pcss +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_MPollEndBody_icon { - height: 14px; - margin-right: $spacing-8; - vertical-align: middle; - color: $secondary-content; -} diff --git a/res/css/views/rooms/_EventBubbleTile.pcss b/res/css/views/rooms/_EventBubbleTile.pcss index d98babab21e..5fe2d3c8c19 100644 --- a/res/css/views/rooms/_EventBubbleTile.pcss +++ b/res/css/views/rooms/_EventBubbleTile.pcss @@ -355,11 +355,6 @@ Please see LICENSE files in the repository root for full details. /* Keep height equal to text for shield alignment, additional 2px because of 1px padding on text */ height: calc($font-18px + 2px); } - - .mx_MPollEndBody { - /* Prevent the poll end body from exceeding the tile width */ - width: 100%; - } } &:not(.mx_EventTile_noBubble) .mx_EventTile_line:not(.mx_EventTile_mediaLine) { diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx deleted file mode 100644 index 95f8a53f2ae..00000000000 --- a/src/components/views/messages/MPollEndBody.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useEffect, useState, useContext, type JSX } from "react"; -import { MatrixEvent, M_TEXT } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg"; -import MatrixClientContext, { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { _t } from "../../../languageHandler"; -import { textForEvent } from "../../../TextForEvent"; -import { Caption } from "../typography/Caption"; -import { type IBodyProps } from "./IBodyProps"; -import MPollBody from "./MPollBody"; - -const getRelatedPollStartEventId = (event: MatrixEvent): string | undefined => { - const relation = event.getRelation(); - return relation?.event_id; -}; - -/** - * Attempt to retrieve the related poll start event for this end event - * If the event already exists in the rooms timeline, return it - * Otherwise try to fetch the event from the server - * @param event - * @returns - */ -const usePollStartEvent = (event: MatrixEvent): { pollStartEvent?: MatrixEvent; isLoadingPollStartEvent: boolean } => { - const matrixClient = useContext(MatrixClientContext); - const [pollStartEvent, setPollStartEvent] = useState(); - const [isLoadingPollStartEvent, setIsLoadingPollStartEvent] = useState(false); - - const pollStartEventId = getRelatedPollStartEventId(event); - - useEffect(() => { - const room = matrixClient.getRoom(event.getRoomId()); - const fetchPollStartEvent = async (roomId: string, pollStartEventId: string): Promise => { - setIsLoadingPollStartEvent(true); - try { - const startEventJson = await matrixClient.fetchRoomEvent(roomId, pollStartEventId); - const startEvent = new MatrixEvent(startEventJson); - // add the poll to the room polls state - room?.processPollEvents([startEvent, event]); - - // end event is not a valid end to the related start event - // if not sent by the same user - if (startEvent.getSender() === event.getSender()) { - setPollStartEvent(startEvent); - } - } catch (error) { - logger.error("Failed to fetch related poll start event", error); - } finally { - setIsLoadingPollStartEvent(false); - } - }; - - if (pollStartEvent || !room || !pollStartEventId) { - return; - } - - const timelineSet = room.getUnfilteredTimelineSet(); - const localEvent = timelineSet - ?.getTimelineForEvent(pollStartEventId) - ?.getEvents() - .find((e) => e.getId() === pollStartEventId); - - if (localEvent) { - // end event is not a valid end to the related start event - // if not sent by the same user - if (localEvent.getSender() === event.getSender()) { - setPollStartEvent(localEvent); - } - } else { - // pollStartEvent is not in the current timeline, - // fetch it - fetchPollStartEvent(room.roomId, pollStartEventId); - } - }, [event, pollStartEventId, pollStartEvent, matrixClient]); - - return { pollStartEvent, isLoadingPollStartEvent }; -}; - -export const MPollEndBody = ({ mxEvent, ref, ...props }: IBodyProps): JSX.Element => { - const cli = useMatrixClientContext(); - const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent); - - if (!pollStartEvent) { - const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent, cli); - return ( - <> - - {!isLoadingPollStartEvent && pollEndFallbackMessage} - - ); - } - - return ( -
- {_t("timeline|m.poll.end|ended")} - -
- ); -}; diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index fc800b72b0d..6d7da2a1ac9 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -34,7 +34,6 @@ import MVoiceOrAudioBody from "./MVoiceOrAudioBody"; import MVideoBody from "./MVideoBody"; import MStickerBody from "./MStickerBody"; import MPollBody from "./MPollBody"; -import { MPollEndBody } from "./MPollEndBody"; import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; @@ -75,8 +74,6 @@ const baseEvTypes = new Map>([ [EventType.Sticker, MStickerBody], [M_POLL_START.name, MPollBody], [M_POLL_START.altName, MPollBody], - [M_POLL_END.name, MPollEndBody], - [M_POLL_END.altName, MPollEndBody], [M_BEACON_INFO.name, MBeaconBody], [M_BEACON_INFO.altName, MBeaconBody], ]); diff --git a/src/shouldHideEvent.ts b/src/shouldHideEvent.ts index f8bc2fc5179..64a0e1f0f91 100644 --- a/src/shouldHideEvent.ts +++ b/src/shouldHideEvent.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type MatrixEvent, EventType, RelationType } from "matrix-js-sdk/src/matrix"; +import { type MatrixEvent, EventType, RelationType, M_POLL_END } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import SettingsStore from "./settings/SettingsStore"; @@ -50,6 +50,9 @@ function memberEventDiff(ev: MatrixEvent): IDiff { * hitting the settings store */ export default function shouldHideEvent(ev: MatrixEvent, ctx?: IRoomState): boolean { + // Hide all poll end events + if (M_POLL_END.matches(ev.getType())) return true; + // Accessing the settings store directly can be expensive if done frequently, // so we should prefer using cached values if a RoomContext is available const isEnabled = ctx diff --git a/test/unit-tests/components/views/messages/MPollEndBody-test.tsx b/test/unit-tests/components/views/messages/MPollEndBody-test.tsx deleted file mode 100644 index 7015e3d1d99..00000000000 --- a/test/unit-tests/components/views/messages/MPollEndBody-test.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, waitFor } from "jest-matrix-react"; -import { type EventTimeline, type MatrixEvent, Room, M_TEXT } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { type IBodyProps } from "../../../../../src/components/views/messages/IBodyProps"; -import { MPollEndBody } from "../../../../../src/components/views/messages/MPollEndBody"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import { type RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; -import { type MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; -import { - flushPromises, - getMockClientWithEventEmitter, - makePollEndEvent, - makePollStartEvent, - mockClientMethodsEvents, - mockClientMethodsUser, - setupRoomWithPollEvents, -} from "../../../../test-utils"; - -describe("", () => { - const userId = "@alice:domain.org"; - const roomId = "!room:domain.org"; - const mockClient = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), - ...mockClientMethodsEvents(), - getRoom: jest.fn(), - relations: jest.fn(), - fetchRoomEvent: jest.fn(), - }); - const pollStartEvent = makePollStartEvent("Question?", userId, undefined, { roomId }); - const pollEndEvent = makePollEndEvent(pollStartEvent.getId()!, roomId, userId, 123); - - const setupRoomWithEventsTimeline = async (pollEnd: MatrixEvent, pollStart?: MatrixEvent): Promise => { - if (pollStart) { - await setupRoomWithPollEvents([pollStart], [], [pollEnd], mockClient); - } - const room = mockClient.getRoom(roomId) || new Room(roomId, mockClient, userId); - - // end events validate against this - jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation( - (_evt: MatrixEvent, id: string) => { - return id === mockClient.getSafeUserId(); - }, - ); - - const timelineSet = room.getUnfilteredTimelineSet(); - const getTimelineForEventSpy = jest.spyOn(timelineSet, "getTimelineForEvent"); - // if we have a pollStart, mock the room timeline to include it - if (pollStart) { - const eventTimeline = { - getEvents: jest.fn().mockReturnValue([pollEnd, pollStart]), - } as unknown as EventTimeline; - getTimelineForEventSpy.mockReturnValue(eventTimeline); - } - mockClient.getRoom.mockReturnValue(room); - - return room; - }; - - const defaultProps = { - mxEvent: pollEndEvent, - highlightLink: "unused", - mediaEventHelper: {} as unknown as MediaEventHelper, - onMessageAllowed: () => {}, - permalinkCreator: {} as unknown as RoomPermalinkCreator, - ref: undefined as any, - }; - - const getComponent = (props: Partial = {}) => - render(, { - wrapper: ({ children }) => ( - {children} - ), - }); - - beforeEach(() => { - mockClient.getRoom.mockReset(); - mockClient.relations.mockResolvedValue({ - events: [], - }); - mockClient.fetchRoomEvent.mockResolvedValue(pollStartEvent.getEffectiveEvent()); - }); - - afterEach(() => { - jest.spyOn(logger, "error").mockRestore(); - }); - - describe("when poll start event exists in current timeline", () => { - it("renders an ended poll", async () => { - await setupRoomWithEventsTimeline(pollEndEvent, pollStartEvent); - const { container } = getComponent(); - - // ended poll rendered - expect(container).toMatchSnapshot(); - - // didnt try to fetch start event while it was already in timeline - expect(mockClient.fetchRoomEvent).not.toHaveBeenCalled(); - }); - - it("does not render a poll tile when end event is invalid", async () => { - // sender of end event does not match start event - const invalidEndEvent = makePollEndEvent(pollStartEvent.getId()!, roomId, "@mallory:domain.org", 123); - await setupRoomWithEventsTimeline(invalidEndEvent, pollStartEvent); - const { getByText } = getComponent({ mxEvent: invalidEndEvent }); - - // no poll tile rendered - expect(getByText("The poll has ended. Something.")).toBeTruthy(); - }); - }); - - describe("when poll start event does not exist in current timeline", () => { - it("fetches the related poll start event and displays a poll tile", async () => { - await setupRoomWithEventsTimeline(pollEndEvent); - const { container, getByTestId, getByRole, queryByRole } = getComponent(); - - // while fetching event, only icon is shown - expect(container).toMatchSnapshot(); - - await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument()); - await waitFor(() => expect(queryByRole("progressbar")).not.toBeInTheDocument()); - - expect(mockClient.fetchRoomEvent).toHaveBeenCalledWith(roomId, pollStartEvent.getId()); - - // quick check for poll tile - expect(getByTestId("pollQuestion").innerHTML).toEqual("Question?"); - expect(getByTestId("totalVotes").innerHTML).toEqual("Final result based on 0 votes"); - }); - - it("does not render a poll tile when end event is invalid", async () => { - // sender of end event does not match start event - const invalidEndEvent = makePollEndEvent(pollStartEvent.getId()!, roomId, "@mallory:domain.org", 123); - await setupRoomWithEventsTimeline(invalidEndEvent); - const { getByText } = getComponent({ mxEvent: invalidEndEvent }); - - // flush the fetch event promise - await flushPromises(); - - // no poll tile rendered - expect(getByText("The poll has ended. Something.")).toBeTruthy(); - }); - - it("logs an error and displays the text fallback when fetching the start event fails", async () => { - await setupRoomWithEventsTimeline(pollEndEvent); - mockClient.fetchRoomEvent.mockRejectedValue({ code: 404 }); - const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {}); - const { getByText } = getComponent(); - - // flush the fetch event promise - await flushPromises(); - - // poll end event fallback text used - expect(getByText("The poll has ended. Something.")).toBeTruthy(); - expect(logSpy).toHaveBeenCalledWith("Failed to fetch related poll start event", { code: 404 }); - }); - - it("logs an error and displays the extensible event text when fetching the start event fails", async () => { - await setupRoomWithEventsTimeline(pollEndEvent); - mockClient.fetchRoomEvent.mockRejectedValue({ code: 404 }); - const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {}); - const { getByText } = getComponent(); - - // flush the fetch event promise - await flushPromises(); - - // poll end event fallback text used - expect(getByText("The poll has ended. Something.")).toBeTruthy(); - expect(logSpy).toHaveBeenCalledWith("Failed to fetch related poll start event", { code: 404 }); - }); - - it("displays fallback text when the poll end event does not have text", async () => { - const endWithoutText = makePollEndEvent(pollStartEvent.getId()!, roomId, userId, 123); - delete endWithoutText.getContent()[M_TEXT.name]; - await setupRoomWithEventsTimeline(endWithoutText); - mockClient.fetchRoomEvent.mockRejectedValue({ code: 404 }); - const { getByText } = getComponent({ mxEvent: endWithoutText }); - - // flush the fetch event promise - await flushPromises(); - - // default fallback text used - expect(getByText("@alice:domain.org has ended a poll")).toBeTruthy(); - }); - }); -}); diff --git a/test/unit-tests/components/views/messages/__snapshots__/MPollEndBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MPollEndBody-test.tsx.snap deleted file mode 100644 index 6872894a65d..00000000000 --- a/test/unit-tests/components/views/messages/__snapshots__/MPollEndBody-test.tsx.snap +++ /dev/null @@ -1,117 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[` when poll start event does not exist in current timeline fetches the related poll start event and displays a poll tile 1`] = ` -
-
-
-`; - -exports[` when poll start event exists in current timeline renders an ended poll 1`] = ` -
-
- - Ended a poll - -
- - Question? - -
-
-
-
-
- Socks -
-
- 0 votes -
-
-
-
-
-
-
-
-
-
-
- Shoes -
-
- 0 votes -
-
-
-
-
-
-
-
-
- Final result based on 0 votes -
-
-
-
-
-
-
-`; From f7a388bf281c4b1ba698feaa233387d1710739bd Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 13 Nov 2025 19:43:57 +0000 Subject: [PATCH 02/15] Add better aria labels for screen reader and change ui to match mobile UX. - Checkmark and progress bar are only green if the poll is ended. - Updated the Poll icon for open and ended state and added labels - Right align total votes count and update text --- .../components/views/polls/_PollOption.pcss | 22 +++++- res/css/views/messages/_MPollBody.pcss | 26 +++---- src/components/views/messages/MPollBody.tsx | 21 +++-- .../views/messages/MessageEvent.tsx | 1 - src/components/views/polls/PollOption.tsx | 76 +++++++++++-------- .../polls/pollHistory/PollListItemEnded.tsx | 3 +- src/i18n/strings/en_EN.json | 18 ++--- 7 files changed, 91 insertions(+), 76 deletions(-) diff --git a/res/css/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss index 42ec7c8dac6..7def0aa3150 100644 --- a/res/css/components/views/polls/_PollOption.pcss +++ b/res/css/components/views/polls/_PollOption.pcss @@ -54,7 +54,7 @@ Please see LICENSE files in the repository root for full details. .mx_PollOption_popularityBackground { .mx_PollOption_popularityAmount { - background-color: var(--cpd-color-icon-accent-tertiary); + background-color: var(--cpd-color-border-interactive-hovered); } } @@ -62,8 +62,8 @@ Please see LICENSE files in the repository root for full details. .mx_StyledRadioButton_checked { input[type="radio"]:checked + div { border-width: 2px; - border-color: var(--cpd-color-icon-accent-tertiary); - background-color: var(--cpd-color-icon-accent-tertiary); + border-color: var(--cpd-color-border-interactive-hovered); + background-color: var(--cpd-color-border-interactive-hovered); background-image: url("@vector-im/compound-design-tokens/icons/check.svg"); background-size: 12px; background-repeat: no-repeat; @@ -76,6 +76,22 @@ Please see LICENSE files in the repository root for full details. } } +.mx_PollOption_ended.mx_PollOption_checked { + .mx_PollOption_popularityBackground { + .mx_PollOption_popularityAmount { + background-color: var(--cpd-color-icon-accent-tertiary); + } + } + + /* override checked radio button styling to show checkmark instead */ + .mx_StyledRadioButton_checked { + input[type="radio"]:checked + div { + border-color: var(--cpd-color-icon-accent-tertiary); + background-color: var(--cpd-color-icon-accent-tertiary); + } + } +} + /* options not actionable in these states */ .mx_PollOption_checked, .mx_PollOption_ended { diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index 09160c083a4..5ff58b51edb 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -20,6 +20,14 @@ Please see LICENSE files in the repository root for full details. margin-top: 0; margin-bottom: 8px; letter-spacing: var(--cpd-font-letter-spacing-heading-lg); + display: flex; + align-items: center; + gap: 12px; + + svg { + flex-shrink: 0; + color: inherit; + } .mx_MPollBody_edited { color: $roomtopic-color; @@ -28,26 +36,10 @@ Please see LICENSE files in the repository root for full details. } } - legend::before { - content: ""; - position: relative; - display: inline-block; - margin-right: 12px; - top: 3px; - left: 3px; - height: 20px; - width: 20px; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - mask-image: url("$(res)/img/element-icons/room/composer/poll.svg"); - } - .mx_MPollBody_totalVotes { display: flex; flex-direction: inline; - justify-content: start; + justify-content: end; color: $secondary-content; font-size: $font-12px; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 15f2efce4b4..cf6240f5834 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -22,6 +22,8 @@ import { import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; import { type PollStartEvent, type PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent"; +import PollsIcon from "@vector-im/compound-design-tokens/assets/web/icons/polls"; +import PollsEndIcon from "@vector-im/compound-design-tokens/assets/web/icons/polls-end"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -306,32 +308,26 @@ export default class MPollBody extends React.Component { let totalText: string; if (showResults && poll.undecryptableRelationsCount) { totalText = _t("poll|total_decryption_errors"); - } else if (poll.isEnded) { - totalText = _t("right_panel|poll|final_result", { count: totalVotes }); - } else if (!disclosed) { - totalText = _t("poll|total_not_ended"); - } else if (myVote === undefined) { - if (totalVotes === 0) { - totalText = _t("poll|total_no_votes"); - } else { - totalText = _t("poll|total_n_votes", { count: totalVotes }); - } } else { - totalText = _t("poll|total_n_votes_voted", { count: totalVotes }); + totalText = _t("poll|total_votes_label", { count: totalVotes }); } const editedSpan = this.props.mxEvent.replacingEvent() ? ( ({_t("common|edited")}) ) : null; + const PollIcon = poll.isEnded ? PollsEndIcon : PollsIcon; + const pollLabel = poll.isEnded ? _t("poll|ended_poll_label") : _t("poll|poll_label"); + return (
+ {pollEvent.question.text} {editedSpan}
- {pollEvent.answers.map((answer: PollAnswerSubevent) => { + {pollEvent.answers.map((answer: PollAnswerSubevent, index: number) => { let answerVotes = 0; if (showResults) { @@ -346,6 +342,7 @@ export default class MPollBody extends React.Component { key={answer.id} pollId={pollId} answer={answer} + optionNumber={index + 1} isChecked={checked} isEnded={poll.isEnded} voteCount={answerVotes} diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 6d7da2a1ac9..6d124c88a8f 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -15,7 +15,6 @@ import { MatrixEventEvent, M_BEACON_INFO, M_LOCATION, - M_POLL_END, M_POLL_START, type IContent, } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index a8de8fb6ef1..9bb979da85c 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -36,50 +36,62 @@ const PollOptionContent: React.FC = ({ isWinner, answer, interface PollOptionProps extends PollOptionContentProps { pollId: string; totalVoteCount: number; + optionNumber: number; isEnded?: boolean; isChecked?: boolean; onOptionSelected?: (id: string) => void; children?: ReactNode; } -const EndedPollOption: React.FC> = ({ - isChecked, - children, - answer, -}) => ( -
- {children} -
-); - -const ActivePollOption: React.FC> = ({ +const ActivePollOption: React.FC & { children: ReactNode }> = ({ pollId, isChecked, + isEnded, + optionNumber, + isWinner, + voteCount, + displayVoteCount, children, answer, onOptionSelected, -}) => ( - onOptionSelected?.(answer.id)} - > - {children} - -); +}) => { + // Build comprehensive aria-label + let ariaLabel = `${_t("poll|options_label", { number: optionNumber })}, ${answer.text}`; + + if (displayVoteCount) { + const votesText = _t("timeline|m.poll|count_of_votes", { count: voteCount }); + if (isWinner) { + ariaLabel += `, ${_t("poll|winning_option_label")}, ${votesText}`; + } else { + ariaLabel += `, ${votesText}`; + } + } + + if (isChecked) { + ariaLabel += `, ${_t("poll|you_voted_for_this")}`; + } + + return ( + onOptionSelected?.(answer.id)} + > + + + ); +}; export const PollOption: React.FC = ({ pollId, answer, voteCount, totalVoteCount, + optionNumber, displayVoteCount, isEnded, isChecked, @@ -92,13 +104,17 @@ export const PollOption: React.FC = ({ }); const isWinner = isEnded && isChecked; const answerPercent = totalVoteCount === 0 ? 0 : Math.round((100.0 * voteCount) / totalVoteCount); - const PollOptionWrapper = isEnded ? EndedPollOption : ActivePollOption; return (
onOptionSelected?.(answer.id)}> - = ({ voteCount={voteCount} displayVoteCount={displayVoteCount} /> - +
diff --git a/src/components/views/polls/pollHistory/PollListItemEnded.tsx b/src/components/views/polls/pollHistory/PollListItemEnded.tsx index 8e2ed22127c..ed7f7fffe21 100644 --- a/src/components/views/polls/pollHistory/PollListItemEnded.tsx +++ b/src/components/views/polls/pollHistory/PollListItemEnded.tsx @@ -100,13 +100,14 @@ export const PollListItemEnded: React.FC = ({ event, poll, onClick }) =>
{!!winningAnswers?.length && (
- {winningAnswers?.map(({ answer, voteCount }) => ( + {winningAnswers?.map(({ answer, voteCount }, index) => ( a.id === answer.id) + 1} displayVoteCount isChecked isEnded diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 86f9197e927..cdde21a52d6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1760,6 +1760,7 @@ "end_message": "The poll has ended. Top answer: %(topAnswer)s", "end_message_no_votes": "The poll has ended. No votes were cast.", "end_title": "End Poll", + "ended_poll_label": "Poll ended", "error_ending_description": "Sorry, the poll did not end. Please try again.", "error_ending_title": "Failed to end poll", "error_voting_description": "Sorry, your vote was not registered. Please try again.", @@ -1771,25 +1772,19 @@ "options_heading": "Create options", "options_label": "Option %(number)s", "options_placeholder": "Write an option", + "poll_label": "Poll", "topic_heading": "What is your poll question or topic?", "topic_label": "Question or topic", "topic_placeholder": "Write something…", "total_decryption_errors": "Due to decryption errors, some votes may not be counted", - "total_n_votes": { - "one": "%(count)s vote cast. Vote to see the results", - "other": "%(count)s votes cast. Vote to see the results" - }, - "total_n_votes_voted": { - "one": "Based on %(count)s vote", - "other": "Based on %(count)s votes" - }, - "total_no_votes": "No votes cast", - "total_not_ended": "Results will be visible when the poll is ended", + "total_votes_label": "Total votes: %(count)s", "type_closed": "Closed poll", "type_heading": "Poll type", "type_open": "Open poll", "unable_edit_description": "Sorry, you can't edit a poll after votes have been cast.", - "unable_edit_title": "Can't edit poll" + "unable_edit_title": "Can't edit poll", + "winning_option_label": "this option won the vote", + "you_voted_for_this": "you voted for this option" }, "power_level": { "admin": "Admin", @@ -3524,7 +3519,6 @@ } }, "m.poll.end": { - "ended": "Ended a poll", "sender_ended": "%(senderName)s has ended a poll" }, "m.poll.start": "%(senderName)s has started a poll - %(pollQuestion)s", From 056fbfd69ae15d7fc839389abf5c019d8b29f36f Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 14 Nov 2025 16:15:26 +0000 Subject: [PATCH 03/15] Update jest tests --- .../views/messages/MPollBody-test.tsx | 68 +- .../__snapshots__/MPollBody-test.tsx.snap | 1234 ++++++++++++----- 2 files changed, 920 insertions(+), 382 deletions(-) diff --git a/test/unit-tests/components/views/messages/MPollBody-test.tsx b/test/unit-tests/components/views/messages/MPollBody-test.tsx index 52b015bfd03..35eaea0e758 100644 --- a/test/unit-tests/components/views/messages/MPollBody-test.tsx +++ b/test/unit-tests/components/views/messages/MPollBody-test.tsx @@ -83,7 +83,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); expect(votesCount(renderResult, "wings")).toBe(""); - await waitFor(() => expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("No votes cast")); + await waitFor(() => expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 0")); expect(renderResult.getByText("What should we order for the party?")).toBeTruthy(); }); @@ -99,7 +99,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 4"); }); it("ignores end poll events from unauthorised users", async () => { @@ -118,7 +118,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 4"); }); it("hides scores if I have not voted", async () => { @@ -133,7 +133,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); expect(votesCount(renderResult, "wings")).toBe(""); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("4 votes cast. Vote to see the results"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 4"); }); it("hides a single vote if I have not voted", async () => { @@ -143,7 +143,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); expect(votesCount(renderResult, "wings")).toBe(""); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("1 vote cast. Vote to see the results"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 1"); }); it("takes someone's most recent vote if they voted several times", async () => { @@ -159,7 +159,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 2"); }); it("uses my local vote", async () => { @@ -180,7 +180,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("1 vote"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 4"); }); it("overrides my other votes with my local vote", async () => { @@ -202,7 +202,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("1 vote"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 2"); // And my vote is highlighted expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(true); @@ -234,7 +234,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 1"); }); it("doesn't cancel my local vote if someone else votes", async () => { @@ -266,7 +266,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 2"); // And my vote is highlighted expect(voteButton(renderResult, "pizza").className.includes(CHECKED)).toBe(true); @@ -293,7 +293,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 2"); }); it("allows un-voting by passing an empty vote", async () => { @@ -307,7 +307,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("1 vote"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 1"); }); it("allows re-voting after un-voting", async () => { @@ -322,7 +322,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("2 votes"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 2"); }); it("treats any invalid answer as a spoiled ballot", async () => { @@ -340,7 +340,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 0"); }); it("allows re-voting after a spoiled ballot", async () => { @@ -357,7 +357,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 1"); }); it("renders nothing if poll has no answers", async () => { @@ -391,7 +391,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "italian")).toBe(""); expect(votesCount(renderResult, "wings")).toBe(""); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Results will be visible when the poll is ended"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 5"); }); it("highlights my vote if the poll is undisclosed", async () => { @@ -425,7 +425,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 5"); }); it("sends a vote event when I choose an option", async () => { @@ -505,11 +505,11 @@ describe("MPollBody", () => { expect(runFindTopAnswer([])).toEqual(""); }); - it("shows non-radio buttons if the poll is ended", async () => { + it("shows disabled radio buttons if the poll is ended", async () => { const events = [newPollEndEvent()]; const { container } = await newMPollBody([], events); - expect(container.querySelector(".mx_StyledRadioButton")).not.toBeInTheDocument(); - expect(container.querySelector('input[type="radio"]')).not.toBeInTheDocument(); + expect(container.querySelector(".mx_StyledRadioButton")).toBeInTheDocument(); + expect(container.querySelector('input[type="radio"][disabled]')).toBeInTheDocument(); }); it("counts votes as normal if the poll is ended", async () => { @@ -526,7 +526,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe('
1 vote'); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
1 vote'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 2"); }); it("counts a single vote as normal if the poll is ended", async () => { @@ -537,7 +537,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe('
1 vote'); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 1"); }); it("shows ended vote counts of different numbers", async () => { @@ -551,13 +551,13 @@ describe("MPollBody", () => { const ends = [newPollEndEvent("@me:example.com", 25)]; const renderResult = await newMPollBody(votes, ends); - expect(renderResult.container.querySelectorAll(".mx_StyledRadioButton")).toHaveLength(0); - expect(renderResult.container.querySelectorAll('input[type="radio"]')).toHaveLength(0); + expect(renderResult.container.querySelectorAll(".mx_StyledRadioButton")).toHaveLength(4); + expect(renderResult.container.querySelectorAll('input[type="radio"][disabled]')).toHaveLength(4); expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes"); expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 5"); }); it("ignores votes that arrived after poll ended", async () => { @@ -577,7 +577,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 5"); }); it("counts votes that arrived after an unauthorised poll end event", async () => { @@ -601,7 +601,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 5"); }); }); @@ -629,7 +629,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Total votes: 5"); }); it("highlights the winning vote in an ended poll", async () => { @@ -646,9 +646,9 @@ describe("MPollBody", () => { expect(endedVoteChecked(renderResult, "wings")).toBe(true); expect(endedVoteChecked(renderResult, "pizza")).toBe(false); - // Double-check by looking for the endedOptionWinner class - expect(endedVoteDiv(renderResult, "wings").className.includes("mx_PollOption_endedOptionWinner")).toBe(true); - expect(endedVoteDiv(renderResult, "pizza").className.includes("mx_PollOption_endedOptionWinner")).toBe(false); + // Double-check by looking for the checked class + expect(endedVoteDiv(renderResult, "wings").className.includes("mx_PollOption_checked")).toBe(true); + expect(endedVoteDiv(renderResult, "pizza").className.includes("mx_PollOption_checked")).toBe(false); }); it("highlights multiple winning votes", async () => { @@ -731,9 +731,7 @@ describe("MPollBody", () => { }); pollEvent.makeReplaced(replacingEvent); const { getByTestId, container } = await newMPollBodyFromEvent(pollEvent, []); - expect(getByTestId("pollQuestion").innerHTML).toEqual( - 'new question (edited)', - ); + expect(getByTestId("pollQuestion").textContent).toEqual("new question (edited)"); const inputs = container.querySelectorAll('input[type="radio"]'); expect(inputs).toHaveLength(3); expect(inputs[0].getAttribute("value")).toEqual("n1"); @@ -951,7 +949,7 @@ function endedVoteChecked({ getByTestId }: RenderResult, value: string): boolean } function endedVoteDiv({ getByTestId }: RenderResult, value: string): Element { - return getByTestId(`pollOption-${value}`).firstElementChild!; + return getByTestId(`pollOption-${value}`); } function endedVotesCount(renderResult: RenderResult, value: string): string { diff --git a/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index cc8c2b3cc63..82a50204ac9 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap @@ -8,6 +8,21 @@ exports[`MPollBody renders a finished poll 1`] = ` + + + + What should we order for the party?
-
+ +
+
+
-
- 0 votes +
+
+ Pizza +
+
+ 0 votes +
+
-
+
+
@@ -49,25 +84,45 @@ exports[`MPollBody renders a finished poll 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-poutine" > -
+ +
+
+
- Poutine -
-
-
+
+
@@ -81,28 +136,48 @@ exports[`MPollBody renders a finished poll 1`] = ` class="mx_PollOption mx_PollOption_checked mx_PollOption_ended" data-testid="pollOption-italian" > -
+ +
+
+
- Italian -
- -
+
+
@@ -116,25 +191,45 @@ exports[`MPollBody renders a finished poll 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-wings" > -
+ +
+
+
- Wings -
-
-
+
+
@@ -149,7 +244,7 @@ exports[`MPollBody renders a finished poll 1`] = ` class="mx_MPollBody_totalVotes" data-testid="totalVotes" > - Final result based on 3 votes + Total votes: 3
@@ -163,6 +258,21 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` + + + + What should we order for the party?
-
+ +
+
+
- Pizza -
- -
+
+
@@ -207,25 +337,45 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-poutine" > -
+ +
+
+
-
- 0 votes +
+
+ Poutine +
+
+ 0 votes +
+
-
+
+
@@ -239,25 +389,45 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-italian" > -
+ +
+
+
- Italian -
-
-
+
+
@@ -271,28 +441,48 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` class="mx_PollOption mx_PollOption_checked mx_PollOption_ended" data-testid="pollOption-wings" > -
+ +
+
+
- Wings -
- -
+
+
@@ -307,7 +497,7 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` class="mx_MPollBody_totalVotes" data-testid="totalVotes" > - Final result based on 4 votes + Total votes: 4
@@ -321,6 +511,21 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` + + + + What should we order for the party?
-
+ +
+
+
- Pizza -
-
-
+
+
@@ -362,25 +587,45 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-poutine" > -
+ +
+
+
-
- 0 votes +
+
+ Poutine +
+
+ 0 votes +
+
-
+
+
@@ -394,25 +639,45 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-italian" > -
+ +
+
+
- Italian -
-
-
+
+
@@ -426,25 +691,45 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` class="mx_PollOption mx_PollOption_ended" data-testid="pollOption-wings" > -
+ +
+
+
- Wings -
-
-
+
+
@@ -459,7 +744,7 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` class="mx_MPollBody_totalVotes" data-testid="totalVotes" > - Final result based on 0 votes + Total votes: 0
@@ -473,6 +758,18 @@ exports[`MPollBody renders a poll that I have not voted in 1`] = ` + + + What should we order for the party?