Poll history: fetch last 30 days of polls (#10157)

* use timeline pagination

* fetch last 30 days of poll history

* add comments, tidy

* more comments

* finish comment

* wait for responses to resolve before displaying in list

* dont use state for list

* return unsubscribe

* strict fixes

* unnecessary event type in filter

* add catch
This commit is contained in:
Kerry 2023-02-21 09:07:57 +13:00 committed by GitHub
parent 3fafa4b58d
commit d66248c17c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 432 additions and 21 deletions

View file

@ -46,3 +46,14 @@ limitations under the License.
justify-content: center; justify-content: center;
color: $secondary-content; color: $secondary-content;
} }
.mx_PollHistoryList_loading {
color: $secondary-content;
text-align: center;
// center in all free space
// when there are no results
&.mx_PollHistoryList_noResultsYet {
margin: auto auto;
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog";
import { IDialogProps } from "../IDialogProps"; import { IDialogProps } from "../IDialogProps";
import { PollHistoryList } from "./PollHistoryList"; import { PollHistoryList } from "./PollHistoryList";
import { PollHistoryFilter } from "./types"; import { PollHistoryFilter } from "./types";
import { usePolls } from "./usePollHistory"; import { usePollsWithRelations } from "./usePollHistory";
import { useFetchPastPolls } from "./fetchPastPolls";
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & { type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
roomId: string; roomId: string;
@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri
const filterPolls = const filterPolls =
(filter: PollHistoryFilter) => (filter: PollHistoryFilter) =>
(poll: Poll): boolean => (poll: Poll): boolean =>
(filter === "ACTIVE") !== poll.isEnded; // exclude polls while they are still loading
// to avoid jitter in list
!poll.isFetchingResponses && (filter === "ACTIVE") !== poll.isEnded;
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => { const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
return [...polls.values()] return [...polls.values()]
.filter(filterPolls(filter)) .filter(filterPolls(filter))
@ -43,19 +47,20 @@ const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter)
}; };
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => { export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => {
const { polls } = usePolls(roomId, matrixClient); const room = matrixClient.getRoom(roomId)!;
const { isLoading } = useFetchPastPolls(room, matrixClient);
const { polls } = usePollsWithRelations(roomId, matrixClient);
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE"); const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
useEffect(() => { const pollStartEvents = filterAndSortPolls(polls, filter);
setPollStartEvents(filterAndSortPolls(polls, filter)); const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
}, [filter, polls]);
return ( return (
<BaseDialog title={_t("Polls history")} onFinished={onFinished}> <BaseDialog title={_t("Polls history")} onFinished={onFinished}>
<div className="mx_PollHistoryDialog_content"> <div className="mx_PollHistoryDialog_content">
<PollHistoryList <PollHistoryList
pollStartEvents={pollStartEvents} pollStartEvents={pollStartEvents}
isLoading={isLoading || isLoadingPollResponses}
polls={polls} polls={polls}
filter={filter} filter={filter}
onFilterChange={setFilter} onFilterChange={setFilter}

View file

@ -19,18 +19,37 @@ import classNames from "classnames";
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { FilterTabGroup } from "../../elements/FilterTabGroup";
import InlineSpinner from "../../elements/InlineSpinner";
import { PollHistoryFilter } from "./types"; import { PollHistoryFilter } from "./types";
import { PollListItem } from "./PollListItem"; import { PollListItem } from "./PollListItem";
import { PollListItemEnded } from "./PollListItemEnded"; import { PollListItemEnded } from "./PollListItemEnded";
import { FilterTabGroup } from "../../elements/FilterTabGroup";
const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => (
<div
className={classNames("mx_PollHistoryList_loading", {
mx_PollHistoryList_noResultsYet: noResultsYet,
})}
>
<InlineSpinner />
{_t("Loading polls")}
</div>
);
type PollHistoryListProps = { type PollHistoryListProps = {
pollStartEvents: MatrixEvent[]; pollStartEvents: MatrixEvent[];
polls: Map<string, Poll>; polls: Map<string, Poll>;
filter: PollHistoryFilter; filter: PollHistoryFilter;
onFilterChange: (filter: PollHistoryFilter) => void; onFilterChange: (filter: PollHistoryFilter) => void;
isLoading?: boolean;
}; };
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, polls, filter, onFilterChange }) => { export const PollHistoryList: React.FC<PollHistoryListProps> = ({
pollStartEvents,
polls,
filter,
isLoading,
onFilterChange,
}) => {
return ( return (
<div className="mx_PollHistoryList"> <div className="mx_PollHistoryList">
<FilterTabGroup<PollHistoryFilter> <FilterTabGroup<PollHistoryFilter>
@ -42,7 +61,7 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
{ id: "ENDED", label: "Past polls" }, { id: "ENDED", label: "Past polls" },
]} ]}
/> />
{!!pollStartEvents.length ? ( {!!pollStartEvents.length && (
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}> <ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
{pollStartEvents.map((pollStartEvent) => {pollStartEvents.map((pollStartEvent) =>
filter === "ACTIVE" ? ( filter === "ACTIVE" ? (
@ -55,14 +74,17 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
/> />
), ),
)} )}
{isLoading && <LoadingPolls />}
</ol> </ol>
) : ( )}
{!pollStartEvents.length && !isLoading && (
<span className="mx_PollHistoryList_noResults"> <span className="mx_PollHistoryList_noResults">
{filter === "ACTIVE" {filter === "ACTIVE"
? _t("There are no active polls in this room") ? _t("There are no active polls in this room")
: _t("There are no past polls in this room")} : _t("There are no past polls in this room")}
</span> </span>
)} )}
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
</div> </div>
); );
}; };

