Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show who voted for what in poll results #28305

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions playwright/e2e/polls/polls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { test, expect } from "../../element-web-test";
import { Bot } from "../../pages/bot";
import type { Locator, Page } from "@playwright/test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import type { Locator, Page } from "@playwright/test";
import { expect, test } from "../../element-web-test";
import { Bot } from "../../pages/bot";

test.describe("Polls", () => {
type CreatePollOptions = {
Expand Down Expand Up @@ -59,6 +59,35 @@ test.describe("Polls", () => {
).toContainText(`${votes} vote`);
};

const getPollResultsDialog = (page: Page): Locator => {
return page.locator(".mx_PollResultsDialog");
};

const getPollResultsDialogOption = (page: Page, optionText: string): Locator => {
return getPollResultsDialog(page)
.locator(".mx_AnswerEntry")
.filter({ hasText: optionText });
};

const expectDetailedPollOptionVoteCount = async (
page: Page,
pollId: string,
optionText: string,
votes: number,
optLocator?: Locator,
): Promise<void> => {
await expect(
getPollResultsDialogOption(page, optionText)
.locator(".mx_AnswerEntry_Header")
.locator(".mx_AnswerEntry_Header_answerName"),
).toContainText(optionText);
await expect(
getPollResultsDialogOption(page, optionText)
.locator(".mx_AnswerEntry_Header")
.locator(".mx_AnswerEntry_Header_voteCount"),
).toContainText(`${votes} vote`);
};

const botVoteForOption = async (
page: Page,
bot: Bot,
Expand Down Expand Up @@ -219,6 +248,67 @@ test.describe("Polls", () => {
await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
});

test("should allow to view detailed results after voting", async ({ page, app, bot, user }) => {
const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId);
await page.goto("/#/room/" + roomId);
// wait until Bob joined
await expect(page.getByText("BotBob joined the room")).toBeAttached();

const locator = await app.openMessageComposerOptions();
await locator.getByRole("menuitem", { name: "Poll" }).click();

// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
//cy.get(".mx_CompoundDialog").percySnapshotElement("Polls Composer");

const pollParams = {
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe?"],
};
await createPoll(page, pollParams);

// Wait for message to send, get its ID and save as @pollId
const pollId = await page
.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
.filter({ hasText: pollParams.title })
.getAttribute("data-scroll-tokens");
await expect(getPollTile(page, pollId)).toMatchScreenshot("Polls_Timeline_tile_no_votes.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});

// Bot votes 'Maybe' in the poll
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);

// no votes shown until I vote, check bots vote has arrived
await expect(
page.locator(".mx_MPollBody_totalVotes").getByText("1 vote cast. Vote to see the results"),
).toBeAttached();

// vote 'Maybe'
await getPollOption(page, pollId, pollParams.options[2]).click();
// both me and bot have voted Maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2);

// click the 'vote to see results' message
await page.locator(".mx_MPollBody_totalVotes").getByText("Based on 2 votes. Click here to see full results").click();

// expect the detailed results to be shown
await expect(getPollResultsDialog(page)).toBeAttached();

// expect results to be correctly shown
await expectDetailedPollOptionVoteCount(page, pollId, pollParams.options[2], 2);
const voterEntries = getPollResultsDialogOption(page, pollParams.options[2]).locator(".mx_VoterEntry");
expect((await voterEntries.all()).length).toBe(2);
expect(voterEntries.filter({ hasText: bot.credentials.displayName })).not.toBeNull();
expect(voterEntries.filter({hasText: user.displayName})).not.toBeNull();

// close the dialog
await page.locator(".mx_Dialog").getByRole("button", { name: "Close" }).click();

// expect the dialog to be closed
await expect(getPollResultsDialog(page)).not.toBeAttached();
});

test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => {
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
await botCharlie.prepareClient();
Expand Down
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@import "./components/views/dialogs/polls/_PollDetailHeader.pcss";
@import "./components/views/dialogs/polls/_PollListItem.pcss";
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
@import "./components/views/dialogs/polls/_PollResultsDialog.pcss";
@import "./components/views/elements/_AppPermission.pcss";
@import "./components/views/elements/_AppWarning.pcss";
@import "./components/views/elements/_FilterDropdown.pcss";
Expand Down
24 changes: 24 additions & 0 deletions res/css/components/views/dialogs/polls/_PollResultsDialog.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.mx_AnswerEntry:not(:last-child) {
margin-bottom: $spacing-8;
}

.mx_AnswerEntry_Header {
display: flex;
align-items: center;
margin-bottom: $spacing-8;
}

.mx_AnswerEntry_Header_answerName {
font-weight: bolder;
flex-grow: 1
}

.mx_VoterEntry {
display: flex;
align-items: center;
margin-left: $spacing-16;
}

.mx_VoterEntry_AvatarWrapper {
margin-right: $spacing-8;
}
8 changes: 8 additions & 0 deletions res/css/components/views/polls/_PollOption.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ Please see LICENSE files in the repository root for full details.
justify-content: space-between;
}

.mx_PollOption_votesWrapper {
display: flex;
}

.mx_PollOption_facePile {
margin-right: $spacing-8
}

.mx_PollOption_optionVoteCount {
color: $secondary-content;
font-size: $font-12px;
Expand Down
82 changes: 82 additions & 0 deletions src/components/views/dialogs/PollResultsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wrote this file, the copyright is yours.


SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { PollAnswerSubevent, PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { RoomMember } from "matrix-js-sdk/src/matrix";
import React from "react";

import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import MemberAvatar from "../avatars/MemberAvatar";
import { UserVote } from "../messages/MPollBody";
import BaseDialog from "./BaseDialog";

interface IProps {
pollEvent: PollStartEvent;
votes: Map<string, UserVote[]>;
members: RoomMember[];
}

export default function PollResultsDialog(props: IProps): JSX.Element {
return (
<BaseDialog
title={props.pollEvent.question.text}
onFinished={() => Modal.closeCurrentModal()}
className="mx_PollResultsDialog"
>
{
props.pollEvent.answers.map((answer) => {
const votes = props.votes.get(answer.id) || [];
if (votes.length === 0) return;

return <AnswerEntry
key={answer.id}
answer={answer}
members={props.members}
votes={votes}
/>;
})
}
</BaseDialog>
);
}

function AnswerEntry(props: {
answer: PollAnswerSubevent;
members: RoomMember[];
votes: UserVote[];
}): JSX.Element {
const { answer, members, votes } = props;
return (
<div key={answer.id} className="mx_AnswerEntry">
<div className="mx_AnswerEntry_Header">
<span className="mx_AnswerEntry_Header_answerName">{answer.text}</span>
<span className="mx_AnswerEntry_Header_voteCount">{_t("poll|result_dialog|count_of_votes", { count: votes.length })}</span>
</div>
{votes.length === 0 && <div>No one voted for this.</div>}
{votes.map((vote) => {
const member = members.find(m => m.userId === vote.sender);
if (member) return <VoterEntry
key={vote.sender}
vote={vote}
member={member}
/>;
})}
</div>
);
}

function VoterEntry(props: { vote: UserVote; member: RoomMember }): JSX.Element {
const { vote, member } = props;
return <div key={vote.sender} className="mx_VoterEntry">
<div className="mx_VoterEntry_AvatarWrapper">
<MemberAvatar member={member} size="36px" aria-hidden="true" />
</div>
{member.name}
</div>;
}
Loading
Loading