Playwright: Convert /e2e/polls (#12065)
* Convert tests * Conver tests * Move type import down * Remove reference to cypress types * Update screenshot * Remove client.evaluate * More changes * Use credentials from bot object * Use credentials * Remove comment * Update screenshots * Use sendMessage method * Create dummy test * Use userId from credentials
This commit is contained in:
parent
c0036b385a
commit
82840a19f9
8 changed files with 501 additions and 563 deletions
19
cypress/e2e/dummy.spec.ts
Normal file
19
cypress/e2e/dummy.spec.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2024 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" />
|
||||
|
||||
it("Dummy test to make CI pass", () => {});
|
|
@ -1,191 +0,0 @@
|
|||
/*
|
||||
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";
|
||||
|
||||
describe("Poll history", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
|
||||
type CreatePollOptions = {
|
||||
title: string;
|
||||
options: {
|
||||
"id": string;
|
||||
"org.matrix.msc1767.text": string;
|
||||
}[];
|
||||
};
|
||||
const createPoll = async ({ title, options }: CreatePollOptions, roomId, client: MatrixClient) => {
|
||||
return await client.sendEvent(roomId, "org.matrix.msc3381.poll.start", {
|
||||
"org.matrix.msc3381.poll.start": {
|
||||
question: {
|
||||
"org.matrix.msc1767.text": title,
|
||||
"body": title,
|
||||
"msgtype": "m.text",
|
||||
},
|
||||
kind: "org.matrix.msc3381.poll.disclosed",
|
||||
max_selections: 1,
|
||||
answers: options,
|
||||
},
|
||||
"org.matrix.msc1767.text": "poll fallback text",
|
||||
});
|
||||
};
|
||||
|
||||
const botVoteForOption = async (
|
||||
bot: MatrixClient,
|
||||
roomId: string,
|
||||
pollId: string,
|
||||
optionId: string,
|
||||
): Promise<void> => {
|
||||
// We can't use the js-sdk types for this stuff directly, so manually construct the event.
|
||||
await 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],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const endPoll = async (bot: MatrixClient, roomId: string, pollId: string): Promise<void> => {
|
||||
// We can't use the js-sdk types for this stuff directly, so manually construct the event.
|
||||
await bot.sendEvent(roomId, "org.matrix.msc3381.poll.end", {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: pollId,
|
||||
},
|
||||
"org.matrix.msc1767.text": "The poll has ended",
|
||||
});
|
||||
};
|
||||
|
||||
function openPollHistory(): void {
|
||||
cy.findByRole("button", { name: "Room info" }).click();
|
||||
cy.get(".mx_RoomSummaryCard").within(() => {
|
||||
cy.findByRole("menuitem", { name: "Poll history" }).click();
|
||||
});
|
||||
}
|
||||
|
||||
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 display active and past polls", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
const pollParams1 = {
|
||||
title: "Does the polls feature work?",
|
||||
options: ["Yes", "No", "Maybe"].map((option) => ({
|
||||
"id": option,
|
||||
"org.matrix.msc1767.text": option,
|
||||
})),
|
||||
};
|
||||
|
||||
const pollParams2 = {
|
||||
title: "Which way",
|
||||
options: ["Left", "Right"].map((option) => ({
|
||||
"id": option,
|
||||
"org.matrix.msc1767.text": option,
|
||||
})),
|
||||
};
|
||||
|
||||
cy.createRoom({}).as("roomId");
|
||||
|
||||
cy.get<string>("@roomId").then((roomId) => {
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit("/#/room/" + roomId);
|
||||
// wait until Bob joined
|
||||
cy.findByText("BotBob joined the room").should("exist");
|
||||
});
|
||||
|
||||
// active poll
|
||||
cy.get<string>("@roomId")
|
||||
.then(async (roomId) => {
|
||||
const { event_id: pollId } = await createPoll(pollParams1, roomId, bot);
|
||||
await botVoteForOption(bot, roomId, pollId, pollParams1.options[1].id);
|
||||
return pollId;
|
||||
})
|
||||
.as("pollId1");
|
||||
|
||||
// ended poll
|
||||
cy.get<string>("@roomId")
|
||||
.then(async (roomId) => {
|
||||
const { event_id: pollId } = await createPoll(pollParams2, roomId, bot);
|
||||
await botVoteForOption(bot, roomId, pollId, pollParams1.options[1].id);
|
||||
await endPoll(bot, roomId, pollId);
|
||||
return pollId;
|
||||
})
|
||||
.as("pollId2");
|
||||
|
||||
openPollHistory();
|
||||
|
||||
// these polls are also in the timeline
|
||||
// focus on the poll history dialog
|
||||
cy.get(".mx_Dialog").within(() => {
|
||||
// active poll is in active polls list
|
||||
// open poll detail
|
||||
cy.findByText(pollParams1.title).click();
|
||||
|
||||
// vote in the poll
|
||||
cy.findByText("Yes").click();
|
||||
cy.findByTestId("totalVotes").within(() => {
|
||||
cy.findByText("Based on 2 votes");
|
||||
});
|
||||
|
||||
// navigate back to list
|
||||
cy.get(".mx_PollHistory_header").within(() => {
|
||||
cy.findByRole("button", { name: "Active polls" }).click();
|
||||
});
|
||||
|
||||
// go to past polls list
|
||||
cy.findByText("Past polls").click();
|
||||
|
||||
cy.findByText(pollParams2.title).should("exist");
|
||||
});
|
||||
|
||||
// end poll1 while dialog is open
|
||||
cy.all([cy.get<string>("@roomId"), cy.get<string>("@pollId1")]).then(async ([roomId, pollId]) => {
|
||||
return endPoll(bot, roomId, pollId);
|
||||
});
|
||||
|
||||
cy.get(".mx_Dialog").within(() => {
|
||||
// both ended polls are in past polls list
|
||||
cy.findByText(pollParams2.title).should("exist");
|
||||
cy.findByText(pollParams1.title).should("exist");
|
||||
|
||||
cy.findByText("Active polls").click();
|
||||
|
||||
// no more active polls
|
||||
cy.findByText("There are no active polls in this room").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,372 +0,0 @@
|
|||
/*
|
||||
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 { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const hidePercyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { 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.findByRole("textbox", { name: "Question or topic" }).type(title);
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const optionId = `#pollcreate_option_${index}`;
|
||||
|
||||
// click 'add option' button if needed
|
||||
if (pollCreateDialog.find(optionId).length === 0) {
|
||||
cy.findByRole("button", { name: "Add option" }).scrollIntoView().click();
|
||||
}
|
||||
cy.get(optionId).scrollIntoView().type(option);
|
||||
});
|
||||
});
|
||||
cy.get(".mx_Dialog").within(() => {
|
||||
cy.findByRole("button", { name: "Create Poll" }).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.findByRole("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.findByText("BotBob joined the room").should("exist");
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.findByRole("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?",
|
||||
// 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: hidePercyCSS });
|
||||
|
||||
// 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").within(() => {
|
||||
cy.findByText("1 vote cast. Vote to see the results");
|
||||
});
|
||||
|
||||
// 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.findByRole("menuitem", { name: "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.findByRole("menuitem", { name: "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.findByRole("menuitem", { name: "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.findByRole("menuitem", { name: "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.findByText("BotBob and one other were invited and joined", { timeout: 10000 }).should("exist");
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.findByRole("menuitem", { name: "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.findByRole("button", { name: "Open thread" }).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").within(() => {
|
||||
cy.findByText("2 votes cast. Vote to see the results").should("exist");
|
||||
});
|
||||
// and thread view
|
||||
cy.get(".mx_ThreadView .mx_MPollBody_totalVotes").within(() => {
|
||||
cy.findByText("2 votes cast. Vote to see the results").should("exist");
|
||||
});
|
||||
|
||||
// Take snapshots of poll on ThreadView
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble']").should("be.visible");
|
||||
cy.get(".mx_ThreadView").percySnapshotElement("ThreadView with a poll on bubble layout", {
|
||||
percyCSS: hidePercyCSS,
|
||||
});
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
cy.get(".mx_ThreadView .mx_EventTile[data-layout='group']").should("be.visible");
|
||||
cy.get(".mx_ThreadView").percySnapshotElement("ThreadView with a poll on group layout", {
|
||||
percyCSS: hidePercyCSS,
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
149
playwright/e2e/polls/pollHistory.spec.ts
Normal file
149
playwright/e2e/polls/pollHistory.spec.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
import type { Client } from "../../pages/client";
|
||||
|
||||
test.describe("Poll history", () => {
|
||||
type CreatePollOptions = {
|
||||
title: string;
|
||||
options: {
|
||||
"id": string;
|
||||
"org.matrix.msc1767.text": string;
|
||||
}[];
|
||||
};
|
||||
const createPoll = async (createOptions: CreatePollOptions, roomId: string, client: Client) => {
|
||||
return client.sendEvent(roomId, null, "org.matrix.msc3381.poll.start", {
|
||||
"org.matrix.msc3381.poll.start": {
|
||||
question: {
|
||||
"org.matrix.msc1767.text": createOptions.title,
|
||||
"body": createOptions.title,
|
||||
"msgtype": "m.text",
|
||||
},
|
||||
kind: "org.matrix.msc3381.poll.disclosed",
|
||||
max_selections: 1,
|
||||
answers: createOptions.options,
|
||||
},
|
||||
"org.matrix.msc1767.text": "poll fallback text",
|
||||
});
|
||||
};
|
||||
|
||||
const botVoteForOption = async (bot: Bot, roomId: string, pollId: string, optionId: string): Promise<void> => {
|
||||
// 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],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const endPoll = async (bot: Bot, roomId: string, pollId: string): Promise<void> => {
|
||||
// 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.end", {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: pollId,
|
||||
},
|
||||
"org.matrix.msc1767.text": "The poll has ended",
|
||||
});
|
||||
};
|
||||
|
||||
async function openPollHistory(page: Page): Promise<void> {
|
||||
await page.getByRole("button", { name: "Room info" }).click();
|
||||
await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "Poll history" }).click();
|
||||
}
|
||||
|
||||
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 display active and past polls", async ({ page, app, user, bot }) => {
|
||||
const pollParams1 = {
|
||||
title: "Does the polls feature work?",
|
||||
options: ["Yes", "No", "Maybe"].map((option) => ({
|
||||
"id": option,
|
||||
"org.matrix.msc1767.text": option,
|
||||
})),
|
||||
};
|
||||
|
||||
const pollParams2 = {
|
||||
title: "Which way",
|
||||
options: ["Left", "Right"].map((option) => ({
|
||||
"id": option,
|
||||
"org.matrix.msc1767.text": option,
|
||||
})),
|
||||
};
|
||||
|
||||
const roomId = 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();
|
||||
|
||||
// active poll
|
||||
const { event_id: pollId1 } = await createPoll(pollParams1, roomId, bot);
|
||||
await botVoteForOption(bot, roomId, pollId1, pollParams1.options[1].id);
|
||||
|
||||
// ended poll
|
||||
const { event_id: pollId2 } = await createPoll(pollParams2, roomId, bot);
|
||||
await botVoteForOption(bot, roomId, pollId2, pollParams1.options[1].id);
|
||||
await endPoll(bot, roomId, pollId2);
|
||||
|
||||
await openPollHistory(page);
|
||||
|
||||
// these polls are also in the timeline
|
||||
// focus on the poll history dialog
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
|
||||
// active poll is in active polls list
|
||||
// open poll detail
|
||||
await dialog.getByText(pollParams1.title).click();
|
||||
await dialog.getByText("Yes").click();
|
||||
// vote in the poll
|
||||
await expect(dialog.getByTestId("totalVotes").getByText("Based on 2 votes")).toBeAttached();
|
||||
// navigate back to list
|
||||
await dialog.locator(".mx_PollHistory_header").getByRole("button", { name: "Active polls" }).click();
|
||||
|
||||
// go to past polls list
|
||||
await dialog.getByText("Past polls").click();
|
||||
|
||||
await expect(dialog.getByText(pollParams2.title)).toBeAttached();
|
||||
|
||||
// end poll1 while dialog is open
|
||||
await endPoll(bot, roomId, pollId1);
|
||||
|
||||
await expect(dialog.getByText(pollParams2.title)).toBeAttached();
|
||||
await expect(dialog.getByText(pollParams1.title)).toBeAttached();
|
||||
dialog.getByText("Active polls").click();
|
||||
|
||||
// no more active polls
|
||||
await expect(page.getByText("There are no active polls in this room")).toBeAttached();
|
||||
});
|
||||
});
|
333
playwright/e2e/polls/polls.spec.ts
Normal file
333
playwright/e2e/polls/polls.spec.ts
Normal file
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
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"));
|
||||
});
|
||||
});
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Loading…
Reference in a new issue