View file

@ -0,0 +1,129 @@
/*
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 { useEffect, useState } from "react";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix";
import { Filter, IFilterDefinition } from "matrix-js-sdk/src/filter";
import { logger } from "matrix-js-sdk/src/logger";
/**
* Page timeline backwards until either:
* - event older than endOfHistoryPeriodTimestamp is encountered
* - end of timeline is reached
* @param timelineSet - timelineset to page
* @param matrixClient - client
* @param endOfHistoryPeriodTimestamp - epoch timestamp to fetch until
* @returns void
*/
const pagePolls = async (
timelineSet: EventTimelineSet,
matrixClient: MatrixClient,
endOfHistoryPeriodTimestamp: number,
): Promise<void> => {
const liveTimeline = timelineSet.getLiveTimeline();
const events = liveTimeline.getEvents();
const oldestEventTimestamp = events[0]?.getTs() || Date.now();
const hasMorePages = !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS);
if (!hasMorePages || oldestEventTimestamp <= endOfHistoryPeriodTimestamp) {
return;
}
await matrixClient.paginateEventTimeline(liveTimeline, {
backwards: true,
});
return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
};
const ONE_DAY_MS = 60000 * 60 * 24;
/**
* Fetches timeline history for given number of days in past
* @param timelineSet - timelineset to page
* @param matrixClient - client
* @param historyPeriodDays - number of days of history to fetch, from current day
* @returns isLoading - true while fetching history
*/
const useTimelineHistory = (
timelineSet: EventTimelineSet | null,
matrixClient: MatrixClient,
historyPeriodDays: number,
): { isLoading: boolean } => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!timelineSet) {
return;
}
const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays;
const doFetchHistory = async (): Promise<void> => {
setIsLoading(true);
try {
await pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
} catch (error) {
logger.error("Failed to fetch room polls history", error);
} finally {
setIsLoading(false);
}
};
doFetchHistory();
}, [timelineSet, historyPeriodDays, matrixClient]);
return { isLoading };
};
const filterDefinition: IFilterDefinition = {
room: {
timeline: {
types: [M_POLL_START.name, M_POLL_START.altName],
},
},
};
/**
* Fetch poll start events in the last N days of room history
* @param room - room to fetch history for
* @param matrixClient - client
* @param historyPeriodDays - number of days of history to fetch, from current day
* @returns isLoading - true while fetching history
*/
export const useFetchPastPolls = (
room: Room,
matrixClient: MatrixClient,
historyPeriodDays = 30,
): { isLoading: boolean } => {
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
useEffect(() => {
const filter = new Filter(matrixClient.getSafeUserId());
filter.setDefinition(filterDefinition);
const getFilteredTimelineSet = async (): Promise<void> => {
const filterId = await matrixClient.getOrCreateFilter(`POLL_HISTORY_FILTER_${room.roomId}}`, filter);
filter.filterId = filterId;
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
setTimelineSet(timelineSet);
};
getFilteredTimelineSet();
}, [room, matrixClient]);
const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays);
return { isLoading };
};

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useEffect, useState } from "react";
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix"; import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
@ -21,6 +22,7 @@ import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
/** /**
* Get poll instances from a room * Get poll instances from a room
* Updates to include new polls
* @param roomId - id of room to retrieve polls for * @param roomId - id of room to retrieve polls for
* @param matrixClient - client * @param matrixClient - client
* @returns {Map<string, Poll>} - Map of Poll instances * @returns {Map<string, Poll>} - Map of Poll instances
@ -37,9 +39,58 @@ export const usePolls = (
throw new Error("Cannot find room"); throw new Error("Cannot find room");
} }
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls); // copy room.polls map so changes can be detected
const polls = useEventEmitterState(room, PollEvent.New, () => new Map<string, Poll>(room.polls));
// @TODO(kerrya) watch polls for end events, trigger refiltering
return { polls }; return { polls };
}; };
/**
* Get all poll instances from a room
* Fetch their responses (using cached poll responses)
* Updates on:
* - new polls added to room
* - new responses added to polls
* - changes to poll ended state
* @param roomId - id of room to retrieve polls for
* @param matrixClient - client
* @returns {Map<string, Poll>} - Map of Poll instances
*/
export const usePollsWithRelations = (
roomId: string,
matrixClient: MatrixClient,
): {
polls: Map<string, Poll>;
} => {
const { polls } = usePolls(roomId, matrixClient);
const [pollsWithRelations, setPollsWithRelations] = useState<Map<string, Poll>>(polls);
useEffect(() => {
const onPollUpdate = async (): Promise<void> => {
// trigger rerender by creating a new poll map
setPollsWithRelations(new Map(polls));
};
if (polls) {
for (const poll of polls.values()) {
// listen to changes in responses and end state
poll.on(PollEvent.End, onPollUpdate);
poll.on(PollEvent.Responses, onPollUpdate);
// trigger request to get all responses
// if they are not already in cache
poll.getResponses();
}
setPollsWithRelations(polls);
}
// unsubscribe
return () => {
if (polls) {
for (const poll of polls.values()) {
poll.off(PollEvent.End, onPollUpdate);
poll.off(PollEvent.Responses, onPollUpdate);
}
}
};
}, [polls, setPollsWithRelations]);
return { polls: pollsWithRelations };
};

