Poll history - filter by active or ended (#10098)

* wip

* remove dupe

* use poll model relations in all cases

* update mpollbody tests to use poll instance

* update poll fetching login in pinned messages card

* add pinned polls to room polls state

* add spinner while relations are still loading

* handle no poll in end poll dialog

* strict errors

* render a poll body that errors for poll end events

* add fetching logic to pollend tile

* extract poll testing utilities

* test mpollend

* strict fix

* more strict fix

* strict fix for forwardref

* add filter component

* update poll test utils

* add unstyled filter tab group

* filtertabgroup snapshot

* lint

* update test util setupRoomWithPollEvents to allow testing multiple polls in one room

* style filter tabs

* test error message for past polls

* sort polls list by latest

* move FilterTabGroup into generic components

* comments

* Update src/components/views/dialogs/polls/types.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Kerry 2023-02-13 09:19:45 +13:00 committed by GitHub
parent f0f50485d7
commit 18ab325eaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 388 additions and 61 deletions

View file

@ -15,15 +15,17 @@ limitations under the License.
*/
import React from "react";
import { render } from "@testing-library/react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { fireEvent, render } from "@testing-library/react";
import { Room } from "matrix-js-sdk/src/matrix";
import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/polls/PollHistoryDialog";
import {
getMockClientWithEventEmitter,
makePollEndEvent,
makePollStartEvent,
mockClientMethodsUser,
mockIntlDateTimeFormat,
setupRoomWithPollEvents,
unmockIntlDateTimeFormat,
} from "../../../../test-utils";
@ -33,6 +35,8 @@ describe("<PollHistoryDialog />", () => {
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
relations: jest.fn(),
decryptEventIfNeeded: jest.fn(),
});
const room = new Room(roomId, mockClient, userId);
@ -49,6 +53,7 @@ describe("<PollHistoryDialog />", () => {
beforeEach(() => {
mockClient.getRoom.mockReturnValue(room);
mockClient.relations.mockResolvedValue({ events: [] });
const timeline = room.getLiveTimeline();
jest.spyOn(timeline, "getEvents").mockReturnValue([]);
});
@ -63,24 +68,58 @@ describe("<PollHistoryDialog />", () => {
expect(() => getComponent()).toThrow("Cannot find room");
});
it("renders a no polls message when there are no polls in the timeline", () => {
it("renders a no polls message when there are no active polls in the timeline", () => {
const { getByText } = getComponent();
expect(getByText("There are no polls in this room")).toBeTruthy();
expect(getByText("There are no active polls in this room")).toBeTruthy();
});
it("renders a list of polls when there are polls in the timeline", async () => {
it("renders a no past polls message when there are no past polls in the timeline", () => {
const { getByText } = getComponent();
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 () => {
const timestamp = 1675300825090;
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
const pollStart3 = makePollStartEvent("What?", userId, undefined, { ts: timestamp + 70000, id: "$3" });
const pollEnd3 = makePollEndEvent(pollStart3.getId()!, roomId, userId, timestamp + 1);
await setupRoomWithPollEvents([pollStart2, pollStart3, pollStart1], [], [pollEnd3], mockClient, room);
const { container, queryByText, getByTestId } = getComponent();
expect(getByTestId("filter-tab-PollHistoryDialog_filter-ACTIVE").firstElementChild).toBeChecked();
expect(container).toMatchSnapshot();
// this poll is ended, and default filter is ACTIVE
expect(queryByText("What?")).not.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" });
const pollStart3 = makePollStartEvent("What?", userId, undefined, { ts: 1675200725090, id: "$3" });
const message = new MatrixEvent({
type: "m.room.message",
content: {},
});
const timeline = room.getLiveTimeline();
jest.spyOn(timeline, "getEvents").mockReturnValue([pollStart1, pollStart2, pollStart3, message]);
const { container } = getComponent();
const pollEnd3 = makePollEndEvent(pollStart3.getId()!, roomId, userId, 1675200725090 + 1);
await setupRoomWithPollEvents([pollStart1, pollStart2, pollStart3], [], [pollEnd3], mockClient, room);
expect(container).toMatchSnapshot();
const { getByText, queryByText, getByTestId } = getComponent();
expect(getByText("Question?")).toBeInTheDocument();
expect(getByText("Where?")).toBeInTheDocument();
// this poll is ended, and default filter is ACTIVE
expect(queryByText("What?")).not.toBeInTheDocument();
fireEvent.click(getByText("Past polls"));
expect(getByTestId("filter-tab-PollHistoryDialog_filter-ENDED").firstElementChild).toBeChecked();
// active polls no longer shown
expect(queryByText("Question?")).not.toBeInTheDocument();
expect(queryByText("Where?")).not.toBeInTheDocument();
// this poll is ended
expect(getByText("What?")).toBeInTheDocument();
});
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PollHistoryDialog /> renders a list of polls when there are polls in the timeline 1`] = `
exports[`<PollHistoryDialog /> renders a list of active polls when there are polls in the timeline 1`] = `
<div>
<div
data-focus-guard="true"
@ -35,25 +35,38 @@ exports[`<PollHistoryDialog /> renders a list of polls when there are polls in t
<div
class="mx_PollHistoryList"
>
<fieldset
class="mx_FilterTabGroup"
>
<label
data-testid="filter-tab-PollHistoryDialog_filter-ACTIVE"
>
<input
checked=""
name="PollHistoryDialog_filter"
type="radio"
value="ACTIVE"
/>
<span>
Active polls
</span>
</label>
<label
data-testid="filter-tab-PollHistoryDialog_filter-ENDED"
>
<input
name="PollHistoryDialog_filter"
type="radio"
value="ENDED"
/>
<span>
Past polls
</span>
</label>
</fieldset>
<ol
class="mx_PollHistoryList_list"
>
<li
class="mx_PollListItem"
data-testid="pollListItem-$1"
>
<span>
02/02/23
</span>
<div
class="mx_PollListItem_icon"
/>
<span
class="mx_PollListItem_question"
>
Question?
</span>
</li>
<li
class="mx_PollListItem"
data-testid="pollListItem-$2"
@ -72,10 +85,10 @@ exports[`<PollHistoryDialog /> renders a list of polls when there are polls in t
</li>
<li
class="mx_PollListItem"
data-testid="pollListItem-$3"
data-testid="pollListItem-$1"
>
<span>
31/01/23
02/02/23
</span>
<div
class="mx_PollListItem_icon"
@ -83,7 +96,7 @@ exports[`<PollHistoryDialog /> renders a list of polls when there are polls in t
<span
class="mx_PollListItem_question"
>
What?
Question?
</span>
</li>
</ol>

View file

@ -0,0 +1,54 @@
/*
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 { fireEvent, render } from "@testing-library/react";
import { FilterTabGroup } from "../../../../src/components/views/elements/FilterTabGroup";
describe("<FilterTabGroup />", () => {
enum TestOption {
Apple = "Apple",
Banana = "Banana",
Orange = "Orange",
}
const defaultProps = {
"name": "test",
"value": TestOption.Apple,
"onFilterChange": jest.fn(),
"tabs": [
{ id: TestOption.Apple, label: `Label for ${TestOption.Apple}` },
{ id: TestOption.Banana, label: `Label for ${TestOption.Banana}` },
{ id: TestOption.Orange, label: `Label for ${TestOption.Orange}` },
],
"data-testid": "test",
};
const getComponent = (props = {}) => <FilterTabGroup<TestOption> {...defaultProps} {...props} />;
it("renders options", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("calls onChange handler on selection", () => {
const onFilterChange = jest.fn();
const { getByText } = render(getComponent({ onFilterChange }));
fireEvent.click(getByText("Label for Banana"));
expect(onFilterChange).toHaveBeenCalledWith(TestOption.Banana);
});
});

View file

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FilterTabGroup /> renders options 1`] = `
<div>
<fieldset
class="mx_FilterTabGroup"
data-testid="test"
>
<label
data-testid="filter-tab-test-Apple"
>
<input
checked=""
name="test"
type="radio"
value="Apple"
/>
<span>
Label for Apple
</span>
</label>
<label
data-testid="filter-tab-test-Banana"
>
<input
name="test"
type="radio"
value="Banana"
/>
<span>
Label for Banana
</span>
</label>
<label
data-testid="filter-tab-test-Orange"
>
<input
name="test"
type="radio"
value="Orange"
/>
<span>
Label for Orange
</span>
</label>
</fieldset>
</div>
`;

View file

@ -227,7 +227,7 @@ describe("MPollBody", () => {
content: newPollStart(undefined, undefined, true),
});
const props = getMPollBodyPropsFromEvent(mxEvent);
const room = await setupRoomWithPollEvents(mxEvent, votes, [], mockClient);
const room = await setupRoomWithPollEvents([mxEvent], votes, [], mockClient);
const renderResult = renderMPollBodyWithWrapper(props);
// wait for /relations promise to resolve
await flushPromises();
@ -255,7 +255,7 @@ describe("MPollBody", () => {
content: newPollStart(undefined, undefined, true),
});
const props = getMPollBodyPropsFromEvent(mxEvent);
const room = await setupRoomWithPollEvents(mxEvent, votes, [], mockClient);
const room = await setupRoomWithPollEvents([mxEvent], votes, [], mockClient);
const renderResult = renderMPollBodyWithWrapper(props);
// wait for /relations promise to resolve
await flushPromises();
@ -700,7 +700,7 @@ describe("MPollBody", () => {
});
const ends = [newPollEndEvent("@me:example.com", 25)];
await setupRoomWithPollEvents(pollEvent, [], ends, mockClient);
await setupRoomWithPollEvents([pollEvent], [], ends, mockClient);
const poll = mockClient.getRoom(pollEvent.getRoomId()!)!.polls.get(pollEvent.getId()!)!;
// start fetching, dont await
poll.getResponses();
@ -920,7 +920,7 @@ async function newMPollBodyFromEvent(
): Promise<RenderResult> {
const props = getMPollBodyPropsFromEvent(mxEvent);
await setupRoomWithPollEvents(mxEvent, relationEvents, endEvents, mockClient);
await setupRoomWithPollEvents([mxEvent], relationEvents, endEvents, mockClient);
return renderMPollBodyWithWrapper(props);
}
@ -1036,7 +1036,7 @@ async function runIsPollEnded(ends: MatrixEvent[]) {
content: newPollStart(),
});
await setupRoomWithPollEvents(pollEvent, [], ends, mockClient);
await setupRoomWithPollEvents([pollEvent], [], ends, mockClient);
return isPollEnded(pollEvent, mockClient);
}

View file

@ -50,7 +50,7 @@ describe("<MPollEndBody />", () => {
const setupRoomWithEventsTimeline = async (pollEnd: MatrixEvent, pollStart?: MatrixEvent): Promise<Room> => {
if (pollStart) {
await setupRoomWithPollEvents(pollStart, [], [pollEnd], mockClient);
await setupRoomWithPollEvents([pollStart], [], [pollEnd], mockClient);
}
const room = mockClient.getRoom(roomId) || new Room(roomId, mockClient, userId);

View file

@ -89,13 +89,14 @@ export const makePollEndEvent = (pollStartEventId: string, roomId: string, sende
* @returns
*/
export const setupRoomWithPollEvents = async (
mxEvent: MatrixEvent,
pollStartEvents: MatrixEvent[],
relationEvents: Array<MatrixEvent>,
endEvents: Array<MatrixEvent> = [],
mockClient: Mocked<MatrixClient>,
existingRoom?: Room,
): Promise<Room> => {
const room = new Room(mxEvent.getRoomId()!, mockClient, mockClient.getSafeUserId());
room.processPollEvents([mxEvent, ...relationEvents, ...endEvents]);
const room = existingRoom || new Room(pollStartEvents[0].getRoomId()!, mockClient, mockClient.getSafeUserId());
room.processPollEvents([...pollStartEvents, ...relationEvents, ...endEvents]);
// set redaction allowed for current user only
// poll end events are validated against this
@ -106,8 +107,10 @@ export const setupRoomWithPollEvents = async (
// wait for events to process on room
await flushPromises();
mockClient.getRoom.mockReturnValue(room);
mockClient.relations.mockResolvedValue({
events: [...relationEvents, ...endEvents],
mockClient.relations.mockImplementation(async (_roomId: string, eventId: string) => {
return {
events: [...relationEvents, ...endEvents].filter((event) => event.getRelation()?.event_id === eventId),
};
});
return room;
};