353 lines
13 KiB
TypeScript
353 lines
13 KiB
TypeScript
/*
|
|
Copyright 2022 - 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.
|
|
*/
|
|
|
|
/// <reference types="cypress" />
|
|
|
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
import { MatrixClient } from "../../global";
|
|
import Chainable = Cypress.Chainable;
|
|
|
|
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
|
|
|
|
describe("Polls", () => {
|
|
let homeserver: HomeserverInstance;
|
|
|
|
type CreatePollOptions = {
|
|
title: string;
|
|
options: string[];
|
|
};
|
|
const createPoll = ({ title, options }: CreatePollOptions) => {
|
|
if (options.length < 2) {
|
|
throw new Error("Poll must have at least two options");
|
|
}
|
|
cy.get(".mx_PollCreateDialog").within((pollCreateDialog) => {
|
|
cy.get("#poll-topic-input").type(title);
|
|
|
|
options.forEach((option, index) => {
|
|
const optionId = `#pollcreate_option_${index}`;
|
|
|
|
// click 'add option' button if needed
|
|
if (pollCreateDialog.find(optionId).length === 0) {
|
|
cy.get(".mx_PollCreateDialog_addOption").scrollIntoView().click();
|
|
}
|
|
cy.get(optionId).scrollIntoView().type(option);
|
|
});
|
|
});
|
|
cy.get('.mx_Dialog button[type="submit"]').click();
|
|
};
|
|
|
|
const getPollTile = (pollId: string): Chainable<JQuery> => {
|
|
return cy.get(`.mx_EventTile[data-scroll-tokens="${pollId}"]`);
|
|
};
|
|
|
|
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
|
|
return getPollTile(pollId).contains(".mx_PollOption .mx_StyledRadioButton", optionText);
|
|
};
|
|
|
|
const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => {
|
|
getPollOption(pollId, optionText).within(() => {
|
|
cy.get(".mx_PollOption_optionVoteCount").should("contain", `${votes} vote`);
|
|
});
|
|
};
|
|
|
|
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
|
|
getPollOption(pollId, optionText).within((ref) => {
|
|
cy.get('input[type="radio"]')
|
|
.invoke("attr", "value")
|
|
.then((optionId) => {
|
|
// We can't use the js-sdk types for this stuff directly, so manually construct the event.
|
|
bot.sendEvent(roomId, "org.matrix.msc3381.poll.response", {
|
|
"m.relates_to": {
|
|
rel_type: "m.reference",
|
|
event_id: pollId,
|
|
},
|
|
"org.matrix.msc3381.poll.response": {
|
|
answers: [optionId],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
cy.window().then((win) => {
|
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
|
});
|
|
cy.startHomeserver("default").then((data) => {
|
|
homeserver = data;
|
|
|
|
cy.initTestUser(homeserver, "Tom");
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.stopHomeserver(homeserver);
|
|
});
|
|
|
|
it("should be creatable and votable", () => {
|
|
let bot: MatrixClient;
|
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
|
bot = _bot;
|
|
});
|
|
|
|
let roomId: string;
|
|
cy.createRoom({}).then((_roomId) => {
|
|
roomId = _roomId;
|
|
cy.inviteUser(roomId, bot.getUserId());
|
|
cy.visit("/#/room/" + roomId);
|
|
// wait until Bob joined
|
|
cy.contains(".mx_TextualEvent", "BotBob joined the room").should("exist");
|
|
});
|
|
|
|
cy.openMessageComposerOptions().within(() => {
|
|
cy.get('[aria-label="Poll"]').click();
|
|
});
|
|
|
|
cy.get(".mx_CompoundDialog").percySnapshotElement("Polls Composer");
|
|
|
|
const pollParams = {
|
|
title: "Does the polls feature work?",
|
|
// Since we're going to take a screenshot anyways, we include some
|
|
// non-ASCII characters here to stress test the app's font config
|
|
// while we're at it.
|
|
options: ["Yes", "Noo⃐o⃑o⃩o⃪o⃫o⃬o⃭o⃮o⃯", "のらねこ Maybe?"],
|
|
};
|
|
createPoll(pollParams);
|
|
|
|
// Wait for message to send, get its ID and save as @pollId
|
|
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
|
|
.invoke("attr", "data-scroll-tokens")
|
|
.as("pollId");
|
|
|
|
cy.get<string>("@pollId").then((pollId) => {
|
|
getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hideTimestampCSS });
|
|
|
|
// Bot votes 'Maybe' in the poll
|
|
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
|
|
|
|
// no votes shown until I vote, check bots vote has arrived
|
|
cy.get(".mx_MPollBody_totalVotes").should("contain", "1 vote cast");
|
|
|
|
// vote 'Maybe'
|
|
getPollOption(pollId, pollParams.options[2]).click("topLeft");
|
|
// both me and bot have voted Maybe
|
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
|
|
|
// change my vote to 'Yes'
|
|
getPollOption(pollId, pollParams.options[0]).click("topLeft");
|
|
|
|
// 1 vote for yes
|
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
|
// 1 vote for maybe
|
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 1);
|
|
|
|
// Bot updates vote to 'No'
|
|
botVoteForOption(bot, roomId, pollId, pollParams.options[1]);
|
|
|
|
// 1 vote for yes
|
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
|
// 1 vote for no
|
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
|
// 0 for maybe
|
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
|
});
|
|
});
|
|
|
|
it("should be editable from context menu if no votes have been cast", () => {
|
|
let bot: MatrixClient;
|
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
|
bot = _bot;
|
|
});
|
|
|
|
let roomId: string;
|
|
cy.createRoom({}).then((_roomId) => {
|
|
roomId = _roomId;
|
|
cy.inviteUser(roomId, bot.getUserId());
|
|
cy.visit("/#/room/" + roomId);
|
|
});
|
|
|
|
cy.openMessageComposerOptions().within(() => {
|
|
cy.get('[aria-label="Poll"]').click();
|
|
});
|
|
|
|
const pollParams = {
|
|
title: "Does the polls feature work?",
|
|
options: ["Yes", "No", "Maybe"],
|
|
};
|
|
createPoll(pollParams);
|
|
|
|
// Wait for message to send, get its ID and save as @pollId
|
|
cy.get(".mx_RoomView_body .mx_EventTile")
|
|
.contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
|
.invoke("attr", "data-scroll-tokens")
|
|
.as("pollId");
|
|
|
|
cy.get<string>("@pollId").then((pollId) => {
|
|
// Open context menu
|
|
getPollTile(pollId).rightclick();
|
|
|
|
// Select edit item
|
|
cy.get(".mx_ContextualMenu").within(() => {
|
|
cy.get('[aria-label="Edit"]').click();
|
|
});
|
|
|
|
// Expect poll editing dialog
|
|
cy.get(".mx_PollCreateDialog");
|
|
});
|
|
});
|
|
|
|
it("should not be editable from context menu if votes have been cast", () => {
|
|
let bot: MatrixClient;
|
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
|
bot = _bot;
|
|
});
|
|
|
|
let roomId: string;
|
|
cy.createRoom({}).then((_roomId) => {
|
|
roomId = _roomId;
|
|
cy.inviteUser(roomId, bot.getUserId());
|
|
cy.visit("/#/room/" + roomId);
|
|
});
|
|
|
|
cy.openMessageComposerOptions().within(() => {
|
|
cy.get('[aria-label="Poll"]').click();
|
|
});
|
|
|
|
const pollParams = {
|
|
title: "Does the polls feature work?",
|
|
options: ["Yes", "No", "Maybe"],
|
|
};
|
|
createPoll(pollParams);
|
|
|
|
// Wait for message to send, get its ID and save as @pollId
|
|
cy.get(".mx_RoomView_body .mx_EventTile")
|
|
.contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
|
.invoke("attr", "data-scroll-tokens")
|
|
.as("pollId");
|
|
|
|
cy.get<string>("@pollId").then((pollId) => {
|
|
// Bot votes 'Maybe' in the poll
|
|
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
|
|
|
|
// wait for bot's vote to arrive
|
|
cy.get(".mx_MPollBody_totalVotes").should("contain", "1 vote cast");
|
|
|
|
// Open context menu
|
|
getPollTile(pollId).rightclick();
|
|
|
|
// Select edit item
|
|
cy.get(".mx_ContextualMenu").within(() => {
|
|
cy.get('[aria-label="Edit"]').click();
|
|
});
|
|
|
|
// Expect error dialog
|
|
cy.get(".mx_ErrorDialog");
|
|
});
|
|
});
|
|
|
|
it("should be displayed correctly in thread panel", () => {
|
|
let botBob: MatrixClient;
|
|
let botCharlie: MatrixClient;
|
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
|
botBob = _bot;
|
|
});
|
|
cy.getBot(homeserver, { displayName: "BotCharlie" }).then((_bot) => {
|
|
botCharlie = _bot;
|
|
});
|
|
|
|
let roomId: string;
|
|
cy.createRoom({}).then((_roomId) => {
|
|
roomId = _roomId;
|
|
cy.inviteUser(roomId, botBob.getUserId());
|
|
cy.inviteUser(roomId, botCharlie.getUserId());
|
|
cy.visit("/#/room/" + roomId);
|
|
// wait until the bots joined
|
|
cy.contains(".mx_TextualEvent", "and one other were invited and joined").should("exist");
|
|
});
|
|
|
|
cy.openMessageComposerOptions().within(() => {
|
|
cy.get('[aria-label="Poll"]').click();
|
|
});
|
|
|
|
const pollParams = {
|
|
title: "Does the polls feature work?",
|
|
options: ["Yes", "No", "Maybe"],
|
|
};
|
|
createPoll(pollParams);
|
|
|
|
// Wait for message to send, get its ID and save as @pollId
|
|
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
|
|
.invoke("attr", "data-scroll-tokens")
|
|
.as("pollId");
|
|
|
|
cy.get<string>("@pollId").then((pollId) => {
|
|
// Bob starts thread on the poll
|
|
botBob.sendMessage(roomId, pollId, {
|
|
body: "Hello there",
|
|
msgtype: "m.text",
|
|
});
|
|
|
|
// open the thread summary
|
|
cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
|
|
|
|
// Bob votes 'Maybe' in the poll
|
|
botVoteForOption(botBob, roomId, pollId, pollParams.options[2]);
|
|
// Charlie votes 'No'
|
|
botVoteForOption(botCharlie, roomId, pollId, pollParams.options[1]);
|
|
|
|
// no votes shown until I vote, check votes have arrived in main tl
|
|
cy.get(".mx_RoomView_body .mx_MPollBody_totalVotes").should("contain", "2 votes cast");
|
|
// and thread view
|
|
cy.get(".mx_ThreadView .mx_MPollBody_totalVotes").should("contain", "2 votes cast");
|
|
|
|
cy.get(".mx_RoomView_body").within(() => {
|
|
// vote 'Maybe' in the main timeline poll
|
|
getPollOption(pollId, pollParams.options[2]).click("topLeft");
|
|
// both me and bob have voted Maybe
|
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
|
});
|
|
|
|
cy.get(".mx_ThreadView").within(() => {
|
|
// votes updated in thread view too
|
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
|
// change my vote to 'Yes'
|
|
getPollOption(pollId, pollParams.options[0]).click("topLeft");
|
|
});
|
|
|
|
// Bob updates vote to 'No'
|
|
botVoteForOption(botBob, roomId, pollId, pollParams.options[1]);
|
|
|
|
// me: yes, bob: no, charlie: no
|
|
const expectVoteCounts = () => {
|
|
// I voted yes
|
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
|
// Bob and Charlie voted no
|
|
expectPollOptionVoteCount(pollId, pollParams.options[1], 2);
|
|
// 0 for maybe
|
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
|
};
|
|
|
|
// check counts are correct in main timeline tile
|
|
cy.get(".mx_RoomView_body").within(() => {
|
|
expectVoteCounts();
|
|
});
|
|
// and in thread view tile
|
|
cy.get(".mx_ThreadView").within(() => {
|
|
expectVoteCounts();
|
|
});
|
|
});
|
|
});
|
|
});
|