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 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4c90e47
Show Profile Pictures according to Votes on Poll Options
timvahlbrock Oct 25, 2024
f6f0fe2
Show vote face pile only if there are less than five members in the room
timvahlbrock Oct 26, 2024
aa46664
Fix winner icon and votes text
timvahlbrock Oct 26, 2024
fa76ae6
Revert "Enable React StrictMode (#28258)"
timvahlbrock Oct 27, 2024
a5fdd41
Add dialog to display detailed votes
timvahlbrock Oct 31, 2024
baabfbc
Refactor PollResultsDialog
timvahlbrock Oct 31, 2024
b739f23
Move styling of results dialog to css
timvahlbrock Nov 1, 2024
653be23
Show text using i18n and modify existing usages
timvahlbrock Nov 1, 2024
6b92c92
Merge branch 'element-hq:develop' into poll-votes
timvahlbrock Nov 1, 2024
2083213
Fix existing tests but 3
timvahlbrock Nov 1, 2024
22729f4
Fix remaining existing tests
timvahlbrock Nov 1, 2024
3730cb4
Move poll option styling to css file
timvahlbrock Nov 1, 2024
c53fea0
Add test that dialog is opened when totalVotes are clicked
timvahlbrock Nov 1, 2024
dc03851
Reapply "Enable React StrictMode (#28258)"
timvahlbrock Nov 1, 2024
df7eea6
Merge branch 'develop' into poll-votes
timvahlbrock Nov 1, 2024
ffd0e2d
Merge branch 'develop' into poll-votes
timvahlbrock Nov 1, 2024
e55c9a1
Merge remote-tracking branch 'origin/develop' into poll-votes
timvahlbrock Nov 8, 2024
ebbdf80
Add e2e test for detailed poll results
timvahlbrock Nov 16, 2024
03f092a
Merge remote-tracking branch 'origin/develop' into poll-votes
timvahlbrock Nov 16, 2024
8fabb24
Update snapshots
timvahlbrock Nov 22, 2024
d5bbe8e
Update PollEndBody tests
timvahlbrock Nov 22, 2024
51671e5
Fix formatting
timvahlbrock Nov 22, 2024
1434022
Merge remote-tracking branch 'origin/develop' into poll-votes
timvahlbrock Nov 22, 2024
397f9b6
Update Poll Results Dialog Copyright
timvahlbrock Nov 22, 2024
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
97 changes: 94 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,33 @@ 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 +246,70 @@ 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
70 changes: 70 additions & 0 deletions src/components/views/dialogs/PollResultsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright 2024 Tim Vahlbrock

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