2024-01-09 06:28:40 +00:00
|
|
|
/*
|
2024-09-09 13:57:16 +00:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2024-01-09 06:28:40 +00:00
|
|
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
|
2024-09-09 13:57:16 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2024-01-09 06:28:40 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
import { test, expect } from "../../element-web-test";
|
|
|
|
import { Bot } from "../../pages/bot";
|
|
|
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
|
|
import { Layout } from "../../../src/settings/enums/Layout";
|
|
|
|
import type { Locator, Page } from "@playwright/test";
|
|
|
|
|
|
|
|
test.describe("Polls", () => {
|
|
|
|
type CreatePollOptions = {
|
|
|
|
title: string;
|
|
|
|
options: string[];
|
|
|
|
};
|
|
|
|
const createPoll = async (page: Page, { title, options }: CreatePollOptions) => {
|
|
|
|
if (options.length < 2) {
|
|
|
|
throw new Error("Poll must have at least two options");
|
|
|
|
}
|
|
|
|
const dialog = page.locator(".mx_PollCreateDialog");
|
|
|
|
await dialog.getByRole("textbox", { name: "Question or topic" }).fill(title);
|
|
|
|
for (const [index, value] of options.entries()) {
|
|
|
|
const optionIdLocator = dialog.locator(`#pollcreate_option_${index}`);
|
|
|
|
// click 'add option' button if needed
|
|
|
|
if ((await optionIdLocator.count()) === 0) {
|
|
|
|
const button = dialog.getByRole("button", { name: "Add option" });
|
|
|
|
await button.scrollIntoViewIfNeeded();
|
|
|
|
await button.click();
|
|
|
|
}
|
|
|
|
await optionIdLocator.scrollIntoViewIfNeeded();
|
|
|
|
await optionIdLocator.fill(value);
|
|
|
|
}
|
|
|
|
await page.locator(".mx_Dialog").getByRole("button", { name: "Create Poll" }).click();
|
|
|
|
};
|
|
|
|
|
|
|
|
const getPollTile = (page: Page, pollId: string, optLocator?: Locator): Locator => {
|
|
|
|
return (optLocator ?? page).locator(`.mx_EventTile[data-scroll-tokens="${pollId}"]`);
|
|
|
|
};
|
|
|
|
|
|
|
|
const getPollOption = (page: Page, pollId: string, optionText: string, optLocator?: Locator): Locator => {
|
|
|
|
return getPollTile(page, pollId, optLocator)
|
|
|
|
.locator(".mx_PollOption .mx_StyledRadioButton")
|
|
|
|
.filter({ hasText: optionText });
|
|
|
|
};
|
|
|
|
|
|
|
|
const expectPollOptionVoteCount = async (
|
|
|
|
page: Page,
|
|
|
|
pollId: string,
|
|
|
|
optionText: string,
|
|
|
|
votes: number,
|
|
|
|
optLocator?: Locator,
|
|
|
|
): Promise<void> => {
|
|
|
|
await expect(
|
|
|
|
getPollOption(page, pollId, optionText, optLocator).locator(".mx_PollOption_optionVoteCount"),
|
|
|
|
).toContainText(`${votes} vote`);
|
|
|
|
};
|
|
|
|
|
|
|
|
const botVoteForOption = async (
|
|
|
|
page: Page,
|
|
|
|
bot: Bot,
|
|
|
|
roomId: string,
|
|
|
|
pollId: string,
|
|
|
|
optionText: string,
|
|
|
|
): Promise<void> => {
|
|
|
|
const locator = getPollOption(page, pollId, optionText);
|
|
|
|
const optionId = await locator.first().getByRole("radio").getAttribute("value");
|
|
|
|
|
|
|
|
// We can't use the js-sdk types for this stuff directly, so manually construct the event.
|
|
|
|
await bot.sendEvent(roomId, null, "org.matrix.msc3381.poll.response", {
|
|
|
|
"m.relates_to": {
|
|
|
|
rel_type: "m.reference",
|
|
|
|
event_id: pollId,
|
|
|
|
},
|
|
|
|
"org.matrix.msc3381.poll.response": {
|
|
|
|
answers: [optionId],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
test.use({
|
|
|
|
displayName: "Tom",
|
|
|
|
botCreateOpts: { displayName: "BotBob" },
|
|
|
|
});
|
|
|
|
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
await page.addInitScript(() => {
|
|
|
|
// Collapse left panel for these tests
|
|
|
|
window.localStorage.setItem("mx_lhs_size", "0");
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should be creatable and votable", 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);
|
|
|
|
|
|
|
|
// change my vote to 'Yes'
|
|
|
|
await getPollOption(page, pollId, pollParams.options[0]).click();
|
|
|
|
|
|
|
|
// 1 vote for yes
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1);
|
|
|
|
// 1 vote for maybe
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 1);
|
|
|
|
|
|
|
|
// Bot updates vote to 'No'
|
|
|
|
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
|
|
|
|
|
|
|
|
// 1 vote for yes
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1);
|
|
|
|
// 1 vote for no
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1);
|
|
|
|
// 0 for maybe
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should be editable from context menu if no votes have been cast", async ({ page, app, user, bot }) => {
|
|
|
|
const roomId: string = await app.client.createRoom({});
|
|
|
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
|
|
|
await page.goto("/#/room/" + roomId);
|
|
|
|
|
|
|
|
const locator = await app.openMessageComposerOptions();
|
|
|
|
await locator.getByRole("menuitem", { name: "Poll" }).click();
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
// Open context menu
|
|
|
|
await getPollTile(page, pollId).click({ button: "right" });
|
|
|
|
|
|
|
|
// Select edit item
|
|
|
|
await page.getByRole("menuitem", { name: "Edit" }).click();
|
|
|
|
|
|
|
|
// Expect poll editing dialog
|
|
|
|
await expect(page.locator(".mx_PollCreateDialog")).toBeAttached();
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should not be editable from context menu if votes have been cast", async ({ page, app, user, bot }) => {
|
|
|
|
const roomId: string = await app.client.createRoom({});
|
|
|
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
|
|
|
await page.goto("/#/room/" + roomId);
|
|
|
|
|
|
|
|
const locator = await app.openMessageComposerOptions();
|
|
|
|
await locator.getByRole("menuitem", { name: "Poll" }).click();
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
// Bot votes 'Maybe' in the poll
|
|
|
|
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
|
|
|
|
|
|
|
|
// wait for bot's vote to arrive
|
|
|
|
await expect(page.locator(".mx_MPollBody_totalVotes")).toContainText("1 vote cast");
|
|
|
|
|
|
|
|
// Open context menu
|
|
|
|
await getPollTile(page, pollId).click({ button: "right" });
|
|
|
|
|
|
|
|
// Select edit item
|
|
|
|
await page.getByRole("menuitem", { name: "Edit" }).click();
|
|
|
|
|
|
|
|
// Expect poll editing dialog
|
|
|
|
await expect(page.locator(".mx_ErrorDialog")).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();
|
|
|
|
|
|
|
|
const roomId: string = await app.client.createRoom({});
|
|
|
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
|
|
|
await app.client.inviteUser(roomId, botCharlie.credentials.userId);
|
|
|
|
await page.goto("/#/room/" + roomId);
|
|
|
|
|
|
|
|
// wait until the bots joined
|
|
|
|
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 });
|
|
|
|
|
|
|
|
const locator = await app.openMessageComposerOptions();
|
|
|
|
await locator.getByRole("menuitem", { name: "Poll" }).click();
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
// Bob starts thread on the poll
|
|
|
|
await bot.sendMessage(
|
|
|
|
roomId,
|
|
|
|
{
|
|
|
|
body: "Hello there",
|
|
|
|
msgtype: "m.text",
|
|
|
|
},
|
|
|
|
pollId,
|
|
|
|
);
|
|
|
|
|
|
|
|
// open the thread summary
|
|
|
|
await page.getByRole("button", { name: "Open thread" }).click();
|
|
|
|
|
|
|
|
// Bob votes 'Maybe' in the poll
|
|
|
|
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
|
|
|
|
|
|
|
|
// Charlie votes 'No'
|
|
|
|
await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]);
|
|
|
|
|
|
|
|
// no votes shown until I vote, check votes have arrived in main tl
|
|
|
|
await expect(
|
|
|
|
page
|
|
|
|
.locator(".mx_RoomView_body .mx_MPollBody_totalVotes")
|
|
|
|
.getByText("2 votes cast. Vote to see the results"),
|
|
|
|
).toBeAttached();
|
|
|
|
|
|
|
|
// and thread view
|
|
|
|
await expect(
|
|
|
|
page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"),
|
|
|
|
).toBeAttached();
|
|
|
|
|
|
|
|
// Take snapshots of poll on ThreadView
|
|
|
|
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
|
|
|
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
|
|
|
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", {
|
|
|
|
mask: [page.locator(".mx_MessageTimestamp")],
|
|
|
|
});
|
|
|
|
|
|
|
|
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
|
|
|
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
|
|
|
|
|
|
|
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", {
|
|
|
|
mask: [page.locator(".mx_MessageTimestamp")],
|
|
|
|
});
|
|
|
|
|
|
|
|
const roomViewLocator = page.locator(".mx_RoomView_body");
|
|
|
|
// vote 'Maybe' in the main timeline poll
|
|
|
|
await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click();
|
|
|
|
// both me and bob have voted Maybe
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator);
|
|
|
|
|
|
|
|
const threadViewLocator = page.locator(".mx_ThreadView");
|
|
|
|
// votes updated in thread view too
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator);
|
|
|
|
// change my vote to 'Yes'
|
|
|
|
await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click();
|
|
|
|
|
|
|
|
// Bob updates vote to 'No'
|
|
|
|
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
|
|
|
|
|
|
|
|
// me: yes, bob: no, charlie: no
|
|
|
|
const expectVoteCounts = async (optLocator: Locator) => {
|
|
|
|
// I voted yes
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator);
|
|
|
|
// Bob and Charlie voted no
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator);
|
|
|
|
// 0 for maybe
|
|
|
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator);
|
|
|
|
};
|
|
|
|
|
|
|
|
// check counts are correct in main timeline tile
|
|
|
|
await expectVoteCounts(page.locator(".mx_RoomView_body"));
|
|
|
|
|
|
|
|
// and in thread view tile
|
|
|
|
await expectVoteCounts(page.locator(".mx_ThreadView"));
|
|
|
|
});
|
|
|
|
});
|