-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Message Pinning: rework the message pinning list in the right panel (#…
…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
1 parent
88cf643
commit 6f3dc30
Showing
22 changed files
with
2,099 additions
and
507 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Binary file added
BIN
+79.1 KB
...ts/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+13.3 KB
...nned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-2-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.5 KB
...nned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-3-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+13.1 KB
...ht/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.