View file

@ -3131,6 +3131,7 @@
"Not a valid Security Key": "Not a valid Security Key", "Not a valid Security Key": "Not a valid Security Key",
"Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.", "Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.",
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "If you've forgotten your Security Key you can <button>set up new recovery options</button>", "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "If you've forgotten your Security Key you can <button>set up new recovery options</button>",
"Loading polls": "Loading polls",
"There are no active polls in this room": "There are no active polls in this room", "There are no active polls in this room": "There are no active polls in this room",
"There are no past polls in this room": "There are no past polls in this room", "There are no past polls in this room": "There are no past polls in this room",
"Send custom account data event": "Send custom account data event", "Send custom account data event": "Send custom account data event",

View file

@ -16,10 +16,13 @@ limitations under the License.
import React from "react"; import React from "react";
import { fireEvent, render } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import { Room } from "matrix-js-sdk/src/matrix"; import { Filter } from "matrix-js-sdk/src/filter";
import { EventTimeline, Room } from "matrix-js-sdk/src/matrix";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/polls/PollHistoryDialog"; import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/polls/PollHistoryDialog";
import { import {
flushPromises,
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
makePollEndEvent, makePollEndEvent,
makePollStartEvent, makePollStartEvent,
@ -30,6 +33,8 @@ import {
} from "../../../../test-utils"; } from "../../../../test-utils";
describe("<PollHistoryDialog />", () => { describe("<PollHistoryDialog />", () => {
// 14.03.2022 16:15
const now = 1647270879403;
const userId = "@alice:domain.org"; const userId = "@alice:domain.org";
const roomId = "!room:domain.org"; const roomId = "!room:domain.org";
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
@ -37,8 +42,19 @@ describe("<PollHistoryDialog />", () => {
getRoom: jest.fn(), getRoom: jest.fn(),
relations: jest.fn(), relations: jest.fn(),
decryptEventIfNeeded: jest.fn(), decryptEventIfNeeded: jest.fn(),
getOrCreateFilter: jest.fn(),
paginateEventTimeline: jest.fn(),
});
let room = new Room(roomId, mockClient, userId);
const expectedFilter = new Filter(userId);
expectedFilter.setDefinition({
room: {
timeline: {
types: [M_POLL_START.name, M_POLL_START.altName],
},
},
}); });
const room = new Room(roomId, mockClient, userId);
const defaultProps = { const defaultProps = {
roomId, roomId,
@ -52,10 +68,16 @@ describe("<PollHistoryDialog />", () => {
}); });
beforeEach(() => { beforeEach(() => {
room = new Room(roomId, mockClient, userId);
mockClient.getRoom.mockReturnValue(room); mockClient.getRoom.mockReturnValue(room);
mockClient.relations.mockResolvedValue({ events: [] }); mockClient.relations.mockResolvedValue({ events: [] });
const timeline = room.getLiveTimeline(); const timeline = room.getLiveTimeline();
jest.spyOn(timeline, "getEvents").mockReturnValue([]); jest.spyOn(timeline, "getEvents").mockReturnValue([]);
jest.spyOn(room, "getOrCreateFilteredTimelineSet");
mockClient.getOrCreateFilter.mockResolvedValue(expectedFilter.filterId!);
mockClient.paginateEventTimeline.mockReset().mockResolvedValue(false);
jest.spyOn(Date, "now").mockReturnValue(now);
}); });
afterAll(() => { afterAll(() => {
@ -68,21 +90,161 @@ describe("<PollHistoryDialog />", () => {
expect(() => getComponent()).toThrow("Cannot find room"); expect(() => getComponent()).toThrow("Cannot find room");
}); });
it("renders a no polls message when there are no active polls in the timeline", () => { it("renders a loading message while poll history is fetched", async () => {
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
const liveTimeline = timelineSet.getLiveTimeline();
jest.spyOn(liveTimeline, "getPaginationToken").mockReturnValueOnce("test-pagination-token");
const { queryByText, getByText } = getComponent();
expect(mockClient.getOrCreateFilter).toHaveBeenCalledWith(
`POLL_HISTORY_FILTER_${room.roomId}}`,
expectedFilter,
);
// no results not shown until loading finished
expect(queryByText("There are no active polls in this room")).not.toBeInTheDocument();
expect(getByText("Loading polls")).toBeInTheDocument();
// flush filter creation request
await flushPromises();
expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS);
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true });
// only one page
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
// finished loading
expect(queryByText("Loading polls")).not.toBeInTheDocument();
expect(getByText("There are no active polls in this room")).toBeInTheDocument();
});
it("fetches poll history until end of timeline is reached while within time limit", async () => {
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
const liveTimeline = timelineSet.getLiveTimeline();
// mock three pages of timeline history
jest.spyOn(liveTimeline, "getPaginationToken")
.mockReturnValueOnce("test-pagination-token-1")
.mockReturnValueOnce("test-pagination-token-2")
.mockReturnValueOnce("test-pagination-token-3");
const { queryByText, getByText } = getComponent();
expect(mockClient.getOrCreateFilter).toHaveBeenCalledWith(
`POLL_HISTORY_FILTER_${room.roomId}}`,
expectedFilter,
);
// flush filter creation request
await flushPromises();
// once per page
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
// finished loading
expect(queryByText("Loading polls")).not.toBeInTheDocument();
expect(getByText("There are no active polls in this room")).toBeInTheDocument();
});
it("fetches poll history until event older than history period is reached", async () => {
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
const liveTimeline = timelineSet.getLiveTimeline();
const thirtyOneDaysAgoTs = now - 60000 * 60 * 24 * 31;
jest.spyOn(liveTimeline, "getEvents")
.mockReturnValueOnce([])
.mockReturnValueOnce([makePollStartEvent("Question?", userId, undefined, { ts: thirtyOneDaysAgoTs })]);
// mock three pages of timeline history
jest.spyOn(liveTimeline, "getPaginationToken")
.mockReturnValueOnce("test-pagination-token-1")
.mockReturnValueOnce("test-pagination-token-2")
.mockReturnValueOnce("test-pagination-token-3");
getComponent();
// flush filter creation request
await flushPromises();
// after first fetch the time limit is reached
// stop paging
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
});
it("displays loader and list while paging timeline", async () => {
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
const liveTimeline = timelineSet.getLiveTimeline();
const tenDaysAgoTs = now - 60000 * 60 * 24 * 10;
jest.spyOn(liveTimeline, "getEvents").mockReset().mockReturnValue([]);
// mock three pages of timeline history
jest.spyOn(liveTimeline, "getPaginationToken")
.mockReturnValueOnce("test-pagination-token-1")
.mockReturnValueOnce("test-pagination-token-2")
.mockReturnValueOnce("test-pagination-token-3");
// reference to pagination resolve, so we can assert between pages
let resolvePagination1: (value: boolean) => void | undefined;
let resolvePagination2: (value: boolean) => void | undefined;
mockClient.paginateEventTimeline
.mockImplementationOnce(async (_p) => {
const pollStart = makePollStartEvent("Question?", userId, undefined, { ts: now, id: "1" });
jest.spyOn(liveTimeline, "getEvents").mockReturnValue([pollStart]);
room.processPollEvents([pollStart]);
return new Promise((resolve) => (resolvePagination1 = resolve));
})
.mockImplementationOnce(async (_p) => {
const pollStart = makePollStartEvent("Older question?", userId, undefined, {
ts: tenDaysAgoTs,
id: "2",
});
jest.spyOn(liveTimeline, "getEvents").mockReturnValue([pollStart]);
room.processPollEvents([pollStart]);
return new Promise((resolve) => (resolvePagination2 = resolve));
});
const { getByText, queryByText } = getComponent();
await flushPromises();
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
resolvePagination1!(true);
await flushPromises();
// first page has results, display immediately
expect(getByText("Question?")).toBeInTheDocument();
// but we are still fetching history, diaply loader
expect(getByText("Loading polls")).toBeInTheDocument();
resolvePagination2!(true);
await flushPromises();
// additional results addeds
expect(getByText("Older question?")).toBeInTheDocument();
expect(getByText("Question?")).toBeInTheDocument();
// finished paging
expect(queryByText("Loading polls")).not.toBeInTheDocument();
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
});
it("renders a no polls message when there are no active polls in the room", async () => {
const { getByText } = getComponent(); const { getByText } = getComponent();
await flushPromises();
expect(getByText("There are no active polls in this room")).toBeTruthy(); expect(getByText("There are no active polls in this room")).toBeTruthy();
}); });
it("renders a no past polls message when there are no past polls in the timeline", () => { it("renders a no past polls message when there are no past polls in the room", async () => {
const { getByText } = getComponent(); const { getByText } = getComponent();
await flushPromises();
fireEvent.click(getByText("Past polls")); fireEvent.click(getByText("Past polls"));
expect(getByText("There are no past polls in this room")).toBeTruthy(); expect(getByText("There are no past polls in this room")).toBeTruthy();
}); });
it("renders a list of active polls when there are polls in the timeline", async () => { it("renders a list of active polls when there are polls in the room", async () => {
const timestamp = 1675300825090; const timestamp = 1675300825090;
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" }); const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" }); const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
@ -92,6 +254,9 @@ describe("<PollHistoryDialog />", () => {
const { container, queryByText, getByTestId } = getComponent(); const { container, queryByText, getByTestId } = getComponent();
// flush relations calls for polls
await flushPromises();
expect(getByTestId("filter-tab-PollHistoryDialog_filter-ACTIVE").firstElementChild).toBeChecked(); expect(getByTestId("filter-tab-PollHistoryDialog_filter-ACTIVE").firstElementChild).toBeChecked();
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
@ -99,6 +264,32 @@ describe("<PollHistoryDialog />", () => {
expect(queryByText("What?")).not.toBeInTheDocument(); expect(queryByText("What?")).not.toBeInTheDocument();
}); });
it("updates when new polls are added to the room", async () => {
const timestamp = 1675300825090;
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
// initially room has only one poll
await setupRoomWithPollEvents([pollStart1], [], [], mockClient, room);
const { getByText } = getComponent();
// wait for relations
await flushPromises();
expect(getByText("Question?")).toBeInTheDocument();
// add another poll
// paged history requests using cli.paginateEventTimeline
// call this with new events
await room.processPollEvents([pollStart2]);
// await relations for new poll
await flushPromises();
expect(getByText("Question?")).toBeInTheDocument();
// list updated to include new poll
expect(getByText("Where?")).toBeInTheDocument();
});
it("filters ended polls", async () => { it("filters ended polls", async () => {
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: 1675300825090, id: "$1" }); const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: 1675300825090, id: "$1" });
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: 1675300725090, id: "$2" }); const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: 1675300725090, id: "$2" });
@ -107,6 +298,7 @@ describe("<PollHistoryDialog />", () => {
await setupRoomWithPollEvents([pollStart1, pollStart2, pollStart3], [], [pollEnd3], mockClient, room); await setupRoomWithPollEvents([pollStart1, pollStart2, pollStart3], [], [pollEnd3], mockClient, room);
const { getByText, queryByText, getByTestId } = getComponent(); const { getByText, queryByText, getByTestId } = getComponent();
await flushPromises();
expect(getByText("Question?")).toBeInTheDocument(); expect(getByText("Question?")).toBeInTheDocument();
expect(getByText("Where?")).toBeInTheDocument(); expect(getByText("Where?")).toBeInTheDocument();

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PollHistoryDialog /> renders a list of active polls when there are polls in the timeline 1`] = ` exports[`<PollHistoryDialog /> renders a list of active polls when there are polls in the room 1`] = `
<div> <div>
<div <div
data-focus-guard="true" data-focus-guard="true"