Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Message Pinning: rework the message pinning list in the right panel (#…
Browse files Browse the repository at this point in the history
…12825)

* Fix pinning event loading after restart

* Update deps

* Replace pinned event list

* Add a dialog to confirm to unpin all messages

* Use `EmptyState` when there is no pinned messages

* Rework `PinnedEventTile` tests

* Add comments and refactor `PinnedMessageCard`

* Rework `PinnedMessageCard` tests

* Add tests for `UnpinAllDialog`

* Add e2e tests for pinned messages

* Replace 3px custom gap by 4px gap

* Use string interpolation for `Pin` action.

* Update playright sceenshot for empty state
  • Loading branch information
florianduros committed Aug 16, 2024
1 parent 88cf643 commit 6f3dc30
Show file tree
Hide file tree
Showing 22 changed files with 2,099 additions and 507 deletions.
226 changes: 226 additions & 0 deletions playwright/e2e/pinned-messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* 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.
*/

import { Page } from "@playwright/test";

import { test as base, expect } from "../../element-web-test";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";

/**
* Set up for pinned message tests.
*/
export const test = base.extend<{
room1Name?: string;
room1: { name: string; roomId: string };
util: Helpers;
}>({
displayName: "Alice",
botCreateOpts: { displayName: "Other User" },

room1Name: "Room 1",
room1: async ({ room1Name: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await use({ name, roomId });
},

util: async ({ page, app, bot }, use) => {
await use(new Helpers(page, app, bot));
},
});

export class Helpers {
constructor(
private page: Page,
private app: ElementAppPage,
private bot: Bot,
) {}

/**
* Sends messages into given room as a bot
* @param room - the name of the room to send messages into
* @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf`
*/
async receiveMessages(room: string | { name: string }, messages: string[]) {
await this.sendMessageAsClient(this.bot, room, messages);
}

/**
* Use the supplied client to send messages or perform actions as specified by
* the supplied {@link Message} items.
*/
private async sendMessageAsClient(cli: Client, roomName: string | { name: string }, messages: string[]) {
const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name);
const roomId = await room.evaluate((room) => room.roomId);

for (const message of messages) {
await cli.sendMessage(roomId, { body: message, msgtype: "m.text" });

// TODO: without this wait, some tests that send lots of messages flake
// from time to time. I (andyb) have done some investigation, but it
// needs more work to figure out. The messages do arrive over sync, but
// they never appear in the timeline, and they never fire a
// Room.timeline event. I think this only happens with events that refer
// to other events (e.g. replies), so it might be caused by the
// referring event arriving before the referred-to event.
await this.page.waitForTimeout(100);
}
}

/**
* Find a room by its name
* @param roomName
* @private
*/
private async findRoomByName(roomName: string) {
return this.app.client.evaluateHandle((cli, roomName) => {
return cli.getRooms().find((r) => r.name === roomName);
}, roomName);
}

/**
* Open the room with the supplied name.
*/
async goTo(room: string | { name: string }) {
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
}

/**
* Pin the given message
* @param message
*/
async pinMessage(message: string) {
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
await timelineMessage.click({ button: "right" });
await this.page.getByRole("menuitem", { name: "Pin" }).click();
}

/**
* Pin the given messages
* @param messages
*/
async pinMessages(messages: string[]) {
for (const message of messages) {
await this.pinMessage(message);
}
}

/**
* Open the room info panel
*/
async openRoomInfo() {
await this.page.getByRole("button", { name: "Room info" }).nth(1).click();
}

/**
* Assert that the pinned count in the room info is correct
* Open the room info and check the pinned count
* @param count
*/
async assertPinnedCountInRoomInfo(count: number) {
await expect(this.page.getByRole("menuitem", { name: "Pinned messages" })).toHaveText(
`Pinned messages${count}`,
);
}

/**
* Open the pinned messages list
*/
async openPinnedMessagesList() {
await this.page.getByRole("menuitem", { name: "Pinned messages" }).click();
}

/**
* Return the right panel
* @private
*/
private getRightPanel() {
return this.page.locator("#mx_RightPanel");
}

/**
* Assert that the pinned message list contains the given messages
* @param messages
*/
async assertPinnedMessagesList(messages: string[]) {
const rightPanel = this.getRightPanel();
await expect(rightPanel.getByRole("heading", { name: "Pinned messages" })).toHaveText(
`${messages.length} Pinned messages`,
);
await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-messages-${messages.length}.png`);

const list = rightPanel.getByRole("list");
await expect(list.getByRole("listitem")).toHaveCount(messages.length);

for (const message of messages) {
await expect(list.getByText(message)).toBeVisible();
}
}

/**
* Assert that the pinned message list is empty
*/
async assertEmptyPinnedMessagesList() {
const rightPanel = this.getRightPanel();
await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`);
}

