mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 10:45:51 +03:00
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:
parent
3fafa4b58d
commit
d66248c17c
8 changed files with 432 additions and 21 deletions
|
@ -46,3 +46,14 @@ limitations under the License.
|
|||
justify-content: center;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
|
@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog";
|
|||
import { IDialogProps } from "../IDialogProps";
|
||||
import { PollHistoryList } from "./PollHistoryList";
|
||||
import { PollHistoryFilter } from "./types";
|
||||
import { usePolls } from "./usePollHistory";
|
||||
import { usePollsWithRelations } from "./usePollHistory";
|
||||
import { useFetchPastPolls } from "./fetchPastPolls";
|
||||
|
||||
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
|
||||
roomId: string;
|
||||
|
@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri
|
|||
const filterPolls =
|
||||
(filter: PollHistoryFilter) =>
|
||||
(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[] => {
|
||||
return [...polls.values()]
|
||||
.filter(filterPolls(filter))
|
||||
|
@ -43,19 +47,20 @@ const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter)
|
|||
};
|
||||
|
||||
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 [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
|
||||
|
||||
useEffect(() => {
|
||||
setPollStartEvents(filterAndSortPolls(polls, filter));
|
||||
}, [filter, polls]);
|
||||
const pollStartEvents = filterAndSortPolls(polls, filter);
|
||||
const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
|
||||
|
||||
return (
|
||||
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
|
||||
<div className="mx_PollHistoryDialog_content">
|
||||
<PollHistoryList
|
||||
pollStartEvents={pollStartEvents}
|
||||
isLoading={isLoading || isLoadingPollResponses}
|
||||
polls={polls}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
|
|
|
@ -19,18 +19,37 @@ import classNames from "classnames";
|
|||
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { FilterTabGroup } from "../../elements/FilterTabGroup";
|
||||
import InlineSpinner from "../../elements/InlineSpinner";
|
||||
import { PollHistoryFilter } from "./types";
|
||||
import { PollListItem } from "./PollListItem";
|
||||
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 = {
|
||||
pollStartEvents: MatrixEvent[];
|
||||
polls: Map<string, Poll>;
|
||||
filter: PollHistoryFilter;
|
||||
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 (
|
||||
<div className="mx_PollHistoryList">
|
||||
<FilterTabGroup<PollHistoryFilter>
|
||||
|
@ -42,7 +61,7 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
|
|||
{ id: "ENDED", label: "Past polls" },
|
||||
]}
|
||||
/>
|
||||
{!!pollStartEvents.length ? (
|
||||
{!!pollStartEvents.length && (
|
||||
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
|
||||
{pollStartEvents.map((pollStartEvent) =>
|
||||
filter === "ACTIVE" ? (
|
||||
|
@ -55,14 +74,17 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
|
|||
/>
|
||||
),
|
||||
)}
|
||||
{isLoading && <LoadingPolls />}
|
||||
</ol>
|
||||
) : (
|
||||
)}
|
||||
{!pollStartEvents.length && !isLoading && (
|
||||
<span className="mx_PollHistoryList_noResults">
|
||||
{filter === "ACTIVE"
|
||||
? _t("There are no active polls in this room")
|
||||
: _t("There are no past polls in this room")}
|
||||
</span>
|
||||
)}
|
||||
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
129
src/components/views/dialogs/polls/fetchPastPolls.ts
Normal file
129
src/components/views/dialogs/polls/fetchPastPolls.ts
Normal 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 };
|
||||
};
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
|
@ -21,6 +22,7 @@ import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
|
|||
|
||||
/**
|
||||
* Get poll instances from a room
|
||||
* Updates to include new polls
|
||||
* @param roomId - id of room to retrieve polls for
|
||||
* @param matrixClient - client
|
||||
* @returns {Map<string, Poll>} - Map of Poll instances
|
||||
|
@ -37,9 +39,58 @@ export const usePolls = (
|
|||
throw new Error("Cannot find room");
|
||||
}
|
||||
|
||||
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls);
|
||||
|
||||
// @TODO(kerrya) watch polls for end events, trigger refiltering
|
||||
// copy room.polls map so changes can be detected
|
||||
const polls = useEventEmitterState(room, PollEvent.New, () => new Map<string, Poll>(room.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 };
|
||||
};
|
||||
|
|
|
@ -3131,6 +3131,7 @@
|
|||
"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.",
|
||||
"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 past polls in this room": "There are no past polls in this room",
|
||||
"Send custom account data event": "Send custom account data event",
|
||||
|
|
|
@ -16,10 +16,13 @@ limitations under the License.
|
|||
|
||||
import React from "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 {
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
makePollEndEvent,
|
||||
makePollStartEvent,
|
||||
|
@ -30,6 +33,8 @@ import {
|
|||
} from "../../../../test-utils";
|
||||
|
||||
describe("<PollHistoryDialog />", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
const userId = "@alice:domain.org";
|
||||
const roomId = "!room:domain.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
|
@ -37,8 +42,19 @@ describe("<PollHistoryDialog />", () => {
|
|||
getRoom: jest.fn(),
|
||||
relations: 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 = {
|
||||
roomId,
|
||||
|
@ -52,10 +68,16 @@ describe("<PollHistoryDialog />", () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room(roomId, mockClient, userId);
|
||||
mockClient.getRoom.mockReturnValue(room);
|
||||
mockClient.relations.mockResolvedValue({ events: [] });
|
||||
const timeline = room.getLiveTimeline();
|
||||
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(() => {
|
||||
|
@ -68,21 +90,161 @@ describe("<PollHistoryDialog />", () => {
|
|||
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();
|
||||
await flushPromises();
|
||||
|
||||
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();
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(getByText("Past polls"));
|
||||
|
||||
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 pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
|
||||
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
|
||||
|
@ -92,6 +254,9 @@ describe("<PollHistoryDialog />", () => {
|
|||
|
||||
const { container, queryByText, getByTestId } = getComponent();
|
||||
|
||||
// flush relations calls for polls
|
||||
await flushPromises();
|
||||
|
||||
expect(getByTestId("filter-tab-PollHistoryDialog_filter-ACTIVE").firstElementChild).toBeChecked();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
|
@ -99,6 +264,32 @@ describe("<PollHistoryDialog />", () => {
|
|||
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 () => {
|
||||
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: 1675300825090, id: "$1" });
|
||||
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: 1675300725090, id: "$2" });
|
||||
|
@ -107,6 +298,7 @@ describe("<PollHistoryDialog />", () => {
|
|||
await setupRoomWithPollEvents([pollStart1, pollStart2, pollStart3], [], [pollEnd3], mockClient, room);
|
||||
|
||||
const { getByText, queryByText, getByTestId } = getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(getByText("Question?")).toBeInTheDocument();
|
||||
expect(getByText("Where?")).toBeInTheDocument();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// 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
|
||||
data-focus-guard="true"
|
||||
|
|
Loading…
Reference in a new issue