diff --git a/cypress/e2e/create-room/create-room.spec.ts b/cypress/e2e/create-room/create-room.spec.ts index 1ebc1a7df7a8..a5a81b03f58e 100644 --- a/cypress/e2e/create-room/create-room.spec.ts +++ b/cypress/e2e/create-room/create-room.spec.ts @@ -17,13 +17,6 @@ limitations under the License. /// import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import Chainable = Cypress.Chainable; - -function openCreateRoomDialog(): Chainable> { - cy.findByRole("button", { name: "Add room" }).click(); - cy.findByRole("menuitem", { name: "New room" }).click(); - return cy.get(".mx_CreateRoomDialog"); -} describe("Create Room", () => { let homeserver: HomeserverInstance; @@ -44,7 +37,7 @@ describe("Create Room", () => { const name = "Test room 1"; const topic = "This room is dedicated to this test and this test only!"; - openCreateRoomDialog().within(() => { + cy.openCreateRoomDialog().within(() => { // Fill name & topic cy.findByRole("textbox", { name: "Name" }).type(name); cy.findByRole("textbox", { name: "Topic (optional)" }).type(topic); diff --git a/cypress/e2e/knock/create-knock-room.spec.ts b/cypress/e2e/knock/create-knock-room.spec.ts new file mode 100644 index 000000000000..631d78b15de3 --- /dev/null +++ b/cypress/e2e/knock/create-knock-room.spec.ts @@ -0,0 +1,101 @@ +/* +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 { JoinRule } from "matrix-js-sdk/src/matrix"; + +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { waitForRoom } from "../utils"; + +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 === JoinRule.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 === JoinRule.Knock, + ); + }); + }); + }); + }); +}); diff --git a/cypress/e2e/knock/knock-into-room.spec.ts b/cypress/e2e/knock/knock-into-room.spec.ts new file mode 100644 index 000000000000..d9ab8ff1f388 --- /dev/null +++ b/cypress/e2e/knock/knock-into-room.spec.ts @@ -0,0 +1,179 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { UserCredentials } from "../../support/login"; +import { waitForRoom } from "../utils"; + +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", () => { + 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"); + }); + + 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 + cy.findByRole("treeitem", { name: /Cybersecurity/ }).should("not.exist"); + }); +}); diff --git a/cypress/e2e/knock/manage-knocks.spec.ts b/cypress/e2e/knock/manage-knocks.spec.ts new file mode 100644 index 000000000000..4bde23d54b43 --- /dev/null +++ b/cypress/e2e/knock/manage-knocks.spec.ts @@ -0,0 +1,143 @@ +/* +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 { 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/cypress/e2e/utils.ts b/cypress/e2e/utils.ts new file mode 100644 index 000000000000..24485d6e4646 --- /dev/null +++ b/cypress/e2e/utils.ts @@ -0,0 +1,45 @@ +/* +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 { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +export function waitForRoom( + win: Cypress.AUTWindow, + matrixClient: MatrixClient, + roomId: string, + predicate: (room: Room) => boolean, +): Promise { + return new Promise((resolve, reject) => { + const room = matrixClient.getRoom(roomId); + + if (predicate(room)) { + resolve(); + return; + } + + function onEvent(ev: MatrixEvent) { + if (ev.getRoomId() !== roomId) return; + + if (predicate(room)) { + matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent); + resolve(); + } + } + + matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent); + }); +} diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts index a3c98b4f6fd2..58e4c096791f 100644 --- a/cypress/e2e/widgets/events.spec.ts +++ b/cypress/e2e/widgets/events.spec.ts @@ -19,9 +19,10 @@ limitations under the License. import { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; -import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { UserCredentials } from "../../support/login"; +import { waitForRoom } from "../utils"; const DEMO_WIDGET_ID = "demo-widget-id"; const DEMO_WIDGET_NAME = "Demo Widget"; @@ -68,30 +69,6 @@ const DEMO_WIDGET_HTML = ` `; -function waitForRoom(win: Cypress.AUTWindow, roomId: string, predicate: (room: Room) => boolean): Promise { - const matrixClient = win.mxMatrixClientPeg.get(); - - return new Promise((resolve, reject) => { - const room = matrixClient.getRoom(roomId); - - if (predicate(room)) { - resolve(); - return; - } - - function onEvent(ev: MatrixEvent) { - if (ev.getRoomId() !== roomId) return; - - if (predicate(room)) { - matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent); - resolve(); - } - } - - matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent); - }); -} - describe("Widget Events", () => { let homeserver: HomeserverInstance; let user: UserCredentials; @@ -182,7 +159,7 @@ describe("Widget Events", () => { // widget should receive 'm.room.topic' event after invite cy.window().then(async (win) => { - await waitForRoom(win, roomId, (room) => { + await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { const events = room.getLiveTimeline().getEvents(); return events.some( (e) => @@ -207,7 +184,7 @@ describe("Widget Events", () => { // widget should receive updated 'm.room.topic' event after re-invite cy.window().then(async (win) => { - await waitForRoom(win, roomId, (room) => { + await waitForRoom(win, win.mxMatrixClientPeg.get(), roomId, (room) => { const events = room.getLiveTimeline().getEvents(); return events.some( (e) => diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts index d94811af6ecb..d86a1649455f 100644 --- a/cypress/support/settings.ts +++ b/cypress/support/settings.ts @@ -39,6 +39,11 @@ declare global { */ openUserSettings(tab?: string): Chainable>; + /** + * Open room creation dialog. + */ + openCreateRoomDialog(): Chainable>; + /** * Open room settings (via room header menu), returning a handle to the resulting dialog. * @param tab the name of the tab to switch to after opening, optional. @@ -140,6 +145,12 @@ Cypress.Commands.add("openUserSettings", (tab?: string): Chainable> => { + cy.findByRole("button", { name: "Add room" }).click(); + cy.findByRole("menuitem", { name: "New room" }).click(); + return cy.get(".mx_CreateRoomDialog"); +}); + Cypress.Commands.add("openRoomSettings", (tab?: string): Chainable> => { cy.findByRole("button", { name: "Room options" }).click(); cy.get(".mx_RoomTile_contextMenu").within(() => {