/**
* Open the unpin all dialog
*/
async openUnpinAllDialog() {
await this.openRoomInfo();
await this.openPinnedMessagesList();
await this.page.getByRole("button", { name: "Unpin all" }).click();
}

/**
* Return the unpin all dialog
*/
getUnpinAllDialog() {
return this.page.locator(".mx_Dialog", { hasText: "Unpin all messages?" });
}

/**
* Click on the Continue button of the unoin all dialog
*/
async confirmUnpinAllDialog() {
await this.getUnpinAllDialog().getByRole("button", { name: "Continue" }).click();
}

/**
* Go back from the pinned messages list
*/
async backPinnedMessagesList() {
await this.page.locator("#mx_RightPanel").getByTestId("base-card-back-button").click();
}

/**
* Open the contextual menu of a message in the pin message list and click on unpin
* @param message
*/
async unpinMessageFromMessageList(message: string) {
const item = this.getRightPanel().getByRole("list").getByRole("listitem").filter({
hasText: message,
});

await item.getByRole("button").click();
await this.page.getByRole("menu", { name: "Open menu" }).getByRole("menuitem", { name: "Unpin" }).click();
}
}

export { expect };
79 changes: 79 additions & 0 deletions playwright/e2e/pinned-messages/pinned-messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.
*/

import { test } from "./index";
import { expect } from "../../element-web-test";

test.describe("Pinned messages", () => {
test.use({
labsFlags: ["feature_pinning"],
});

test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.openRoomInfo();
await util.assertPinnedCountInRoomInfo(0);
await util.openPinnedMessagesList();
await util.assertEmptyPinnedMessagesList();
});

test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);

await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
await util.openRoomInfo();
await util.assertPinnedCountInRoomInfo(3);
});

test("should pin messages and show them in the pinned message panel", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);

// Pin the messages
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
await util.openRoomInfo();
await util.openPinnedMessagesList();
await util.assertPinnedMessagesList(["Msg1", "Msg2", "Msg4"]);
});

test("should unpin one message", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);

await util.openRoomInfo();
await util.openPinnedMessagesList();
await util.unpinMessageFromMessageList("Msg2");
await util.assertPinnedMessagesList(["Msg1", "Msg4"]);
await util.backPinnedMessagesList();
await util.assertPinnedCountInRoomInfo(2);
});

test("should unpin all messages", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);

await util.openUnpinAllDialog();
await expect(util.getUnpinAllDialog()).toMatchScreenshot("unpin-all-dialog.png");
await util.confirmUnpinAllDialog();

await util.assertEmptyPinnedMessagesList();
await util.backPinnedMessagesList();
await util.assertPinnedCountInRoomInfo(0);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions res/css/_common.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ legend {
.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button),
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button),
.mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
.mx_Dialog_buttons input[type="submit"] {
Expand All @@ -624,14 +624,14 @@ legend {
.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):last-child {
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child {
margin-right: 0px;
}

.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):focus,
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus {
Expand All @@ -643,7 +643,7 @@ legend {
.mx_Dialog_buttons
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button),
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button),
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: var(--cpd-color-text-on-solid-primary);
background-color: var(--cpd-color-bg-action-primary-rest);
Expand All @@ -656,7 +656,7 @@ legend {
.mx_Dialog_buttons
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
.mx_ThemeChoicePanel_CustomTheme button
),
):not(.mx_UnpinAllDialog button),
.mx_Dialog_buttons input[type="submit"].danger {
background-color: var(--cpd-color-bg-critical-primary);
border: solid 1px var(--cpd-color-bg-critical-primary);
Expand All @@ -672,7 +672,7 @@ legend {
.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):disabled,
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled,
.mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled {
Expand Down
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
@import "./views/dialogs/_SpaceSettingsDialog.pcss";
@import "./views/dialogs/_SpotlightDialog.pcss";
@import "./views/dialogs/_TermsDialog.pcss";
@import "./views/dialogs/_UnpinAllDialog.pcss";
@import "./views/dialogs/_UntrustedDeviceDialog.pcss";
@import "./views/dialogs/_UploadConfirmDialog.pcss";
@import "./views/dialogs/_UserSettingsDialog.pcss";
Expand Down
Loading

0 comments on commit 6f3dc30

Please sign in to comment.