diff --git a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png index 2f799712b68..544bbda2640 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png and b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index 8280d305f97..134f8fbf81a 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index 8ffe05ad882..8cc5a4896ce 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ 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/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss index 42ec7c8dac6..1611d703249 100644 --- a/res/css/components/views/polls/_PollOption.pcss +++ b/res/css/components/views/polls/_PollOption.pcss @@ -50,11 +50,9 @@ Please see LICENSE files in the repository root for full details. } .mx_PollOption_checked { - border-color: var(--cpd-color-border-interactive-hovered); - .mx_PollOption_popularityBackground { .mx_PollOption_popularityAmount { - background-color: var(--cpd-color-icon-accent-tertiary); + background-color: var(--cpd-color-icon-primary); } } @@ -62,8 +60,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-icon-primary); + background-color: var(--cpd-color-icon-primary); background-image: url("@vector-im/compound-design-tokens/icons/check.svg"); background-size: 12px; background-repeat: no-repeat; @@ -76,6 +74,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 { @@ -94,6 +108,6 @@ Please see LICENSE files in the repository root for full details. width: 0%; height: 8px; border-radius: 8px; - background-color: $quaternary-content; + background-color: var(--cpd-color-icon-primary); } } diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index 09160c083a4..7d0806c00ef 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -6,8 +6,10 @@ 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. */ +$poll-max-width: 550px; + .mx_MPollBody { - margin-top: 8px; + margin-top: var(--cpd-space-2x); min-width: 0; /* Override fieldset default min-width: min-content */ width: 100%; /* Ensure fieldset takes full available width */ border: none; /* Remove default fieldset border */ @@ -18,8 +20,16 @@ Please see LICENSE files in the repository root for full details. font-size: $font-15px; line-height: $font-24px; margin-top: 0; - margin-bottom: 8px; + margin-bottom: var(--cpd-space-2x); letter-spacing: var(--cpd-font-letter-spacing-heading-lg); + display: flex; + align-items: center; + gap: var(--cpd-space-3x); + + svg { + flex-shrink: 0; + color: var(--cpd-color-icon-primary); + } .mx_MPollBody_edited { color: $roomtopic-color; @@ -28,28 +38,13 @@ 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; + max-width: $poll-max-width; .mx_Spinner { flex: 0; @@ -67,5 +62,5 @@ Please see LICENSE files in the repository root for full details. display: grid; gap: $spacing-16; margin-bottom: $spacing-8; - max-width: 550px; + max-width: $poll-max-width; } 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/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 15f2efce4b4..983a4d51478 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"; @@ -324,14 +326,18 @@ export default class MPollBody extends React.Component { ({_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 +352,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/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..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"; @@ -34,7 +33,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 +73,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/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..52dc282a39c 100644 --- a/src/components/views/polls/pollHistory/PollListItemEnded.tsx +++ b/src/components/views/polls/pollHistory/PollListItemEnded.tsx @@ -28,6 +28,7 @@ type EndedPollState = { winningAnswers: { answer: PollAnswerSubevent; voteCount: number; + optionNumber: number; }[]; totalVoteCount: number; }; @@ -44,6 +45,7 @@ const getWinningAnswers = (poll: Poll, responseRelations: Relations): EndedPollS .map((answer) => ({ answer, voteCount: votes.get(answer.id) || 0, + optionNumber: poll.pollEvent.answers.findIndex((a) => a.id === answer.id) + 1, })), }; }; @@ -100,13 +102,14 @@ export const PollListItemEnded: React.FC = ({ event, poll, onClick }) =>
{!!winningAnswers?.length && (
- {winningAnswers?.map(({ answer, voteCount }) => ( + {winningAnswers?.map(({ answer, voteCount, optionNumber }) => ( { 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 () => { @@ -551,8 +551,8 @@ 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"); @@ -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/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__/MPollBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index cc8c2b3cc63..6e106ae5748 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 -
-
-
+
+
@@ -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 -
- -
+
+
@@ -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 -
-
-
+
+
@@ -473,6 +758,18 @@ exports[`MPollBody renders a poll that I have not voted in 1`] = ` + + + What should we order for the party?