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..bf8c2c8fcdef
--- /dev/null
+++ b/cypress/e2e/knock/create-knock-room.spec.ts
@@ -0,0 +1,99 @@
+/*
+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";
+
+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',
+ );
+ });
+ });
+ });
+ });
+});
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 = `