mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-18 06:35:35 +08:00
2393510a7f
* Fix getRelationsForEvent tsc strictness * Use shared type for GetRelationsForEvent * Fix lint * Add alternative type * getRelationsForEvent is not required * Relations are optional * Reactions are optional * We expect relations in these tests * Add more protection if the eventID is not defined * Allow null too * Better test typing * User ID is not necessary unless something is selected * It's okay to [].includes(null) * Null is as good as undefined here * Null or undefined is good here * We have some expectations for the tests * The room and user can be undefined too * Protec * Reactions are optional * Try match signatures * Null or undefined * More null or undefined * Protec * Fix typo (wrong variable) * Remove optional params See https://github.com/matrix-org/matrix-react-sdk/pull/9558#discussion_r1017515913 * Fix up last maaaaybe relevant lint Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
1328 lines
50 KiB
TypeScript
1328 lines
50 KiB
TypeScript
/*
|
|
Copyright 2021 - 2022 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";
|
|
// eslint-disable-next-line deprecate/import
|
|
import { mount, ReactWrapper } from "enzyme";
|
|
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
|
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
|
|
import {
|
|
M_POLL_END,
|
|
M_POLL_KIND_DISCLOSED,
|
|
M_POLL_KIND_UNDISCLOSED,
|
|
M_POLL_RESPONSE,
|
|
M_POLL_START,
|
|
M_POLL_START_EVENT_CONTENT,
|
|
M_TEXT,
|
|
POLL_ANSWER,
|
|
} from "matrix-events-sdk";
|
|
import { MockedObject } from "jest-mock";
|
|
|
|
import {
|
|
UserVote,
|
|
allVotes,
|
|
findTopAnswer,
|
|
pollEndTs,
|
|
isPollEnded,
|
|
} from "../../../../src/components/views/messages/MPollBody";
|
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
|
import { IBodyProps } from "../../../../src/components/views/messages/IBodyProps";
|
|
import { getMockClientWithEventEmitter } from "../../../test-utils";
|
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
|
import MPollBody from "../../../../src/components/views/messages/MPollBody";
|
|
|
|
const CHECKED = "mx_MPollBody_option_checked";
|
|
|
|
const mockClient = getMockClientWithEventEmitter({
|
|
getUserId: jest.fn().mockReturnValue("@me:example.com"),
|
|
sendEvent: jest.fn().mockReturnValue(Promise.resolve({ "event_id": "fake_send_id" })),
|
|
getRoom: jest.fn(),
|
|
});
|
|
|
|
setRedactionAllowedForMeOnly(mockClient);
|
|
|
|
describe("MPollBody", () => {
|
|
beforeEach(() => {
|
|
mockClient.sendEvent.mockClear();
|
|
});
|
|
|
|
it("finds no votes if there are none", () => {
|
|
expect(
|
|
allVotes(
|
|
{ getRoomId: () => "$room" } as MatrixEvent,
|
|
MatrixClientPeg.get(),
|
|
new RelatedRelations([newVoteRelations([])]),
|
|
new RelatedRelations([newEndRelations([])]),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("can find all the valid responses to a poll", () => {
|
|
const ev1 = responseEvent();
|
|
const ev2 = responseEvent();
|
|
const badEvent = badResponseEvent();
|
|
|
|
const voteRelations = new RelatedRelations([
|
|
newVoteRelations([ev1, badEvent, ev2]),
|
|
]);
|
|
expect(
|
|
allVotes(
|
|
{ getRoomId: () => "$room" } as MatrixEvent,
|
|
MatrixClientPeg.get(),
|
|
voteRelations,
|
|
new RelatedRelations([newEndRelations([])]),
|
|
),
|
|
).toEqual([
|
|
new UserVote(
|
|
ev1.getTs(),
|
|
ev1.getSender(),
|
|
ev1.getContent()[M_POLL_RESPONSE.name].answers,
|
|
),
|
|
new UserVote(
|
|
badEvent.getTs(),
|
|
badEvent.getSender(),
|
|
[], // should be spoiled
|
|
),
|
|
new UserVote(
|
|
ev2.getTs(),
|
|
ev2.getSender(),
|
|
ev2.getContent()[M_POLL_RESPONSE.name].answers,
|
|
),
|
|
]);
|
|
});
|
|
|
|
it("finds the first end poll event", () => {
|
|
const endRelations = new RelatedRelations([
|
|
newEndRelations([
|
|
endEvent("@me:example.com", 25),
|
|
endEvent("@me:example.com", 12),
|
|
endEvent("@me:example.com", 45),
|
|
endEvent("@me:example.com", 13),
|
|
]),
|
|
]);
|
|
|
|
setRedactionAllowedForMeOnly(mockClient);
|
|
|
|
expect(
|
|
pollEndTs(
|
|
{ getRoomId: () => "$room" } as MatrixEvent,
|
|
mockClient,
|
|
endRelations,
|
|
),
|
|
).toBe(12);
|
|
});
|
|
|
|
it("ignores unauthorised end poll event when finding end ts", () => {
|
|
const endRelations = new RelatedRelations([
|
|
newEndRelations([
|
|
endEvent("@me:example.com", 25),
|
|
endEvent("@unauthorised:example.com", 12),
|
|
endEvent("@me:example.com", 45),
|
|
endEvent("@me:example.com", 13),
|
|
]),
|
|
]);
|
|
|
|
setRedactionAllowedForMeOnly(mockClient);
|
|
|
|
expect(
|
|
pollEndTs(
|
|
{ getRoomId: () => "$room" } as MatrixEvent,
|
|
mockClient,
|
|
endRelations,
|
|
),
|
|
).toBe(13);
|
|
});
|
|
|
|
it("counts only votes before the end poll event", () => {
|
|
const voteRelations = new RelatedRelations([
|
|
newVoteRelations([
|
|
responseEvent("sf@matrix.org", "wings", 13),
|
|
responseEvent("jr@matrix.org", "poutine", 40),
|
|
responseEvent("ak@matrix.org", "poutine", 37),
|
|
responseEvent("id@matrix.org", "wings", 13),
|
|
responseEvent("ps@matrix.org", "wings", 19),
|
|
]),
|
|
]);
|
|
const endRelations = new RelatedRelations([
|
|
newEndRelations([
|
|
endEvent("@me:example.com", 25),
|
|
]),
|
|
]);
|
|
expect(
|
|
allVotes(
|
|
{ getRoomId: () => "$room" } as MatrixEvent,
|
|
MatrixClientPeg.get(),
|
|
voteRelations,
|
|
endRelations,
|
|
),
|
|
).toEqual([
|
|
new UserVote(13, "sf@matrix.org", ["wings"]),
|
|
new UserVote(13, "id@matrix.org", ["wings"]),
|
|
new UserVote(19, "ps@matrix.org", ["wings"]),
|
|
]);
|
|
});
|
|
|
|
it("renders no votes if none were made", () => {
|
|
const votes = [];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("");
|
|
expect(votesCount(body, "poutine")).toBe("");
|
|
expect(votesCount(body, "italian")).toBe("");
|
|
expect(votesCount(body, "wings")).toBe("");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("No votes cast");
|
|
expect(body.find('h2').html())
|
|
.toEqual("<h2>What should we order for the party?</h2>");
|
|
});
|
|
|
|
it("finds votes from multiple people", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza"),
|
|
responseEvent("@bellc:example.com", "pizza"),
|
|
responseEvent("@catrd:example.com", "poutine"),
|
|
responseEvent("@dune2:example.com", "wings"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("2 votes");
|
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes");
|
|
});
|
|
|
|
it("ignores end poll events from unauthorised users", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza"),
|
|
responseEvent("@bellc:example.com", "pizza"),
|
|
responseEvent("@catrd:example.com", "poutine"),
|
|
responseEvent("@dune2:example.com", "wings"),
|
|
];
|
|
const ends = [
|
|
endEvent("@notallowed:example.com", 12),
|
|
];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
// Even though an end event was sent, we render the poll as unfinished
|
|
// because this person is not allowed to send these events
|
|
expect(votesCount(body, "pizza")).toBe("2 votes");
|
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes");
|
|
});
|
|
|
|
it("hides scores if I have not voted", () => {
|
|
const votes = [
|
|
responseEvent("@alice:example.com", "pizza"),
|
|
responseEvent("@bellc:example.com", "pizza"),
|
|
responseEvent("@catrd:example.com", "poutine"),
|
|
responseEvent("@dune2:example.com", "wings"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("");
|
|
expect(votesCount(body, "poutine")).toBe("");
|
|
expect(votesCount(body, "italian")).toBe("");
|
|
expect(votesCount(body, "wings")).toBe("");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe(
|
|
"4 votes cast. Vote to see the results");
|
|
});
|
|
|
|
it("hides a single vote if I have not voted", () => {
|
|
const votes = [
|
|
responseEvent("@alice:example.com", "pizza"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("");
|
|
expect(votesCount(body, "poutine")).toBe("");
|
|
expect(votesCount(body, "italian")).toBe("");
|
|
expect(votesCount(body, "wings")).toBe("");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe(
|
|
"1 vote cast. Vote to see the results");
|
|
});
|
|
|
|
it("takes someone's most recent vote if they voted several times", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 12),
|
|
responseEvent("@me:example.com", "wings", 20), // latest me
|
|
responseEvent("@qbert:example.com", "pizza", 14),
|
|
responseEvent("@qbert:example.com", "poutine", 16), // latest qbert
|
|
responseEvent("@qbert:example.com", "wings", 15),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
|
});
|
|
|
|
it("uses my local vote", () => {
|
|
// Given I haven't voted
|
|
const votes = [
|
|
responseEvent("@nf:example.com", "pizza", 15),
|
|
responseEvent("@fg:example.com", "pizza", 15),
|
|
responseEvent("@hi:example.com", "pizza", 15),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
|
|
// When I vote for Italian
|
|
clickRadio(body, "italian");
|
|
|
|
// My vote is counted
|
|
expect(votesCount(body, "pizza")).toBe("3 votes");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("1 vote");
|
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
|
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes");
|
|
});
|
|
|
|
it("overrides my other votes with my local vote", () => {
|
|
// Given two of us have voted for Italian
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 12),
|
|
responseEvent("@me:example.com", "poutine", 13),
|
|
responseEvent("@me:example.com", "italian", 14),
|
|
responseEvent("@nf:example.com", "italian", 15),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
|
|
// When I click Wings
|
|
clickRadio(body, "wings");
|
|
|
|
// Then my vote is counted for Wings, and not for Italian
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("1 vote");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
|
|
|
// And my vote is highlighted
|
|
expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(true);
|
|
expect(voteButton(body, "italian").hasClass(CHECKED)).toBe(false);
|
|
});
|
|
|
|
it("cancels my local vote if another comes in", () => {
|
|
// Given I voted locally
|
|
const votes = [responseEvent("@me:example.com", "pizza", 100)];
|
|
const body = newMPollBody(votes);
|
|
const props: IBodyProps = body.instance().props as IBodyProps;
|
|
const voteRelations = props!.getRelationsForEvent!(
|
|
"$mypoll", "m.reference", M_POLL_RESPONSE.name);
|
|
expect(voteRelations).toBeDefined();
|
|
clickRadio(body, "pizza");
|
|
|
|
// When a new vote from me comes in
|
|
voteRelations!.addEvent(responseEvent("@me:example.com", "wings", 101));
|
|
|
|
// Then the new vote is counted, not the old one
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote");
|
|
});
|
|
|
|
it("doesn't cancel my local vote if someone else votes", () => {
|
|
// Given I voted locally
|
|
const votes = [responseEvent("@me:example.com", "pizza")];
|
|
const body = newMPollBody(votes);
|
|
const props: IBodyProps = body.instance().props as IBodyProps;
|
|
const voteRelations = props!.getRelationsForEvent!(
|
|
"$mypoll", "m.reference", M_POLL_RESPONSE.name);
|
|
expect(voteRelations).toBeDefined();
|
|
clickRadio(body, "pizza");
|
|
|
|
// When a new vote from someone else comes in
|
|
voteRelations!.addEvent(responseEvent("@xx:example.com", "wings", 101));
|
|
|
|
// Then my vote is still for pizza
|
|
// NOTE: the new event does not affect the counts for other people -
|
|
// that is handled through the Relations, not by listening to
|
|
// these timeline events.
|
|
expect(votesCount(body, "pizza")).toBe("1 vote");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
|
|
|
// And my vote is highlighted
|
|
expect(voteButton(body, "pizza").hasClass(CHECKED)).toBe(true);
|
|
expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(false);
|
|
});
|
|
|
|
it("highlights my vote even if I did it on another device", () => {
|
|
// Given I voted italian
|
|
const votes = [
|
|
responseEvent("@me:example.com", "italian"),
|
|
responseEvent("@nf:example.com", "wings"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
|
|
// But I didn't click anything locally
|
|
|
|
// Then my vote is highlighted, and others are not
|
|
expect(voteButton(body, "italian").hasClass(CHECKED)).toBe(true);
|
|
expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(false);
|
|
});
|
|
|
|
it("ignores extra answers", () => {
|
|
// When cb votes for 2 things, we consider the first only
|
|
const votes = [
|
|
responseEvent("@cb:example.com", ["pizza", "wings"]),
|
|
responseEvent("@me:example.com", "wings"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("1 vote");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
|
});
|
|
|
|
it("allows un-voting by passing an empty vote", () => {
|
|
const votes = [
|
|
responseEvent("@nc:example.com", "pizza", 12),
|
|
responseEvent("@nc:example.com", [], 13),
|
|
responseEvent("@me:example.com", "italian"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("1 vote");
|
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote");
|
|
});
|
|
|
|
it("allows re-voting after un-voting", () => {
|
|
const votes = [
|
|
responseEvent("@op:example.com", "pizza", 12),
|
|
responseEvent("@op:example.com", [], 13),
|
|
responseEvent("@op:example.com", "italian", 14),
|
|
responseEvent("@me:example.com", "italian"),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("2 votes");
|
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
|
});
|
|
|
|
it("treats any invalid answer as a spoiled ballot", () => {
|
|
// Note that uy's second vote has a valid first answer, but
|
|
// the ballot is still spoiled because the second answer is
|
|
// invalid, even though we would ignore it if we continued.
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 12),
|
|
responseEvent("@me:example.com", ["pizza", "doesntexist"], 13),
|
|
responseEvent("@uy:example.com", "italian", 14),
|
|
responseEvent("@uy:example.com", "doesntexist", 15),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 0 votes");
|
|
});
|
|
|
|
it("allows re-voting after a spoiled ballot", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 12),
|
|
responseEvent("@me:example.com", ["pizza", "doesntexist"], 13),
|
|
responseEvent("@uy:example.com", "italian", 14),
|
|
responseEvent("@uy:example.com", "doesntexist", 15),
|
|
responseEvent("@uy:example.com", "poutine", 16),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(body.find('input[type="radio"]')).toHaveLength(4);
|
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote");
|
|
});
|
|
|
|
it("renders nothing if poll has no answers", () => {
|
|
const answers = [];
|
|
const votes = [];
|
|
const ends = [];
|
|
const body = newMPollBody(votes, ends, answers);
|
|
expect(body.html()).toBeNull();
|
|
});
|
|
|
|
it("renders the first 20 answers if 21 were given", () => {
|
|
const answers = Array.from(Array(21).keys()).map((i) => {
|
|
return { "id": `id${i}`, [M_TEXT.name]: `Name ${i}` };
|
|
});
|
|
const votes = [];
|
|
const ends = [];
|
|
const body = newMPollBody(votes, ends, answers);
|
|
expect(
|
|
body.find('.mx_MPollBody_option').length,
|
|
).toBe(20);
|
|
});
|
|
|
|
it("hides scores if I voted but the poll is undisclosed", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza"),
|
|
responseEvent("@alice:example.com", "pizza"),
|
|
responseEvent("@bellc:example.com", "pizza"),
|
|
responseEvent("@catrd:example.com", "poutine"),
|
|
responseEvent("@dune2:example.com", "wings"),
|
|
];
|
|
const body = newMPollBody(votes, [], null, false);
|
|
expect(votesCount(body, "pizza")).toBe("");
|
|
expect(votesCount(body, "poutine")).toBe("");
|
|
expect(votesCount(body, "italian")).toBe("");
|
|
expect(votesCount(body, "wings")).toBe("");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe(
|
|
"Results will be visible when the poll is ended");
|
|
});
|
|
|
|
it("highlights my vote if the poll is undisclosed", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza"),
|
|
responseEvent("@alice:example.com", "poutine"),
|
|
responseEvent("@bellc:example.com", "poutine"),
|
|
responseEvent("@catrd:example.com", "poutine"),
|
|
responseEvent("@dune2:example.com", "wings"),
|
|
];
|
|
const body = newMPollBody(votes, [], null, false);
|
|
|
|
// My vote is marked
|
|
expect(body.find('input[value="pizza"]').prop("checked")).toBeTruthy();
|
|
|
|
// Sanity: other items are not checked
|
|
expect(body.find('input[value="poutine"]').prop("checked")).toBeFalsy();
|
|
});
|
|
|
|
it("shows scores if the poll is undisclosed but ended", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza"),
|
|
responseEvent("@alice:example.com", "pizza"),
|
|
responseEvent("@bellc:example.com", "pizza"),
|
|
responseEvent("@catrd:example.com", "poutine"),
|
|
responseEvent("@dune2:example.com", "wings"),
|
|
];
|
|
const ends = [
|
|
endEvent("@me:example.com", 12),
|
|
];
|
|
const body = newMPollBody(votes, ends, null, false);
|
|
expect(endedVotesCount(body, "pizza")).toBe("3 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("1 vote");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("1 vote");
|
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe(
|
|
"Final result based on 5 votes");
|
|
});
|
|
|
|
it("sends a vote event when I choose an option", () => {
|
|
const votes = [];
|
|
const body = newMPollBody(votes);
|
|
clickRadio(body, "wings");
|
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings"));
|
|
});
|
|
|
|
it("sends only one vote event when I click several times", () => {
|
|
const votes = [];
|
|
const body = newMPollBody(votes);
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "wings");
|
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(
|
|
...expectedResponseEventCall("wings"),
|
|
);
|
|
});
|
|
|
|
it("sends no vote event when I click what I already chose", () => {
|
|
const votes = [responseEvent("@me:example.com", "wings")];
|
|
const body = newMPollBody(votes);
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "wings");
|
|
expect(mockClient.sendEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("sends several events when I click different options", () => {
|
|
const votes = [];
|
|
const body = newMPollBody(votes);
|
|
clickRadio(body, "wings");
|
|
clickRadio(body, "italian");
|
|
clickRadio(body, "poutine");
|
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(3);
|
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings"));
|
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("italian"));
|
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("poutine"));
|
|
});
|
|
|
|
it("sends no events when I click in an ended poll", () => {
|
|
const ends = [
|
|
endEvent("@me:example.com", 25),
|
|
];
|
|
const votes = [
|
|
responseEvent("@uy:example.com", "wings", 15),
|
|
responseEvent("@uy:example.com", "poutine", 15),
|
|
];
|
|
const body = newMPollBody(votes, ends);
|
|
clickEndedOption(body, "wings");
|
|
clickEndedOption(body, "italian");
|
|
clickEndedOption(body, "poutine");
|
|
expect(mockClient.sendEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("finds the top answer among several votes", () => {
|
|
// 2 votes for poutine, 1 for pizza. "me" made an invalid vote.
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 12),
|
|
responseEvent("@me:example.com", ["pizza", "doesntexist"], 13),
|
|
responseEvent("@uy:example.com", "italian", 14),
|
|
responseEvent("@uy:example.com", "doesntexist", 15),
|
|
responseEvent("@uy:example.com", "poutine", 16),
|
|
responseEvent("@ab:example.com", "pizza", 17),
|
|
responseEvent("@fa:example.com", "poutine", 18),
|
|
];
|
|
|
|
expect(runFindTopAnswer(votes, [])).toEqual("Poutine");
|
|
});
|
|
|
|
it("finds all top answers when there is a draw", () => {
|
|
const votes = [
|
|
responseEvent("@uy:example.com", "italian", 14),
|
|
responseEvent("@ab:example.com", "pizza", 17),
|
|
responseEvent("@fa:example.com", "poutine", 18),
|
|
];
|
|
expect(runFindTopAnswer(votes, [])).toEqual("Italian, Pizza and Poutine");
|
|
});
|
|
|
|
it("finds all top answers ignoring late votes", () => {
|
|
const votes = [
|
|
responseEvent("@uy:example.com", "italian", 14),
|
|
responseEvent("@ab:example.com", "pizza", 17),
|
|
responseEvent("@io:example.com", "poutine", 30), // Late
|
|
responseEvent("@fa:example.com", "poutine", 18),
|
|
responseEvent("@of:example.com", "poutine", 31), // Late
|
|
];
|
|
const ends = [
|
|
endEvent("@me:example.com", 25),
|
|
];
|
|
expect(runFindTopAnswer(votes, ends)).toEqual("Italian, Pizza and Poutine");
|
|
});
|
|
|
|
it("is silent about the top answer if there are no votes", () => {
|
|
expect(runFindTopAnswer([], [])).toEqual("");
|
|
});
|
|
|
|
it("is silent about the top answer if there are no votes when ended", () => {
|
|
expect(runFindTopAnswer([], [endEvent("@me:example.com", 13)])).toEqual("");
|
|
});
|
|
|
|
it("shows non-radio buttons if the poll is ended", () => {
|
|
const events = [endEvent()];
|
|
const body = newMPollBody([], events);
|
|
expect(body.find(".mx_StyledRadioButton")).toHaveLength(0);
|
|
expect(body.find('input[type="radio"]')).toHaveLength(0);
|
|
});
|
|
|
|
it("counts votes as normal if the poll is ended", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 12),
|
|
responseEvent("@me:example.com", "wings", 20), // latest me
|
|
responseEvent("@qbert:example.com", "pizza", 14),
|
|
responseEvent("@qbert:example.com", "poutine", 16), // latest qbert
|
|
responseEvent("@qbert:example.com", "wings", 15),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
expect(endedVotesCount(body, "pizza")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("1 vote");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("1 vote");
|
|
expect(
|
|
body.find(".mx_MPollBody_totalVotes").text(),
|
|
).toBe("Final result based on 2 votes");
|
|
});
|
|
|
|
it("counts a single vote as normal if the poll is ended", () => {
|
|
const votes = [responseEvent("@qbert:example.com", "poutine", 16)];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
expect(endedVotesCount(body, "pizza")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("1 vote");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("0 votes");
|
|
expect(
|
|
body.find(".mx_MPollBody_totalVotes").text(),
|
|
).toBe("Final result based on 1 vote");
|
|
});
|
|
|
|
it("shows ended vote counts of different numbers", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "wings", 20),
|
|
responseEvent("@qb:example.com", "wings", 14),
|
|
responseEvent("@xy:example.com", "wings", 15),
|
|
responseEvent("@fg:example.com", "pizza", 15),
|
|
responseEvent("@hi:example.com", "pizza", 15),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
expect(body.find(".mx_StyledRadioButton")).toHaveLength(0);
|
|
expect(body.find('input[type="radio"]')).toHaveLength(0);
|
|
expect(endedVotesCount(body, "pizza")).toBe("2 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("3 votes");
|
|
expect(
|
|
body.find(".mx_MPollBody_totalVotes").text(),
|
|
).toBe("Final result based on 5 votes");
|
|
});
|
|
|
|
it("ignores votes that arrived after poll ended", () => {
|
|
const votes = [
|
|
responseEvent("@sd:example.com", "wings", 30), // Late
|
|
responseEvent("@ff:example.com", "wings", 20),
|
|
responseEvent("@ut:example.com", "wings", 14),
|
|
responseEvent("@iu:example.com", "wings", 15),
|
|
responseEvent("@jf:example.com", "wings", 35), // Late
|
|
responseEvent("@wf:example.com", "pizza", 15),
|
|
responseEvent("@ld:example.com", "pizza", 15),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
expect(endedVotesCount(body, "pizza")).toBe("2 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("3 votes");
|
|
expect(
|
|
body.find(".mx_MPollBody_totalVotes").text(),
|
|
).toBe("Final result based on 5 votes");
|
|
});
|
|
|
|
it("counts votes that arrived after an unauthorised poll end event", () => {
|
|
const votes = [
|
|
responseEvent("@sd:example.com", "wings", 30), // Late
|
|
responseEvent("@ff:example.com", "wings", 20),
|
|
responseEvent("@ut:example.com", "wings", 14),
|
|
responseEvent("@iu:example.com", "wings", 15),
|
|
responseEvent("@jf:example.com", "wings", 35), // Late
|
|
responseEvent("@wf:example.com", "pizza", 15),
|
|
responseEvent("@ld:example.com", "pizza", 15),
|
|
];
|
|
const ends = [
|
|
endEvent("@unauthorised:example.com", 5), // Should be ignored
|
|
endEvent("@me:example.com", 25),
|
|
];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
expect(endedVotesCount(body, "pizza")).toBe("2 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("3 votes");
|
|
expect(
|
|
body.find(".mx_MPollBody_totalVotes").text(),
|
|
).toBe("Final result based on 5 votes");
|
|
});
|
|
|
|
it("ignores votes that arrived after the first end poll event", () => {
|
|
// From MSC3381:
|
|
// "Votes sent on or before the end event's timestamp are valid votes"
|
|
|
|
const votes = [
|
|
responseEvent("@sd:example.com", "wings", 30), // Late
|
|
responseEvent("@ff:example.com", "wings", 20),
|
|
responseEvent("@ut:example.com", "wings", 14),
|
|
responseEvent("@iu:example.com", "wings", 25), // Just on time
|
|
responseEvent("@jf:example.com", "wings", 35), // Late
|
|
responseEvent("@wf:example.com", "pizza", 15),
|
|
responseEvent("@ld:example.com", "pizza", 15),
|
|
];
|
|
const ends = [
|
|
endEvent("@me:example.com", 65),
|
|
endEvent("@me:example.com", 25),
|
|
endEvent("@me:example.com", 75),
|
|
];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
expect(endedVotesCount(body, "pizza")).toBe("2 votes");
|
|
expect(endedVotesCount(body, "poutine")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "italian")).toBe("0 votes");
|
|
expect(endedVotesCount(body, "wings")).toBe("3 votes");
|
|
expect(
|
|
body.find(".mx_MPollBody_totalVotes").text(),
|
|
).toBe("Final result based on 5 votes");
|
|
});
|
|
|
|
it("highlights the winning vote in an ended poll", () => {
|
|
// Given I voted for pizza but the winner is wings
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 20),
|
|
responseEvent("@qb:example.com", "wings", 14),
|
|
responseEvent("@xy:example.com", "wings", 15),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
// Then the winner is highlighted
|
|
expect(endedVoteChecked(body, "wings")).toBe(true);
|
|
expect(endedVoteChecked(body, "pizza")).toBe(false);
|
|
|
|
// Double-check by looking for the endedOptionWinner class
|
|
expect(
|
|
endedVoteDiv(body, "wings").hasClass("mx_MPollBody_endedOptionWinner"),
|
|
).toBe(true);
|
|
expect(
|
|
endedVoteDiv(body, "pizza").hasClass("mx_MPollBody_endedOptionWinner"),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("highlights multiple winning votes", () => {
|
|
const votes = [
|
|
responseEvent("@me:example.com", "pizza", 20),
|
|
responseEvent("@xy:example.com", "wings", 15),
|
|
responseEvent("@fg:example.com", "poutine", 15),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
|
|
expect(endedVoteChecked(body, "pizza")).toBe(true);
|
|
expect(endedVoteChecked(body, "wings")).toBe(true);
|
|
expect(endedVoteChecked(body, "poutine")).toBe(true);
|
|
expect(endedVoteChecked(body, "italian")).toBe(false);
|
|
expect(body.find(".mx_MPollBody_option_checked")).toHaveLength(3);
|
|
});
|
|
|
|
it("highlights nothing if poll has no votes", () => {
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody([], ends);
|
|
expect(body.find(".mx_MPollBody_option_checked")).toHaveLength(0);
|
|
});
|
|
|
|
it("says poll is not ended if there is no end event", () => {
|
|
const ends = [];
|
|
expect(runIsPollEnded(ends)).toBe(false);
|
|
});
|
|
|
|
it("says poll is ended if there is an end event", () => {
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
expect(runIsPollEnded(ends)).toBe(true);
|
|
});
|
|
|
|
it("says poll is not ended if endRelations is undefined", () => {
|
|
const pollEvent = new MatrixEvent();
|
|
setRedactionAllowedForMeOnly(mockClient);
|
|
expect(isPollEnded(pollEvent, mockClient, undefined)).toBe(false);
|
|
});
|
|
|
|
it("says poll is not ended if asking for relations returns undefined", () => {
|
|
const pollEvent = new MatrixEvent({
|
|
"event_id": "$mypoll",
|
|
"room_id": "#myroom:example.com",
|
|
"content": newPollStart([]),
|
|
});
|
|
mockClient.getRoom.mockImplementation((_roomId) => {
|
|
return {
|
|
currentState: {
|
|
maySendRedactionForEvent: (_evt: MatrixEvent, userId: string) => {
|
|
return userId === "@me:example.com";
|
|
},
|
|
},
|
|
} as unknown as Room;
|
|
});
|
|
const getRelationsForEvent =
|
|
(eventId: string, relationType: string, eventType: string) => {
|
|
expect(eventId).toBe("$mypoll");
|
|
expect(relationType).toBe("m.reference");
|
|
expect(M_POLL_END.matches(eventType)).toBe(true);
|
|
return undefined;
|
|
};
|
|
expect(
|
|
isPollEnded(
|
|
pollEvent,
|
|
MatrixClientPeg.get(),
|
|
getRelationsForEvent,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("Displays edited content and new answer IDs if the poll has been edited", () => {
|
|
const pollEvent = new MatrixEvent({
|
|
"type": M_POLL_START.name,
|
|
"event_id": "$mypoll",
|
|
"room_id": "#myroom:example.com",
|
|
"content": newPollStart(
|
|
[
|
|
{ "id": "o1", [M_TEXT.name]: "old answer 1" },
|
|
{ "id": "o2", [M_TEXT.name]: "old answer 2" },
|
|
],
|
|
"old question",
|
|
),
|
|
});
|
|
const replacingEvent = new MatrixEvent({
|
|
"type": M_POLL_START.name,
|
|
"event_id": "$mypollreplacement",
|
|
"room_id": "#myroom:example.com",
|
|
"content": {
|
|
"m.new_content": newPollStart(
|
|
[
|
|
{ "id": "n1", [M_TEXT.name]: "new answer 1" },
|
|
{ "id": "n2", [M_TEXT.name]: "new answer 2" },
|
|
{ "id": "n3", [M_TEXT.name]: "new answer 3" },
|
|
],
|
|
"new question",
|
|
),
|
|
},
|
|
});
|
|
pollEvent.makeReplaced(replacingEvent);
|
|
const body = newMPollBodyFromEvent(pollEvent, []);
|
|
expect(body.find('h2').html())
|
|
.toEqual(
|
|
"<h2>new question"
|
|
+ "<span class=\"mx_MPollBody_edited\"> (edited)</span>"
|
|
+ "</h2>",
|
|
);
|
|
const inputs = body.find('input[type="radio"]');
|
|
expect(inputs).toHaveLength(3);
|
|
expect(inputs.at(0).prop("value")).toEqual("n1");
|
|
expect(inputs.at(1).prop("value")).toEqual("n2");
|
|
expect(inputs.at(2).prop("value")).toEqual("n3");
|
|
const options = body.find('.mx_MPollBody_optionText');
|
|
expect(options).toHaveLength(3);
|
|
expect(options.at(0).text()).toEqual("new answer 1");
|
|
expect(options.at(1).text()).toEqual("new answer 2");
|
|
expect(options.at(2).text()).toEqual("new answer 3");
|
|
});
|
|
|
|
it("renders a poll with no votes", () => {
|
|
const votes = [];
|
|
const body = newMPollBody(votes);
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders a poll with only non-local votes", () => {
|
|
const votes = [
|
|
responseEvent("@op:example.com", "pizza", 12),
|
|
responseEvent("@op:example.com", [], 13),
|
|
responseEvent("@op:example.com", "italian", 14),
|
|
responseEvent("@me:example.com", "wings", 15),
|
|
responseEvent("@qr:example.com", "italian", 16),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders a poll with local, non-local and invalid votes", () => {
|
|
const votes = [
|
|
responseEvent("@a:example.com", "pizza", 12),
|
|
responseEvent("@b:example.com", [], 13),
|
|
responseEvent("@c:example.com", "italian", 14),
|
|
responseEvent("@d:example.com", "italian", 14),
|
|
responseEvent("@e:example.com", "wings", 15),
|
|
responseEvent("@me:example.com", "italian", 16),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
clickRadio(body, "italian");
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders a poll that I have not voted in", () => {
|
|
const votes = [
|
|
responseEvent("@op:example.com", "pizza", 12),
|
|
responseEvent("@op:example.com", [], 13),
|
|
responseEvent("@op:example.com", "italian", 14),
|
|
responseEvent("@yo:example.com", "wings", 15),
|
|
responseEvent("@qr:example.com", "italian", 16),
|
|
];
|
|
const body = newMPollBody(votes);
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders a finished poll with no votes", () => {
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody([], ends);
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders a finished poll", () => {
|
|
const votes = [
|
|
responseEvent("@op:example.com", "pizza", 12),
|
|
responseEvent("@op:example.com", [], 13),
|
|
responseEvent("@op:example.com", "italian", 14),
|
|
responseEvent("@yo:example.com", "wings", 15),
|
|
responseEvent("@qr:example.com", "italian", 16),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders a finished poll with multiple winners", () => {
|
|
const votes = [
|
|
responseEvent("@ed:example.com", "pizza", 12),
|
|
responseEvent("@rf:example.com", "pizza", 12),
|
|
responseEvent("@th:example.com", "wings", 13),
|
|
responseEvent("@yh:example.com", "wings", 14),
|
|
responseEvent("@th:example.com", "poutine", 13),
|
|
responseEvent("@yh:example.com", "poutine", 14),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends);
|
|
expect(body).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders an undisclosed, unfinished poll", () => {
|
|
const votes = [
|
|
responseEvent("@ed:example.com", "pizza", 12),
|
|
responseEvent("@rf:example.com", "pizza", 12),
|
|
responseEvent("@th:example.com", "wings", 13),
|
|
responseEvent("@yh:example.com", "wings", 14),
|
|
responseEvent("@th:example.com", "poutine", 13),
|
|
responseEvent("@yh:example.com", "poutine", 14),
|
|
];
|
|
const ends = [];
|
|
const body = newMPollBody(votes, ends, null, false);
|
|
expect(body.html()).toMatchSnapshot();
|
|
});
|
|
|
|
it("renders an undisclosed, finished poll", () => {
|
|
const votes = [
|
|
responseEvent("@ed:example.com", "pizza", 12),
|
|
responseEvent("@rf:example.com", "pizza", 12),
|
|
responseEvent("@th:example.com", "wings", 13),
|
|
responseEvent("@yh:example.com", "wings", 14),
|
|
responseEvent("@th:example.com", "poutine", 13),
|
|
responseEvent("@yh:example.com", "poutine", 14),
|
|
];
|
|
const ends = [endEvent("@me:example.com", 25)];
|
|
const body = newMPollBody(votes, ends, null, false);
|
|
expect(body.html()).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
function newVoteRelations(relationEvents: Array<MatrixEvent>): Relations {
|
|
return newRelations(relationEvents, M_POLL_RESPONSE.name);
|
|
}
|
|
|
|
function newEndRelations(relationEvents: Array<MatrixEvent>): Relations {
|
|
return newRelations(relationEvents, M_POLL_END.name);
|
|
}
|
|
|
|
function newRelations(
|
|
relationEvents: Array<MatrixEvent>,
|
|
eventType: string,
|
|
): Relations {
|
|
const voteRelations = new Relations("m.reference", eventType, null);
|
|
for (const ev of relationEvents) {
|
|
voteRelations.addEvent(ev);
|
|
}
|
|
return voteRelations;
|
|
}
|
|
|
|
function newMPollBody(
|
|
relationEvents: Array<MatrixEvent>,
|
|
endEvents: Array<MatrixEvent> = [],
|
|
answers?: POLL_ANSWER[],
|
|
disclosed = true,
|
|
): ReactWrapper {
|
|
const mxEvent = new MatrixEvent({
|
|
"type": M_POLL_START.name,
|
|
"event_id": "$mypoll",
|
|
"room_id": "#myroom:example.com",
|
|
"content": newPollStart(answers, null, disclosed),
|
|
});
|
|
return newMPollBodyFromEvent(mxEvent, relationEvents, endEvents);
|
|
}
|
|
|
|
function newMPollBodyFromEvent(
|
|
mxEvent: MatrixEvent,
|
|
relationEvents: Array<MatrixEvent>,
|
|
endEvents: Array<MatrixEvent> = [],
|
|
): ReactWrapper {
|
|
const voteRelations = newVoteRelations(relationEvents);
|
|
const endRelations = newEndRelations(endEvents);
|
|
return mount(<MPollBody
|
|
mxEvent={mxEvent}
|
|
getRelationsForEvent={
|
|
(eventId: string, relationType: string, eventType: string) => {
|
|
expect(eventId).toBe("$mypoll");
|
|
expect(relationType).toBe("m.reference");
|
|
if (M_POLL_RESPONSE.matches(eventType)) {
|
|
return voteRelations;
|
|
} else if (M_POLL_END.matches(eventType)) {
|
|
return endRelations;
|
|
} else {
|
|
fail("Unexpected eventType: " + eventType);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We don't use any of these props, but they're required.
|
|
highlightLink="unused"
|
|
highlights={[]}
|
|
mediaEventHelper={null}
|
|
onHeightChanged={() => {}}
|
|
onMessageAllowed={() => {}}
|
|
permalinkCreator={null}
|
|
/>, {
|
|
wrappingComponent: MatrixClientContext.Provider,
|
|
wrappingComponentProps: {
|
|
value: mockClient,
|
|
},
|
|
});
|
|
}
|
|
|
|
function clickRadio(wrapper: ReactWrapper, value: string) {
|
|
const div = wrapper.find(`StyledRadioButton[value="${value}"]`);
|
|
expect(div).toHaveLength(1);
|
|
div.simulate("click");
|
|
}
|
|
|
|
function clickEndedOption(wrapper: ReactWrapper, value: string) {
|
|
const div = wrapper.find(`div[data-value="${value}"]`);
|
|
expect(div).toHaveLength(1);
|
|
div.simulate("click");
|
|
}
|
|
|
|
function voteButton(wrapper: ReactWrapper, value: string): ReactWrapper {
|
|
return wrapper.find(
|
|
`div.mx_MPollBody_option`,
|
|
).findWhere(w => w.key() === value);
|
|
}
|
|
|
|
function votesCount(wrapper: ReactWrapper, value: string): string {
|
|
return wrapper.find(
|
|
`StyledRadioButton[value="${value}"] .mx_MPollBody_optionVoteCount`,
|
|
).text();
|
|
}
|
|
|
|
function endedVoteChecked(wrapper: ReactWrapper, value: string): boolean {
|
|
return endedVoteDiv(wrapper, value)
|
|
.closest(".mx_MPollBody_option")
|
|
.hasClass("mx_MPollBody_option_checked");
|
|
}
|
|
|
|
function endedVoteDiv(wrapper: ReactWrapper, value: string): ReactWrapper {
|
|
return wrapper.find(`div[data-value="${value}"]`);
|
|
}
|
|
|
|
function endedVotesCount(wrapper: ReactWrapper, value: string): string {
|
|
return wrapper.find(
|
|
`div[data-value="${value}"] .mx_MPollBody_optionVoteCount`,
|
|
).text();
|
|
}
|
|
|
|
function newPollStart(
|
|
answers?: POLL_ANSWER[],
|
|
question?: string,
|
|
disclosed = true,
|
|
): M_POLL_START_EVENT_CONTENT {
|
|
if (!answers) {
|
|
answers = [
|
|
{ "id": "pizza", [M_TEXT.name]: "Pizza" },
|
|
{ "id": "poutine", [M_TEXT.name]: "Poutine" },
|
|
{ "id": "italian", [M_TEXT.name]: "Italian" },
|
|
{ "id": "wings", [M_TEXT.name]: "Wings" },
|
|
];
|
|
}
|
|
|
|
if (!question) {
|
|
question = "What should we order for the party?";
|
|
}
|
|
|
|
const answersFallback = answers
|
|
.map((a, i) => `${i + 1}. ${a[M_TEXT.name]}`)
|
|
.join("\n");
|
|
|
|
const fallback = `${question}\n${answersFallback}`;
|
|
|
|
return {
|
|
[M_POLL_START.name]: {
|
|
"question": {
|
|
[M_TEXT.name]: question,
|
|
},
|
|
"kind": (
|
|
disclosed
|
|
? M_POLL_KIND_DISCLOSED.name
|
|
: M_POLL_KIND_UNDISCLOSED.name
|
|
),
|
|
"answers": answers,
|
|
},
|
|
[M_TEXT.name]: fallback,
|
|
};
|
|
}
|
|
|
|
function badResponseEvent(): MatrixEvent {
|
|
return new MatrixEvent(
|
|
{
|
|
"event_id": nextId(),
|
|
"type": M_POLL_RESPONSE.name,
|
|
"sender": "@malicious:example.com",
|
|
"content": {
|
|
"m.relates_to": {
|
|
"rel_type": "m.reference",
|
|
"event_id": "$mypoll",
|
|
},
|
|
// Does not actually contain a response
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function responseEvent(
|
|
sender = "@alice:example.com",
|
|
answers: string | Array<string> = "italian",
|
|
ts = 0,
|
|
): MatrixEvent {
|
|
const ans = typeof answers === "string" ? [answers] : answers;
|
|
return new MatrixEvent(
|
|
{
|
|
"event_id": nextId(),
|
|
"room_id": "#myroom:example.com",
|
|
"origin_server_ts": ts,
|
|
"type": M_POLL_RESPONSE.name,
|
|
"sender": sender,
|
|
"content": {
|
|
"m.relates_to": {
|
|
"rel_type": "m.reference",
|
|
"event_id": "$mypoll",
|
|
},
|
|
[M_POLL_RESPONSE.name]: {
|
|
"answers": ans,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function expectedResponseEvent(answer: string) {
|
|
return {
|
|
"content": {
|
|
[M_POLL_RESPONSE.name]: {
|
|
"answers": [answer],
|
|
},
|
|
"m.relates_to": {
|
|
"event_id": "$mypoll",
|
|
"rel_type": "m.reference",
|
|
},
|
|
},
|
|
"roomId": "#myroom:example.com",
|
|
"eventType": M_POLL_RESPONSE.name,
|
|
"txnId": undefined,
|
|
"callback": undefined,
|
|
};
|
|
}
|
|
function expectedResponseEventCall(answer: string) {
|
|
const {
|
|
content, roomId, eventType,
|
|
} = expectedResponseEvent(answer);
|
|
return [
|
|
roomId, eventType, content,
|
|
];
|
|
}
|
|
|
|
function endEvent(
|
|
sender = "@me:example.com",
|
|
ts = 0,
|
|
): MatrixEvent {
|
|
return new MatrixEvent(
|
|
{
|
|
"event_id": nextId(),
|
|
"room_id": "#myroom:example.com",
|
|
"origin_server_ts": ts,
|
|
"type": M_POLL_END.name,
|
|
"sender": sender,
|
|
"content": {
|
|
"m.relates_to": {
|
|
"rel_type": "m.reference",
|
|
"event_id": "$mypoll",
|
|
},
|
|
[M_POLL_END.name]: {},
|
|
[M_TEXT.name]: "The poll has ended. Something.",
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function runIsPollEnded(ends: MatrixEvent[]) {
|
|
const pollEvent = new MatrixEvent({
|
|
"event_id": "$mypoll",
|
|
"room_id": "#myroom:example.com",
|
|
"type": M_POLL_START.name,
|
|
"content": newPollStart(),
|
|
});
|
|
|
|
setRedactionAllowedForMeOnly(mockClient);
|
|
|
|
const getRelationsForEvent =
|
|
(eventId: string, relationType: string, eventType: string) => {
|
|
expect(eventId).toBe("$mypoll");
|
|
expect(relationType).toBe("m.reference");
|
|
expect(M_POLL_END.matches(eventType)).toBe(true);
|
|
return newEndRelations(ends);
|
|
};
|
|
|
|
return isPollEnded(pollEvent, mockClient, getRelationsForEvent);
|
|
}
|
|
|
|
function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) {
|
|
const pollEvent = new MatrixEvent({
|
|
"event_id": "$mypoll",
|
|
"room_id": "#myroom:example.com",
|
|
"type": M_POLL_START.name,
|
|
"content": newPollStart(),
|
|
});
|
|
|
|
const getRelationsForEvent =
|
|
(eventId: string, relationType: string, eventType: string) => {
|
|
expect(eventId).toBe("$mypoll");
|
|
expect(relationType).toBe("m.reference");
|
|
if (M_POLL_RESPONSE.matches(eventType)) {
|
|
return newVoteRelations(votes);
|
|
} else if (M_POLL_END.matches(eventType)) {
|
|
return newEndRelations(ends);
|
|
} else {
|
|
fail(`eventType should be end or vote but was ${eventType}`);
|
|
}
|
|
};
|
|
|
|
return findTopAnswer(pollEvent, MatrixClientPeg.get(), getRelationsForEvent);
|
|
}
|
|
|
|
function setRedactionAllowedForMeOnly(matrixClient: MockedObject<MatrixClient>) {
|
|
matrixClient.getRoom.mockImplementation((_roomId: string) => {
|
|
return {
|
|
currentState: {
|
|
maySendRedactionForEvent: (_evt: MatrixEvent, userId: string) => {
|
|
return userId === "@me:example.com";
|
|
},
|
|
},
|
|
} as Room;
|
|
});
|
|
}
|
|
|
|
let EVENT_ID = 0;
|
|
function nextId(): string {
|
|
EVENT_ID++;
|
|
return EVENT_ID.toString();
|
|
}
|