/* Copyright 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React from "react"; import { render, waitFor } from "@testing-library/react"; import { EventTimeline, MatrixEvent, Room, M_TEXT } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { IBodyProps } from "../../../../src/components/views/messages/IBodyProps"; import { MPollEndBody } from "../../../../src/components/views/messages/MPollEndBody"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { 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, onHeightChanged: () => {}, 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 } = getComponent(); // while fetching event, only icon is shown expect(container).toMatchSnapshot(); await waitFor(() => expect(getByRole("progressbar")).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(); }); }); });