From e92ca4fcd2c43233ab5722a650ec8928393c17fa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Dec 2023 14:08:36 +0000 Subject: [PATCH] Migrate knock/* from Cypress to Playwright (#12030) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- cypress/e2e/knock/create-knock-room.spec.ts | 136 -------- cypress/e2e/knock/knock-into-room.spec.ts | 318 ------------------ cypress/e2e/knock/manage-knocks.spec.ts | 142 -------- .../e2e/knock/create-knock-room.spec.ts | 92 +++++ playwright/e2e/knock/knock-into-room.spec.ts | 301 +++++++++++++++++ playwright/e2e/knock/manage-knocks.spec.ts | 118 +++++++ playwright/e2e/utils.ts | 66 ++++ playwright/element-web-test.ts | 17 +- playwright/pages/ElementAppPage.ts | 7 + playwright/pages/Spotlight.ts | 58 ++++ playwright/pages/client.ts | 67 ++++ 11 files changed, 721 insertions(+), 601 deletions(-) delete mode 100644 cypress/e2e/knock/create-knock-room.spec.ts delete mode 100644 cypress/e2e/knock/knock-into-room.spec.ts delete mode 100644 cypress/e2e/knock/manage-knocks.spec.ts create mode 100644 playwright/e2e/knock/create-knock-room.spec.ts create mode 100644 playwright/e2e/knock/knock-into-room.spec.ts create mode 100644 playwright/e2e/knock/manage-knocks.spec.ts create mode 100644 playwright/e2e/utils.ts create mode 100644 playwright/pages/Spotlight.ts diff --git a/cypress/e2e/knock/create-knock-room.spec.ts b/cypress/e2e/knock/create-knock-room.spec.ts deleted file mode 100644 index dbbcf49492..0000000000 --- a/cypress/e2e/knock/create-knock-room.spec.ts +++ /dev/null @@ -1,136 +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. -*/ - -/// - -import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { waitForRoom } from "../utils"; -import { Filter } from "../../support/settings"; - -describe("Create Knock Room", () => { - let homeserver: HomeserverInstance; - - beforeEach(() => { - cy.enableLabsFeature("feature_ask_to_join"); - - cy.startHomeserver("default").then((data) => { - homeserver = data; - - cy.initTestUser(homeserver, "Alice"); - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - it("should create a knock room", () => { - cy.openCreateRoomDialog().within(() => { - cy.findByRole("textbox", { name: "Name" }).type("Cybersecurity"); - cy.findByRole("button", { name: "Room visibility" }).click(); - cy.findByRole("option", { name: "Ask to join" }).click(); - - cy.findByRole("button", { name: "Create room" }).click(); - }); - - cy.get(".mx_LegacyRoomHeader").within(() => { - cy.findByText("Cybersecurity"); - }); - - cy.hash().then((urlHash) => { - const roomId = urlHash.replace("#/room/", ""); - - // Room should have a knock join rule - cy.window().then(async (win) => { - await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock", - ); - }); - }); - }); - }); - - it("should create a room and change a join rule to knock", () => { - cy.openCreateRoomDialog().within(() => { - cy.findByRole("textbox", { name: "Name" }).type("Cybersecurity"); - - cy.findByRole("button", { name: "Create room" }).click(); - }); - - cy.get(".mx_LegacyRoomHeader").within(() => { - cy.findByText("Cybersecurity"); - }); - - cy.hash().then((urlHash) => { - const roomId = urlHash.replace("#/room/", ""); - - cy.openRoomSettings("Security & Privacy"); - - cy.findByRole("group", { name: "Access" }).within(() => { - cy.findByRole("radio", { name: "Private (invite only)" }).should("be.checked"); - cy.findByRole("radio", { name: "Ask to join" }).check({ force: true }); - }); - - // Room should have a knock join rule - cy.window().then(async (win) => { - await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock", - ); - }); - }); - }); - }); - - it("should create a public knock room", () => { - cy.openCreateRoomDialog().within(() => { - cy.findByRole("textbox", { name: "Name" }).type("Cybersecurity"); - cy.findByRole("button", { name: "Room visibility" }).click(); - cy.findByRole("option", { name: "Ask to join" }).click(); - cy.findByRole("checkbox", { name: "Make this room visible in the public room directory." }).click({ - force: true, - }); - - cy.findByRole("button", { name: "Create room" }).click(); - }); - - cy.get(".mx_LegacyRoomHeader").within(() => { - cy.findByText("Cybersecurity"); - }); - - cy.hash().then((urlHash) => { - const roomId = urlHash.replace("#/room/", ""); - - // Room should have a knock join rule - cy.window().then(async (win) => { - await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock", - ); - }); - }); - }); - - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightResults().eq(0).should("contain", "Cybersecurity"); - }); - }); -}); diff --git a/cypress/e2e/knock/knock-into-room.spec.ts b/cypress/e2e/knock/knock-into-room.spec.ts deleted file mode 100644 index 4d6a0eebe7..0000000000 --- a/cypress/e2e/knock/knock-into-room.spec.ts +++ /dev/null @@ -1,318 +0,0 @@ -/* -Copyright 2023 Mikhail Aheichyk -Copyright 2023 Nordeck IT + Consulting GmbH. - -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 type { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { UserCredentials } from "../../support/login"; -import { waitForRoom } from "../utils"; -import { Filter } from "../../support/settings"; - -describe("Knock Into Room", () => { - let homeserver: HomeserverInstance; - let user: UserCredentials; - let bot: MatrixClient; - - let roomId; - - beforeEach(() => { - cy.enableLabsFeature("feature_ask_to_join"); - - cy.startHomeserver("default").then((data) => { - homeserver = data; - - cy.initTestUser(homeserver, "Alice").then((_user) => { - user = _user; - }); - - cy.getBot(homeserver, { displayName: "Bob" }).then(async (_bot) => { - bot = _bot; - - const { room_id: newRoomId } = await bot.createRoom({ - name: "Cybersecurity", - initial_state: [ - { - type: "m.room.join_rules", - content: { - join_rule: "knock", - }, - state_key: "", - }, - ], - }); - - roomId = newRoomId; - }); - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - it("should knock into the room then knock is approved and user joins the room then user is kicked and joins again", () => { - cy.viewRoomById(roomId); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Join the discussion" }).click(); - - cy.findByRole("heading", { name: "Ask to join?" }); - cy.findByRole("textbox"); - cy.findByRole("button", { name: "Request access" }).click(); - - cy.findByRole("heading", { name: "Request to join sent" }); - }); - - // Knocked room should appear in Rooms - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }); - - cy.window().then(async (win) => { - // bot waits for knock request from Alice - await waitForRoom(win, bot, roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "knock" && - e.getContent()?.displayname === "Alice", - ); - }); - - // bot invites Alice - await bot.invite(roomId, user.userId); - }); - - cy.findByRole("group", { name: "Invites" }).findByRole("treeitem", { name: "Cybersecurity" }); - - // Alice have to accept invitation in order to join the room. - // It will be not needed when homeserver implements auto accept knock requests. - cy.get(".mx_RoomView").findByRole("button", { name: "Accept" }).click(); - - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }); - - cy.findByText("Alice joined the room").should("exist"); - - cy.window().then(async (win) => { - // bot kicks Alice - await bot.kick(roomId, user.userId); - }); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Re-join" }).click(); - - cy.findByRole("heading", { name: "Ask to join Cybersecurity?" }); - cy.findByRole("button", { name: "Request access" }).click(); - }); - - cy.window().then(async (win) => { - // bot waits for knock request from Alice - await waitForRoom(win, bot, roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "knock" && - e.getContent()?.displayname === "Alice", - ); - }); - - // bot invites Alice - await bot.invite(roomId, user.userId); - }); - - // Alice have to accept invitation in order to join the room. - // It will be not needed when homeserver implements auto accept knock requests. - cy.get(".mx_RoomView").findByRole("button", { name: "Accept" }).click(); - - cy.findByText("Alice was invited, joined, was removed, was invited, and joined").should("exist"); - }); - - it("should knock into the room then knock is approved and user joins the room then user is banned/unbanned and joins again", () => { - cy.viewRoomById(roomId); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Join the discussion" }).click(); - - cy.findByRole("heading", { name: "Ask to join?" }); - cy.findByRole("textbox"); - cy.findByRole("button", { name: "Request access" }).click(); - - cy.findByRole("heading", { name: "Request to join sent" }); - }); - - // Knocked room should appear in Rooms - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }); - - cy.window().then(async (win) => { - // bot waits for knock request from Alice - await waitForRoom(win, bot, roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "knock" && - e.getContent()?.displayname === "Alice", - ); - }); - - // bot invites Alice - await bot.invite(roomId, user.userId); - }); - - cy.findByRole("group", { name: "Invites" }).findByRole("treeitem", { name: "Cybersecurity" }); - - // Alice have to accept invitation in order to join the room. - // It will be not needed when homeserver implements auto accept knock requests. - cy.get(".mx_RoomView").findByRole("button", { name: "Accept" }).click(); - - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }); - - cy.findByText("Alice joined the room").should("exist"); - - cy.window().then(async (win) => { - // bot bans Alice - await bot.ban(roomId, user.userId); - }); - - cy.get(".mx_RoomPreviewBar").findByText("You were banned from Cybersecurity by Bob").should("exist"); - - cy.window().then(async (win) => { - // bot unbans Alice - await bot.unban(roomId, user.userId); - }); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Re-join" }).click(); - - cy.findByRole("heading", { name: "Ask to join Cybersecurity?" }); - cy.findByRole("button", { name: "Request access" }).click(); - }); - - cy.window().then(async (win) => { - // bot waits for knock request from Alice - await waitForRoom(win, bot, roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "knock" && - e.getContent()?.displayname === "Alice", - ); - }); - - // bot invites Alice - await bot.invite(roomId, user.userId); - }); - - // Alice have to accept invitation in order to join the room. - // It will be not needed when homeserver implements auto accept knock requests. - cy.get(".mx_RoomView").findByRole("button", { name: "Accept" }).click(); - - cy.findByText("Alice was invited, joined, was banned, was unbanned, was invited, and joined").should("exist"); - }); - - it("should knock into the room and knock is cancelled by user himself", () => { - cy.viewRoomById(roomId); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Join the discussion" }).click(); - - cy.findByRole("heading", { name: "Ask to join?" }); - cy.findByRole("textbox"); - cy.findByRole("button", { name: "Request access" }).click(); - - cy.findByRole("heading", { name: "Request to join sent" }); - }); - - // Knocked room should appear in Rooms - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Cancel request" }).click(); - - cy.findByRole("heading", { name: "Ask to join Cybersecurity?" }); - cy.findByRole("button", { name: "Request access" }); - }); - - cy.findByRole("group", { name: "Historical" }).findByRole("treeitem", { name: "Cybersecurity" }); - }); - - it("should knock into the room then knock is cancelled by another user and room is forgotten", () => { - cy.viewRoomById(roomId); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("button", { name: "Join the discussion" }).click(); - - cy.findByRole("heading", { name: "Ask to join?" }); - cy.findByRole("textbox"); - cy.findByRole("button", { name: "Request access" }).click(); - - cy.findByRole("heading", { name: "Request to join sent" }); - }); - - // Knocked room should appear in Rooms - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }); - - cy.window().then(async (win) => { - // bot waits for knock request from Alice - await waitForRoom(win, bot, roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "knock" && - e.getContent()?.displayname === "Alice", - ); - }); - - // bot kicks Alice - await bot.kick(roomId, user.userId); - }); - - // Room should stay in Rooms and have red badge when knock is denied - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity" }).should("not.exist"); - cy.findByRole("group", { name: "Rooms" }).findByRole("treeitem", { name: "Cybersecurity 1 unread mention." }); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("heading", { name: "You have been denied access" }); - cy.findByRole("button", { name: "Forget this room" }).click(); - }); - - // Room should disappear from the list completely when forgotten - // Should be enabled when issue is fixed: https://github.com/vector-im/element-web/issues/26195 - // cy.findByRole("treeitem", { name: /Cybersecurity/ }).should("not.exist"); - }); - - it("should knock into the public knock room via spotlight", () => { - cy.window().then((win) => { - bot.setRoomDirectoryVisibility(roomId, win.matrixcs.Visibility.Public); - }); - - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightResults().eq(0).should("contain", "Cybersecurity"); - cy.spotlightResults().eq(0).click(); - }); - - cy.get(".mx_RoomPreviewBar").within(() => { - cy.findByRole("heading", { name: "Ask to join?" }); - cy.findByRole("textbox"); - cy.findByRole("button", { name: "Request access" }).click(); - - cy.findByRole("heading", { name: "Request to join sent" }); - }); - }); -}); diff --git a/cypress/e2e/knock/manage-knocks.spec.ts b/cypress/e2e/knock/manage-knocks.spec.ts deleted file mode 100644 index f31f206a9b..0000000000 --- a/cypress/e2e/knock/manage-knocks.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* -Copyright 2023 Mikhail Aheichyk -Copyright 2023 Nordeck IT + Consulting GmbH. - -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 type { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { waitForRoom } from "../utils"; - -describe("Manage Knocks", () => { - let homeserver: HomeserverInstance; - let bot: MatrixClient; - let roomId: string; - - beforeEach(() => { - cy.enableLabsFeature("feature_ask_to_join"); - - cy.startHomeserver("default").then((data) => { - homeserver = data; - - cy.initTestUser(homeserver, "Alice"); - - cy.createRoom({ - name: "Cybersecurity", - initial_state: [ - { - type: "m.room.join_rules", - content: { - join_rule: "knock", - }, - state_key: "", - }, - ], - }).then((newRoomId) => { - roomId = newRoomId; - cy.viewRoomById(newRoomId); - }); - - cy.getBot(homeserver, { displayName: "Bob" }).then(async (_bot) => { - bot = _bot; - }); - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - it("should approve knock using bar", () => { - bot.knockRoom(roomId); - - cy.get(".mx_RoomKnocksBar").within(() => { - cy.findByRole("heading", { name: "Asking to join" }); - cy.findByText(/^Bob/); - cy.findByRole("button", { name: "Approve" }).click(); - }); - - cy.get(".mx_RoomKnocksBar").should("not.exist"); - - cy.findByText("Alice invited Bob"); - }); - - it("should deny knock using bar", () => { - bot.knockRoom(roomId); - - cy.get(".mx_RoomKnocksBar").within(() => { - cy.findByRole("heading", { name: "Asking to join" }); - cy.findByText(/^Bob/); - cy.findByRole("button", { name: "Deny" }).click(); - }); - - cy.get(".mx_RoomKnocksBar").should("not.exist"); - - // Should receive Bob's "m.room.member" with "leave" membership when access is denied - cy.window().then(async (win) => { - await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "leave" && - e.getContent()?.displayname === "Bob", - ); - }); - }); - }); - - it("should approve knock using people tab", () => { - bot.knockRoom(roomId, { reason: "Hello, can I join?" }); - - cy.openRoomSettings("People"); - - cy.findByRole("group", { name: "Asking to join" }).within(() => { - cy.findByText(/^Bob/); - cy.findByText("Hello, can I join?"); - cy.findByRole("button", { name: "Approve" }).click(); - - cy.findByText(/^Bob/).should("not.exist"); - }); - - cy.findByText("Alice invited Bob"); - }); - - it("should deny knock using people tab", () => { - bot.knockRoom(roomId, { reason: "Hello, can I join?" }); - - cy.openRoomSettings("People"); - - cy.findByRole("group", { name: "Asking to join" }).within(() => { - cy.findByText(/^Bob/); - cy.findByText("Hello, can I join?"); - cy.findByRole("button", { name: "Deny" }).click(); - - cy.findByText(/^Bob/).should("not.exist"); - }); - - // Should receive Bob's "m.room.member" with "leave" membership when access is denied - cy.window().then(async (win) => { - await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { - const events = room.getLiveTimeline().getEvents(); - return events.some( - (e) => - e.getType() === "m.room.member" && - e.getContent()?.membership === "leave" && - e.getContent()?.displayname === "Bob", - ); - }); - }); - }); -}); diff --git a/playwright/e2e/knock/create-knock-room.spec.ts b/playwright/e2e/knock/create-knock-room.spec.ts new file mode 100644 index 0000000000..8763c0fd6a --- /dev/null +++ b/playwright/e2e/knock/create-knock-room.spec.ts @@ -0,0 +1,92 @@ +/* +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. +*/ + +import { test, expect } from "../../element-web-test"; +import { waitForRoom } from "../utils"; +import { Filter } from "../../pages/Spotlight"; + +test.describe("Create Knock Room", () => { + test.use({ + displayName: "Alice", + labsFlags: ["feature_ask_to_join"], + }); + + test("should create a knock room", async ({ page, app, user }) => { + const dialog = await app.openCreateRoomDialog(); + await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity"); + await dialog.getByRole("button", { name: "Room visibility" }).click(); + await dialog.getByRole("option", { name: "Ask to join" }).click(); + await dialog.getByRole("button", { name: "Create room" }).click(); + + await expect(page.locator(".mx_LegacyRoomHeader").getByText("Cybersecurity")).toBeVisible(); + + const urlHash = await page.evaluate(() => window.location.hash); + const roomId = urlHash.replace("#/room/", ""); + + // Room should have a knock join rule + await waitForRoom(page, app.client, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some((e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock"); + }); + }); + + test("should create a room and change a join rule to knock", async ({ page, app, user }) => { + const dialog = await app.openCreateRoomDialog(); + await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity"); + await dialog.getByRole("button", { name: "Create room" }).click(); + + await expect(page.locator(".mx_LegacyRoomHeader").getByText("Cybersecurity")).toBeVisible(); + + const urlHash = await page.evaluate(() => window.location.hash); + const roomId = urlHash.replace("#/room/", ""); + + await app.settings.openRoomSettings("Security & Privacy"); + + const settingsGroup = page.getByRole("group", { name: "Access" }); + await expect(settingsGroup.getByRole("radio", { name: "Private (invite only)" })).toBeChecked(); + await settingsGroup.getByText("Ask to join").click(); + + // Room should have a knock join rule + await waitForRoom(page, app.client, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some((e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock"); + }); + }); + + test("should create a public knock room", async ({ page, app, user }) => { + const dialog = await app.openCreateRoomDialog(); + await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity"); + await dialog.getByRole("button", { name: "Room visibility" }).click(); + await dialog.getByRole("option", { name: "Ask to join" }).click(); + await dialog.getByText("Make this room visible in the public room directory.").click(); + await dialog.getByRole("button", { name: "Create room" }).click(); + + await expect(page.locator(".mx_LegacyRoomHeader").getByText("Cybersecurity")).toBeVisible(); + + const urlHash = await page.evaluate(() => window.location.hash); + const roomId = urlHash.replace("#/room/", ""); + + // Room should have a knock join rule + await waitForRoom(page, app.client, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some((e) => e.getType() === "m.room.join_rules" && e.getContent().join_rule === "knock"); + }); + + const spotlightDialog = await app.openSpotlight(); + await spotlightDialog.filter(Filter.PublicRooms); + await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity"); + }); +}); diff --git a/playwright/e2e/knock/knock-into-room.spec.ts b/playwright/e2e/knock/knock-into-room.spec.ts new file mode 100644 index 0000000000..21c5a145e3 --- /dev/null +++ b/playwright/e2e/knock/knock-into-room.spec.ts @@ -0,0 +1,301 @@ +/* +Copyright 2023 Mikhail Aheichyk +Copyright 2023 Nordeck IT + Consulting GmbH. +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 type { Visibility } from "matrix-js-sdk/src/matrix"; +import { test, expect } from "../../element-web-test"; +import { waitForRoom } from "../utils"; +import { Filter } from "../../pages/Spotlight"; + +test.describe("Knock Into Room", () => { + test.use({ + displayName: "Alice", + labsFlags: ["feature_ask_to_join"], + botCreateOpts: { + displayName: "Bob", + }, + room: async ({ bot }, use) => { + const roomId = await bot.createRoom({ + name: "Cybersecurity", + initial_state: [ + { + type: "m.room.join_rules", + content: { + join_rule: "knock", + }, + state_key: "", + }, + ], + }); + await use({ roomId }); + }, + }); + + test("should knock into the room then knock is approved and user joins the room then user is kicked and joins again", async ({ + page, + app, + user, + bot, + room, + }) => { + await app.viewRoomById(room.roomId); + + const roomPreviewBar = page.locator(".mx_RoomPreviewBar"); + await roomPreviewBar.getByRole("button", { name: "Join the discussion" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join?" })).toBeVisible(); + await expect(roomPreviewBar.getByRole("textbox")).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + + await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible(); + + // Knocked room should appear in Rooms + await expect( + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + // bot waits for knock request from Alice + await waitForRoom(page, bot, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "knock" && + e.getContent()?.displayname === "Alice", + ); + }); + + // bot invites Alice + await bot.inviteUser(room.roomId, user.userId); + + await expect( + page.getByRole("group", { name: "Invites" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + // Alice have to accept invitation in order to join the room. + // It will be not needed when homeserver implements auto accept knock requests. + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + await expect( + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + await expect(page.getByText("Alice joined the room")).toBeVisible(); + + // bot kicks Alice + await bot.kick(room.roomId, user.userId); + + await roomPreviewBar.getByRole("button", { name: "Re-join" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join Cybersecurity?" })).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + + // bot waits for knock request from Alice + await waitForRoom(page, bot, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "knock" && + e.getContent()?.displayname === "Alice", + ); + }); + + // bot invites Alice + await bot.inviteUser(room.roomId, user.userId); + + // Alice have to accept invitation in order to join the room. + // It will be not needed when homeserver implements auto accept knock requests. + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + await expect(page.getByText("Alice was invited, joined, was removed, was invited, and joined")).toBeVisible(); + }); + + test("should knock into the room then knock is approved and user joins the room then user is banned/unbanned and joins again", async ({ + page, + app, + user, + bot, + room, + }) => { + await app.viewRoomById(room.roomId); + + const roomPreviewBar = page.locator(".mx_RoomPreviewBar"); + await roomPreviewBar.getByRole("button", { name: "Join the discussion" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join?" })).toBeVisible(); + await expect(roomPreviewBar.getByRole("textbox")).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible(); + + // Knocked room should appear in Rooms + await expect( + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + // bot waits for knock request from Alice + await waitForRoom(page, bot, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "knock" && + e.getContent()?.displayname === "Alice", + ); + }); + + // bot invites Alice + await bot.inviteUser(room.roomId, user.userId); + + await expect( + page.getByRole("group", { name: "Invites" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + // Alice have to accept invitation in order to join the room. + // It will be not needed when homeserver implements auto accept knock requests. + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + await expect( + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + await expect(page.getByText("Alice joined the room")).toBeVisible(); + + // bot bans Alice + await bot.ban(room.roomId, user.userId); + + await expect( + page.locator(".mx_RoomPreviewBar").getByText("You were banned from Cybersecurity by Bob"), + ).toBeVisible(); + + // bot unbans Alice + await bot.unban(room.roomId, user.userId); + + await roomPreviewBar.getByRole("button", { name: "Re-join" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join Cybersecurity?" })).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + + // bot waits for knock request from Alice + await waitForRoom(page, bot, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "knock" && + e.getContent()?.displayname === "Alice", + ); + }); + + // bot invites Alice + await bot.inviteUser(room.roomId, user.userId); + + // Alice have to accept invitation in order to join the room. + // It will be not needed when homeserver implements auto accept knock requests. + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + await expect( + page.getByText("Alice was invited, joined, was banned, was unbanned, was invited, and joined"), + ).toBeVisible(); + }); + + test("should knock into the room and knock is cancelled by user himself", async ({ page, app, bot, room }) => { + await app.viewRoomById(room.roomId); + + const roomPreviewBar = page.locator(".mx_RoomPreviewBar"); + await roomPreviewBar.getByRole("button", { name: "Join the discussion" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join?" })).toBeVisible(); + await expect(roomPreviewBar.getByRole("textbox")).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible(); + + // Knocked room should appear in Rooms + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }); + + await roomPreviewBar.getByRole("button", { name: "Cancel request" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join Cybersecurity?" })).toBeVisible(); + await expect(roomPreviewBar.getByRole("button", { name: "Request access" })).toBeVisible(); + + await expect( + page.getByRole("group", { name: "Historical" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + }); + + test("should knock into the room then knock is cancelled by another user and room is forgotten", async ({ + page, + app, + user, + bot, + room, + }) => { + await app.viewRoomById(room.roomId); + + const roomPreviewBar = page.locator(".mx_RoomPreviewBar"); + await roomPreviewBar.getByRole("button", { name: "Join the discussion" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join?" })).toBeVisible(); + await expect(roomPreviewBar.getByRole("textbox")).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible(); + + // Knocked room should appear in Rooms + await expect( + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).toBeVisible(); + + // bot waits for knock request from Alice + await waitForRoom(page, bot, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "knock" && + e.getContent()?.displayname === "Alice", + ); + }); + + // bot kicks Alice + await bot.kick(room.roomId, user.userId); + + // Room should stay in Rooms and have red badge when knock is denied + await expect( + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity", exact: true }), + ).not.toBeVisible(); + await expect( + page + .getByRole("group", { name: "Rooms" }) + .getByRole("treeitem", { name: "Cybersecurity 1 unread mention." }), + ).toBeVisible(); + + await expect(roomPreviewBar.getByRole("heading", { name: "You have been denied access" })).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Forget this room" }).click(); + + // Room should disappear from the list completely when forgotten + // Should be enabled when issue is fixed: https://github.com/vector-im/element-web/issues/26195 + // await expect(page.getByRole("treeitem", { name: /Cybersecurity/ })).not.toBeVisible(); + }); + + test("should knock into the public knock room via spotlight", async ({ page, app, bot, room }) => { + await bot.setRoomDirectoryVisibility(room.roomId, "public" as Visibility); + + const spotlightDialog = await app.openSpotlight(); + await spotlightDialog.filter(Filter.PublicRooms); + await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity"); + await spotlightDialog.results.nth(0).click(); + + const roomPreviewBar = page.locator(".mx_RoomPreviewBar"); + await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join?" })).toBeVisible(); + await expect(roomPreviewBar.getByRole("textbox")).toBeVisible(); + await roomPreviewBar.getByRole("button", { name: "Request access" }).click(); + await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible(); + }); +}); diff --git a/playwright/e2e/knock/manage-knocks.spec.ts b/playwright/e2e/knock/manage-knocks.spec.ts new file mode 100644 index 0000000000..3fb5c68551 --- /dev/null +++ b/playwright/e2e/knock/manage-knocks.spec.ts @@ -0,0 +1,118 @@ +/* +Copyright 2023 Mikhail Aheichyk +Copyright 2023 Nordeck IT + Consulting GmbH. +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 { waitForRoom } from "../utils"; + +test.describe("Manage Knocks", () => { + test.use({ + displayName: "Alice", + labsFlags: ["feature_ask_to_join"], + botCreateOpts: { + displayName: "Bob", + }, + room: async ({ app, user }, use) => { + const roomId = await app.client.createRoom({ + name: "Cybersecurity", + initial_state: [ + { + type: "m.room.join_rules", + content: { + join_rule: "knock", + }, + state_key: "", + }, + ], + }); + await app.viewRoomById(roomId); + await use({ roomId }); + }, + }); + + test("should approve knock using bar", async ({ page, bot, room }) => { + await bot.knockRoom(room.roomId); + + const roomKnocksBar = page.locator(".mx_RoomKnocksBar"); + await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible(); + await expect(roomKnocksBar.getByText(/^Bob/)).toBeVisible(); + await roomKnocksBar.getByRole("button", { name: "Approve" }).click(); + + await expect(roomKnocksBar).not.toBeVisible(); + + await expect(page.getByText("Alice invited Bob")).toBeVisible(); + }); + + test("should deny knock using bar", async ({ page, app, bot, room }) => { + bot.knockRoom(room.roomId); + + const roomKnocksBar = page.locator(".mx_RoomKnocksBar"); + await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible(); + await expect(roomKnocksBar.getByText(/^Bob/)).toBeVisible(); + await roomKnocksBar.getByRole("button", { name: "Deny" }).click(); + + await expect(roomKnocksBar).not.toBeVisible(); + + // Should receive Bob's "m.room.member" with "leave" membership when access is denied + await waitForRoom(page, app.client, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "leave" && + e.getContent()?.displayname === "Bob", + ); + }); + }); + + test("should approve knock using people tab", async ({ page, app, bot, room }) => { + await bot.knockRoom(room.roomId, { reason: "Hello, can I join?" }); + + await app.settings.openRoomSettings("People"); + + const settingsGroup = page.getByRole("group", { name: "Asking to join" }); + await expect(settingsGroup.getByText(/^Bob/)).toBeVisible(); + await expect(settingsGroup.getByText("Hello, can I join?")).toBeVisible(); + await settingsGroup.getByRole("button", { name: "Approve" }).click(); + await expect(settingsGroup.getByText(/^Bob/)).not.toBeVisible(); + + await expect(page.getByText("Alice invited Bob")).toBeVisible(); + }); + + test("should deny knock using people tab", async ({ page, app, bot, room }) => { + await bot.knockRoom(room.roomId, { reason: "Hello, can I join?" }); + + await app.settings.openRoomSettings("People"); + + const settingsGroup = page.getByRole("group", { name: "Asking to join" }); + await expect(settingsGroup.getByText(/^Bob/)).toBeVisible(); + await expect(settingsGroup.getByText("Hello, can I join?")).toBeVisible(); + await settingsGroup.getByRole("button", { name: "Deny" }).click(); + await expect(settingsGroup.getByText(/^Bob/)).not.toBeVisible(); + + // Should receive Bob's "m.room.member" with "leave" membership when access is denied + await waitForRoom(page, app.client, room.roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "m.room.member" && + e.getContent()?.membership === "leave" && + e.getContent()?.displayname === "Bob", + ); + }); + }); +}); diff --git a/playwright/e2e/utils.ts b/playwright/e2e/utils.ts new file mode 100644 index 0000000000..30aff64dd8 --- /dev/null +++ b/playwright/e2e/utils.ts @@ -0,0 +1,66 @@ +/* +Copyright 2023 Mikhail Aheichyk +Copyright 2023 Nordeck IT + Consulting GmbH. +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 { uniqueId } from "lodash"; + +import type { Page } from "@playwright/test"; +import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { Client } from "../pages/client"; + +/** + * Resolves when room state matches predicate. + * @param page Page instance + * @param client Client instance that can be user or bot + * @param roomId room id to find room and check + * @param predicate defines condition that is used to check the room state + */ +export async function waitForRoom( + page: Page, + client: Client, + roomId: string, + predicate: (room: Room) => boolean, +): Promise { + const predicateId = uniqueId("waitForRoom"); + await page.exposeFunction(predicateId, predicate); + await client.evaluateHandle( + (matrixClient, { roomId, predicateId }) => { + return new Promise((resolve) => { + const room = matrixClient.getRoom(roomId); + + if (window[predicateId](room)) { + resolve(room); + return; + } + + function onEvent(ev: MatrixEvent) { + if (ev.getRoomId() !== roomId) return; + + if (window[predicateId](room)) { + matrixClient.removeListener("event" as ClientEvent, onEvent); + resolve(room); + } + } + + matrixClient.on("event" as ClientEvent, onEvent); + }); + }, + { roomId, predicateId }, + ); +} + +export const CommandOrControl = process.platform === "darwin" ? "Meta" : "Control"; diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 95ab529bb7..ecea296332 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -81,19 +81,25 @@ export const test = base.extend< uut?: Locator; // Unit Under Test, useful place to refer a prepared locator botCreateOpts: CreateBotOpts; bot: Bot; + labsFlags: string[]; webserver: Webserver; } >({ cryptoBackend: ["legacy", { option: true }], config: CONFIG_JSON, - page: async ({ context, page, config, cryptoBackend }, use) => { + page: async ({ context, page, config, cryptoBackend, labsFlags }, use) => { await context.route(`http://localhost:8080/config.json*`, async (route) => { const json = { ...CONFIG_JSON, ...config }; + json["features"] = { + ...json["features"], + // Enable the lab features + ...labsFlags.reduce((obj, flag) => { + obj[flag] = true; + return obj; + }, {}), + }; if (cryptoBackend === "rust") { - json["features"] = { - ...json["features"], - feature_rust_crypto: true, - }; + json.features.feature_rust_crypto = true; } await route.fulfill({ json }); }); @@ -145,6 +151,7 @@ export const test = base.extend< displayName, }); }, + labsFlags: [], user: async ({ page, homeserver, credentials }, use) => { await page.addInitScript( ({ baseUrl, credentials }) => { diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index 742acc13f4..4e065a4b17 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -19,6 +19,7 @@ import { type Locator, type Page, expect } from "@playwright/test"; import { Settings } from "./settings"; import { Client } from "./client"; import { Labs } from "./labs"; +import { Spotlight } from "./Spotlight"; export class ElementAppPage { public constructor(public readonly page: Page) {} @@ -148,4 +149,10 @@ export class ElementAppPage { public async getClipboardText(): Promise { return this.page.evaluate("navigator.clipboard.readText()"); } + + public async openSpotlight(): Promise { + const spotlight = new Spotlight(this.page); + await spotlight.open(); + return spotlight; + } } diff --git a/playwright/pages/Spotlight.ts b/playwright/pages/Spotlight.ts new file mode 100644 index 0000000000..e638ab0aad --- /dev/null +++ b/playwright/pages/Spotlight.ts @@ -0,0 +1,58 @@ +/* +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 type { Locator, Page } from "@playwright/test"; +import { CommandOrControl } from "../e2e/utils"; + +export enum Filter { + People = "people", + PublicRooms = "public_rooms", +} + +export class Spotlight { + private root: Locator; + + constructor(private page: Page) {} + + public async open() { + await this.page.keyboard.press(`${CommandOrControl}+KeyK`); + this.root = this.page.locator('[role=dialog][aria-label="Search Dialog"]'); + } + + public async filter(filter: Filter) { + let selector: string; + switch (filter) { + case Filter.People: + selector = "#mx_SpotlightDialog_button_startChat"; + break; + case Filter.PublicRooms: + selector = "#mx_SpotlightDialog_button_explorePublicRooms"; + break; + default: + selector = ".mx_SpotlightDialog_filter"; + break; + } + await this.root.locator(selector).click(); + } + + public async search(query: string) { + await this.root.locator(".mx_SpotlightDialog_searchBox").getByRole("textbox", { name: "Search" }).fill(query); + } + + public get results() { + return this.root.locator(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option"); + } +} diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts index b3a6cdcaed..fcd0d86e02 100644 --- a/playwright/pages/client.ts +++ b/playwright/pages/client.ts @@ -26,6 +26,8 @@ import type { MatrixEvent, ReceiptType, IRoomDirectoryOptions, + KnockRoomOpts, + Visibility, } from "matrix-js-sdk/src/matrix"; import { Credentials } from "../plugins/homeserver"; @@ -215,6 +217,56 @@ export class Client { }); } + /** + * Knocks the given room. + * @param roomId the id of the room to knock + * @param opts the options to use when knocking + */ + public async knockRoom(roomId: string, opts?: KnockRoomOpts): Promise { + const client = await this.prepareClient(); + await client.evaluate((client, { roomId, opts }) => client.knockRoom(roomId, opts), { roomId, opts }); + } + + /** + * Kicks the given user from the given room. + * @param roomId the id of the room to kick from + * @param userId the id of the user to kick + * @param reason the reason for the kick + */ + public async kick(roomId: string, userId: string, reason?: string): Promise { + const client = await this.prepareClient(); + await client.evaluate((client, { roomId, userId, reason }) => client.kick(roomId, userId, reason), { + roomId, + userId, + reason, + }); + } + + /** + * Bans the given user from the given room. + * @param roomId the id of the room to ban from + * @param userId the id of the user to ban + * @param reason the reason for the ban + */ + public async ban(roomId: string, userId: string, reason?: string): Promise { + const client = await this.prepareClient(); + await client.evaluate((client, { roomId, userId, reason }) => client.ban(roomId, userId, reason), { + roomId, + userId, + reason, + }); + } + + /** + * Unban the given user from the given room. + * @param roomId the id of the room to unban from + * @param userId the id of the user to unban + */ + public async unban(roomId: string, userId: string): Promise { + const client = await this.prepareClient(); + await client.evaluate((client, { roomId, userId }) => client.unban(roomId, userId), { roomId, userId }); + } + /** * @param {MatrixEvent} event * @param {ReceiptType} receiptType @@ -261,4 +313,19 @@ export class Client { }); }, credentials); } + + /** + * Sets the directory visibility for a room. + * @param roomId ID of the room to set the directory visibility for + * @param visibility The new visibility for the room + */ + public async setRoomDirectoryVisibility(roomId: string, visibility: Visibility): Promise { + const client = await this.prepareClient(); + return client.evaluate( + async (client, { roomId, visibility }) => { + await client.setRoomDirectoryVisibility(roomId, visibility); + }, + { roomId, visibility }, + ); + } }